Mô hình hóa các mối quan hệ SQL trong EF Core

Object Relational Mappers (ORM) cho phép chúng ta định nghĩa mối quan hệ giữa các thực thể trong cơ sở dữ liệu quan hệ của chúng ta bằng cách sử dụng các đối tượng C#.

Tùy thuộc vào ORM mà chúng ta sử dụng, chúng ta cũng có thể lấy thư viện để tạo lược đồ của các mối quan hệ. Entity Framework Core là một trong những công cụ có mô hình di chuyển SQL tích hợp sẵn.

Bài viết này sẽ khám phá hầu hết các mối quan hệ được tìm thấy trong cơ sở dữ liệu quan hệ và cách chúng ta sẽ mô hình hóa các mối quan hệ đó bằng cách tiếp cận Code First.

Chúng ta sẽ sử dụng SQLite, một nhà cung cấp cơ sở dữ liệu dựa trên tệp cục bộ, nhưng nhiều kỹ thuật mô hình hóa này sẽ hoạt động trên hầu hết các cơ sở dữ liệu quan hệ và có khả năng mang lại một lược đồ tương tự.

Chúng ta sẽ giữ các định nghĩa thực thể của mình ở mức tối thiểu để chúng ta có thể thấy những gì cần thiết khi mô hình hóa loại mối quan hệ. Những định nghĩa này là điểm khởi đầu, vì vậy các nhà phát triển nên thoải mái thử nghiệm và biến chúng thành của riêng mình.

Mối quan hệ độc lập

Kiểu quan hệ đầu tiên là kiểu độc lập. Trong trường hợp này, định nghĩa thực thể không có kết nối với bất kỳ thực thể nào khác trong ngữ cảnh cơ sở dữ liệu của chúng ta. Chúng dễ thiết kế và thường yêu cầu một cột định danh duy nhất.

public class NotRelated
{
    public int Id { get; set; }
}

Trong DbContext của chúng ta, chúng ta cần định nghĩa thuộc tính DbSet.

public DbSet<NotRelated> NotRelateds { get; set; }

Mối quan hệ một-một

Khi định nghĩa mối quan hệ một-một, chúng ta vẫn cần chỉ định thực thể nào trong kết nối trước. Đặc tả cho phép EF Core chèn một dòng và sau đó thực hiện các truy vấn bổ sung để liên kết hai thực thể với nhau. Trước tiên, hãy định nghĩa mô hình một-một của chúng ta.

public class OneToOneLeft
{
    public int Id { get; set; }
    public int OneToOneRightId { get; set; }
    public OneToOneRight Right { get; set; }
}

public class OneToOneRight
{
    public int Id { get; set; }
    public int OneToOneLeftId { get; set; }
    public OneToOneLeft Left { get; set; }
}

Chúng ta sẽ nhận thấy rằng cả hai thực thể đều có tham chiếu đến thực thể quan hệ một-một của chúng. Trong trường hợp này, chúng ta thấy OneToOneLeft có một tham chiếu đến OneToOneRight và ngược lại. Để định nghĩa thứ tự, chúng ta sẽ cần thêm một số mã bổ sung vào phương thức OnModelCreating của chúng ta trong DbContext.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    // 1 to 1 (bidirectional)
    modelBuilder.Entity<OneToOneLeft>()
        .HasOne<OneToOneRight>()
        .WithOne(r => r.Left)
        .HasForeignKey<OneToOneRight>(r => r.Id);
    
    base.OnModelCreating(modelBuilder);
}

Cuối cùng, chúng ta có thể thêm cả hai thực thể DbSet vào DbContext của chúng ta.

// 1 to 1 (bidirectional)
public DbSet<OneToOneLeft> OneToOneLefts { get; set; }
public DbSet<OneToOneRight> OneToOneRights { get; set; }

Mối quan hệ sở hữu một-một

EF Core có một khái niệm về các thực thể được sở hữu. Thực thể được sở hữu là một thực thể mà chúng ta chỉ có thể truy cập thông qua mối quan hệ mẹ của nó. Ví dụ, lấy mối quan hệ giữa OneToOneOwner OneToOneOwned.

public class OneToOneOwner
{
    public int Id { get; set; }
    public OneToOneOwned Owned { get; set; } 
}

[Owned]
public class OneToOneOwned
{
    public string Value { get; set; }
}

Như chúng ta có thể nhận thấy, cột OneToOneOwned không có cột định danh. Theo mặc định, EF Core sẽ lưu trữ các giá trị sở hữu một-một trong cùng một bảng với thực thể sở hữu. Việc tối ưu hóa nâng cao hiệu suất truy vấn trong khi vẫn duy trì ý định lập mô hình C#. Hãy xem lược đồ SQL được tạo cho bảng OneToOneOwners của chúng ta.

create table OneToOneOwners
(
   Id INTEGER not null
      constraint PK_OneToOneOwners
         primary key autoincrement,
   Owned_Value TEXT
);

EF Core có khả năng tách bảng nếu chúng ta vẫn muốn lưu trữ dữ liệu thuộc sở hữu của thực thể trong một bảng riêng biệt, nhưng chúng ta sẽ không đi sâu vào điều đó cho bài viết này. Để thêm mối quan hệ sở hữu một-một vào DbContext, chúng ta cần thêm DbSet của thực thể mẹ, chính là OneToOneOwner.

// 1 to 1 (owned)
public DbSet<OneToOneOwner> OneToOneOwners { get; set; }

Mối quan hệ một-nhiều

Mối quan hệ một-nhiều là một trong những mối quan hệ linh hoạt hơn khi xử lý mô hình cơ sở dữ liệu. Với EF Core, chúng ta có thể sử dụng hầu hết các kiểu tập hợp được tìm thấy trong C#, nhưng tôi thường khuyên bạn nên sử dụng List<T> vì nó có một trong những cách triển khai interface hữu ích hơn với các phương thức như Add AddRange.

Hãy bắt đầu bằng cách xác định thực thể mẹ của chúng ta là OneToMany và các thực thể con của nó là OneToManyItem.

public class OneToMany
{
    public int Id { get; set; }
    public List<OneToManyItem> Items { get; set; } 
}

public class OneToManyItem
{
    public int Id { get; set; }
    public int OneToManyId { get; set; }
    public OneToMany OneToMany { get; set; }
}

Như bạn có thể thấy, thuộc tính Items trong OneToMany List<OneToManyItem>. Chúng ta cũng có thể thấy rằng OneToManyItem có thuộc tính điều hướng quay trở lại thực thể mẹ. Thuộc tính điều hướng tới thực thể mẹ không phải lúc nào cũng cần thiết, nhưng nó có thể hữu ích khi viết các truy vấn LINQ.

Cuối cùng, chúng ta có thể thêm cả hai thực thể vào DbContext của mình. Không phải lúc nào cũng cần thêm thực thể con làm thuộc tính DbSet, nhưng một lần nữa, nó có thể giúp chúng ta khi xây dựng các truy vấn LINQ.

// 1 to Many
public DbSet<OneToMany> OneToManys { get; set; }
public DbSet<OneToManyItem> OneToManyItems { get; set; }

Mối quan hệ sở hữu một-nhiều

Không giống như mối quan hệ sở hữu một-một, quan hệ sở hữu một-nhiều sẽ tạo ra các bảng riêng biệt. Trong trường hợp của chúng ta, chúng ta có các thực thể OneToManyOwner OneToManyOwnedItem.

public class OneToManyOwner
{
    public int Id { get; set; }
    public List<OneToManyOwnedItem> Items { get; set; }
}

[Owned]
public class OneToManyOwnedItem
{
    public string Name { get; set; }
}

Mặc dù chúng ta không xác định thuộc tính Id trên thực thể OneToManyOwnedItem của mình, nhưng EF Core sẽ tạo thứ được gọi là thuộc tính bóng (shadow property) cho khóa chính Id. Chúng ta có thể thấy điều này trong lược đồ SQL đã tạo.

create table OneToManyOwnedItem
(
   OneToManyOwnerId INTEGER not null
      constraint FK_OneToManyOwnedItem_OneToManyOwners_OneToManyOwnerId
         references OneToManyOwners
            on delete cascade,
   Id INTEGER not null,
   Name TEXT,
   constraint PK_OneToManyOwnedItem
      primary key (OneToManyOwnerId, Id)
);

Để thêm thực thể OneToManyOwner vào DbContext, chúng ta chỉ cần thêm một thuộc tính DbSet.

// 1 to Many (owned)
public DbSet<OneToManyOwner> OneToManyOwners { get; set; }

Mối quan hệ nhiều-nhiều ẩn

Các nhà phát triển từ Entity Framework 6 Code-First sẽ quen thuộc với mối quan hệ nhiều-nhiều ẩn. EF Core quản lý bảng mối quan hệ kết nối hai thực thể, trừu tượng hóa nó khỏi nhà phát triển. Mối quan hệ nhiều-nhiều ẩn chỉ có sẵn kể từ EF Core 5. Chúng ta hãy xem xét hai thực thể ManyToManyLeft ManyToManyRight.

public class ManyToManyLeft
{
    public int Id { get; set; }
    public List<ManyToManyRight> Rights { get; set; }
}

public class ManyToManyRight
{
    public int Id { get; set; }
    public List<ManyToManyLeft> Lefts { get; set; }
}

Như chúng ta có thể thấy, cả hai thực thể đều có một tập hợp các thực thể kia. Trong trường hợp này, EF Core quản lý một bảng ẩn ManyToManyLeftManyToManyRight trong lược đồ cơ sở dữ liệu của chúng ta.

create table ManyToManyLeftManyToManyRight
(
   LeftsId INTEGER not null
      constraint FK_ManyToManyLeftManyToManyRight_ManyToManyLefts_LeftsId
         references ManyToManyLefts
            on delete cascade,
   RightsId INTEGER not null
      constraint FK_ManyToManyLeftManyToManyRight_ManyToManyRights_RightsId
         references ManyToManyRights
            on delete cascade,
   constraint PK_ManyToManyLeftManyToManyRight
      primary key (LeftsId, RightsId)
);

create index IX_ManyToManyLeftManyToManyRight_RightsId
   on ManyToManyLeftManyToManyRight (RightsId);

Mối quan hệ nhiều-nhiều ẩn là lý tưởng cho các tình huống trong đó kết nối giữa hai thực thể là vấn đề của thực tế, có nghĩa là bản thân mối quan hệ không có thuộc tính phân biệt. Như chúng ta sẽ thấy trong phần tiếp theo, chúng ta cũng có thể lập mô hình nhiều-nhiều với mối quan hệ được thực hiện như một thực thể.

Để hoàn thành mô hình, chúng ta chỉ cần thêm cả hai thực thể kết nối vào DbContext của chúng ta.

// Many To Many (Transparent)
public DbSet<ManyToManyLeft> ManyToManyLefts { get; set; }
public DbSet<ManyToManyRight> ManyToManyRights { get; set; }

Mối quan hệ nhiều-nhiều được mô hình hóa

Mô hình hóa mối quan hệ nhiều-nhiều với một thực thể kết nối rất hữu ích cho các tình huống mà bản thân mối quan hệ có các thuộc tính xác định. Một ví dụ có thể là giữa một cá nhân và một ngôi nhà, trong đó cá nhân có thể sở hữu, cho thuê để sở hữu hoặc cho thuê.

Hãy xem xét một ví dụ với các đối tượng ManyToManyWithModeledLeft, ManyToManyWithModeledRight ManyToManyRelationship sau đây.

public class ManyToManyWithModeledLeft
{
    public int Id { get; set; }
    public ManyToManyRelationship Relationship { get; set; }
}

public class ManyToManyWithModeledRight
{
    public int Id { get; set; }
    public ManyToManyRelationship Relationship { get; set; }
}

public class ManyToManyRelationship
{
    public int Id { get; set; }
    public List<ManyToManyWithModeledLeft> Lefts { get; set; }
    public List<ManyToManyWithModeledRight> Rights { get; set; }
}

Như chúng ta có thể nhận thấy, mô hình nhiều-nhiều là sự kết hợp của các mối quan hệ một-một và một-nhiều. Các thực thể ManyToManyWithModeledLeft ManyToManyWithModeledRight có mối quan hệ một-một với thực thể ManyToManyRelationship. Thực thể ManyToManyRelationship có hai tập hợp điều hướng cho cả hai thực thể ManyToManyWithModeledLeft ManyToManyWithModeledRight.

Lợi thế của cách tiếp cận này là cách chúng ta có thể đính kèm dữ liệu bổ sung vào mối quan hệ, thay vì để EF Core tự thêm dữ liệu vào bảng ẩn trong kịch bản nhiều-nhiều ẩn.

Chúng ta sẽ cần thêm tất cả các mối quan hệ này vào DbContext của chúng ta.

// Many To Many (Modeled Relationship)
public DbSet<ManyToManyWithModeledLeft> ManyToManyWithModeledLefts { get; set; }
public DbSet<ManyToManyWithModeledRight> ManyToManyWithModeledRights { get; set; }
public DbSet<ManyToManyRelationship> ManyToManyRelationships { get; set; }

Mối quan hệ phân cấp

Mối quan hệ phân cấp là khi các dòng trong cùng một bảng tham chiếu đến một dòng khác, thường là trong mối quan hệ cha/con. Hãy xem cách mô hình hóa loại mối quan hệ này.

public class Hierarchical
{
    public int Id { get; set; }
    public int ParentId { get; set; }
    public Hierarchical Parent { get; set; }
    public List<Hierarchical> Children { get; set; }
}

Chúng ta có thể thấy rằng chúng ta có thuộc tính điều hướng của thuộc tính Parent và thuộc tính điều hướng tập hợp của Children. Mặc dù mối quan hệ này dễ mô hình hóa, nhưng việc truy vấn bằng LINQ tương đối khó khăn. Sử dụng mẫu mô hình này một cách hết sức thận trọng, vì các truy vấn phân cấp có thể tốn kém để thực thi ngay cả khi sử dụng các cấu trúc SQL thô.

Chúng ta có thể thấy lược đồ SQL khi tạo di chuyển EF Core.

create table Hierarchicals
(
   Id INTEGER not null
      constraint PK_Hierarchicals
         primary key autoincrement,
   ParentId INTEGER not null
      constraint FK_Hierarchicals_Hierarchicals_ParentId
         references Hierarchicals
            on delete cascade
);

create index IX_Hierarchicals_ParentId
   on Hierarchicals (ParentId);

Để thêm mối quan hệ này vào DbContext, chúng ta chỉ cần thêm thực thể Hierarchial của mình dưới dạng một thuộc tính DbSet duy nhất.

// Hierarchical
public DbSet<Hierarchical> Hierarchicals { get; set; }

Phần kết luận

Chúng ta đã khám phá các mối quan hệ độc lập, một-nhiều, nhiều-nhiều và phân cấp trong EF Core.

Mặc dù có nhiều hoán vị của từng loại mối quan hệ, nhưng bài viết này sẽ giúp người mới bắt đầu có một điểm khởi đầu để xác định mối quan hệ và phục vụ như một bài viết tham khảo cho ngay cả những nhà phát triển EF Core có kinh nghiệm nhất.

Nếu Comdy hữu ích và giúp bạn tiết kiệm thời gian làm việc

Bạn có thể vui lòng đưa Comdy vào whitelist của trình chặn quảng cáo ❤️ để hỗ trợ chúng tôi trong việc trả tiền cho dịch vụ lưu trữ web để duy trì hoạt động của trang web.

Entity Framework Core
Bài Viết Liên Quan:
Bộ chuyển đổi giá trị của Entity Framework Core 5
Trung Nguyen 12/11/2021
Bộ chuyển đổi giá trị của Entity Framework Core 5

Bài viết này sẽ giúp bạn tìm hiểu về bộ chuyển đổi giá trị - một trong những cải tiến mạnh mẽ của Entity Framework Core 5.

Truy vấn SQL thô với Entity Framework Core 5
Trung Nguyen 11/11/2021
Truy vấn SQL thô với Entity Framework Core 5

Trong bài viết này, chúng ta sẽ khám phá cách làm việc với các tính năng của Entity Framework Core 5 (EF Core) để thực thi truy vấn SQL thô.

Cách thêm view vào DbContext trong Entity Framework Core
Trung Nguyen 07/11/2021
Cách thêm view vào DbContext trong Entity Framework Core

Với EF Core 5, chúng ta có thể thêm view vào trong DbContext và tạo view trong database bằng cách sử dụng cơ chế chuyển đổi cơ sở dữ liệu tích hợp sẵn.

Dữ liệu đệ quy với Entity Framework Core và SQL Server
Trung Nguyen 01/11/2021
Dữ liệu đệ quy với Entity Framework Core và SQL Server

Trong bài viết này, chúng ta sẽ khám phá cách lập mô hình dữ liệu đệ quy với Entity Framework Core và SQL Server