Cấu hình mối quan hệ trong Entity Framework

Trong cơ sở dữ liệu quan hệ, mối quan hệ là một liên kết tồn tại giữa các bảng của cơ sở dữ liệu quan hệ thông qua các khóa ngoại.

Khóa ngoại (Foreign Key) là một cột hoặc tổ hợp các cột được sử dụng để thiết lập và thực thi một liên kết giữa dữ liệu trong hai bảng.

Có ba loại mối quan hệ giữa các bảng và chúng khác nhau tùy thuộc vào cách định nghĩa các cột liên quan.

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

Để hiểu rõ hơn về mối quan hệ giữa các thực thể trong Entity Framwork bạn có thể xem bài viết:

Mối quan hệ trong Entity Framework | Comdy
Mối quan hệ trong Entity Framework là gì? Có mấy loại mối quan hệ và cách triển khai trong Entity Framework?

Bài viết này sẽ hướng dẫn bạn cách cấu hình ba loại mối quan hệ trên trong Entity Framework Code First.

Cấu hình mối quan hệ một-một trong Entity Framework

Ở phần này, bạn sẽ tìm hiểu cách cấu hình mối quan hệ một-một giữa hai thực thể.

Chúng tôi sẽ thực hiện mối quan hệ một-một giữa thực thể StudentStudentAddress như sau:


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }

    public virtual StudentAddress Address { get; set; }
}
     
public class StudentAddress 
{
    public int StudentAddressId { get; set; }
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public int Zipcode { get; set; }
    public string State { get; set; }
    public string Country { get; set; }

    public virtual Student Student { get; set; }
}

Mối quan hệ một-một xảy ra khi khóa chính của một bảng trở thành khóa chính và khóa ngoại của một bảng khác trong cơ sở dữ liệu quan hệ như SQL Server.

Vì vậy, chúng ta cần cấu hình các thực thể ở trên để EF tạo các bảng Students và bảng StudentAddresses trong DB.

Nó sẽ tạo cột StudentId trong bảng Student là khóa chính và cột StudentAddressId trong bảng StudentAddresses vừa là khóa chính vừa là khóa ngoại.

Cấu hình mối quan hệ một-một bằng cách sử dụng các attribute chú thích dữ liệu

Trong trường hợp này, chúng tôi sẽ sử dụng các thuộc tính chú thích dữ liệu trên các thực thể StudentStudentAddress để thiết lập mối quan hệ một-một.

Nếu bạn bỏ lỡ bài viết hướng dẫn cách sử dụng attribute chú thích dữ liệu thì có thể xem tại đây:

Attribute chú thích dữ liệu trong Entity Framework (phần 1) | 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?

Thực thể Student tuân theo quy ước mặc định của Code First vì nó có thuộc tính StudentId sẽ là thuộc tính khóa.

Vì vậy, chúng ta không cần phải áp dụng bất kỳ attribute chú thích dữ liệu nào trên nó bởi vì EF sẽ tạo cột StudentId là khóa chính của bảng Students trong cơ sở dữ liệu.

Đối với thực thể StudentAddress, chúng ta cần cấu hình StudentAddressId vừa là khóa chính vừa là khóa ngoại.

Thuộc tính StudentAddressId theo quy ước mặc định sẽ là khóa chính. Vì vậy, chúng ta không cần phải áp dụng bất kỳ attribute chú thích dữ liệu nào cho khóa chính.

Tuy nhiên, chúng ta cũng cần cấu hình nó là khóa ngoại của thực thể Student.

Vì vậy, chúng ta sẽ khai báo [ForeignKey("Student")] trên thuộc tính StudentAddressId để cấu hình nó làm khóa ngoại cho thực thể Student, như được trình bày dưới đây.


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }

    public virtual StudentAddress Address { get; set; }
}
     
public class StudentAddress 
{
    [ForeignKey("Student")]
    public int StudentAddressId { get; set; }
        
    public string Address1 { get; set; }
    public string Address2 { get; set; }
    public string City { get; set; }
    public int Zipcode { get; set; }
    public string State { get; set; }
    public string Country { get; set; }

    public virtual Student Student { get; set; }
}

Như vậy là bạn đã sử dụng attribute chú thích dữ liệu để cấu hình mối quan hệ một-một giữa hai thực thể.

Lưu ý: Thực thể Student có thuộc tính điều hướng StudentAddress và thực thể StudentAddress có thuộc tính điều hướng Student. Với mối quan hệ một-một, một thực thể Student có thể được lưu trữ mà không có thực thể StudentAddress nhưng ngược lại thì không thể. EF sẽ đưa ra một ngoại lệ nếu bạn cố lưu thực thể StudentAddress mà không có thực thể Student.

Cấu hình mối quan hệ một-một bằng cách sử dụng Fluent API

Ở phần này, chúng tôi sẽ sử dụng Fluent API để định cấu hình mối quan hệ một-một giữa các thực thể StudentStudentAddress.

Nếu bạn bỏ lỡ bài viết hướng dẫn cách sử dụng Fluent API thì có thể xem ở đâ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?

Ví dụ sau đây thiết lập mối quan hệ một-một giữa StudentStudentAddress sử dụng Fluent API.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // Configure Student & StudentAddress entity
    modelBuilder.Entity<Student>()
                .HasOptional(s => s.Address) // Mark Address property optional in Student entity
                .WithRequired(ad => ad.Student); // mark Student property as required in StudentAddress entity. Cannot save StudentAddress without Student
}

Trong ví dụ trên, chúng ta bắt đầu với thực thể Student. Phương thức HasOptional() cấu hình thuộc tính điều hướng Address của thực thể Student là tùy chọn (không bắt buộc khi lưu thực thể Student).

Sau đó, phương thức WithRequired() cấu hình thuộc tính điều hướng Student của thực thể StudentAddress là bắt buộc (bắt buộc phải có khi lưu thực thể StudentAddress; nó sẽ đưa ra một ngoại lệ khi thực thể StudentAddress được lưu mà không có thuộc tính điều hướng Student). Điều này cũng sẽ làm cho cột StudentAddressId trở thành khóa ngoại.

Như vậy là bạn đã biết cách cấu hình mối quan hệ một-một giữa hai thực thể bằng cách sử dụng Fluent API.

EF API sẽ tạo các bảng sau trong cơ sở dữ liệu.

Cấu hình mối quan hệ một-một trong Entity Framework Code First

Ở cách cấu hình trên, một thực thể Student có thể được lưu mà không có thực thể StudentAddress nhưng ngược lại thì không thể. EF sẽ đưa ra một ngoại lệ nếu bạn cố lưu thực thể StudentAddress mà không có thực thể Student.

Tuy nhiên chúng ta có thể cấu hình mối quan hệ một-một giữa các thực thể bằng Fluent API trong đó cả hai đầu đều bắt buộc.

Nghĩa là khi lưu một đối tượng thực thể Student bắt buộc phải có đối tượng thực thể StudentAddress và ngược lại.


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    // Configure StudentId as FK for StudentAddress
    modelBuilder.Entity<Student>()
                .HasRequired(s => s.Address) 
                .WithRequiredPrincipal(ad => ad.Student); 

}

Trong ví dụ trên, phương thức HasRequired(s => s.Address) cấu hình thuộc tính Address của thực thể StudentAddress là bắt buộc và phương thức WithRequiredPrincipal(ad => ad.Student) ấu hình thuộc tính Student của thực thể StudentAddress là bắt buộc.

Do đó, khi bạn cố gắng lưu thực thể Student mà không có thực thể StudentAddress hoặc ngược lại thì EF sẽ ném ra một ngoại lệ.

Cấu hình mối quan hệ nhiều-nhiều trong Entity Framework

Ở phần này, chúng ta sẽ tìm hiểu cách cấu hình mối quan hệ nhiều-nhiều giữa các lớp thực thể StudentCourse. Một sinh viên có thể tham gia nhiều khóa học và một khóa học có nhiều sinh viên.

Cấu hình mối quan hệ nhiều-nhiều bằng cách tuân theo quy ước mặc định của Code First

EF 6 có các quy ước mặc định cho mối quan hệ nhiều-nhiều. Bạn cần có một thuộc tính điều hướng kiểu tập hợp ở cả hai đầu.

Ví dụ, lớp Student có thuộc tính điều hướng kiểu ICollection<Course> và lớp Course có thuộc tính điều hướng kiểu ICollection<Student> để tạo mối quan hệ nhiều-nhiều giữa chúng mà không cần bất kỳ cấu hình nào, như được trình bày bên dưới:


public class Student
{
    public Student() 
    {
        this.Courses = new HashSet<Course>();
    }

    public int StudentId { get; set; }
    [Required]
    public string StudentName { get; set; }

    public virtual ICollection<Course> Courses { get; set; }
}
        
public class Course
{
    public Course()
    {
        this.Students = new HashSet<Student>();
    }

    public int CourseId { get; set; }
    public string CourseName { get; set; }

    public virtual ICollection<Student> Students { get; set; }
}

Sau đây là lớp Context bao gồm các thực thể StudentCourse.


public class SchoolDBContext : DBContext
{
    public SchoolDBContext() : base("SchoolDB-DataAnnotations")
    {
    }

    public DbSet<Student> Students { get; set; }
    public DbSet<Course> Courses { get; set; }
        
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
    }
}

EF API sẽ tạo ra ba bảng là Students, Courses và bảng tham gia tên là StudentCourses trong cơ sở dữ liệu. Bảng StudentCourses sẽ bao gồm khóa chính của cả hai bảng StudentsCourses - đó là Student_StudentIdCourse_CourseId như hình dưới đây.

Cấu hình mối quan hệ nhiều-nhiều trong Entity Framework Code First
Lưu ý: EF tự động tạo bảng tham gia với tên của cả hai thực thể và hậu tố 's'.

Cấu hình mối quan hệ nhiều-nhiều bằng Fluent API

Như bạn đã thấy ở trên, các quy ước mặc định của Code First cho mối quan hệ nhiều-nhiều sẽ tạo ra bảng tham gia với các quy ước đặt tên mặc định.

Sử dụng Fluent API để tùy chỉnh tên bảng và tên cột tham gia, như được trình bày bên dưới:


protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
                .HasMany<Course>(s => s.Courses)
                .WithMany(c => c.Students)
                .Map(cs =>
                        {
                            cs.MapLeftKey("StudentRefId");
                            cs.MapRightKey("CourseRefId");
                            cs.ToTable("StudentCourse");
                        });
}

Trong ví dụ trên, các phương thức HasMany()WithMany() được sử dụng để cấu hình mối quan hệ nhiều-nhiều giữa các thực thể StudentCourse.

Phương thức Map() nhận tham số kiểu delegate Action, do đó chúng ta có thể truyền các biểu thức lambda để tùy chỉnh tên cột trong bảng tham gia.

Chúng ta có thể chỉ định tên thuộc tính khóa chính của bảng Student trong phương thức MapLeftKey() (chúng ta đã bắt đầu với thực thể Student, vì vậy nó sẽ là bảng bên trái) và khóa chính của bảng Course trong phương thức MapRightKey().

Phương thức ToTable() chỉ định tên của bảng tham gia (trong trường hợp này là StudentCourse).

Đoạn mã trên sẽ tạo bảng tham gia StudentCourse với hai khóa chính là StudentRefId, CourseRefId và chúng cũng sẽ là khóa ngoại, như được hiển thị bên dưới:

Cấu hình mối quan hệ nhiều-nhiều trong Entity Framework Code First

Theo cách này, bạn có thể ghi đè các quy ước mặc định của Code First cho mối quan hệ nhiều-nhiều và tùy chỉnh tên bảng tham gia và các cột của nó.

Cấu hình mối quan hệ một-nhiều trong Entity Framework

Ở phần này, chúng ta sẽ tìm hiểu cách cấu hình mối quan hệ một-nhiều giữa hai thực thể trong Entity Framework 6.x bằng cách sử dụng phương pháp tiếp cận Code First.

Chúng ta sẽ cấu hình mối quan hệ một-nhiều giữa các thực thể StudentGrade - một lớp có nhiều sinh viên.


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
}
       
public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }
}

Các quy ước cho mối quan hệ một-nhiều

Có một số quy ước nhất định trong Entity Framework mà nếu các lớp thực thể tuân theo sẽ tự động dẫn đến mối quan hệ một-nhiều giữa hai bảng trong cơ sở dữ liệu. Bạn không cần phải cấu hình bất cứ điều gì khác.

Hãy xem một ví dụ về các quy ước tạo ra mối quan hệ một-nhiều trong Entity Framework Code First.

Quy ước 1:

Chúng tôi muốn thiết lập mối quan hệ một-nhiều giữa các thực thể StudentGrade. Có nghĩa là mỗi thực thể Student sẽ trỏ đến một thực thể Grade.

Điều này có thể thực hiện bằng cách tạo một thuộc tính điều hướng tham chiếu kiểu Grade trong lớp thực thể Student, như ví dụ bên dưới.


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Grade Grade { get; set; }
}

public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }
}

Trong ví dụ trên, lớp Student có một thuộc tính điều hướng tham chiếu của lớp Grade. Vì vậy, có thể có nhiều học sinh trong một lớp.

Điều này sẽ dẫn đến mối quan hệ một-nhiều giữa bảng Students và bảng Grades trong cơ sở dữ liệu, trong đó bảng Students có khóa ngoại Grade_GradeId như hình bên dưới.

Cấu hình mối quan hệ một-nhiều trong Entity Framework Code First

Lưu ý rằng thuộc tính tham chiếu là nullable, vì vậy nó tạo ra một cột khóa ngoại  Grade_GradeId có thể null trong bảng Students.

Quy ước 2:

Có một thuộc tính điều hướng kiểu tập hợp trong thực thể chính như dưới đây.


public class Student
{
    public int StudentId { get; set; }
    public string StudentName { get; set; }
}

public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }

    public ICollection<Student> Students { get; set; } 
}

Trong ví dụ trên, thực thể Grade có một thuộc tính điều hướng tập hợp kiểu ICollection<Student>.

Điều này cũng dẫn đến mối quan hệ một-nhiều giữa các thực thể StudentGrade. Ví dụ này tạo ra kết quả tương tự trong cơ sở dữ liệu như quy ước 1.

Quy ước 3:

Có các thuộc tính điều hướng ở cả hai đầu cũng sẽ dẫn đến mối quan hệ một-nhiều, như được trình bày bên dưới.


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Grade Grade { get; set; }
}

public class Grade
{
    public int GradeID { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }
    
    public ICollection<Student> Student { get; set; }
}

Trong ví dụ trên, lớp thực thể Student có một thuộc tính điều hướng tham chiếu kiểu Grade và lớp thực thể Grade có một thuộc tính điều hướng tập hợp kiểu ICollection<Student> dẫn đến mối quan hệ một-nhiều. Ví dụ này tạo ra kết quả tương tự trong cơ sở dữ liệu như quy ước 1.

Quy ước 4:

Một mối quan hệ được chỉ định đầy đủ ở cả hai đầu sẽ tạo ra mối quan hệ một-nhiều, như hình dưới đây.


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public int GradeId { get; set; }
    public Grade Grade { get; set; }
}

public class Grade
{

    public int GradeId { get; set; }
    public string GradeName { get; set; }
    
    public ICollection<Student> Student { get; set; }
}

Trong ví dụ trên, lớp thực thể Student có thuộc tính khóa ngoại GradeId và thuộc tính tham chiếu của nó là Grade. Điều này sẽ tạo mối quan hệ một-nhiều với cột khóa ngoại NotNull trong bảng Students, như được hiển thị bên dưới.

Cấu hình mối quan hệ một-nhiều trong Entity Framework Code First

Nếu kiểu dữ liệu của GradeId là số nguyên nullable, thì nó sẽ tạo khóa ngoại null.


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public int? GradeId { get; set; }
    public Grade Grade { get; set; }
}

Đoạn mã trên sẽ tạo một cột GradeId có thể null trong cơ sở dữ liệu vì chúng tôi đã sử dụng kiểu Nullable<int> (int? là viết tắt của Nullable<int>)

Cấu hình mối quan hệ một-nhiều bằng Fluent API

Nói chung, bạn không cần định cấu hình mối quan hệ một-nhiều trong Entity Framework vì các quy ước ở trên sẽ giúp bạn làm điều này.

Tuy nhiên, bạn có thể định cấu hình các mối quan hệ bằng Fluent API tại một nơi để làm cho nó dễ bảo trì hơn.

Hãy xem các lớp thực thể StudentGrade sau.


public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }

    public int CurrentGradeId { get; set; }
    public Grade CurrentGrade { get; set; }
}

public class Grade
{
    public int GradeId { get; set; }
    public string GradeName { get; set; }
    public string Section { get; set; }

    public ICollection<Student> Students { get; set; }
}

Bạn có thể cấu hình mối quan hệ một-nhiều cho các thực thể ở trên bằng Fluent API bằng cách ghi đè phương thức OnModelCreating trong lớp Context, như ví dụ bên dưới.


public class SchoolContext : DbContext
{
    public DbSet<Student> Students { get; set; }
    public DbSet<Grade> Grades { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        // configures one-to-many relationship
        modelBuilder.Entity<Student>()
            .HasRequired<Grade>(s => s.CurrentGrade)
            .WithMany(g => g.Students)
            .HasForeignKey<int>(s => s.CurrentGradeId);
    }
}

Chúng ta hãy hiểu mã trên từng bước.

  • Đầu tiên chúng ta cần chỉ định cấu hình cho lớp thực thể nào bằng khai báo modelBuilder.Entity<student>() - cấu hình cho thực thể Student.
  • Khai báo .HasRequired<Grade>(s => s.CurrentGrade) chỉ định rằng lớp thực thể Student yêu cầu thuộc tính CurrentGrade. Điều này sẽ tạo một cột khóa ngoài NotNull trong DB.
  • Khai báo .WithMany(g => g.Students) chỉ định rằng lớp thực thể Grade có nhiều thực thể Student.
  • Bây giờ, nếu thực thể Student không tuân theo quy ước thuộc tính Id cho khóa ngoại, thì chúng ta có thể chỉ định tên của khóa ngoại bằng phương thức HasForeignKey. Khai báo .HasForeignKey<int>(s => s.CurrentGradeId) chỉ định thuộc tính khóa ngoại trong thực thể Student.

Ngoài ra, bạn cũng có thể cấu hình mối quan hệ bắt đầu với thực thể Grade thay vì thực thể Student như ở ví dụ trên. Ví dụ sau đây tạo ra kết quả tương tự như trên.


modelBuilder.Entity<Grade>()
    .HasMany<Student>(g => g.Students)
    .WithRequired(s => s.CurrentGrade)
    .HasForeignKey<int>(s => s.CurrentGradeId);
    

Ví dụ trên sẽ tạo các bảng sau trong cơ sở dữ liệu.

Cấu hình mối quan hệ một-nhiều trong Entity Framework Code First

Cấu hình khóa ngoại NotNull bằng Fluent API

Trong quy ước 1, chúng ta đã thấy rằng nó tạo ra một mối quan hệ một-nhiều tùy chọn, từ đó tạo ra một cột khóa ngoại có thể Null trong cơ sở dữ liệu.

Để biến nó thành cột NotNull, hãy sử dụng phương thức HasRequired() như bên dưới.


modelBuilder.Entity<Student>()
    .HasRequired<Grade>(s => s.CurrentGrade)
    .WithMany(g => g.Students);
    

Cấu hình Cascade Delete bằng Fluent API

Cascade Delete có nghĩa là tự động xóa các các bản ghi con liên quan khi bản ghi cha bị xóa. Ví dụ, nếu lớp bị xóa thì tất cả các sinh viên trong lớp đó cũng sẽ bị xóa tự động. Ví dụ sau cấu hình Cascade Delete bằng phương thức WillCascadeOnDelete.


modelBuilder.Entity<Grade>()
    .HasMany<Student>(g => g.Students)
    .WithRequired(s => s.CurrentGrade)
    .WillCascadeOnDelete();
    
Lưu ý: Chúng tôi khuyến nghị bạn không nên sử dụng cascade delete để tránh gặp phải vấn đề mất mát dữ liệu ngoài ý muốn.


Bài viết liên quan:

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.

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 có gì mới? Truy vấn trong EF Core khác EF ở những điểm nào.