Table splitting, entity splitting, complex type

    0

    Trong bài học này bạn sẽ làm quen với hai cách thức ánh xạ khác thường trong Entity Framework: table splitting và entity splitting. Mặc dù không quá phổ biến nhưng hai loại kỹ thuật này đóng vai trò quan trọng trong một số tình huống để tăng hiệu suất truy xuất dữ liệu. Bạn cũng sẽ học cách làm việc với property thuộc loại complex type – property có kiểu là các class hoặc struct tự tạo thay vì kiểu cơ sở của C#.

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

    Phân biệt các tình huống ánh xạ đặc biệt

    Khi cấu hình ánh xạ class C# với bảng CSDL qua Entity Framework, bạn sẽ nhanh chóng gặp phải tình huống sau đây:

    Trong một class có thể có một vài property chứa dữ liệu rất lớn so với những property còn lại. Ví dụ, khi quản lý thông tin về nhân sự, bạn có thể muốn lưu ảnh chân dung. Ảnh chân dung mặc dù không nhất thiết phải là những bức ảnh phân giải cao nhưng so với những trường dữ liệu khác, nó có kích thước lớn hơn đáng kể. Thông thường nhất, ảnh chân dung có thể lưu ở dạng một mảng byte (đọc trực tiếp từ file ảnh). Trong CSDL, những loại dữ liệu kích thước lớn như thế thường được lưu thành một trường BLOB (varbinary(MAX)).

    Trong tình huống này, bạn muốn tách rời trường BLOB ra một bảng riêng để không ảnh hưởng đến toàn bộ class khi tải dữ liệu từ CSDL vào object. Trường hợp này được gọi là entity splitting.

    Trong một bảng CSDL có một số trường ít được sử dụng hơn các trường khác. Bạn không muốn tải hết các trường này mỗi khi tạo object. Thay vào đó, bạn muốn ánh xạ các trường ít sử dụng vào một class riêng. Cách làm này giúp bạn chỉ phải tải những property cần thiết khi tạo object và qua đó giúp tăng hiệu suất.

    Trong hai tình huống trên, bạn gặp hai yêu cầu trái ngược: ánh xạ một domain class thành nhiều bảng CSDL; ánh xạ một bảng CSDL vào nhiều domain class. Trường hợp này được gọi là table splitting.

    Bạn có thể gặp một tình huống khác. Lấy ví dụ bạn xây dựng class Address để lưu thông tin về địa chỉ. Do cả Person và Company đều có thể có địa chỉ riêng, theo cách suy nghĩ hướng đối tượng, bạn chỉ đơn giản là tạo property kiểu Address trong class Person và Company.

    Tuy nhiên, bạn sẽ gặp phải hai trường hợp:

    (1) nếu bạn có trường Id trong Address (và tạo property DbSet<Address> trong Context), Address sẽ được ánh xạ thành một bảng dữ liệu, đồng thời tạo quan hệ một – nhiều với Person và Company. Nếu đây phù hợp với yêu cầu của bạn thi OK.

    (2) nếu bạn không có trường Id và không tạo property DbSet<Address> trong Context, Address sẽ không được ánh xạ thành một bảng độc lập. Thay vào đó, các property riêng của Address sẽ được đưa vào cùng một bảng với các property của domain class sử dụng kiểu Address.

    Trong trường hợp thứ hai, Address được gọi là một kiểu phức tạp (complex type), nhằm phân biệt với các kiểu đơn giản/kiểu cơ sở (như string, DateTime, int, bool, v.v.) mà bạn quen thuộc.

    Đây là các ví dụ về việc mô hình class của chương trình không nhất thiết phải đồng nhất với mô hình lưu trữ trên CSDL. Điều này có chút khác biệt với những điểm bạn đã biết ở các bài trước, trong đó class model tương đồng với storage model.

    Trong bài học này, bạn sẽ học các kỹ thuật để thực hiện các yêu cầu trên.

    Entity Splitting – ánh xạ một class thành nhiều bảng

    Đây là tình huống khi một class có thể chứa một trường có kích thước quá lớn. Thường gặp nhất là lưu ảnh hoặc file dữ liệu có liên quan. SQL Server cung cấp một loại trường dữ liệu riêng để lưu loại thông tin này: Blob. Trường Blob có thể xem là một mảng byte mà bạn có thể dùng để lưu thông tin bất kỳ.

    Thực tế, dù là dữ liệu ảnh hay file nhưng khi làm việc với chương trình, chúng đều được biểu diễn ở dạng một mảng byte. Tùy thuộc vào cấu trúc mảng byte, bạn có thể sử dụng các phương pháp khác nhau để trình bày nội dung của nó.

    Ví dụ, một bức ảnh (png, jpg, bmp, v.v.), một file dữ liệu, v.v., khi lưu trên ổ cứng đều là các mảng byte có cấu trúc riêng. Khi đọc mảng byte này vào chương trình, bạn có thể biểu diễn nó thành hình ảnh sử dụng component (control) riêng trên windows forms hay wpf. Nếu mảng byte là file pdf, bạn có thể tích hợp thư viện đọc pdf để hiển thị nó.

    Tuy nhiên, việc lưu những trường dữ liệu lớn như vậy trong cùng một bảng dữ liệu có một nhược điểm: mỗi khi bạn tải bản ghi đó vào object, bạn đồng thời phải đọc cả mảng byte lớn của trường blob mà không phải lúc nào bạn cũng cần sử dụng đến. Việc tải các khối dữ liệu lớn như vậy ảnh hưởng đến hiệu suất truy xuất dữ liệu và của cả chương trình.

    Vì vậy các lập trình viên thường có xu hướng tách riêng những trường dữ liệu lớn như vậy thành một bảng riêng và chỉ tải nó vào object khi có nhu cầu. Tuy nhiên, việc tách riêng một/một nhóm property ra một bảng riêng cần đảm bảo rằng việc sử dụng class đó độc lập với việc lưu trữ. Nói cách khác, code sử dụng class đó không cần quan tâm đến cấu trúc lưu trữ thực sự trong CSDL.

    Loại kỹ thuật tách một class thành nhiều bảng tương ứng trong Entity Framework được gọi là Entity Splitting. Kỹ thuật này cho phép tách một số property của cùng một class vào một (số) bảng dữ liệu.

    Kỹ thuật này được thực hiện nhờ fluent API với phương thức Map của lớp EntityTypeConfiguration.

    Hãy cùng thực hiện ví dụ sau đây. Tạo project P01_EntitySplitting và cài đặt Entity Framework cho project này.

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

    using System;
    using System.Linq;
    using System.Data.Entity;
    using System.Data.Entity.ModelConfiguration;
    using System.IO;
    
    namespace P01_EntitySplitting
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public byte[] Photo { get; set; }
            public byte[] CurriculumVitae { get; set; }
        }
    
        public class PersonMap : EntityTypeConfiguration<Person>
        {
            public PersonMap()
            {
                Map(p =>
                {
                    p.Properties(ph => new { ph.Photo, ph.CurriculumVitae });
                    p.ToTable("PeopleData");
                });
    
                Map(p =>
                {
                    p.Properties(ph => new { ph.FirstName, ph.LastName });
                    p.ToTable("People");
                });
            }
        }
    
        public class Context : DbContext
        {
            public Context() : base("EntitySplitting")
            {
                var initializer = new DropCreateDatabaseAlways<Context>();
                Database.SetInitializer(initializer);
            }
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Configurations.Add(new PersonMap());
            }
    
            public DbSet<Person> People { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    context.Database.Initialize(false);
    
                    var trump = new Person
                    {
                        FirstName = "Donald",
                        LastName = "Trump",
                        Photo = File.ReadAllBytes("D:\\trump.jpg")
                    };
    
                    context.People.Add(trump);
                    context.SaveChanges();
                }
    
                Console.Write("Press any key ...");
                Console.ReadKey();
    
                using (var context = new Context())
                {
                    var trump = context.People.FirstOrDefault();
                    File.WriteAllBytes("temp.jpg", trump.Photo);
                    System.Diagnostics.Process.Start("temp.jpg");
                }
    
                Console.ReadKey();
            }
        }
    }

    Trong ví dụ trên chúng ta gặp lại cách sử dụng buddy class (lớp PersonMap) để cấu hình với fluent API.

    public class PersonMap : EntityTypeConfiguration<Person>
    { ...

    Trong constructor của PersonMap chúng ta gọi phương thức Map để lần lượt chỉ định những property nào sẽ được đặt vào bảng nào. Cụ thể, Photo và CurriculumVitae sẽ vào bảng PeopleData, FirstName và LastName sẽ vào bảng People.

    public PersonMap()
    {
        Map(p =>
        {   // Photo và CurriculumVitae sẽ vào bảng PeopleData
            p.Properties(ph => new { ph.Photo, ph.CurriculumVitae });
            p.ToTable("PeopleData");
        });
    
        Map(p =>
        {   // FirstName và LastName sẽ vào bảng People
            p.Properties(ph => new { ph.FirstName, ph.LastName });
            p.ToTable("People");
        });
    }

    Kết quả thu được là hai bảng dữ liệu với quan hệ một – một như sau:

    entity splitting - ánh xạ một class vào nhiều bảng

    Như vậy, cấu trúc class và cấu trúc bảng dữ liệu không còn tương đồng nhau trong trường hợp này.

    Table splitting – ánh xạ nhiều class tới cùng một bảng

    Thực ra mô hình này không xa lạ lắm. Trong bài học về quan hệ kế thừa trong Entity Framework bạn đã gặp tình huống này: nhiều class khác nhau trong một chuỗi kế thừa tự động được ánh xạ vào cùng một bảng theo cách tiếp cận Table per Hierarchy (TPH). Đây là cách mặc định Entity Framework xử lý quan hệ kế thừa.

    Chúng ta xem xét một tình huống khác: ánh xạ các bảng không nằm trong chuỗi kế thừa vào cùng một bảng. Kỹ thuật này có tên gọi là table splitting. Dễ hình dung table splitting có ý nghĩa trái ngược với entity splitting mà bạn đã biết ở phần trên.

    Hãy cùng thực hiện một ví dụ. Tạo project P02_TableSplitting và cài đặt Entity Framework cho project này.

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

    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity;
    
    namespace P02_TableSplitting
    {
        //[Table("People")] // sử dụng attribute Table hoặc fluent api ToTable
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public virtual Account Account { get; set; }
        }
    
        //[Table("People")] // sử dụng attribute Table hoặc fluent api ToTable
        public class Account
        {
            public int Id { get; set; }
            public string UserName { get; set; }
            public string Password { get; set; }
    
            public virtual Person Person { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("TableSplitting")
            {
    
            }
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Entity<Person>()
                    .ToTable("People") // sử dụng attribute Table hoặc fluent api ToTable
                    .HasRequired(p => p.Account)
                    .WithRequiredPrincipal(a => a.Person);
    
                modelBuilder.Entity<Account>()
                    .ToTable("People"); // sử dụng attribute Table hoặc fluent api ToTable
            }
    
            public DbSet<Person> People { get; set; }
            public DbSet<Account> Accounts { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                using (var context = new Context())
                {
                    context.Database.CreateIfNotExists();
                }
            }
        }
    }
    

    Bạn thực hiện cấu hình bằng fluent API như sau:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Person>()
            .ToTable("People") // sử dụng attribute Table hoặc fluent api ToTable
            .HasRequired(p => p.Account)
            .WithRequiredPrincipal(a => a.Person);
    
        modelBuilder.Entity<Account>()
            .ToTable("People"); // sử dụng attribute Table hoặc fluent api ToTable
    }

    Trước hết phải yêu cầu ánh xạ cả hai entity class sang cùng một bảng People. Bạn có thể dùng fluent API (dùng phương thức ToTable) hoặc attribute Table. Nếu dùng cái nọ thì thôi cái kia.

    Thứ hai, bạn xác định một class làm principal, class còn lại làm dependant. Trong tình huống trên, chúng ta xác định Person là principal, Account là dependant. Nếu xuất phát từ principal, bạn sử dụng HasRequiredWithRequiredPrincipal như trên. Ứng với mỗi phương thức này bạn sử dụng lambda expression để chỉnh định navigation property tham gia vào quan hệ.

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

    modelBuilder.Entity<Person>()
        .ToTable("People");
    modelBuilder.Entity<Account>()
        .ToTable("People")
        .HasRequired(a => a.Person)
        .WithRequiredDependent(p => p.Account);

    Cả hai cách cho cùng một kết quả.

    Thực chất ở đây bạn đang tạo ra quan hệ một – một bắt buộc ở cả hai phía giữa hai class. Chỉ khi này, các property của chúng mới có thể đặt vào cùng một bảng CSDL. Các class tham gia vào quan hệ 1 – 0..1 như đã học không thể đặt chung vào cùng một bảng dữ liệu.

    Complex type

    Entity Framework định nghĩa complex type là những class hoặc struct tương tự như domain class (entity class) nhưng không chứa các trường khóa cũng như không được ánh xạ thành một bảng riêng như domain class. Complex type có thể hiểu là một tập hợp các property có thể tái sử dụng trong các class khác.

    Quay trở lại ví dụ về địa chỉ. Cả cá nhân và công ty đều có địa chỉ riêng. Địa chỉ của cá nhân và công ty chứa cùng các loại thông tin (quốc gia, tỉnh/thành phố, quận/huyện, đường, số nhà). Dĩ nhiên, bạn có thể đưa các thông tin này trực tiếp vào class cho người (Person) và công ty (Company).

    Nếu quen thuộc với tư duy “hướng đối tượng”, bạn sẽ tạo ngay một class Address với các thông tin cần thiết:

    public class Address
    {
        public string Street { get; set; }
        public string City { get; set; }
        public string State { get; set; }
        public string Zip { get; set; }
    }

    và sử dụng kiểu Address để tạo ra các property tương ứng trong PersonCompany.

    public class Person
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    
        public Address Address { get; set; }
    }
    
    public class Company
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public Address Address { get; set; }
    }

    Trong lớp Context bạn chỉ cần khai báo hai property People và Companies:

    public class Context : DbContext
    {
        public Context() : base("ComplexType")
        {
    
        }
    
        public DbSet<Person> People { get; set; }
        public DbSet<Company> Companies { get; set; }
    }

    Khi này Address đóng vai trò là một complex type, thay vì là một entity (domain) class.

    Nếu chạy chương trình, bạn sẽ thu được cơ sở dữ liệu với cấu trúc như sau:

    Như bạn thấy, với vai trò một complex type, các property của Address không tạo ra một bảng dữ liệu riêng. Thay vào đó, các property của Address được ánh xạ thành các property của hai bảng tương ứng với class Person (bảng People) và Company (bảng Companies).

    Bạn cũng đã thấy quy tắc đặt tên các cột ánh xạ từ property của complex type: Address_Street, Address_City, v.v..

    Dưới đây là full code của ví dụ trên:

    using System.Data.Entity;
    
    namespace P03_ComplexType
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public Address Address { get; set; }
        }
    
        public class Company
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public Address Address { get; set; }
        }
    
        public class Address
        {
            public string Street { get; set; }
            public string City { get; set; }
            public string State { get; set; }
            public string Zip { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("ComplexType")
            {
    
            }
    
            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.CreateIfNotExists();
                }
            }
        }
    }

    Nếu không muốn tên gọi các trường tuân theo quy tắc trên (và các cấu hình khác), bạn có thể dùng annotation attribute hoặc fluent Api để cấu hình cho lớp Address.

    Cấu hình complex type với annotation attribute

    Sử dụng attribute Column như bạn đã học cho các property của Address.

    public class Address
    {
        [Column("street")]
        [MaxLength(40)]
        [Required]
        public string Street { get; set; }
    
        [Column("city")]
        [MaxLength(40)]
        [Required]
        public string City { get; set; }
    
        [Column("state")]
        public string State { get; set; }
    
        [Column("zip")]
        [MaxLength(10)]
        public string Zip { get; set; }
    }

    Khi này bạn sẽ thu được CSDL như sau:

    Bạn có thể thấy cấu hình với attribute cho Address giống hệt như cấu hình cho các entity class thông thường.

    Cấu hình complex type với fluent api

    Để sử dụng fluent api với complex type bạn thực hiện hơi khác một chút so với entity class thông thường.

    Nếu sử dụng cách cấu hình với buddy class:

    public class AddressMap : ComplexTypeConfiguration<Address>
    {
        public AddressMap()
        {
            Property(p => p.Street)
                .HasMaxLength(40)
                .IsRequired()
                .HasColumnName("Street");
    
            Property(p => p.Zip)
                .HasMaxLength(10)
                .HasColumnName("Zip")
                .IsUnicode(false);
        }
    }

    Lưu ý lớp AddressMap phải kế thừa từ ComplexTypeConfiguration (đối với entity class, lớp buddy phải kế thừa từ EntityTypeConfiguration).

    Nếu không dùng buddy class, trong phương thức ghi đè OnModelCreating phải phải viết như sau:

    modelBuilder.ComplexType<Address>()
        .Property(p => p.State)
        .HasColumnName("State")
        .HasMaxLength(40);
    
    modelBuilder.ComplexType<Address>()
        .Property(p => p.City)
        .HasColumnName("City");

    Để ý rằng bạn phải dùng phương thức ComplexType (thay vì Entity như với entity class). Các phương thức cấu hình còn lại không có gì khác biệt.

    Giống như đối với entity class, bạn có thể kết hợp nhiều cách cấu hình cùng lúc: vừa dùng attribute, vừa dùng buddy class vừa cấu hình trực tiếp trong OnModelCreating.

    Full code của ví dụ cấu hình như sau:

    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.Data.Entity;
    using System.Data.Entity.ModelConfiguration;
    
    namespace P03_ComplexType
    {
        public class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
    
            public Address Address { get; set; }
        }
    
        public class Company
        {
            public int Id { get; set; }
            public string Name { get; set; }
    
            public Address Address { get; set; }
        }
    
        public class Address
        {
            [Column("street")]
            [MaxLength(40)]
            [Required]
            public string Street { get; set; }
    
            [Column("city")]
            [MaxLength(40)]
            [Required]
            public string City { get; set; }
    
            [Column("state")]
            public string State { get; set; }
    
            [Column("zip")]
            [MaxLength(10)]
            public string Zip { get; set; }
        }
    
        public class AddressMap : ComplexTypeConfiguration<Address>
        {
            public AddressMap()
            {
                Property(p => p.Street)
                    .HasMaxLength(40)
                    .IsRequired()
                    .HasColumnName("Street");
    
                Property(p => p.Zip)
                    .HasMaxLength(10)
                    .HasColumnName("Zip")
                    .IsUnicode(false);
            }
        }
    
        public class Context : DbContext
        {
            public Context() : base("ComplexType")
            {
    
            }
    
            protected override void OnModelCreating(DbModelBuilder modelBuilder)
            {
                modelBuilder.Configurations.Add(new AddressMap());
    
                modelBuilder.ComplexType<Address>()
                    .Property(p => p.State)
                    .HasColumnName("State")
                    .HasMaxLength(40);
    
                modelBuilder.ComplexType<Address>()
                    .Property(p => p.City)
                    .HasColumnName("City");
            }
    
            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.CreateIfNotExists();
                }
            }
        }
    }

    Kết luận

    Trong bài học này bạn đã làm quen với hai cách thức ánh xạ khác thường trong Entity Framework: table splitting và entity splitting. Mặc dù không quá phổ biến nhưng hai loại kỹ thuật này đóng vai trò quan trọng trong một số tình huống để tăng hiệu suất truy xuất dữ liệu.

    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ề