Bagikan melalui


Warisan

EF dapat memetakan hierarki jenis .NET ke database. Ini memungkinkan Anda untuk menulis entitas .NET Anda dalam kode seperti biasa, menggunakan jenis dasar dan turunan, dan membuat EF membuat skema database yang sesuai, menerbitkan kueri, dll. Detail aktual tentang bagaimana hierarki jenis dipetakan bergantung pada penyedia; halaman ini menjelaskan dukungan pewarisan dalam konteks database relasional.

Pemetaan hierarki jenis entitas

Berdasarkan konvensi, EF tidak akan secara otomatis memindai jenis dasar atau turunan; ini berarti bahwa jika Anda ingin jenis CLR dalam hierarki Anda dipetakan, Anda harus secara eksplisit menentukan jenis tersebut pada model Anda. Misalnya, menentukan hanya jenis dasar hierarki tidak akan menyebabkan EF Core secara implisit menyertakan semua subtipenya.

Sampel berikut mengekspos DbSet untuk Blog dan subkelasnya RssBlog. Jika Blog memiliki subkelas lain, subkelas tersebut tidak akan disertakan dalam model.

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

Catatan

Kolom database secara otomatis dibuat nullable seperlunya saat menggunakan pemetaan TPH. Misalnya, RssUrl kolom dapat diubah ke null karena instans reguler Blog tidak memiliki properti tersebut.

Jika Anda tidak ingin mengekspos DbSet untuk satu atau beberapa entitas dalam hierarki, Anda juga dapat menggunakan API Fasih untuk memastikan entitas tersebut disertakan dalam model.

Tip

Jika Anda tidak mengandalkan konvensi, Anda dapat menentukan jenis dasar secara eksplisit menggunakan HasBaseType. Anda juga dapat menggunakan .HasBaseType((Type)null) untuk menghapus jenis entitas dari hierarki.

Konfigurasi tabel per hierarki dan diskriminator

Secara default, EF memetakan pewarisan menggunakan pola table-per-hierarchy (TPH). TPH menggunakan satu tabel untuk menyimpan data untuk semua jenis dalam hierarki, dan kolom diskriminator digunakan untuk mengidentifikasi jenis mana yang diwakili setiap baris.

Model di atas dipetakan ke skema database berikut (perhatikan kolom yang dibuat Discriminator secara implisit, yang mengidentifikasi jenis mana Blog yang disimpan di setiap baris).

Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern

Anda dapat mengonfigurasi nama dan jenis kolom diskriminator dan nilai yang digunakan untuk mengidentifikasi setiap jenis dalam hierarki:

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

Dalam contoh di atas, EF menambahkan diskriminator secara implisit sebagai properti bayangan pada entitas dasar hierarki. Properti ini dapat dikonfigurasi seperti yang lain:

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

Terakhir, diskriminator juga dapat dipetakan ke properti .NET reguler di entitas Anda:

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

Saat mengkueri entitas turunan, yang menggunakan pola TPH, EF Core menambahkan predikat atas kolom diskriminator dalam kueri. Filter ini memastikan bahwa kami tidak mendapatkan baris tambahan untuk jenis dasar atau jenis saudara tidak dalam hasilnya. Predikat filter ini dilewati untuk jenis entitas dasar karena kueri untuk entitas dasar akan mendapatkan hasil untuk semua entitas dalam hierarki. Saat mewujudkan hasil dari kueri, jika kita menemukan nilai diskriminator, yang tidak dipetakan ke jenis entitas apa pun dalam model, kita melemparkan pengecualian karena kita tidak tahu cara mewujudkan hasilnya. Kesalahan ini hanya terjadi jika database Anda berisi baris dengan nilai diskriminator, yang tidak dipetakan dalam model EF. Jika Anda memiliki data tersebut, maka Anda dapat menandai pemetaan diskriminator dalam model EF Core sebagai tidak lengkap untuk menunjukkan bahwa kita harus selalu menambahkan predikat filter untuk mengkueri jenis apa pun dalam hierarki. IsComplete(false) panggilan pada konfigurasi diskriminator menandai pemetaan menjadi tidak lengkap.

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

Kolom bersama

Secara default, ketika dua jenis entitas saudara dalam hierarki memiliki properti dengan nama yang sama, mereka akan dipetakan ke dua kolom terpisah. Namun, jika jenisnya identik, mereka dapat dipetakan ke kolom database yang sama:

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

Catatan

Penyedia database relasional, seperti SQL Server, tidak akan secara otomatis menggunakan predikat diskriminator saat mengkueri kolom bersama saat menggunakan cast. Kueri Url = (blog as RssBlog).Url juga akan mengembalikan Url nilai untuk baris saudara Blog . Untuk membatasi kueri ke RssBlog entitas, Anda perlu menambahkan filter secara manual pada diskriminator, seperti Url = blog is RssBlog ? (blog as RssBlog).Url : null.

Konfigurasi tabel per jenis

Dalam pola pemetaan TPT, semua jenis dipetakan ke tabel individual. Properti yang hanya termasuk dalam jenis dasar atau jenis turunan disimpan dalam tabel yang memetakan ke jenis tersebut. Tabel yang memetakan ke jenis turunan juga menyimpan kunci asing yang menggabungkan tabel turunan dengan tabel dasar.

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

Tip

Alih-alih memanggil ToTable pada setiap jenis entitas, Anda dapat memanggil modelBuilder.Entity<Blog>().UseTptMappingStrategy() setiap jenis entitas akar dan nama tabel akan dihasilkan oleh EF.

Tip

Untuk mengonfigurasi nama kolom yang berbeda untuk kolom kunci utama di setiap tabel, lihat Konfigurasi faset khusus tabel.

EF akan membuat skema database berikut untuk model di atas.

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

Catatan

Jika batasan kunci utama diganti namanya, nama baru akan diterapkan ke semua tabel yang dipetakan ke hierarki, versi EF di masa mendatang akan memungkinkan penggantian nama batasan hanya untuk tabel tertentu ketika masalah 19970 diperbaiki.

Jika Anda menggunakan konfigurasi massal, Anda dapat mengambil nama kolom untuk tabel tertentu dengan memanggil 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();
}

Peringatan

Dalam banyak kasus, TPT menunjukkan performa yang lebih rendah jika dibandingkan dengan TPH. Lihat dokumen performa untuk informasi selengkapnya.

Perhatian

Kolom untuk jenis turunan dipetakan ke tabel yang berbeda, oleh karena itu batasan FK komposit dan indeks yang menggunakan properti yang diwariskan dan dideklarasikan tidak dapat dibuat dalam database.

Konfigurasi tabel per jenis beton

Catatan

Fitur table-per-concrete-type (TPC) diperkenalkan di EF Core 7.0.

Dalam pola pemetaan TPC, semua jenis dipetakan ke tabel individual. Setiap tabel berisi kolom untuk semua properti pada jenis entitas terkait. Ini mengatasi beberapa masalah performa umum dengan strategi TPT.

Tip

Tim EF menunjukkan dan berbicara secara mendalam tentang pemetaan TPC dalam episode Standup Komunitas Data .NET. Seperti semua episode Community Standup, Anda dapat menonton episode TPC sekarang di YouTube.

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

Tip

Alih-alih memanggil ToTable pada setiap jenis entitas yang hanya memanggil modelBuilder.Entity<Blog>().UseTpcMappingStrategy() pada setiap jenis entitas akar akan menghasilkan nama tabel menurut konvensi.

Tip

Untuk mengonfigurasi nama kolom yang berbeda untuk kolom kunci utama di setiap tabel, lihat Konfigurasi faset khusus tabel.

EF akan membuat skema database berikut untuk model di atas.

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])
);

Skema database TPC

Strategi TPC mirip dengan strategi TPT kecuali bahwa tabel yang berbeda dibuat untuk setiap jenis beton dalam hierarki, tetapi tabel tidak dibuat untuk jenis abstrak - oleh karena itu nama "table-per-concrete-type". Seperti halnya TPT, tabel itu sendiri menunjukkan jenis objek yang disimpan. Namun, tidak seperti pemetaan TPT, setiap tabel berisi kolom untuk setiap properti dalam jenis beton dan jenis dasarnya. Skema database TPC dinormalisasi.

Misalnya, pertimbangkan untuk memetakan hierarki ini:

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

Saat menggunakan SQL Server, tabel yang dibuat untuk hierarki ini adalah:

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]));

Perhatikan bahwa:

  • Tidak ada tabel untuk Animal jenis atau Pet , karena ini ada abstract dalam model objek. Ingatlah bahwa C# tidak mengizinkan instans jenis abstrak, dan oleh karena itu tidak ada situasi di mana instans jenis abstrak akan disimpan ke database.

  • Pemetaan properti dalam jenis dasar diulang untuk setiap jenis beton. Misalnya, setiap tabel memiliki Name kolom, dan Kucing dan Anjing memiliki Vet kolom.

  • Menyimpan beberapa data ke dalam database ini menghasilkan hal berikut:

Tabel Kucing

Id Nama FoodId Dokter hewan EducationLevel
1 Alice 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 Mac 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly Prasekolah
8 Baxter 5dc5019e-6f72-454b-d4b0-08da7aca624f Rumah Sakit Hewan Peliharaan Bothell Bsc

Tabel anjing

Id Nama FoodId Dokter hewan FavoritToy
3 Toast 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly Tn. Tupai

Tabel FarmAnimals

Id Nama FoodId Nilai Species
4 Clyde 1d495075-f527-4498-d4af-08da7aca624f 100,00 Equus africanus asinus

Tabel manusia

Id Nama FoodId FavoriteAnimalId
5 Wendy 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 Arthur 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 Katie nihil 8

Perhatikan bahwa tidak seperti pemetaan TPT, semua informasi untuk satu objek terkandung dalam satu tabel. Dan, tidak seperti pemetaan TPH, tidak ada kombinasi kolom dan baris dalam tabel apa pun di mana itu tidak pernah digunakan oleh model. Kita akan melihat di bawah ini bagaimana karakteristik ini bisa penting untuk kueri dan penyimpanan.

Pembuatan kunci

Strategi pemetaan warisan yang dipilih memiliki konsekuensi tentang bagaimana nilai kunci primer dihasilkan dan dikelola. Kunci dalam TPH mudah, karena setiap instans entitas diwakili oleh satu baris dalam satu tabel. Segala jenis pembuatan nilai kunci dapat digunakan, dan tidak ada batasan tambahan yang diperlukan.

Untuk strategi TPT, selalu ada baris dalam tabel yang dipetakan ke jenis dasar hierarki. Segala jenis pembuatan kunci dapat digunakan pada baris ini, dan kunci untuk tabel lain ditautkan ke tabel ini menggunakan batasan kunci asing.

Hal-hal menjadi sedikit lebih rumit untuk TPC. Pertama, penting untuk dipahami bahwa EF Core mengharuskan semua entitas dalam hierarki memiliki nilai kunci yang unik, bahkan jika entitas memiliki jenis yang berbeda. Misalnya, menggunakan model contoh kami, Anjing tidak dapat memiliki nilai kunci Id yang sama dengan Kucing. Kedua, tidak seperti TPT, tidak ada tabel umum yang dapat bertindak sebagai satu tempat di mana nilai kunci hidup dan dapat dihasilkan. Ini berarti kolom sederhana Identity tidak dapat digunakan.

Untuk database yang mendukung urutan, nilai kunci dapat dihasilkan dengan menggunakan urutan tunggal yang direferensikan dalam batasan default untuk setiap tabel. Ini adalah strategi yang digunakan dalam tabel TPC yang ditunjukkan di atas, di mana setiap tabel memiliki hal berikut:

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

AnimalSequence adalah urutan database yang dibuat oleh EF Core. Strategi ini digunakan secara default untuk hierarki TPC saat menggunakan penyedia database EF Core untuk SQL Server. Penyedia database untuk database lain yang mendukung urutan harus memiliki default yang sama. Strategi pembuatan kunci lainnya yang menggunakan urutan, seperti pola Hi-Lo, juga dapat digunakan dengan TPC.

Meskipun kolom Identitas standar tidak berfungsi dengan TPC, dimungkinkan untuk menggunakan kolom Identitas jika setiap tabel dikonfigurasi dengan benih dan kenaikan yang sesuai sehingga nilai yang dihasilkan untuk setiap tabel tidak akan pernah bertentangan. Contohnya:

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

Penting

Menggunakan strategi ini membuatnya lebih sulit untuk menambahkan jenis turunan nanti karena membutuhkan jumlah total jenis dalam hierarki untuk diketahui sebelumnya.

SQLite tidak mendukung urutan atau Seed/inkrement identitas, dan karenanya pembuatan nilai kunci bilangan bulat tidak didukung saat menggunakan SQLite dengan strategi TPC. Namun, pembuatan sisi klien atau kunci unik global - seperti GUID - didukung pada database apa pun, termasuk SQLite.

Batasan kunci asing

Strategi pemetaan TPC menciptakan skema SQL denormalisasi - ini adalah salah satu alasan mengapa beberapa purist database menentangnya. Misalnya, pertimbangkan kolom FavoriteAnimalIdkunci asing . Nilai dalam kolom ini harus cocok dengan nilai kunci utama beberapa hewan. Ini dapat diberlakukan dalam database dengan batasan FK sederhana saat menggunakan TPH atau TPT. Contohnya:

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

Tetapi ketika menggunakan TPC, kunci utama untuk hewan tertentu disimpan dalam tabel yang sesuai dengan jenis beton hewan itu. Misalnya, kunci utama kucing disimpan di Cats.Id kolom, sementara kunci utama anjing disimpan di Dogs.Id kolom, dan sebagainya. Ini berarti batasan FK tidak dapat dibuat untuk hubungan ini.

Dalam praktiknya, ini bukan masalah selama aplikasi tidak mencoba menyisipkan data yang tidak valid. Misalnya, jika semua data dimasukkan oleh EF Core dan menggunakan navigasi untuk menghubungkan entitas, maka dijamin bahwa kolom FK akan berisi nilai PK yang valid setiap saat.

Ringkasan dan panduan

Singkatnya, TPH biasanya baik-baik saja untuk sebagian besar aplikasi, dan merupakan default yang baik untuk berbagai skenario, jadi jangan tambahkan kompleksitas TPC jika Anda tidak membutuhkannya. Secara khusus, jika kode Anda sebagian besar akan mengkueri entitas dari banyak jenis, seperti menulis kueri terhadap jenis dasar, maka condong ke TPH melalui TPC.

Meskipun demikian, TPC juga merupakan strategi pemetaan yang baik untuk digunakan ketika kode Anda sebagian besar akan meminta entitas dari jenis daun tunggal dan tolok ukur Anda menunjukkan peningkatan dibandingkan dengan TPH.

Gunakan TPT hanya jika dibatasi untuk melakukannya oleh faktor eksternal.