Quan hệ một – nhiều trong Entity Framework

    0

    Relationship (quan hệ) là một thành phần đặc biệt quan trọng của cơ sở dữ liệu quan hệ. Như bạn đã biết, có 3 loại quan hệ cơ bản giữa các bảng dữ liệu là một – nhiều, nhiều – nhiều, một – một. Trong lập trình hướng đối tượng, giữa các class cũng có các mối quan hệ tương tự. Loại quan hệ này được gọi là “has a”. Do đó việc ánh xạ quan hệ giữa các class sang quan hệ giữa các bảng tương ứng khá đơn giản.

    Bạn cũng đã biết, mỗi entity class sẽ được Entity Framework ánh xạ thành một (hoặc một số) bảng dữ liệu. Để tạo ra các bảng có quan hệ, khi xây dựng các entity class cũng phải áp dụng những kỹ thuật nhất định. Entity Framework code-first cung cấp các kỹ thuật để xây dựng tất cả các loại quan hệ trên.

    Trong bài học này bạn sẽ học các kỹ thuật xây dựng entity class để có thể ánh xạ thành quan hệ một – nhiều giữa các bảng tương ứng. Các loại quan hệ còn lại bạn sẽ lần lượt học trong các bài tiếp theo.

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

    Quan hệ một – nhiều có thể gặp ở rất nhiều trường hợp. Ví dụ, một người có thể có nhiều địa chỉ email, nhiều số điện thoại. Ở chiều ngược lại, một địa chỉ email hay một số điện thoại thuộc về một người.

    Nghĩa là các object (hoặc bản ghi) nằm ở hai đầu của quan hệ này có quan hệ (về số lượng) tương tự như quan hệ cha – con: một cha – nhiều con. Bản thân trong CSDL đôi khi cũng gọi bản ghi ở phía “một” là parent, bản ghi ở phía “nhiều” là children.

    Trong Entity Framework, object ở phía “một” được gọi là principal, object ở phía “nhiều” được gọi là dependent. Điều này nói lên rằng, trong quan hệ 1 – nhiều, phía “nhiều” (hay phía dependent) là phía phụ thuộc.

    Bản chất của tạo quan hệ một nhiều là trong bảng con (bảng dependent) phải tạo ra một khóa ngoài tham chiếu tới khóa chính của bảng cha (bảng principal).

    Entity cho phép xây dựng quan hệ một – nhiều giữa hai entity class theo nhiều cách: sử dụng quy ước (convention); sử dụng fluent API, sử dụng annotation attribute.

    Tạo quan hệ 1 – nhiều sử dụng quy ước của code-first

    Hãy cùng thực hiện một ví dụ.

    Tạo một project P01_OneToMany và cài đặt Entity Framework cho project này. Kích hoạt chế độ automatic migration cho project.

    Viết code cho Program.cs như sau:

    using System.Data.Entity;
    
    namespace P01_OneToMany
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        public class Email
        {
            public int Id { get; set; }
            public string EmailAddress { get; set; }
    
            public Person Person { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("OneToMany")
            {
                var initializer = new MigrateDatabaseToLatestVersion<Context, Migrations.Configuration>();
                Database.SetInitializer(initializer);
            }
    
            public DbSet<Person> People { get; set; }
            public DbSet<Email> Emails { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
            }
        }
    }

    Code này không khác biệt gì với các ví dụ mà bạn đã quen thuộc ngoại trừ bạn tạo thêm class Email và thêm property Emails trong Context.

    Lưu ý dòng số 17, trong class Email bạn tạo thêm một property public Person Person { get; set; }. Từ khía cạnh xây dựng class của C# thì đây là một property hoàn toàn bình thường. Tuy nhiên, Entity Framework lại nhìn nhận rằng bạn đang muốn tạo quan hệ một – nhiều giữa Person (phía “một) và Email (phía “nhiều”).

    Nếu chạy lệnh tạo/cập nhật CSDL của migration (update-database) bạn sẽ thu được CSDL như sau:

    Để ý thấy rằng bảng Emails giờ có thêm cột khóa ngoài Person_Id (FK, int, null).

    Nếu sử dụng công cụ Database Diagrams của SSMS bạn sẽ thu được sơ đồ CSDL như sau:

    Sơ đồ này thể hiện rằng giữa People và Emails đang có quan hệ 1 – nhiều, trong đó Person_Id là khóa ngoài của Emails, tham chiếu sang khóa chính Id của People. Emails được gọi là bảng phụ thuộc (dependent table) hoặc bảng con (child table). People gọi là bảng cha (parent table) hoặc bảng chính (principal table).

    Qua ví dụ vừa rồi, bạn đã tạo ra được quan hệ 1 – nhiều sử dụng convention trong Entity Framework code-first. Có một số convention (quy ước) khác nhau để tạo ra quan hệ này.

    Để 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.

    Tạo quan hệ 1 – nhiều từ phía “nhiều”

    Cách làm này yêu cầu bạn chỉnh sửa entity class từ phía “nhiều”. Trong ví dụ trên, phía “nhiều” là class Email (1 người – nhiều email).

    Để tạo quan hệ 1 – nhiều giữa Person Email theo cách này, trong đó mỗi Person có nhiều Email (và mỗi Email thuộc về 1 Person), bạn chỉ cần tạo một property trong Email thuộc kiểu Person. Entity Framework sẽ tự hiểu bạn đang muốn tạo quan hệ một nhiều giữa (nhiều) Email và (một) Person.

    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
        
        public Person Person { get; set; }
    }

    Property Person trong lớp Email có tên gọi là reference navigation property.

    Entity Framework sẽ tự động tạo ra khóa ngoài (Foreign Key) trong bảng phụ thuộc Emails theo thông tin của navigation property với tên gọi theo quy ước <Tên navigation property>_<Tên khóa chính của bảng cha>.

    Như ở trên, tên navigation property trong class Email là Person, khóa chính của Person Id. Vậy tên khóa ngoài tạo ra trong bảng Emails Person_Id. Nếu bạn đặt tên khóa chính của Person PersonId thì khóa ngoài của Email sẽ được tự động tạo với tên Person_PersonId.

    EF có hai quy ước chọn tên khóa chính của entity class: Id và <Tên class>Id. Kiểu dữ liệu phải là số nguyên. Như đối với lớp Person, bạn có thể chọn tên khóa chính là Id hoặc PersonId.

    Bạn cũng để ý rằng khóa ngoài Person_Id của Email đánh dấu là NULL (cho phép nhận giá trị null), nghĩa là cho phép Email tồn tại độc lập (không phải liên kết với Person nào). Bạn có thể sử dụng annotation attribute hoặc fluent API để cấu hình cho khóa ngoài trở thành NotNull.

    Tên của navigation property có thể đặt tự do, không nhất thiết phải trùng với tên class như trên. Tức là, trong class Email, bạn có thể đổi tên trường Person thành Owner hay bất kỳ cái gì mình thích.

    Ví dụ, nếu bạn đặt tên khóa chính và trường navigation như sau:

    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    
        public Person Owner { get; set; }
    }

    Entity Framework sẽ tạo ra khóa ngoài Owner_PersonId trong bảng Emails tương ứng.

    Tạo quan hệ 1 – nhiều từ phía “một”

    Bạn cũng có thể tạo quan hệ 1 – nhiều từ phía “một” (theo ví dụ trên là từ class Person) theo cách sau:

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }    
    
        public ICollection<Email> Emails { get; set; }
    }

    Trong cách làm này bạn bổ sung vào Person một property Emails chứa một danh sách object của Email. Property Emails như trên có tên gọi là collection navigation property. ICollection<T> là một generic interface. Bạn cũng có thể sử dụng các generic collection khác như List<T> hoặc interface khác như IList<T>.

    Bạn để ý rằng, loại property có kiểu liên quan đến entity class khác như Emails hay Person ở trên có tên gọi chung là navigation property trong EF. Nó cho phép bạn truy xuất vào object/danh sách object có liên kết với entity hiện tại. Nếu property này trỏ tới 1 object khác, nó là reference navigation property; Nếu trỏ tới một danh sách object, nó được gọi là collection navigation property.

    Nếu bạn chạy update-database sẽ thu được CSDL giống như trong ví dụ trước.

    Khi bạn tạo quan hệ theo cách này, tên khóa ngoài của bảng phụ thuộc sẽ được đặt theo quy tắc <Tên class chính>_<Khóa của class chính>. Trong tình huống trên, khóa ngoài của bảng Emails sẽ được đặt theo tên và khóa của class Person: Person_Id.

    Giả sử bạn đặt tên như sau:

    public class Person
        {
            public int PersonId { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public virtual ICollection<Email> Emails { get; set; }
        }
    
        public class Email
        {
            public int Id { get; set; }
            public string EmailAddress { get; set; }
        }

    Khóa ngoài của bảng Emails sẽ có tên là Person_PersonId.

    Tạo quan hệ 1 – nhiều từ cả hai phía

    Bạn có thể kết hợp cả hai cách trên, nghĩa là tạo reference navigation ở phía “nhiều” và tạo collection navigation ở phía “một” cùng lúc.

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }    
    
        public ICollection<Email> Emails { get; set; }
    }
    
    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }    
    
        public Person Person { get; set; }
    }

    Trong phương án này, tên của khóa phụ được đặt giống như ở phương án 1 (cấu hình từ phía “nhiều”) do trong class Email giờ xuất hiện navigation property.

    Cả 3 quy ước trên tạo ra cùng một kết quả trong CSDL với đôi chút khác biệt trong cách đặt tên trường khóa ngoài của bảng phụ thuộc. Việc lựa chọn phương án nào tùy thuộc vào yêu cầu sử dụng class của bạn. Nếu bạn chỉ cần truy xuất danh sách từ phía “một” (nhưng không cần truy xuất object liên kết từ phía “nhiều”) thì dùng cách 2. Nếu cần truy xuất từ cả hai phía thì dùng cách 3.

    Tạo “quan hệ toàn diện”

    Ba tình huống trên tạo ra quan hệ 1 – nhiều nhưng không bắt buộc từ phía class con. Nghĩa là Email hoàn toàn có thể tồn tại độc lập mà không cần có Person. Lý do là khóa ngoài của Email tạo ra theo các cách trên là NULL.

    Giờ chúng ta sẽ xem xét một trường hợp khác.

    Giá sử bạn bổ sung thêm một property PersonId vào class Email như sau:

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    
        public ICollection<Email> Emails { get; set; }
    }
    
    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    
        public int PersonId { get; set; }
        public Person Person { get; set; }
    }

    bạn đã tạo ra một quan hệ 1 – nhiều toàn diện (fully defined relationship) giữa Person và Email.

    Cách này cho ra kết quả khác biệt với các cách trên ở chỗ bạn tự đặt tên khóa ngoài cho Email và nó là NotNull.

    Đây là tình huống bạn muốn tạo ra quan hệ phụ thuộc bắt buộc từ Email tới Person. Không giống như ba trường hợp ở trên, giờ đây Email hoàn toàn phụ thuộc vào Person. Email không thể tồn tại thiếu Person.

    Quy ước đặt tên khóa ngoài trong trường hợp này là <Tên navigation property><Tên khóa của class cha>. Như trên, navigation property của Email Person, khóa chính của class Person là Id thì tên khóa ngoài bạn cần đặt là PersonId. Nếu bạn đặt khác (ví dụ Person_Id), EF sẽ coi đây là một trường bình thường và lại tự động tạo khóa ngoài như ba trường hợp đầu tiên.

    Ví dụ, nếu lớp Person đặt tên khóa là PersonId (theo quy ước khác của EF) và navigation property của Email đặt tên là Owner thì trường khóa ngoài phải đặt là OwnerPersonId:

    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    
        public virtual ICollection<Email> Emails { get; set; }
    }
    
    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    
        public int OwnerPersonId { get; set; }
        public virtual Person Owner { get; set; }
    }

    Nếu bạn đặt tên không theo quy ước này, Entity Framework sẽ không coi nó là khóa ngoài mà chỉ coi là một trường số nguyên thông thường.

    Dĩ nhiên, bạn hoàn toàn có thể đặt tên theo ý mình mà không cần tuân thủ các quy ước trên của Entity Framework. Trong tình huống đó, bạn cần sử dụng annotation attribute hoặc fluent API để cấu hình.

    Tạo quan hệ một – nhiều sử dụng annotation attribute

    Bạn thực sự không cần phải dùng annotation attribute để cấu hình quan hệ một – nhiều, trừ khi bạn muốn tự đặt tên khóa ngoài không theo quy ước của Entity Framework.

    Vẫn tiếp tục với ví dụ về Person và Email. Giả sử trong class Email bạn tạo property public int RefToPerson { get; set; } và muốn Entity Framework đặt property này làm khóa ngoài của bảng Emails để tham chiếu sang bảng People.

    Do bạn không tuân theo quy ước, Entity Framework không thể hiểu ý định của bạn nên nó sẽ tự tạo một khóa ngoài riêng theo quy tắc bạn đã biết.

    Khi này, bạn cần báo cho Entity Framework biết rằng RefToPerson sẽ là khóa ngoài tương ứng với navigation property Owner theo cách sau:

    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    
        // dùng annotation attribute nếu tên khóa và trường navigation không theo quy ước
        //[ForeignKey("Owner")] // hoặc
        [ForeignKey(nameof(Owner))]
        public int RefToPerson { get; set; }
        public Person Owner { get; set; }
    }

    Entity Framework sẽ hiểu rằng:

    (1) bạn cần tham chiếu sang bảng People khi nhìn thấy public Person Owner { get; set; } (tên của property không quan trọng)

    (2) để tham chiếu sang People cần dùng public int RefToPerson { get; set; } làm khóa ngoài.

    Bạn chỉ cần đặt attribute ForeignKey(string) phía trên property dự định đặt làm khóa ngoài là xong:

    Trong cách sử dụng này bạn đặt ForeignKey trước khóa ngoài và cấp cho nó tên trường navigation.

    Attribute này nhận tham số là tên của navigation property tham gia vào quan hệ. Trong ví dụ trên, Owner là navigation property của Email tham gia vào quan hệ một – nhiều với Person.

    Bạn sẽ thu được các bảng dữ liệu và quan hệ như sau:

    Phép toán nameof giúp bạn lấy tên các thành viên của class. Dùng nameof tiện lợi hơn so với hard-code chuỗi ký tự tên của thành viên khi bạn cần đổi tên thành viên class. Biểu thức nameof(Owner) sẽ cho kết quả là chuỗi "Owner".

    Bạn cũng có thể dùng ForeignKey theo cách khác:

    public int RefToPerson { get; set; }
    [ForeignKey(nameof(RefToPerson))]
    public Person Owner { get; set; }

    Cách này bạn đặt ForeignKey trước navigation property và cấp cho nó tên của khóa ngoài.

    Hai cách này là tương đương nhau.

    Tạo quan hệ một – nhiều bằng fluent API

    Nói chung bạn cũng không cần sử dụng fluent API để cấu hình quan hệ giữa các entity class vì cách dùng quy ước của Entity Framework đã giải quyết hết các tình huống.

    Tuy nhiên, nếu bạn muốn đặt tên property không theo quy ước trên (vì lí do nào đó) thì có thể sử dụng fluent API để cấu hình quan hệ. Ngoài ra fluent API cũng tiện lợi để đặt thêm các yêu cầu khác (như cascading delete các object có quan hệ).

    Giả sử bạn đặt tên property như dưới đây:

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public ICollection<Email> Emails { get; set; }
    }
    
    public class Email
    {
        public int Id { get; set; }
        public string EmailAddress { get; set; }
    
        public int RefToPerson { get; set; } // tên này đặt không theo quy tắc
        public Person Owner { get; set; }
    }

    Trong lớp Context ghi đè phương thức OnModelCreating như bạn đã biết khi học về fluent API.

    public class Context : DbContext
    {
        public Context() : base("OneToMany")
        {
            var initializer = new MigrateDatabaseToLatestVersion<Context, Migrations.Configuration>();
            Database.SetInitializer(initializer);
        }
    
        public DbSet<Person> People { get; set; }
        public DbSet<Email> Emails { get; set; }
    
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Email>()
            .HasRequired<Person>(e => e.Owner)
            .WithMany(p => p.Emails)
            .HasForeignKey<int>(e => e.RefToPerson);
        }
    }

    Bạn thu được cùng kết quả như khi sử dụng annotation attribute.

    Giờ đây khóa ngoài của Emails có tên là RefToPersonnot null, thay vì tên do EF đặt tự động theo quy ước.

    Cấu hình quan hệ 1 – nhiều bằng fluent API

    Trong ví dụ trên bạn lựa chọn cấu hình quan hệ từ lớp Email (phía “nhiều”): modelBuilder.Entity<Email>().

    Phương thức HasRequired<Person>(e => e.Owner) chỉ định rằng Email có quan hệ bắt buộc với 1 Person thông qua property Owner. Lệnh này đồng thời tạo khóa ngoài not null trong bảng Emails. Nếu muốn khóa ngoài not null, bạn có thể sử dụng HasOptional.

    Phương thức WithMany(p => p.Emails) chuyển sang cấu hình cho phía “một”. Phương thức này chỉ định collection reference property của Person tham gia vào quan hệ với Email.

    Phương thức HasForeignKey(e => e.RefToPerson) chỉ định tên của trường khóa ngoài sẽ tạo trong bảng Emails. Bạn cần dùng phương thức này nếu tên của khóa ngoài không tuân thủ quy ước như đã nói ở trên. Trong trường hợp này, tên khóa ngoài được chọn là RefToPerson.

    Nếu bạn lựa chọn cấu hình xuất phát từ lớp Person (phía “một”), lệnh cấu hình như sau:

    modelBuilder.Entity<Person>()
        .HasMany<Email>(p => p.Emails)
        .WithRequired(e => e.Owner)
        .HasForeignKey(e => e.RefToPerson);

    Khi xuất phát từ phía “một”, bạn cần dùng HasManyWithRequired/WithOptional (thay cho HasRequired/HasOptionalWithMany).

    Cả hai cách này đều cho cùng một kết quả.

    Sử dụng buddy class

    Nếu sử dụng buddy class, bạn có thể viết thêm class cấu hình riêng cho Email hoặc Person như sau:

    public class PersonMap : EntityTypeConfiguration<Person>
    {
        public PersonMap()
        {
            // Cấu hình quan hệ từ phía "một"            
            HasMany(p => p.Emails)
                .WithRequired(e => e.Owner)
                .HasForeignKey(e => e.RefToPerson)
                .WillCascadeOnDelete();
        }
    }
    
    public class EmailMap : EntityTypeConfiguration<Email>
    {
        public EmailMap()
        {
            //Cấu hình quan hệ từ phía "nhiều"
            HasRequired(e => e.Owner)
              .WithMany(p => p.Emails)
              .HasForeignKey(e => e.RefToPerson)
              .WillCascadeOnDelete();
        }
    }

    Bạn chỉ cần một trong hai class trên là đủ.

    Khi đó phương thức OnModelCreating chỉ đơn giản như sau:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new EmailMap());
        // hoặc
        //modelBuilder.Configurations.Add(new PersonMap());
    }

    Bạn thu được cùng kết quả như trên.

    Quan hệ 1 – nhiều với cascade delete

    Trong quan hệ 1 – nhiều, nếu bạn xóa bản ghi của phía một, các bản ghi liên kết với nó ở phía nhiều có thể xảy ra 2 tình huống:

    Nếu khóa ngoài của nó là null, bản ghi phía “nhiều” có thể tồn tại độc lập. Bạn có thể xóa bản ghi của phía một. Ví dụ, bạn có thể xóa Person và bỏ lại các Email liên kết với nó. Khi này các bản ghi phía nhiều còn sót lại được gọi là các bản ghi “mồ côi” (orphaned record) do khóa ngoài của nó giờ không liên kết với bất kỳ bản ghi nào khác. Các bản ghi mồ côi có thể ảnh hưởng đến tính toàn vẹn (integrity) của CSDL.

    Nếu khóa ngoài của nó là not null, bản ghi phía nhiều không thể tồn tại độc lập. Nó dẫn đến kết quả là bạn không thể xóa bản ghi phía 1 nếu vẫn còn bản ghi của phía nhiều liên kết với nó. Nghĩa là bạn phải xóa hết các bản ghi của phía nhiều trước khi xóa bản ghi liên kết tương ứng ở phía 1. Ví dụ, bạn không thể xóa Person nếu chưa xóa hết các Email liên kết với nó.

    Ngoài ra, bạn cũng có thể sử dụng chế độ cascade delete: khi xóa bản ghi phía 1, sẽ đồng thời xóa tất cả bản ghi của phía nhiều liên kết với nó.

    Ví dụ, khi xóa Person thì sẽ đồng thời xóa hết các Email liên kết với nó.

    Trong quan hệ 1 – nhiều bạn có thể chỉ định chế độ cascade delete qua fluent API như sau:

    modelBuilder.Entity<Person>()
        .HasMany<Email>(p => p.Emails)
        .WithRequired(e => e.Owner)
        .HasForeignKey(e => e.OwnerId)
        .WillCascadeOnDelete();

    Cascade delete được sử dụng mặc định cùng với quan hệ một – nhiều trong Entity Framework.

    Một số lưu ý khi xây dựng quan hệ trong Entity Framework

    Thêm dữ liệu mới

    using (var context = new Context())
    {
        var trump = new Person { FirstName = "Donald", LastName = "Trump" };
        trump.Emails = new List<Email>
        {
            new Email { EmailAddress = "donald.trump@gmail.com" },
            new Email { EmailAddress = "trump.donald@yandex.ru" }
        };
    
        var obama = new Person { FirstName = "Barack", LastName = "Obama" };
        obama.Emails = new List<Email>
        {
            new Email { EmailAddress = "barack.obama@gmail.com" },
            new Email { EmailAddress = "obama.barack@yandex.ru" }
        };
    
        var mail = new Email { EmailAddress = "president@the_white_house.us" };
        trump.Emails.Add(mail);
        obama.Emails.Add(mail);
    
        context.People.Add(trump);
        context.People.Add(obama);
        context.SaveChanges();
    }

    Hãy lưu ý tình huống biến var mail = new Email { EmailAddress = "president@the_white_house.us" };. Object này được thêm vào danh sách email của cả trump obama. Tuy nhiên, khi chạy chương trình bạn sẽ thấy chỉ obama có email này. Do đây là quan hệ 1 – nhiều giữa Person và Email, một Email chỉ được thuộc về một người cuối cùng.

    Sử dụng virtual property và lazy loading

    Trong class Person khi tạo collection navigation bạn có thể định nghĩa nó là virtual như sau:

    public virtual ICollection<Email> Emails { get; set; }

    Từ khóa virtual chỉ định rằng lớp kế thừa của Person có thể ghi đè (override) property này. Trong Entity Framework, nếu bạn định nghĩa navigation property với từ khóa virtual, bạn đang muốn Entity Framework áp dụng cơ chế lazy loading khi truy xuất danh sách object – dependent.

    Cơ chế lazy loading chỉ tải các object trong collection khi có nhu cầu. Nghĩa là, khi bạn tải một object Person vào chương trình, danh sách Email của nó sẽ không được tải ngay lập tức. Chỉ khi nào gặp đoạn code truy xuất dữ liệu của property Emails (như in ra màn hình, gọi phương thức ToArray(), ToList(), v.v.) thì các object Email của danh sách đó mới được tải. Cơ chế này giúp hạn chế các thao tác truy xuất dữ liệu chưa cần thiết để tăng hiệu suất cho ứng dụng.

    Hãy tưởng tượng bạn chỉ cần truy xuất thông tin của Person mà không cần dùng đến Emails, không có lý do gì để tải hết cả danh sách Email. Khi này cơ chế lazy loading sẽ rất hiệu quả.

    Cùng xem ví dụ sau: giả sử bạn khai báo property Emails ở trên với từ khóa virtual, bạn sẽ truy xuất danh sách email theo cách sau:

    using (var context = new Context())
    {
        foreach (var p in context.People)
        {
            WriteLine($"{p.FirstName} {p.LastName}");
            foreach (var e in p.Emails)
            {
                WriteLine($"-- {e.EmailAddress}");
            }
        }
    }

    Nói một cách đơn giản, khi bạn sử dụng đến dữ liệu tương ứng, Entity Framework đảm bảo sẽ tải cho bạn qua cơ chế lazy loading.

    Sử dụng eager loading

    Nếu không dùng từ khóa virtual, bạn xác định sẽ sử dụng cơ chế eager loading hoặc explicit loading – chủ động yêu cầu tải các object có liên quan.

    Giờ hãy bỏ từ khóa virtual khỏi property Emails:

    public ICollection<Email> Emails { get; set; }

    Nếu cần tải danh sách Emails, bạn phải sử dụng thêm phương thức Include như sau:

    using (var context = new Context())
    {
        foreach (var p in context.People.Include(p=>p.Emails))
        {
            WriteLine($"{p.FirstName} {p.LastName}");
            foreach (var e in p.Emails)
            {
                WriteLine($"-- {e.EmailAddress}");
            }
        }
    }

    Nếu bỏ phương thức Include(p=>p.Emails), bạn sẽ gặp lỗi System.NullReferenceException: 'Object reference not set to an instance of an object.' khi truy xuất Emails vì Entity Framework sẽ không tự động tải các object của danh sách này.

    Cũng lưu ý rằng, lazy loading hay eager loading chỉ ảnh hưởng đến cách Entity Framework tải các object có liên quan chứ không ảnh hưởng đến cấu trúc bảng dữ liệu được tạo ra. Do đó, việc thêm bớt từ khóa virtual như trên không dẫn đến tạo lại/cập nhật CSDL.

    Lazy loading mặc dù tiện lợi trong nhiều trường hợp, bạn sẽ có thể không muốn sử dụng nó khi serialize object trong ASP.NET. Nếu không cẩn thận, bạn có thể serialize trọn vẹn cả CSDL thay vì chỉ những dữ liệu cần gửi cho client. Khi đó bạn nên tắt chế độ này hoàn toàn.

    Bạn sẽ quay trở lại với lazy và eager loading trong một bài học riêng.

    Sử dụng ICollection thay cho kiểu cụ thể

    Bạn cũng có thể để ý rằng trong lớp Person đang sử dụng ICollection<T> thay cho kiểu collection cụ thể. Cách sử dụng này có tác dụng tốt trong thực tế vì bạn cho phép Entity Framework tự xác định loại collection cụ thể giúp bạn. Ngoài ICollection<T> bạn cũng sẽ thường gặp IEnumerable<T>. Dĩ nhiên, bạn cũng có thể sử dụng kiểu List<T> quen thuộc.

    Vấn đề lựa chọn này có liên quan đến cơ chế lazy loading và một khả năng đặc biệt của collection navigation property: change tracking (theo dõi thay đổi của object). Để thực hiện các tính năng trên, Entity Framework thực tế tự tạo ra các class đặc biệt gọi là proxy. Để tạo ra các lớp proxy này Entity Framework yêu cầu collection navigation property phải thực thi giao diện ICollection.

    Như vậy, bạn có thể sử dụng bất kỳ kiểu collection nào thực thi ICollection. Và đơn giản nhất là sử dụng luôn ICollection làm kiểu dữ liệu.

    Khi sử dụng ICollection, bạn có thể sẽ quên không tạo object thực sự cho nó. Để tránh tình trạng này, trong constructor của Person bạn có thể thêm lời gọi khởi tạo danh sách:

    public Person()
    {
       Emails = new HashSet<Email>(); // có thể thay bằng kiểu khác cũng được, miễn là thực thi ICollection
    }

    Sử dụng khóa ngoài null

    Như bạn đã thấy ở trên, khóa ngoài có thể chỉ định null hoặc not null. Việc lựa chọn phụ thuộc vào ý định của bạn khi sử dụng.

    Trong ví dụ, nếu bạn coi Person là loại thực thế “chính”, Email là thông tin “phụ” và không để cho Email tồn tại độc lập (nghĩa là không thể có Email mà không thuộc về một ai đó), bạn nên để khóa ngoài của Email là not null.

    Tuy nhiên, có nhiều trường hợp, bạn chỉ xem thực thể ở phía “một” là một dạng thông tin lookup/danh mục, còn bản thân thực thể ở phía “nhiều” mới là quan trọng.

    Ví dụ về quản lý sách điện tử, bạn có thể có lớp Category (phân loại sách) và Book. Rõ ràng, Book là loại object độc lập và quan trọng vì nó là đối tượng chính đang cần quản lý. Category khi đó chỉ được xem là một dạng danh mục. Book có thể thuộc về một Category nào đó, nhưng không nhất thiết. Khi này, khóa ngoài CategoryId của Book nên để là null. Rất nhiều trường hợp tương tự như danh mục tỉnh/thành phố, danh mục phòng ban của đơn vị.

    Như vậy, trong tình huống phía “một” (parent/principal) đóng vai trò danh mục, bạn nên sử dụng khóa ngoài null cho phía “nhiều” (children/dependent).

    Kết luận

    Bài học này đã hướng dẫn bạn cách tạo quan hệ 1 – nhiều với Entity Framework code-first thông qua hai cách: sử dụng quy ước và sử dụng fluent API.

    Trong đại đa số các trường hợp, bạn nên sử dụng quy ước cho đơn giản. Nếu bắt buộc phải đặt tên các property không theo quy ước của code-first, bạn cần sử dụng fluent API.

    Bạn nên tải mã nguồn của ví dụ để tìm hiểu thêm do trong các ví dụ ở trên chỉ trích từng phần của mã nguồn.

    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ề