고급 테이블 매핑

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.StatusOrder.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, PhoneNumbersAddresses 테이블로 분할합니다.

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");
        });