고급 테이블 매핑
EF Core는 데이터베이스의 테이블에 엔터티 형식을 매핑할 때 많은 유연성을 제공합니다. 이는 EF에서 만들지 않은 데이터베이스를 사용해야 할 때 더욱 유용합니다.
아래 기술은 테이블 측면에서 설명되지만 뷰에 매핑할 때도 동일한 결과를 달성할 수 있습니다.
테이블 분할
EF Core를 사용하면 두 개 이상의 엔터티를 단일 행에 매핑할 수 있습니다. 이를 테이블 분할 또는 테이블 공유라고 합니다.
구성
엔터티 형식을 분할하는 테이블을 사용하려면 동일한 테이블에 매핑되어야 하는 기본 키를 동일한 열에 매핑하고 한 엔터티 형식의 기본 키와 동일한 테이블의 기본 키 간에 하나 이상의 관계를 구성합니다.
테이블 분할에 대한 일반적인 시나리오는 더 큰 성능 또는 캡슐화를 위해 테이블의 열 하위 집합만 사용하는 것입니다.
이 예제 Order
에서는 DetailedOrder
의 하위 집합을 나타냅니다.
public class Order
{
public int Id { get; set; }
public OrderStatus? Status { get; set; }
public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
public int Id { get; set; }
public OrderStatus? Status { get; set; }
public string BillingAddress { get; set; }
public string ShippingAddress { get; set; }
public byte[] Version { get; set; }
}
필요한 구성 외에도 Property(o => o.Status).HasColumnName("Status")
를 호출하여 DetailedOrder.Status
를 Order.Status
와 동일한 열에 매핑합니다.
modelBuilder.Entity<DetailedOrder>(
dob =>
{
dob.ToTable("Orders");
dob.Property(o => o.Status).HasColumnName("Status");
});
modelBuilder.Entity<Order>(
ob =>
{
ob.ToTable("Orders");
ob.Property(o => o.Status).HasColumnName("Status");
ob.HasOne(o => o.DetailedOrder).WithOne()
.HasForeignKey<DetailedOrder>(o => o.Id);
ob.Navigation(o => o.DetailedOrder).IsRequired();
});
팁
자세한 컨텍스트는 전체 샘플 프로젝트를 참조하세요.
사용
테이블 분할을 사용하여 엔터티 저장 및 쿼리는 다른 엔터티와 동일한 방식으로 수행됩니다.
using (var context = new TableSplittingContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Add(
new Order
{
Status = OrderStatus.Pending,
DetailedOrder = new DetailedOrder
{
Status = OrderStatus.Pending,
ShippingAddress = "221 B Baker St, London",
BillingAddress = "11 Wall Street, New York"
}
});
context.SaveChanges();
}
using (var context = new TableSplittingContext())
{
var pendingCount = context.Orders.Count(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"Current number of pending orders: {pendingCount}");
}
using (var context = new TableSplittingContext())
{
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}
선택적 종속 엔터티
데이터베이스에서 종속 엔터티가 사용하는 모든 열이 NULL
인 경우 쿼리할 때 인스턴스가 만들어지지 않습니다. 이렇게 하면 선택적 종속 엔터티를 모델링할 수 있습니다. 여기서 보안 주체의 관계 속성은 null입니다. 종속의 모든 속성이 선택 사항이며 null
로 설정되어 있지 않을 수도 있는 경우에도 발생합니다.
그러나 추가 검사는 쿼리 성능에 영향을 미칠 수 있습니다. 또한 종속 엔터티 형식에 고유한 종속이 있는 경우 인스턴스를 만들어야 하는지 여부를 결정하는 것은 간단하지 않습니다. 이러한 문제를 방지하려면 종속 엔터티 형식을 필수로 표시할 수 있습니다. 자세한 내용은 필수 일대일 종속성을 참조하세요.
동시성 토큰
테이블을 공유하는 엔터티 형식에 동시성 토큰이 있는 경우 다른 모든 엔터티 형식에도 포함되어야 합니다. 같은 테이블에 매핑된 엔터티 중 한 개만 업데이트하는 경우 부실 동시성 토큰 값을 방지하기 위해 이렇게 변경되었습니다.
동시성 토큰이 소비 코드에 노출되지 않도록 하려면 섀도 속성으로 만들 수 있습니다.
modelBuilder.Entity<Order>()
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
modelBuilder.Entity<DetailedOrder>()
.Property(o => o.Version).IsRowVersion().HasColumnName("Version");
상속
이 섹션을 계속하기 전에 상속에 대한 전용 페이지를 읽는 것이 좋습니다.
테이블 분할을 사용하는 종속 형식에는 상속 계층 구조가 있을 수 있지만 몇 가지 제한 사항이 있습니다.
- 파생 형식이 동일한 테이블에 매핑할 수 없으므로 종속 엔터티 형식 TPC 매핑을 사용할 수 없습니다.
- 종속 엔터티 형식은 TPT 매핑을 사용할 수 있지만 루트 엔터티 형식만 테이블 분할을 사용할 수 있습니다.
- 보안 주체 엔터티 형식이 TPC를 사용하는 경우 하위 항목이 없는 엔터티 형식만 테이블 분할을 사용할 수 있습니다. 그렇지 않으면 파생 형식에 해당하는 테이블에서 종속 열을 복제하여 모든 상호 작용을 복잡하게 해야 합니다.
엔터티 분할
EF Core를 사용하면 엔터티를 둘 이상의 테이블의 행에 매핑할 수 있습니다. 이를 엔터티 분할이라고 합니다.
구성
예를 들어 고객 데이터를 보유하는 세 개의 테이블이 있는 데이터베이스를 고려해 보세요.
- 고객 정보에 관한
Customers
테이블 - 고객의 전화 번호에 관한
PhoneNumbers
테이블 - 고객 주소에 관한
Addresses
테이블
다음은 SQL Server에서 이러한 테이블에 대한 정의입니다.
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [PhoneNumbers] (
[CustomerId] int NOT NULL,
[PhoneNumber] nvarchar(max) NULL,
CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Addresses] (
[CustomerId] int NOT NULL,
[Street] nvarchar(max) NOT NULL,
[City] nvarchar(max) NOT NULL,
[PostCode] nvarchar(max) NULL,
[Country] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
이러한 각 테이블은 일반적으로 형식 간의 관계를 사용하여 고유한 엔터티 형식에 매핑됩니다. 그러나 세 테이블이 모두 항상 함께 사용되는 경우 모두 단일 엔터티 형식에 매핑하는 것이 더 편리할 수 있습니다. 예시:
public class Customer
{
public Customer(string name, string street, string city, string? postCode, string country)
{
Name = name;
Street = street;
City = city;
PostCode = postCode;
Country = country;
}
public int Id { get; set; }
public string Name { get; set; }
public string? PhoneNumber { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string? PostCode { get; set; }
public string Country { get; set; }
}
이는 엔터티 형식의 각 분할에 대해 SplitToTable
을(를) 호출하여 EF7에서 수행됩니다. 예를 들어 다음 코드는 Customer
엔터티 형식을 위에 표시된 Customers
, PhoneNumbers
및 Addresses
테이블로 분할합니다.
modelBuilder.Entity<Customer>(
entityBuilder =>
{
entityBuilder
.ToTable("Customers")
.SplitToTable(
"PhoneNumbers",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.PhoneNumber);
})
.SplitToTable(
"Addresses",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.Street);
tableBuilder.Property(customer => customer.City);
tableBuilder.Property(customer => customer.PostCode);
tableBuilder.Property(customer => customer.Country);
});
});
또한 필요한 경우 각 테이블에 대해 다른 열 이름을 지정할 수 있습니다. 주 테이블의 열 이름을 구성하려면 테이블별 패싯 구성을 참조하세요.
연결 외래 키 구성
매핑된 테이블을 연결하는 FK는 선언된 것과 동일한 속성을 대상으로 합니다. 일반적으로 데이터베이스에는 중복되므로 만들어지지 않습니다. 그러나 엔터티 형식이 둘 이상의 테이블에 매핑되는 경우 예외가 있습니다. 해당 패싯을 변경하려면 관계 구성 Fluent API를 사용할 수 있습니다.
modelBuilder.Entity<Customer>()
.HasOne<Customer>()
.WithOne()
.HasForeignKey<Customer>(a => a.Id)
.OnDelete(DeleteBehavior.Restrict);
제한 사항
- 엔터티 분할은 계층 구조의 엔터티 형식에 사용할 수 없습니다.
- 주 테이블의 모든 행에는 각 분할 테이블에 행이 있어야 합니다(조각은 선택 사항이 아님).
테이블별 패싯 구성
일부 매핑 패턴으로 인해 동일한 CLR 속성이 서로 다른 각 테이블의 열에 매핑됩니다. EF7을 사용하면 이러한 열의 이름이 다를 수 있습니다. 예를 들어 간단한 상속 계층 구조를 고려합니다.
public abstract class Animal
{
public int Id { get; set; }
public string Breed { get; set; } = null!;
}
public class Cat : Animal
{
public string? EducationalLevel { get; set; }
}
public class Dog : Animal
{
public string? FavoriteToy { get; set; }
}
TPT 상속 매핑 전략을 사용하면 이러한 형식이 세 개의 테이블에 매핑됩니다. 그러나 각 테이블의 기본 키 열은 다른 이름을 가질 수 있습니다. 예시:
CREATE TABLE [Animals] (
[Id] int NOT NULL IDENTITY,
[Breed] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);
CREATE TABLE [Cats] (
[CatId] int NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
EF7을 사용하면 중첩된 테이블 작성기를 사용하여 이 매핑을 구성할 수 있습니다.
modelBuilder.Entity<Animal>().ToTable("Animals");
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));
TPC 상속 매핑을 사용하면 Breed
속성이 다른 테이블의 다른 열 이름에 매핑될 수도 있습니다. 다음 TPC 테이블을 예로 들 수 있습니다.
CREATE TABLE [Cats] (
[CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[CatBreed] nvarchar(max) NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[DogBreed] nvarchar(max) NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);
EF7은 다음 테이블 매핑을 지원합니다.
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
builder =>
{
builder.Property(cat => cat.Id).HasColumnName("CatId");
builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
});
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
builder =>
{
builder.Property(dog => dog.Id).HasColumnName("DogId");
builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
});
.NET