Quan hệ nhiều – nhiều trong Entity Framework

    0

    Trong bài học này bạn sẽ học cách thức cấu hình quan hệ nhiều – nhiều trong Entity Framework code-first. Bạn có thể sử dụng quy ước (convention) hoặc fluent API. Như bạn sẽ học trong bài này, sử dụng quy ước là cách thức đơn giản và tự nhiên nhất để cấu hình quan hệ nhiều – nhiều. Tuy nhiên, nếu bạn không muốn tuân thủ quy ước vì lý do nào đó, bạn có thể sử dụng fluent API. Entity Framework hỗ trợ tự động tạo quan hệ nhiều – nhiều giúp bạn.

    Quay trở lại Tự học lập trình ADO.NET và Entity Framework

    Quan hệ nhiều – nhiều

    Quan hệ nhiều – nhiều bạn có thể gặp trong rất nhiều tình huống.

    Lấy ví dụ một người có thể làm cho nhiều công ty và một công ty có thể thuê nhiều người lao động. Nếu bạn trừu tượng hóa “người” và “công ty” thành các class tương ứng Person và Company, giữa chúng sẽ có quan hệ nhiều – nhiều: một object của Person chứa một danh sách object của Company, object của Company có thể chứa một danh sách Person.

    Ở mức độ CSDL, quan hệ nhiều – nhiều được tạo ra thông qua một bảng trung gian, thường gọi là bảng junction hoặc cross reference. Bảng trung gian này “ghép đôi” khóa của các bản ghi từ hai bảng chính. Một cặp ghép này tạo ra một quan hệ giữa hai bản ghi trên hai bảng chính. Như vậy, bảng phụ này cho phép một bản ghi trên bảng chính 1 “ghép cặp” với nhiều bản ghi trên bảng chính 2 và ngược lại.

    Bạn cũng có thể nhìn nhận theo cách khác: bảng phụ có quan hệ nhiều – một đồng thời với cả hai bảng chính (một record của bảng chính có nhiều record liên quan ở bảng phụ)!

    Có hai tình huống xảy ra đối bảng phụ:

    1. nó không chứa thêm bất kỳ dữ liệu riêng nào ngoại trừ hai khóa ngoài để liên kết với hai bảng chính;
    2. nó có thêm những trường dữ liệu của riêng mình, gọi là payload.

    Ứng với mỗi tình huống, cách xây dựng quan hệ trong Entity Framework có chút khác biệt nhỏ:

    Nếu không có payload, Entity Framework có thể tự động tạo ra bảng phụ chỉ chứa khóa ngoài tới hai bảng chính;

    Nếu có payload, bạn phải tự mình tạo ra domain class tương ứng cho bảng phụ với các trường payload. Sau đó bạn phải tạo các quan hệ một – nhiều từ các bảng chính tới bảng phụ. Nói cách khác, trong tình huống này bạn phải tự mình tạo quan hệ một – nhiều hai lần.

    Giống như với quan hệ một – nhiều, bạn có thể cấu hình domain class để tạo ra quan hệ nhiều – nhiều bằng cách sử dụng quy ước (convention) của code-first hoặc sử dụng fluent API.

    Tạo quan hệ nhiều – nhiều với Entity Framework code-first convention

    Hãy cùng thực hiện một ví dụ minh họa sau:

    Tạo project P02_ManyToManycài đặt Entity Framework cho project này. Viết code cho Program.cs như sau:

    Để thử nghiệm việc tạo ra cấu trúc CSDL mới, bạn có thể sử dụng các bộ khởi tạo hoặc migration đã học từ các bài tương ứng. Chúng ta không nhắc lại các bước thực hiện cụ thể trong bài học này nữa.

    using System.Collections.Generic;
    using System.Data.Entity;
    
    namespace P02_ManyToMany
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public virtual ICollection<Company> Companies { get; set; }
        }
    
        public class Company
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public virtual ICollection<Person> People { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("ManyToMany")
            {
                var initializer = new DropCreateDatabaseAlways<Context>();
                Database.SetInitializer(initializer);
            }
    
            public DbSet<Person> People { get; set; }
            public DbSet<Company> Companies { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    context.Database.Initialize(force: true);
                }
            }
        }
    }

    Để ý chúng ta đang dùng bộ khởi tạo DropCreateDatabaseAlways và cách yêu cầu khởi tạo CSDL context.Database.Initialize(force: true);

    Sau khi chạy chương trình bạn sẽ thu được CSDL với cấu trúc như sau:

    CSDL với quan hệ nhiều - nhiều do entity framework tạo ra

    Trong ví dụ trên bạn đã tạo được quan hệ nhiều – nhiều giữa PersonCompany. Quan hệ giữa hai class này khi ánh xạ sang CSDL đã tạo ra một bảng phụ PersonCompanies.

    sơ đồ quan hệ nhiều nhiều do entity framework tạo ra

    Bảng trung gian (junction table) PersonCompanies này do Entity Framework tự động tạo ra. Bảng phụ này chỉ chứa hai khóa ngoài: Person_Id (sang bảng People) và Company_Id (sang bảng Companies). Tên hai khóa ngoài được đặt theo quy ước như bạn đã biết ở bài trước: <Tên class>_<Tên trường Id>.

    Entity Framework tự động chọn tên khóa ngoài theo quy ước bạn đã biết trong bài học trước về xây dựng quan hệ một – nhiều.

    Các navigation property được khai báo cùng từ khóa virtualICollection để sử dụng chế độ lazy loading của Entity Framework:

    public virtual ICollection<Company> Companies { get; set; }
    public virtual ICollection<Person> People { get; set; }

    Bạn cũng để ý thấy rằng không hề có domain class nào tương ứng với bảng PersonCompanies. Lý do là bảng này không chứa bất kỳ thông tin gì khác nên việc tạo ra domain class tương ứng là dư thừa.

    Tạo quan hệ nhiều – nhiều trong Entity Framework với fluent API

    Nếu bạn muốn đặt tên bảng junction, hoặc đặt tên khóa ngoài trong bảng phụ khác với quy ước (convention) của Entity Framework, bạn cần sử dụng fluent API.

    Cũng phụ thuộc vào việc bảng phụ có payload hay không, bạn có hai cách để sử dụng fluent API.

    Trong trường hợp không có payload, bạn có thể cấu hình trực tiếp quan hệ nhiều – nhiều bằng fluent API như sau:

    using System.Collections.Generic;
    using System.Data.Entity;
    
    namespace P02_ManyToMany
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public virtual ICollection<Company> Companies { get; set; }
        }
    
        public class Company
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public virtual ICollection<Person> People { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("ManyToMany")
            {
                var initializer = new DropCreateDatabaseAlways<Context>();
                Database.SetInitializer(initializer);
            }
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Person>()
                    .HasMany<Company>(p => p.Companies)
                    .WithMany(c => c.People)
                    .Map(pc =>
                        {
                            pc.MapLeftKey("RefPersonId");
                            pc.MapRightKey("RefCompanyId");
                            pc.ToTable("PersonWithCompany");
                        });
            }
    
            public DbSet<Person> People { get; set; }
            public DbSet<Company> Companies { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    context.Database.Initialize(true);
                }
            }
        }
    }

    Đoạn code trên lặp lại ví dụ về xây dựng quan hệ nhiều – nhiều không payload mà bạn đã thực hiện. Điều khác biệt là bạn ghi đè phương thức OnModelCreating để đưa thông tin cấu hình riêng thay cho cấu hình mặc định theo quy ước của Entity Framework:

    • Tên bảng junction giờ là PersonWithCompany;
    • Tên các trường khóa ngoài của nó giờ là RefPersonIdRefCompanyId.

    Entity Framework sẽ giúp bạn tự động tạo bảng junction với tên gọi như bạn yêu cầu trong fluent API:

    Như bạn đã thấy, để cấu hình quan hệ nhiều – nhiều với fluent API, trong mỗi model class bạn vẫn phải tạo ra navigation property như khi sử dụng convention.

    Trong phương thức OnModelCreating bạn phải thực hiện các bước:

    Cấu hình cho quan hệ nhiều nhiều với HasMany() và WithMany(). Trong tình huống trên bạn xuất phát từ Person, HasMany<Company>(...) báo rằng Person có quan hệ với nhiều Company, còn WithMany(…) báo lại là Company có quan hệ với nhiều Person.

    Phương thức Map() nhận một phương thức phù hợp với generic delegate Action để chỉ định thông tin về bảng junction, bao gồm tên bảng và tên các khóa ngoài.

    Phương thức ToTable chỉ định tên bảng junction thay cho tên mặc định theo quy ước.

    Khi chỉ định tên khóa ngoài cần lưu ý: MapLeftKey() nhận chuỗi ký tự là tên khóa ngoài tương ứng với bảng mà bạn xuất phát lúc cấu hình.

    Trong ví dụ trên, bạn xuất phát cấu hình từ Person nên giá trị bạn cung cấp cho phương thức này sẽ là tên khóa ngoài ứng với bảng People. MapRightKey() sẽ nhận tên khóa ngoài tương ứng với bảng còn lại.

    Nếu cấu hình xuất phát từ Company, bạn viết code như sau:

    modelBuilder.Entity<Company>() // cấu hình xuất phát từ company
        .HasMany(c => c.People)
        .WithMany(p => p.Companies)
        .Map(pc =>
        {
            pc.MapRightKey("RefPersonId"); // person giờ là bảng bên phải
            pc.MapLeftKey("RefCompanyId"); // company giờ là bảng bên trái
            pc.ToTable("PersonWithCompany");
        });

    Bonus: Tạo quan hệ nhiều – nhiều với payload

    Trong nhiều tình huống, bảng phụ ngoài các khóa ngoài còn có thể chứa thêm thông tin riêng để mô tả cho quan hệ đó.

    Ví dụ, trong quan hệ giữa PersonCompany bạn muốn bổ sung thông tin như thời gian làm việc ở công ty đó, vị trí đảm nhận ở công ty. Các thông tin thêm này được gọi là payload.

    Trong tình huống này, việc tạo quan hệ có chút khác biệt. Bạn phải tự mình tạo một model class để ánh xạ thành bảng junction. Trong model class này bạn tạo các property cho các thông tin bổ sung, đồng thời tạo các quan hệ 1 – nhiều từ hai bảng chính.

    Hãy cùng thực hiện một ví dụ với các payload trong bảng junction.

    using System.Collections.Generic;
    using System.Data.Entity;
    
    namespace P03_ManyToManyWithPayload
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public virtual ICollection<PersonCompany> Companies { get; set; }
        }
    
        public class Company
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public virtual ICollection<PersonCompany> People { get; set; }
        }
    
        public class PersonCompany
        {
            public int Id { get; set; }
    
            public int FromYear { get; set; }
            public int ToYear { get; set; }
            public bool Current { get; set; }
            public string Position { get; set; }
    
            public Person Person { get; set; }
            public Company Company { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("ManyToMany")
            {
                var initializer = new DropCreateDatabaseAlways<Context>();
                Database.SetInitializer(initializer);
            }
    
            public DbSet<Person> People { get; set; }
            public DbSet<Company> Companies { get; set; }
        }
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    context.Database.Initialize(true);
                }
            }
        }
    }

    Khi chạy chương trình bạn sẽ thu được CSDL với cấu trúc như dưới đây

    Hãy để ý trong class Context bạn chỉ thấy hai property:

    public DbSet<Person> People { get; set; }
    public DbSet<Company> Companies { get; set; }

    Không hề có public DbSet<PersonCompany> PersonCompanies {get; set;} nhưng Entity Framework vẫn tự động tạo ra bảng PersonCompanies.

    Lưu ý rằng junction table phải là phía “nhiều” trong quan hệ này, mỗi bảng chính nằm ở phía “một”. Bạn có thể áp dụng những cách khác để tạo quan hệ 1 – nhiều như đã học trong bài trước.

    Bạn có thể làm việc với quan hệ nhiều – nhiều có payload như sau:

    internal class Program
    {
        private static void Main(string[] args)
        {
            using (var context = new Context())
            {
                context.Database.Initialize(true);
    
                var trump = new Person { LastName = "Trump", Companies = new List<PersonCompany>() };
                var obama = new Person { LastName = "Obama", Companies = new List<PersonCompany>() };
                var bush = new Person { LastName = "Bush", Companies = new List<PersonCompany>() };
    
                var ibm = new Company { Name = "IBM", People = new List<PersonCompany>() };
                var intel = new Company { Name = "Intel", People = new List<PersonCompany>() };
                var amd = new Company { Name = "AMD", People = new List<PersonCompany>() };
    
                trump.Companies.Add(new PersonCompany { Company = ibm, FromYear = 2000, ToYear = 2010 });
                intel.People.Add(new PersonCompany { Person = bush, FromYear = 2010, ToYear = 2020 });
                intel.People.Add(new PersonCompany { Person = obama, FromYear = 2011, ToYear = 2021 });
    
                context.People.Add(trump);
                context.People.Add(obama);
                context.People.Add(bush);
    
                context.Companies.Add(ibm);
                context.Companies.Add(intel);
                context.Companies.Add(amd);
    
                context.SaveChanges();
    
                Console.WriteLine("Working history");
                foreach (var p in context.People)
                {
                    Console.WriteLine($"{p.LastName}'s working history:");
                    foreach (var pc in p.Companies)
                        Console.WriteLine($"{pc.Company.Name}: From {pc.FromYear} to {pc.ToYear}");
                }
    
                Console.WriteLine("\r\nCompany history");
                foreach (var c in context.Companies)
                {
                    Console.WriteLine($"{c.Name} history:");
                    foreach (var pc in c.People)
                        Console.WriteLine($"{pc.Person.LastName}: From {pc.FromYear} to {pc.ToYear}");
                }
            }
            Console.ReadKey();
        }
    }

    Bạn hãy tự mình nghịch code trên để hiểu rõ hơn cách làm việc của nó. Mã nguồn ở cuối phần kết luận của bài học.

    Kết luận

    Trong bài học này bạn đã nắm được cách thức cấu hình quan hệ nhiều – nhiều trong Entity Framework code-first sử dụng quy ước (convention) hoặc fluent API.

    Như bạn đã thấy, sử dụng quy ước là cách thức đơn giản và tự nhiên nhất để cấu hình quan hệ nhiều – nhiều. Tuy nhiên, nếu bạn không muốn tuân thủ quy ước vì lý do nào đó, bạn có thể sử dụng fluent API.

    Cũng để ý rằng, Entity Framework chỉ tự động tạo quan hệ nhiều – nhiều giúp bạn nếu không có payload trong bảng phụ. Trong trường hợp còn lại, bạn phải tự tạo bảng phụ và cấu hình hai quan hệ một nhiều tương ứng với hai bảng chính.

    Nếu có thắc mắc hoặc cần trao đổi thêm, mời bạn viết trong phần Bình luận ở cuối trang. Nếu cần trao đổi riêng, hãy gửi email hoặc nhắn tin qua form liên hệ. Nếu bài viết hữu ích với bạn, hãy giúp chúng tôi chia sẻ tới mọi người. Cảm ơn bạn!

    Bình luận

    avatar
      Đăng ký theo dõi  
    Thông báo về