Quan hệ kế thừa trong Entity Framework

    0

    Kế thừa (inheritance) là loại quan hệ rất quan trọng và là một trong những nền tảng của lập trình hướng đối tượng. Trong C#, bạn hầu như không thể phát triển ứng dụng mà không sử dụng quan hệ kế thừa.

    Tuy nhiên cơ sở dữ liệu quan hệ lại không hỗ trợ kế thừa.

    Với vai trò ánh xạ các domain class với các bảng CSDL, Entity Framework có một số giải pháp để tạo cấu trúc CSDL nhằm “mô phỏng” quan hệ kế thừa.

    Trong bài học này, bạn sẽ làm quen với cách xử lý quan hệ kế thừa trong Entity Framework. Cụ thể hơn, bạn sẽ làm quen với ba hướng tiếp cận để xử lý quan hệ kế thừa trong Entity Framework: Table per Hierarchy (TPH), Table per Type (TPT), và Table per Concrete Class (TPC).

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

    Quan hệ kế thừa trong Entity Framework

    Nhìn chung trong lập trình hướng đối tượng có hai loại quan hệ giữa các class: quan hệ “has a” và quan hệ “is a“.

    Quan hệ “has a” là loại quan hệ giữa các class “ngang hàng”, biểu diễn một/nhiều object của class này có quan hệ với/chứa đựng một/nhiều object của class khác. Như bạn đã thấy, có 3 loại quan hệ “has a” cơ bản: một – một, một – nhiều, nhiều – nhiều.

    Cơ sở dữ liệu quan hệ cũng hỗ trợ loại quan hệ này. Giữa các bảng CSDL cũng tạo ra 3 loại quan hệ như trên.

    Trong ba bài học trước bạn đã học cách tạo quan hệ này và cách ánh xạ nó sang quan hệ giữa các bảng CSDL. Bạn cũng thấy rằng, Entity Framework sẽ tạo ra một bảng CSDL tương ứng với mỗi domain class.

    Tuy nhiên, trong C# giữa các class còn có một quan hệ khác: quan hệ kế thừa. Các class kế thừa nhau không tạo ra quan hệ ngang hàng mà tạo ra quan hệ phân cấp (hierarchy). Quan hệ kế thừa là một đặc trưng quan trọng của các ngôn ngữ lập trình hướng đối tượng. Đi kèm với kế thừa là tính đa hình. Loại quan hệ liên quan đến đa hình được gọi là quan hệ “is a“: object của lớp con cũng là object của lớp cha.

    Cơ sở dữ liệu quan hệ không hỗ trợ loại quan hệ “is a”, cũng như không có kế thừa. Entity Framework đưa ra ba giải pháp để có thể “mô phỏng” quan hệ kế thừa trên các bảng CSDL:

    Table per Hierarchy (TPH): tạo một bảng cho cả chuỗi kế thừa. Đây là cách thực hiện kế thừa mặc định trong Entity Framework. Với cách tiếp cận này, Entity gom hết property của tất cả các class trong chuỗi kế thừa vào một bảng duy nhất, đồng thời tạo thêm một trường Discriminator trong bảng này để xác định record đó sẽ tương ứng với object của class con nào.

    Table per Type (TPT): Mỗi class trong chuỗi kế thừa được ánh xạ thành một bảng riêng biệt. Quan hệ kế thừa giữa các class được tạo ra bởi quan hệ một – một giữa bảng tương ứng của class cha với bảng tương ứng của class con.

    Table per Concrete Class (TPC): Cách tiếp cận này cũng tạo ra một bảng cho mỗi class con nhưng không tạo ra bảng tương ứng cho abstract class. Nếu class con kế thừa từ abstract class, tất cả property của lớp cha sẽ chuyển thành các cột tương ứng trong bảng của từng lớp con.

    Bài học này sẽ hướng dẫn chi tiết các thực hiện TPH mặc định. Hai cách thức còn lại sẽ chỉ được giới thiệu sơ lược để bạn hiểu nguyên lý.

    Table per Hierarchy (TPH)

    Table per Hierarchy (TPH) – tạo một bảng cho cả chuỗi kế thừa – là cách xử lý quan hệ kế thừa mặc định trong Entity Framework. Trong cách tiếp cận này, Entity Framework sẽ tạo ra một bảng dữ liệu duy nhất đủ để chứa tất cả các property của tất cả các class trong cây kế thừa.

    Ví dụ về kế thừa với TPH

    Để dễ hiểu, hãy cùng thực hiện một ví dụ. Tạo project P01_Inheritance và cài đặt Entity Framework cho project này.

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

    using System.Data.Entity;
    using System.Linq;
    using static System.Console;
    
    namespace P01_Inheritance
    {
        public abstract class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        public class Student : Person
        {
            public string Major { get; set; }
            public string Specialization { get; set; }
        }
    
        public class Teacher : Person
        {
            public string Department { get; set; }
            public string Qualification { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("Inheritance")
            {
    
            }
    
            public DbSet<Person> People { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Title = "Inheritance";
                using (var ctx = new Context())
                {
                    if (!ctx.Database.Exists())
                    {
                        ctx.Database.Initialize(false);
    
                        ctx.People.AddRange(new Student[]
                        {
                        new Student { FirstName = "Donald", LastName = "Trump", Major = "Computer Science", Specialization = "Information Systems"},
                        new Student { FirstName = "Barack", LastName = "Obama", Major = "Computer Science", Specialization = "Sofware Engineering"},
                        new Student { FirstName = "George", LastName = "Bush", Major = "Computer Science", Specialization = "System Administration"},
                        });
    
                        ctx.People.AddRange(new Teacher[]
                        {
                        new Teacher { FirstName = "Vladimir", LastName = "Putin", Department = "KGB", Qualification = "PhD"},
                        new Teacher { FirstName = "Boris", LastName = "Yeltsin", Department = "KGB", Qualification = "PhD"},
                        });
    
                        ctx.SaveChanges();
                    }
    
                    // Polymorphic Queries                
                    var people = ctx.People.ToList();
                    WriteLine("People:");
                    foreach (var p in people)
                    {
                        Write($"{p.FirstName} {p.LastName}");
                        if (p is Teacher)
                        {
                            WriteLine(" (teacher)");
                        }
                        else
                        {
                            WriteLine(" (student)");
                        }
                    }
    
                    // Non-polymorphic Queries                
                    var students = ctx.People.OfType<Student>().ToList();
                    WriteLine("\r\nStudents:");
                    foreach (var s in students)
                    {
                        WriteLine($"{s.FirstName} {s.LastName}, studying {s.Major}, specialized in {s.Specialization}");
                    }
    
                    var teachers = ctx.People.OfType<Teacher>().ToList();
                    WriteLine("\r\nTeachers:");
                    foreach (var t in teachers)
                    {
                        WriteLine($"{t.FirstName} {t.LastName} ({t.Qualification}), working at {t.Department}");
                    }
                }
    
                ReadKey();
            }
        }
    }

    Kết quả chạy chương trình trên:

    Bạn có thể thấy, Entity Framework xử lý rất tốt quan hệ kế thừa.

    Cách thực hiện kế thừa với TPH

    Trong ví dụ trên bạn đã xây dựng 3 class Person, StudentTeacher. Trong đó Student Teacher đều kế thừa lớp abstract Person tạo thành cây kế thừa như sau:

    Hãy để ý một vấn đề khác: trong lớp Context bạn không thấy yêu cầu tạo bảng tương ứng với Student Teacher mà chỉ có yêu cầu tạo bảng tương ứng với Person.

    public class Context : DbContext
    {
        public Context() : base("Inheritance")
        {
        }
        public DbSet<Person> People { get; set; } // chỉ tạo bảng People tương ứng với class Person
    }

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

    quan hệ kế thừa với table per hierarchy

    Bạn dễ dàng nhận thấy, bảng People chứa tất cả các property của các class trong chuỗi kế thừa: FirstName LastName của Person, Major Specialization của Student, Department Qualification của Teacher. Ngoài ra xuất hiện thêm một cột “lạ” Discriminator.

    Đặc biệt hơn nữa, nếu bạn mở dữ liệu của bảng này sẽ thấy tình trạng như sau:

    Trường Discriminator chứa thông tin về kiểu cụ thể của object. Như trên bạn đã thấy, bản ghi có Id = 1, 2, 3 thuộc về kiểu Student, Id = 4, 5 thuộc về kiểu Teacher.

    Do cùng kế thừa từ Person, Student hay Teacher đều có đủ thông tin về FirstName và LastName. Tuy nhiên, đối với các trường còn lại, tùy vào kiểu cụ thể của object, sẽ có những ô để trống. Ví dụ, với Student thì phần Department và Qualification luôn để trống (vì Student không có các thông tin này). Tương tự với Teacher thì Major và Specialization lại để trống.

    Khi nhìn dữ liệu trên bạn hẳn cũng nhận ra một nhược điểm của cách tiếp cận TPH: nếu các class con càng khác biệt và cấu trúc cây kế thừa phức tạp, bảng dữ liệu trên sẽ càng phình to và có nhiều ô trống.

    TPH là cơ chế thực thi kế thừa mặc định của Entity Framework. Bạn không cần thực hiện bất kỳ thao tác cấu hình thêm nào nữa. Tất cả được Entity Framework thực hiện tự động.

    Truy vấn dữ liệu với kế thừa trong Entity Framework

    Với đặc điểm kết hợp kế thừa và đa hình (polymorphism) trong C#, property People khai báo như trên hoàn toàn có thể nhận danh sách object thuộc kiểu Student Teacher như bạn đã thấy trong phương thức Main:

    ctx.People.AddRange(new Student[]
    {
    new Student { FirstName = "Donald", LastName = "Trump", Major = "Computer Science", Specialization = "Information Systems"},
    new Student { FirstName = "Barack", LastName = "Obama", Major = "Computer Science", Specialization = "Sofware Engineering"},
    new Student { FirstName = "George", LastName = "Bush", Major = "Computer Science", Specialization = "System Administration"},
    });
    
    ctx.People.AddRange(new Teacher[]
    {
    new Teacher { FirstName = "Vladimir", LastName = "Putin", Department = "KGB", Qualification = "PhD"},
    new Teacher { FirstName = "Boris", LastName = "Yeltsin", Department = "KGB", Qualification = "PhD"},
    });

    Điều này hoàn toàn bình thường và phổ biến trong lập trình C#.

    Entity Framework cho phép thực hiện được cả hai loại truy vấn: polymorphicnon-polymorphic.

    Bạn có thể truy vấn lấy danh sách People: var people = ctx.People.ToList();, lấy riêng danh sách Student: var students = ctx.People.OfType<Student>().ToList(); và lấy riêng danh sách Teacher: var teachers = ctx.People.OfType<Teacher>().ToList();.

    Các truy vấn kiểu var people = ctx.People.ToList(); được gọi là Polymorphic Query. Loại truy vấn này không phân biệt kiểu cụ thể của object và chỉ căn cứ vào kiểu cha (Person trong ví dụ trên). Trong quá trình sử dụng, bạn có thể ép kiểu (casting) về kiểu cụ thể hoặc sử dụng các phép toán kiểm tra kiểu (phép toán is như đã sử dụng trong bài).

    foreach (var p in people)
    {
        Write($"{p.FirstName} {p.LastName}");
        if (p is Teacher)
        {
            WriteLine(" (teacher)");
        }
        else
        {
            WriteLine(" (student)");
        }
    }

    Các truy vấn như var students = ctx.People.OfType<Student>().ToList();var teachers = ctx.People.OfType<Teacher>().ToList(); được gọi là non-polymorphic query. Loại truy vấn này yêu cầu lấy các object thuộc về kiểu con cụ thể của Person.

    Bạn có thể thấy, sự khác biệt giữa kế thừa trong lập trình hướng đối tượng và cơ sở dữ liệu quan hệ dường như đã được giải quyết.

    Cách tiếp cận THP rất đơn giản, dễ hiểu nhưng đồng thời nó cũng có hạn chế khi tạo ra các bảng quá lớn nếu cấu trúc kế thừa phức tạp. Đồng thời nó cũng tạo ra nhiều ô trống trong bảng. Nói cách khác, bảng dữ liệu tạo ra trong THP không được chuẩn hóa (normalize).

    Tuy nhiên, cách tiếp cận THP lại có ưu điểm lớn về hiệu suất, kể cả khi cấu trúc kế thừa phức tạp hơn. Lý do là vì tất cả dữ liệu đều nằm trong cùng một bảng, THP không cần phải join các bảng với nhau.

    Table per Type (TPT)

    Hãy cùng thực hiện một ví dụ để hiểu cách tiếp cận TPT.

    Tạo một project mới P02_TablePerType, cài đặt Entity Framework và viết code cho Program.cs như sau:

    using System.Linq;
    using System.Data.Entity;
    using static System.Console;
    using System.ComponentModel.DataAnnotations.Schema;
    
    namespace P02_TablePerType
    {
        public abstract class Person
        {
            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
        }
    
        [Table("Students")]
        public class Student : Person
        {
            public string Major { get; set; }
            public string Specialization { get; set; }
        }
    
        [Table("Teachers")]
        public class Teacher : Person
        {
            public string Department { get; set; }
            public string Qualification { get; set; }
        }
    
        public class Context : DbContext
        {
            public Context() : base("TablePerType")
            {
    
            }
    
            public DbSet<Person> People { get; set; }
        }
    
        class Program
        {
            static void Main(string[] args)
            {
                Title = "Table per Type";
                using (var ctx = new Context())
                {
                    if (!ctx.Database.Exists())
                    {
                        ctx.Database.Initialize(false);
    
                        ctx.People.AddRange(new Student[]
                        {
                            new Student { FirstName = "Donald", LastName = "Trump", Major = "Computer Science", Specialization = "Information Systems"},
                            new Student { FirstName = "Barack", LastName = "Obama", Major = "Computer Science", Specialization = "Sofware Engineering"},
                            new Student { FirstName = "George", LastName = "Bush", Major = "Computer Science", Specialization = "System Administration"},
                                                });
    
                        ctx.People.AddRange(new Teacher[]
                        {
                            new Teacher { FirstName = "Vladimir", LastName = "Putin", Department = "KGB", Qualification = "PhD"},
                            new Teacher { FirstName = "Boris", LastName = "Yeltsin", Department = "KGB", Qualification = "PhD"},
                        });
    
                        ctx.SaveChanges();
                    }
    
                    // Polymorphic Queries                
                    var people = ctx.People.ToList();
                    WriteLine("People:");
                    foreach (var p in people)
                    {
                        Write($"{p.FirstName} {p.LastName}");
                        if (p is Teacher)
                        {
                            WriteLine(" (teacher)");
                        }
                        else
                        {
                            WriteLine(" (student)");
                        }
                    }
    
                    // Non-polymorphic Queries                
                    var students = ctx.People.OfType<Student>().ToList();
                    WriteLine("\r\nStudents:");
                    foreach (var s in students)
                    {
                        WriteLine($"{s.FirstName} {s.LastName}, studying {s.Major}, specialized in {s.Specialization}");
                    }
    
                    var teachers = ctx.People.OfType<Teacher>().ToList();
                    WriteLine("\r\nTeachers:");
                    foreach (var t in teachers)
                    {
                        WriteLine($"{t.FirstName} {t.LastName} ({t.Qualification}), working at {t.Department}");
                    }
                }
    
                ReadKey();
            }
        }
    }

    Code của ví dụ này hoàn toàn giống như ví dụ với TPH. Bạn có thể copy nguyên vẹn code từ ví dụ trước sang. Khác biệt chỉ ở dòng 15 và 22 khi khai báo hai class con Student và Teacher:

    [Table("Students")]
    public class Student : Person
    { ...
    
    [Table("Teachers")]
    public class Teacher : Person
    { ...

    Ở đây bạn sử dụng attribute Table để chỉ định ánh xạ class sang bảng có tên tương ứng: class Student sang bảng Students, class Teacher sang bảng Teachers.

    Khi chạy chương trình bạn sẽ thu được cùng kết quả như ví dụ trên.

    Tuy nhiên, nếu mở sơ đồ CSDL bạn sẽ thấy một kết quả rất khác:

    Quan hệ kế thừa với table per type

    Bạn để ý thấy, class Person giờ ánh xạ sang bảng People, class Teacher => bảng Teachers, class Student => bảng Students. Teachers và Students cùng có quan hệ một – một với People.

    Như vậy, để tạo quan hệ kế thừa với cách tiếp cận TPT cũng rất đơn giản: chỉ cần đặt attribute Table trước các class con.

    Ưu điểm lớn nhất của TPT là các bảng CSDL đều được chuẩn hóa. Việc mở rộng các class (và các bảng tương ứng) cũng rất dễ dàng.

    Tuy nhiên, TPT lại có nhược điểm về hiệu suất khi mở rộng cây kế thừa. Vấn đề nằm ở chỗ, khi truy xuất dữ liệu, TPT phải join nhiều bảng với nhau.

    Đôi lời về cách tiếp cận Table per Concrete Type – TPC. Đây là cách tiếp cận phức tạp nhất. Bạn phải thực hiện nhiều thao tác cấu hình nâng cao. Vì vậy, trong bài học này chúng ta sẽ không xem xét kỹ cách tiếp cận này. Ngoài ra, cách truy vấn polymorphic với TCP không ổn định do các bảng được tạo ra từ mỗi class con không hề có quan hệ gì với nhau. Vì vậy, cách tiếp cận TPC chỉ phù hợp nếu không có nhu cầu thực hiện loại truy vấn này. Tuy nhiên, TPC lại có ưu điểm cả về hiệu suất và chuẩn hóa bảng.

    Kết luận

    Trong bài học này, bạn đã làm quen với cách xử lý quan hệ kế thừa trong Entity Framework. Bạn cũng đi sâu xem xét cách thực hiện mặc định Table per Hierarchy.

    Do sự không tương thích giữa cơ sở dữ liệu quan hệ và ngôn ngữ lập trình hướng đối tượng, mỗi cách thực hiện trên đều có những ưu nhược điểm của riêng nó. Do vậy, khi lựa chọn phương án nào, bạn nên cân đối giữa ưu điểm về cấu trúc CSDL, hiệu suất thực hiện truy vấn, yêu cầu đối với truy vấn, và khả năng mở rộng.

    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ề