Chiến lược kế thừa trong Entity Framework

Chúng ta đã tìm hiểu về các quy ước trong Code First mà Entity Framework (EF) sử dụng để tạo các bảng cơ sở dữ liệu cho từng lớp thực thể cụ thể. Tuy nhiên, bạn có thể thiết kế các lớp thực thể của mình bằng cách sử dụng tính kế thừa.

Trong lập trình hướng đối tượng, các lớp có mối quan hệ "has a" và "is a", trong khi  đó mô hình quan hệ dựa trên SQL chỉ có mối quan hệ "has a" giữa các bảng.

Hệ quản trị cơ sở dữ liệu SQL không hỗ trợ mối quan hệ "is a". Vì vậy, làm thế nào bạn có thể ánh xạ các lớp thực thể có mối quan hệ "is a" vào cơ sở dữ liệu quan hệ?

Dưới đây là ba cách tiếp cận khác nhau để ánh xạ thừa kế vào cơ sở dữ liệu trong Code First:

  • Table per Hierarchy (TPH): Cách tiếp cận này đề nghị tạo một bảng chung cho toàn bộ các lớp trong phân cấp kế thừa. Bảng này có một cột để phân biệt giữa các lớp con. Đây là một chiến lược ánh xạ kế thừa mặc định trong Entity Framework.
  • Table per Type (TPT): Cách tiếp cận này đề nghị tạo mỗi bảng cho từng lớp trong phân cấp kế thừa (tạo bảng cho cả lớp cha và lớp con).
  • Table per Concrete Class (TPC): Cách tiếp cận này đề nghị tạo mỗi bảng cho từng lớp con trong phân cấp kế thừa, nhưng không tạo bảng cho lớp cha. Vì vậy các thuộc tính của lớp cha sẽ là một phần của mỗi bảng của lớp con.

Chiến lược Table per Hierarchy (TPH)

TPH đề nghị tạo một bảng chung cho toàn bộ các lớp trong phân cấp kế thừa.

Bảng này có tất cả các cột cho thuộc tính của tất cả các lớp trong cây phân cấp.

Ngoài ra bảng này có một cột để phân biệt giữa các lớp con.

Đây là một chiến lược ánh xạ kế thừa mặc định trong Entity Framework.

Chiến lược ánh xạ này là người chiến thắng về cả hiệu suất và sự đơn giản.

Tất cả mọi thứ bạn cần đều nằm trong một bảng, bạn không cần phải join qua bảng khác để lấy dữ liệu.

Thay đổi lược đồ rất đơn giản.

public abstract class BillingDetail 
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }        
    public string Number { get; set; }
}
 
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
 
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }                
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
 
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
}

Cột Discriminator

Như bạn có thể thấy trong lược đồ DB ở trên, EF Code First phải thêm một cột đặc biệt để phân biệt giữa các lớp con, đó là cột Discriminator.

Cột Discriminator không phải là thuộc tính của các lớp con trong mô hình đối tượng của chúng ta, nó được sử dụng nội bộ bởi EF Code First.

Theo mặc định, tên cột là "Discriminator" và kiểu của nó là chuỗi. Các giá trị mặc định cho cột Discriminator là tên các lớp con - trong trường hợp này là “BankAccount” hoặc “CreditCard”.

EF Code First tự động thiết lập và truy xuất các giá trị của cột Discriminator.

Chiến lược Table per Type (TPT)

TPT đề nghị tạo mỗi bảng cho từng lớp trong phân cấp kế thừa (tạo bảng cho cả lớp cha và lớp con).

Bảng cho lớp cha chỉ chứa các cột cho các thuộc tính được định nghĩa trong lớp cha.

Bảng cho lớp con chỉ chưa các cột cho các thuộc tính được định nghĩa trong lớp con.

Bảng cho lớp cha và bảng cho lớp con có quan hệ một-một.

Cách tiếp cận này được thể hiện trong hình sau:

Ví dụ: nếu một thể hiện của lớp con CreditCard được lưu vào cơ sở dữ liệu thì:

  • Giá trị của các thuộc tính được khai báo trong lớp cơ sở BillingDetail được lưu trữ trong một dòng mới của bảng BillingDetails.
  • Giá trị của các thuộc tính được khai báo trong lớp con CreditCard được lưu trữ trong một dòng mới của bảng CreditCards.
  • Hai dòng này được liên kết với nhau bằng giá trị khóa chính được chia sẻ.

Sau đó, thể hiện của lớp con có thể được lấy từ cơ sở dữ liệu bằng cách nối bảng lớp con với bảng lớp cơ sở.

Ưu điểm của TPT

Ưu điểm chính của chiến lược này là lược đồ SQL được chuẩn hóa.

Ngoài ra, thay đổi lược đồ thì rất đơn giản (sửa đổi lớp cơ sở hoặc thêm một lớp con mới chỉ là vấn đề sửa đổi / thêm một bảng).

Định nghĩa ràng buộc toàn vẹn cũng rất đơn giản (lưu ý cách cột CardType trong bảng CreditCards bây giờ là một cột not null).

Triển khai TPT trong EF Code First

Chúng ta có thể tạo ánh xạ TPT đơn giản bằng cách đặt attribute Table trên các lớp con để chỉ định tên bảng được ánh xạ.

Lưu ý: Attribute Table là một chú thích dữ liệu mới và đã được thêm vào namespace System.ComponentModel.DataAnnotations.
public abstract class BillingDetail
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
}
 
[Table("BankAccounts")]
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
 
[Table("CreditCards")]
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
 
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
}

Nếu bạn thích sử dụng Fluent API thì bạn có thể tạo ánh xạ TPT bằng cách sử dụng phương thức ToTable() như sau:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<BankAccount>().ToTable("BankAccounts");
    modelBuilder.Entity<CreditCard>().ToTable("CreditCards");
}

Chiến lược Table per Concrete Class (TPC)

TPC đề nghị tạo mỗi bảng cho từng lớp con trong phân cấp kế thừa, nhưng không tạo bảng cho lớp cha.

Với cách tiếp cận này thi các thuộc tính được khai báo trong lớp cha sẽ được thêm vào mỗi bảng của lớp con.

Như bạn có thể thấy, lược đồ SQL không thể hiện sự kế thừa. Các bảng không có bất kỳ mối quan hệ nào với nhau trong cơ sở dữ liệu, chúng chỉ có một số cột giống nhau mà thôi.

Triển khai TPC trong EF Code First

Giống như việc triển khai TPT, chúng ta cần chỉ định một bảng riêng cho từng lớp con.

Chúng ta cũng cần nói với Code First rằng chúng ta muốn tất cả các thuộc tính của lớp cha được ánh xạ như một phần của bảng này.

Trong CTP5, phương thức trợ giúp MapInheritedProperies của lớp EntityMappingConfiguration sẽ thực hiện điều này cho chúng ta.

Dưới đây là mô hình đối tượng hoàn chỉnh cũng như Fluent API để tạo ánh xạ TPC:

public abstract class BillingDetail
{
    public int BillingDetailId { get; set; }
    public string Owner { get; set; }
    public string Number { get; set; }
}
        
public class BankAccount : BillingDetail
{
    public string BankName { get; set; }
    public string Swift { get; set; }
}
        
public class CreditCard : BillingDetail
{
    public int CardType { get; set; }
    public string ExpiryMonth { get; set; }
    public string ExpiryYear { get; set; }
}
    
public class InheritanceMappingContext : DbContext
{
    public DbSet<BillingDetail> BillingDetails { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<BankAccount>().Map(m =>
        {
            m.MapInheritedProperties();
            m.ToTable("BankAccounts");
        });
 
        modelBuilder.Entity<CreditCard>().Map(m =>
        {
            m.MapInheritedProperties();
            m.ToTable("CreditCards");
        });            
    }
}

Tầm quan trọng của lớp EntityMappingConfiguration

Lớp EntityMappingConfiguration hóa ra là chìa khóa để ánh xạ kế thừa trong Code First. Dưới đây là các phương thức của lớp này:

namespace System.Data.Entity.ModelConfiguration.Configuration.Mapping
{
    public class EntityMappingConfiguration<TEntityType> where TEntityType : class
    {
        public ValueConditionConfiguration Requires(string discriminator);
        public void ToTable(string tableName);
        public void MapInheritedProperties();
    }
}

Như bạn đã thấy ở phần trên, chúng tôi đã sử dụng phương thức Requires của nó để tùy chỉnh TPH. Chúng tôi cũng đã sử dụng phương thức ToTable của nó để tạo TPT và bây giờ chúng tôi đang sử dụng phương thức MapInheritedProperies cùng với phương thức ToTable để tạo ánh xạ TPC của chúng tôi.

Cấu hình các lớp trong Entity Framework

Chúng ta tìm hiểu các quy ước trong Code First trong phần trước. Code First xây dựng mô hình khái niệm từ các lớp thực thể của bạn bằng cách sử dụng các quy ước mặc định.

EF 6 Code First tận dụng một mẫu lập trình được gọi là quy ước về cấu hình. Tuy nhiên, bạn có thể ghi đè các quy ước này bằng cách cấu hình các lớp thực thể của bạn để cung cấp cho EF thông tin cần thiết.

Có hai cách để cấu hình các lớp thực thể của bạn:

  1. Attribute chú thích dữ liệu.
  2. Fluent API.

Attribute chú thích dữ liệu

Chú thích dữ liệu là một cấu hình dựa trên attribute đơn giản, bạn có thể áp dụng cho các lớp và thuộc tính của nó.

Các attribute này không chỉ dành riêng cho EF mà còn được sử dụng trong ASP.NET Web Form và ASP.NET MVC. Do đó, chúng được đặt trong một namespace riêng biệt là System.ComponentModel.DataAnnotations.

Ví dụ sau đây minh họa việc sử dụng một số attribute chú thích dữ liệu:

[Table("StudentInfo")]
public class Student
{
    public Student() { }
        
    [Key]
    public int SID { get; set; }

    [Column("Name", TypeName="ntext")]
    [MaxLength(20)]
    public string StudentName { get; set; }

    [NotMapped]
    public int? Age { get; set; }        
        
    public int StdId { get; set; }

    [ForeignKey("StdId")]
    public virtual Standard Standard { get; set; }
}
Lưu ý: Attribute chú thích dữ liệu không hỗ trợ tất cả các tùy chọn cấu hình cho Entity Framework. Vì vậy, bạn có thể sử dụng Fluent API, nó cung cấp tất cả các tùy chọn cấu hình cho EF.

Tìm hiểu chi tiết về attribute chú thích dữ liệu trong Entity Framework tại bài viết này:

Attribute chú thích dữ liệu trong Entity Framework | Comdy
Attribute chú thích dữ liệu trong Entity Framework là gì? Có những loại attribute chú thích dữ liệu nào?

Fluent API

Một cách khác để định cấu hình các lớp là bằng cách sử dụng Entity Framework Fluent API. Fluent API dựa trên mẫu thiết kế Fluent API (hay còn gọi là giao diện thông thạo) trong đó kết quả được tạo thành từ một chuỗi phương thức.

Cấu hình Fluent API có thể được áp dụng khi EF xây dựng mô hình từ các lớp thực thể của bạn. Bạn có thể thêm các cấu hình Fluent API bằng cách ghi đè phương thức OnModelCreating của lớp DbContext trong Entity Framework 6.x, như được trình bày bên dưới:

public class SchoolDBContext: DbContext 
{
    public SchoolDBContext(): base("SchoolDBConnectionString") 
    {
    }

    public DbSet<Student> Students { get; set; }
    public DbSet<Standard> Standards { get; set; }
    public DbSet<StudentAddress> StudentAddress { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //Configure domain classes using modelBuilder here..
    }
}

Bạn có thể sử dụng tham số modelBuilder, một đối tượng của lớp DbModelBuilder để cấu hình các lớp thực thể của bạn. DbModelBuilder được gọi là Fluent API vì bạn có thể gọi các phương thức khác nhau trong một chuỗi phương thức.

Tìm hiểu chi tiết về Fluent API trong Entity Framework tại bài viết này:

Fluent API trong Entity Framework | Comdy
Fluent API trong Entity Framework là gì? Làm sao để khai báo và sử dụng Fluent API trong Entity Framework?
Entity Framework
Bài Viết Liên Quan:
2 kịch bản lưu dữ liệu trong Entity Framework Core
Trung Nguyen 30/04/2020
2 kịch bản lưu dữ liệu trong Entity Framework Core

2 kịch bản lưu dữ liệu trong Entity Framework Core là kịch bản được kết nối và kịch bản ngắt kết nối.

Ứng dụng Entity Framework Core đầu tiên
Trung Nguyen 29/04/2020
Ứng dụng Entity Framework Core đầu tiên

Tạo ứng dụng .NET Core Console đầu tiên và cấu hình sử dụng Entity Framework Core.

Truy vấn trong Entity Framework Core
Trung Nguyen 29/04/2020
Truy vấn trong Entity Framework Core

Truy vấn trong Entity Framework Core có gì mới? Truy vấn trong EF Core khác EF ở những điểm nào.

Entity Framework Core toàn tập
Trung Nguyen 29/04/2020
Entity Framework Core toàn tập

Entity Framework Core toàn tập sẽ hướng dẫn bạn tất cả mọi thứ về Entity Framework Core.