다음을 통해 공유


상속

EF는 .NET 형식 계층 구조를 데이터베이스에 매핑할 수 있습니다. 이렇게 하면 기본 및 파생 형식을 사용하여 .NET 엔터티를 코드로 작성하고 EF가 적절한 데이터베이스 스키마, 문제 쿼리 등을 원활하게 만들 수 있습니다. 형식 계층이 매핑되는 방법에 대한 실제 세부 정보는 공급자에 따라 다릅니다. 이 페이지에서는 관계형 데이터베이스의 컨텍스트에서 상속 지원을 설명합니다.

엔터티 타입 계층 매핑

규칙에 따라 EF는 기본 또는 파생 형식을 자동으로 검색하지 않습니다. 즉, 계층 구조의 CLR 형식을 매핑하려면 모델에서 해당 형식을 명시적으로 지정해야 합니다. 예를 들어 계층의 기본 형식만 지정해도 EF Core가 모든 하위 형식을 암시적으로 포함하지는 않습니다.

다음 샘플에서는 Blog 및 해당 서브클래스 RssBlog대한 DbSet을 노출합니다. Blog 다른 하위 클래스가 있는 경우 모델에 포함되지 않습니다.

internal class MyContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<RssBlog> RssBlogs { get; set; }
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
}

public class RssBlog : Blog
{
    public string RssUrl { get; set; }
}

비고

데이터베이스 열은 TPH 매핑을 사용하는 경우 필요에 따라 자동으로 null을 허용합니다. 예를 들어 일반 RssUrl 인스턴스는 그 속성을 가지고 있지 않으므로, Blog 열은 null이 될 수 있습니다.

계층 구조에서 하나 이상의 엔터티에 대한 DbSet 노출하지 않으려는 경우 Fluent API를 사용하여 모델에 포함되도록 할 수도 있습니다.

팁 (조언)

규칙에 의존하지 않는 경우 HasBaseType사용하여 기본 형식을 명시적으로 지정할 수 있습니다. .HasBaseType((Type)null) 사용하여 계층 구조에서 엔터티 형식을 제거할 수도 있습니다.

계층별 테이블 및 판별자 구성

기본적으로 EF는 TPH(계층별 테이블별) 패턴을 사용하여 상속을 매핑합니다. TPH는 단일 테이블을 사용하여 계층 구조의 모든 형식에 대한 데이터를 저장하고 각 행이 나타내는 형식을 식별하는 데 판별자 열을 사용합니다.

위의 모델은 다음 데이터베이스 스키마에 매핑됩니다(암시적으로 생성된 Discriminator 열은 각 행에 저장되는 Blog 유형을 식별합니다).

계층별 테이블 패턴을 사용하여 블로그 엔터티 계층 구조를 쿼리한 결과 스크린샷

판별자 열의 이름과 형식 및 계층의 각 형식을 식별하는 데 사용되는 값을 구성할 수 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
}

위의 예제에서 EF는 계층의 기본 엔터티에 섀도 속성으로 판별자를 암시적으로 추가했습니다. 이 속성은 다른 속성과 마찬가지로 구성할 수 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property("blog_type")
        .HasMaxLength(200);
}

마지막으로, 판별자는 엔터티의 일반 .NET 속성에 매핑될 수도 있습니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator(b => b.BlogType);

    modelBuilder.Entity<Blog>()
        .Property(e => e.BlogType)
        .HasMaxLength(200)
        .HasColumnName("blog_type");
        
    modelBuilder.Entity<RssBlog>();
}

TPH 패턴을 사용하는 파생 엔터티를 쿼리할 때 EF Core는 쿼리에서 판별자 열에 대한 조건자를 추가합니다. 이 필터는 결과에 없는 기본 형식 또는 형제 형식에 대한 추가 행을 얻지 않도록 합니다. 기본 엔터티에 대한 쿼리는 계층의 모든 엔터티에 대한 결과를 가져오기 때문에 이 필터 조건자는 기본 엔터티 형식에 대해 건너뜁히게 됩니다. 쿼리에서 결과를 구체화할 때 모델의 엔터티 형식에 매핑되지 않은 판별자 값이 발생하는 경우 결과를 구체화하는 방법을 모르기 때문에 예외를 throw합니다. 이 오류는 데이터베이스에 판별자 값이 있는 행이 포함되어 EF 모델에 매핑되지 않은 경우에만 발생합니다. 이러한 데이터가 있는 경우 EF Core 모델의 판별자 매핑을 불완전한 것으로 표시하여 계층 구조의 모든 형식을 쿼리하기 위한 필터 조건자를 항상 추가해야 함을 나타낼 수 있습니다. 판별자 구성에 대한 IsComplete(false) 호출은 매핑이 완료되지 않음을 표시합니다.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator()
        .IsComplete(false);
}

공유 열

기본적으로 계층 구조의 두 형제 엔터티 형식에 이름이 같은 속성이 있는 경우 두 개의 개별 열에 매핑됩니다. 그러나 해당 형식이 동일한 경우 동일한 데이터베이스 열에 매핑할 수 있습니다.

public class MyContext : DbContext
{
    public DbSet<BlogBase> Blogs { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Url)
            .HasColumnName("Url");

        modelBuilder.Entity<RssBlog>()
            .Property(b => b.Url)
            .HasColumnName("Url");
    }
}

public abstract class BlogBase
{
    public int BlogId { get; set; }
}

public class Blog : BlogBase
{
    public string Url { get; set; }
}

public class RssBlog : BlogBase
{
    public string Url { get; set; }
}

비고

SQL Server와 같은 관계형 데이터베이스 공급자는 캐스트를 사용할 때 공유 열을 쿼리할 때 판별자 조건자를 자동으로 사용하지 않습니다. 또한 쿼리 Url = (blog as RssBlog).Url 형제 Url 행에 대한 Blog 값을 반환합니다. 쿼리를 RssBlog 엔터티로 제한하려면 판별자(예: Url = blog is RssBlog ? (blog as RssBlog).Url : null)에 필터를 수동으로 추가해야 합니다.

형식별 테이블 구성

TPT 매핑 패턴에서 모든 형식은 개별 테이블에 매핑됩니다. 기본 형식 또는 파생 형식에만 속하는 속성은 해당 형식에 매핑되는 테이블에 저장됩니다. 파생 형식에 매핑되는 테이블은 파생 테이블을 기본 테이블과 조인하는 외래 키도 저장합니다.

modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

팁 (조언)

각 엔터티 형식에서 ToTable 호출하는 대신 각 루트 엔터티 형식에서 modelBuilder.Entity<Blog>().UseTptMappingStrategy() 호출할 수 있으며 테이블 이름은 EF에서 생성됩니다.

팁 (조언)

각 테이블의 기본 키 열에 대해 서로 다른 열 이름을 구성하려면 테이블별 패싯 구성참조하세요.

EF는 위의 모델에 대해 다음 데이터베이스 스키마를 만듭니다.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL IDENTITY,
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
    CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);

비고

기본 키 제약 조건의 이름을 바꾸면 새 이름이 계층 구조에 매핑된 모든 테이블에 적용되며, 이후 EF 버전에서는 문제 19970 수정될 때 특정 테이블에 대해서만 제약 조건의 이름을 바꿀 수 있습니다.

대량 구성을 사용하는 경우 GetColumnName(IProperty, StoreObjectIdentifier)호출하여 특정 테이블에 대한 열 이름을 검색할 수 있습니다.

foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
    var tableIdentifier = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);

    Console.WriteLine($"{entityType.DisplayName()}\t\t{tableIdentifier}");
    Console.WriteLine(" Property\tColumn");

    foreach (var property in entityType.GetProperties())
    {
        var columnName = property.GetColumnName(tableIdentifier.Value);
        Console.WriteLine($" {property.Name,-10}\t{columnName}");
    }

    Console.WriteLine();
}

경고

대부분의 경우 TPT는 TPH에 비해 열등한 성능을 보여 줍니다. 자세한 내용은 성능 문서를 참조하세요.

주의

파생 형식의 열은 서로 다른 테이블에 매핑되므로 상속된 속성과 선언된 속성을 모두 사용하는 복합 FK 제약 조건 및 인덱스는 데이터베이스에서 만들 수 없습니다.

구체적인 형식별 테이블 구성

TPC 매핑 패턴에서 모든 형식은 개별 테이블에 매핑됩니다. 각 테이블에는 해당 엔터티 형식의 모든 속성에 대한 열이 포함됩니다. 이는 TPT 전략의 몇 가지 일반적인 성능 문제를 해결합니다.

팁 (조언)

EF 팀은 .NET Data Community 스탠드업에피소드에서 TPC 매핑에 대해 자세히 설명했습니다. 모든 커뮤니티 스탠드업 에피소드와 마찬가지로 YouTube에서 TPC 에피소드를 지금 시청할 수있습니다.

modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
    .ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
    .ToTable("RssBlogs");

팁 (조언)

각 엔터티 형식에서 ToTable 호출하는 대신 각 루트 엔터티 형식에서 modelBuilder.Entity<Blog>().UseTpcMappingStrategy() 호출하면 규칙에 따라 테이블 이름이 생성됩니다.

팁 (조언)

각 테이블의 기본 키 열에 대해 서로 다른 열 이름을 구성하려면 테이블별 패싯 구성참조하세요.

EF는 위의 모델에 대해 다음 데이터베이스 스키마를 만듭니다.

CREATE TABLE [Blogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);

CREATE TABLE [RssBlogs] (
    [BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
    [Url] nvarchar(max) NULL,
    [RssUrl] nvarchar(max) NULL,
    CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);

TPC 데이터베이스 스키마

TPC 전략은 계층 구조의 모든 구체적인 형식에 대해 다른 테이블이 생성된다는 점을 제외하고 TPT 전략과 유사하지만 테이블은 추상 형식에 대해 생성되지 . 따라서 이름은 "table-per-concrete-type"입니다. TPT와 마찬가지로 테이블 자체는 저장된 개체의 형식을 나타냅니다. 그러나 TPT 매핑과 달리 각 테이블에는 구체적인 형식 및 기본 형식의 모든 속성에 대한 열이 포함됩니다. TPC 데이터베이스 스키마는 비정규화됩니다.

예를 들어 이 계층을 매핑하는 것이 좋습니다.

public abstract class Animal
{
    protected Animal(string name)
    {
        Name = name;
    }

    public int Id { get; set; }
    public string Name { get; set; }
    public abstract string Species { get; }

    public Food? Food { get; set; }
}

public abstract class Pet : Animal
{
    protected Pet(string name)
        : base(name)
    {
    }

    public string? Vet { get; set; }

    public ICollection<Human> Humans { get; } = new List<Human>();
}

public class FarmAnimal : Animal
{
    public FarmAnimal(string name, string species)
        : base(name)
    {
        Species = species;
    }

    public override string Species { get; }

    [Precision(18, 2)]
    public decimal Value { get; set; }

    public override string ToString()
        => $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Cat : Pet
{
    public Cat(string name, string educationLevel)
        : base(name)
    {
        EducationLevel = educationLevel;
    }

    public string EducationLevel { get; set; }
    public override string Species => "Felis catus";

    public override string ToString()
        => $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Dog : Pet
{
    public Dog(string name, string favoriteToy)
        : base(name)
    {
        FavoriteToy = favoriteToy;
    }

    public string FavoriteToy { get; set; }
    public override string Species => "Canis familiaris";

    public override string ToString()
        => $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}

public class Human : Animal
{
    public Human(string name)
        : base(name)
    {
    }

    public override string Species => "Homo sapiens";

    public Animal? FavoriteAnimal { get; set; }
    public ICollection<Pet> Pets { get; } = new List<Pet>();

    public override string ToString()
        => $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
           $" eats {Food?.ToString() ?? "<Unknown>"}";
}

SQL Server를 사용하는 경우 이 계층 구조에 대해 만든 테이블은 다음과 같습니다.

CREATE TABLE [Cats] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [EducationLevel] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));

CREATE TABLE [Dogs] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Vet] nvarchar(max) NULL,
    [FavoriteToy] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));

CREATE TABLE [FarmAnimals] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [Value] decimal(18,2) NOT NULL,
    [Species] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));

CREATE TABLE [Humans] (
    [Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
    [Name] nvarchar(max) NOT NULL,
    [FoodId] uniqueidentifier NULL,
    [FavoriteAnimalId] int NULL,
    CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));

다음에 유의합니다.

  • 개체 모델에 Animal 때문에 Pet 또는 abstract 형식에 대한 테이블이 없습니다. C#에서는 추상 형식의 인스턴스를 허용하지 않으므로 추상 형식 인스턴스가 데이터베이스에 저장되는 상황은 없습니다.

  • 기본 형식의 속성 매핑은 각 구체적인 형식에 대해 반복됩니다. 예를 들어, 모든 테이블에는 Name 열이 있으며, 고양이와 개에는 모두 Vet 열이 있습니다.

  • 이 데이터베이스에 일부 데이터를 저장하면 다음과 같은 결과가 발생합니다.

고양이 테이블

아이디 이름 FoodId 수의사 교육 수준
1 앨리스 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly 경영학 석사 (MBA)
2 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly 유치원
8 (여덟) 백스터 5dc5019e-6f72-454b-d4b0-08da7aca624f Bothell 애완 동물 병원 BSc

개에 관한 테이블

아이디 이름 FoodId 수의사 FavoriteToy
3 토스트 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly 다람쥐 씨

FarmAnimals 테이블

아이디 이름 FoodId 가치
4 클라이드 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus (당나귀)

인간 데이터 테이블

아이디 이름 FoodId 좋아하는동물ID
5 웬디 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 아서 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 케이티 8 (여덟)

TPT 매핑과 달리 단일 개체에 대한 모든 정보는 단일 테이블에 포함됩니다. 또한 TPH 매핑과 달리 모델에서 사용되지 않는 테이블에는 열과 행의 조합이 없습니다. 아래에서는 이러한 특성이 쿼리 및 스토리지에 어떻게 중요한지 살펴보겠습니다.

키 생성

선택한 상속 매핑 전략은 기본 키 값이 생성되고 관리되는 방식에 영향을 줍니다. 각 엔터티 인스턴스는 단일 테이블의 단일 행으로 표현되므로 TPH의 키는 쉽습니다. 모든 종류의 키 값 생성을 사용할 수 있으며 추가 제약 조건이 필요하지 않습니다.

TPT 전략의 경우 계층 구조의 기본 형식에 매핑된 테이블의 행이 항상 있습니다. 이 행에서 모든 종류의 키 생성을 사용할 수 있으며 다른 테이블의 키는 외래 키 제약 조건을 사용하여 이 테이블에 연결됩니다.

TPC의 경우 상황이 좀 더 복잡해집니다. 먼저, EF Core에서는 엔터티가 서로 다른 유형일지라도 계층 구조의 모든 엔터티에 고유한 키 값이 있어야 합니다. 예를 들어 예제 모델을 사용하여 Dog는 고양이와 동일한 ID 키 값을 가질 수 없습니다. 둘째, TPT와 달리 키 값이 살고 생성될 수 있는 단일 위치로 작동할 수 있는 공통 테이블은 없습니다. 즉, 간단한 Identity 열을 사용할 수 없습니다.

시퀀스를 지원하는 데이터베이스의 경우 각 테이블에 대한 기본 제약 조건에서 참조되는 단일 시퀀스를 사용하여 키 값을 생성할 수 있습니다. 위의 TPC 테이블에 사용되는 전략이며 각 테이블에는 다음이 있습니다.

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence EF Core에서 만든 데이터베이스 시퀀스입니다. 이 전략은 SQL Server용 EF Core 데이터베이스 공급자를 사용할 때 TPC 계층 구조에 기본적으로 사용됩니다. 시퀀스를 지원하는 다른 데이터베이스에 대한 데이터베이스 공급자는 비슷한 기본값을 가져야 합니다. Hi-Lo 패턴과 같은 시퀀스를 사용하는 다른 주요 생성 전략도 TPC와 함께 사용할 수 있습니다.

표준 ID 열은 TPC에서 작동하지 않지만 각 테이블이 적절한 시드 및 증가로 구성된 경우 각 테이블에 대해 생성된 값이 충돌하지 않도록 ID 열을 사용할 수 있습니다. 다음은 그 예입니다.

modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));

중요합니다

이 전략을 사용하면 계층 구조의 총 형식 수를 미리 알려야 하므로 나중에 파생 형식을 추가하기가 더 어려워집니다.

SQLite는 시퀀스 또는 ID 초기값/증분을 지원하지 않으므로 TPC 전략과 함께 SQLite를 사용하는 경우 정수 키 값 생성이 지원되지 않습니다. 그러나 클라이언트 쪽 생성 또는 GUID와 같은 전역 고유 키는 SQLite를 비롯한 모든 데이터베이스에서 지원됩니다.

외래 키 제약 조건

TPC 매핑 전략은 비정규화된 SQL 스키마를 만듭니다. 이는 일부 데이터베이스 순수주의자가 이에 반대하는 이유 중 하나입니다. 예를 들어, 외래 키 열 FavoriteAnimalId을 고려하십시오. 이 열의 값은 일부 동물의 기본 키 값과 일치해야 합니다. 이는 TPH 또는 TPT를 사용할 때 간단한 FK 제약 조건을 사용하여 데이터베이스에 적용할 수 있습니다. 다음은 그 예입니다.

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

그러나 TPC를 사용하는 경우 지정된 동물의 기본 키는 해당 동물의 구체적인 유형에 해당하는 테이블에 저장됩니다. 예를 들어 고양이의 기본 키는 Cats.Id 열에 저장되고, 강아지의 기본 키는 Dogs.Id 열 등에 저장됩니다. 즉, 이 관계에 대해 FK 제약 조건을 만들 수 없습니다.

실제로 애플리케이션이 잘못된 데이터를 삽입하려고 시도하지 않는 한 문제가 되지 않습니다. 예를 들어 모든 데이터가 EF Core에 의해 삽입되고 탐색을 사용하여 엔터티를 연결하면 FK 열에 항상 유효한 PK 값이 포함됩니다.

요약 및 지침

요약하자면, TPH는 일반적으로 대부분의 애플리케이션에 적합하며 광범위한 시나리오에 적합한 기본값이므로 필요하지 않은 경우 TPC의 복잡성을 추가하지 마세요. 특히 코드가 다양한 형식의 엔터티에 대한 쿼리를 주로 작성해야 하는 경우, 기본 형식에 대한 쿼리 작성을 포함하여 TPC보다 TPH를 선택하는 것이 좋습니다.

즉, TPC는 코드가 주로 단일 리프 형식의 엔터티를 쿼리하고 벤치마크가 TPH에 비해 향상된 것으로 표시될 때 사용할 수 있는 좋은 매핑 전략이기도 합니다.

외부 요인에 의해 제한되는 경우에만 TPT를 사용합니다.