Change Tracking – theo dõi object trong Entity Framework

    0

    Change Tracking là cơ chế giúp theo dõi sự thay đổi của từng object do Entity Framework đang quản lý trong bộ nhớ. Việc theo dõi giúp Entity Framework sinh ra những truy vấn phù hợp để thực thi trên cơ sở dữ liệu. Nhờ cơ chế này, việc cập nhật (thêm/sửa/xóa) object từ client code được đơn giản hóa.

    Trong bài học này, chúng ta sẽ xem xét các nguyên lý chung của Change Tracking cũng như một số kỹ thuật khai thác tính năng này trong Entity Framework.

    Các cơ chế Change Tracking

    Ngay từ những bài học đầu tiên về Entity Framework bạn thực tế đã tiếp xúc với cơ chế Change Tracking, mặc dù bạn không hề (và không cần) để ý đến sự tồn tại của nó trong khi viết code.

    Đây là cơ chế theo dõi sự thay đổi của từng object, bao gồm những object bạn tải lên từ cơ sở dữ liệu, cũng như những object mới được đăng ký. Bất kỳ sự thay đổi nào của object, như thêm mới, sửa, xóa trên object và quan hệ giữa chúng đều được lưu lại.

    Cơ chế này giúp Entity Framework sinh ra những truy vấn SQL phù hợp để cập nhật đầy đủ trạng thái của các object trở lại cơ sở dữ liệu.

    Có hai cơ chế theo dõi sự thay đổi của object được sử dụng trong Entity Framework: snapshotproxy.

    Snapshot change tracking

    Trong phương pháp này, Entity Framework sẽ “chụp” lại giá trị của từng property ngay khi nó “nhìn thấy” object lần đầu tiên. Mỗi “ảnh chụp” như thế gọi là một snapshot. Snapshot được tạo ra khi bạn tải object vào bộ nhớ, và khi bạn đăng ký object mới vào DbSet.

    Tạo sao Entity Framework phải chụp lại giá trị của từng property? Bạn có thể đoán ra. Mỗi domain class bạn xây dựng là một POCO. Trong đó không có gì khác ngoài các property. Như vậy nó không chứa bất kỳ logic nào để báo cho Entity Framework về sự thay đổi giá trị của các property. Snapshot là cách duy nhất để lưu lại giá trị của mỗi property ở những thời điểm “quan trọng”.

    Khi Entity Framework cần xác định những sự thay đổi nào đã diễn ra, nó sẽ scan từng object và so sánh giá trị hiện tại với snapshot. Quá trình scan được kích hoạt khi gọi phương thức DetectChanges.

    Đây là cơ chế bạn đã sử dụng trong những bài học trước đây.

    Change tracking proxy

    Cơ chế này sử dụng mẫu thiết kế proxy để tạo ra những class mới kế thừa từ domain class do bạn xây dựng. Các class mới này gọi là các dynamic proxy và được tạo ra khi chương trình đang hoạt động.

    Nếu bạn còn nhớ bài học về lazy loading thì dynamic proxy cũng đồng thời là cơ chế giúp cho lazy loading hoạt động.

    Trong các proxy này Entity Framework xây dựng các logic riêng để trao đổi thông tin về sự thay đổi của object.

    Proxy có hiệu suất rất tốt, đặc biệt là khi phải xử lý một số lượng lớn object. Tuy nhiên, để sử dụng cơ chế này bạn phải áp dụng một số quy tắc đặc biệt khi xây dựng domain class mà chút nữa chúng ta sẽ xem xét chi tiết sau.

    Các trạng thái của object

    Dù sử dụng cơ chế theo dõi nào, mỗi object trong diện “bị theo dõi” tại mỗi thời điểm luôn nhận một trong các trạng thái sau:

    • Unchanged: chưa có thay đổi gì;
    • Modified: đã có property nào đó thay đổi giá trị;
    • Deleted: đã bị đánh dấu xóa;
    • Added: mới được đăng ký.

    Làm việc với Change Tracking Proxy

    Trước khi trao đổi kỹ hơn về proxy, hãy cùng làm một ví dụ sau:

    Bước 1. Tạo một solution trống và thêm một project P02_Proxies (ConsoleApp .NET Framework).

    Bước 2. Cài đặt thư viện Entity Framework cho project.

    Bước 3. Viết code cho file Program.cs như sau:

    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Core.Objects.DataClasses;
    using System.Linq;
    using static System.Console;
    
    namespace P02_Proxies
    {
        public class Initializer : DropCreateDatabaseAlways<Context>
        {
            protected override void Seed(Context context)
            {
                var teachers = new Teacher[]
                {
                    new Teacher{ Name = "Albus Dumbledore", Position = "Prof."},
                    new Teacher{ Name = "Severus Snape", Position = "Prof."},
                };
                context.Teachers.AddRange(teachers);
    
                var courses = new Course[]
                {
                    new Course{ Name = "History of Magic", Credit = 3, Teacher = teachers[0]},
                    new Course{ Name = "Magical Creatures", Credit = 2, Teacher = teachers[1]},
                    new Course{ Name = "Muggle Studies", Credit = 4, Teacher = teachers[0]},
                };
                context.Courses.AddRange(courses);
    
                context.SaveChanges();
            }
        }
    
        public class Context : DbContext
        {
            public Context() : base("Proxies")
            {
                Database.SetInitializer(new Initializer());
            }
    
            public DbSet<Teacher> Teachers { get; set; }
            public DbSet<Course> Courses { get; set; }
        }
    
        public class Teacher
        {
            public virtual int Id { get; set; }
            public virtual string Name { get; set; }
            public virtual string Position { get; set; }
    
            public virtual ICollection<Course> Courses { get; set; }
        }
    
        public class Course
        {
            public virtual int Id { get; set; }
            public virtual string Name { get; set; }
            public virtual int Credit { get; set; }
            public virtual Teacher Teacher { get; set; }
        }
    
        class Program
        {
            static void TestDetectChanges()
            {
                ForegroundColor = System.ConsoleColor.Cyan;
                WriteLine("### Test for state change detection ###");
                ResetColor();
                using (var ctx = new Context())
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
    
                    var teacher = ctx.Teachers.First();
                    WriteLine($"After loading: {ctx.Entry(teacher).State}");
    
                    teacher.Position = "aProf.";
                    WriteLine($"After updating: {ctx.Entry(teacher).State}");
    
                    ctx.Teachers.Remove(teacher);
                    WriteLine($"After deleting: {ctx.Entry(teacher).State}");
    
                    var trump = new Teacher { Name = "Donald Trump" };
                    ctx.Teachers.Add(trump);
                    WriteLine($"After adding: {ctx.Entry(trump).State}");
                }
                WriteLine();
            }
    
            static void TestProxy<T>(T entity) where T : class
            {
                var isProxy = entity is IEntityWithChangeTracker;
                WriteLine($"Object is {(isProxy ? "" : "not")} a proxy");
            }
    
            static void TestProxy()
            {
                ForegroundColor = System.ConsoleColor.Green;
                WriteLine("### Test proxy for loaded objects ###");
                ResetColor();
                using (var ctx = new Context())
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
    
                    var teacher = ctx.Teachers.First();
                    TestProxy(teacher);
    
                    var course = ctx.Courses.First();
                    TestProxy(course);
                }
                WriteLine();
            }
    
            static void ProxyCreation()
            {
                ForegroundColor = System.ConsoleColor.Yellow;
                WriteLine("### Test proxy for created objects ###");
                ResetColor();
    
                using (var ctx = new Context())
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
    
                    var trump = new Teacher { Name = "Donald Trump" };
                    ctx.Teachers.Add(trump);
                    TestProxy(trump);
    
                    var obama = ctx.Teachers.Create();
                    ctx.Teachers.Add(obama);
                    TestProxy(obama);
                }
                WriteLine();
            }
    
            static void TrackNonProxy()
            {
                ForegroundColor = System.ConsoleColor.Magenta;
                WriteLine("### Test tracking non proxy object ###");
                ResetColor();
    
                using (var ctx = new Context())
                {
                    ctx.Configuration.AutoDetectChangesEnabled = false;
    
                    var trump = new Teacher { Name = "Donald Trump" };
                    ctx.Teachers.Add(trump);
                    TestProxy(trump);
                    WriteLine($"After adding Trump: {ctx.Entry(trump).State}");
                    ctx.SaveChanges();
                    trump.Position = "Prof.";
                    WriteLine($"After changing Trump: {ctx.Entry(trump).State}");
    
                    var obama = ctx.Teachers.Create();
                    ctx.Teachers.Add(obama);
                    TestProxy(obama);
                    WriteLine($"After adding Obama: {ctx.Entry(obama).State}");
                    ctx.SaveChanges();
                    obama.Position = "Prof.";
                    WriteLine($"After changing Obama: {ctx.Entry(obama).State}");
                }
                WriteLine();
            }
    
            static void Main(string[] args)
            {
                Title = "Change Tracking Proxies";
    
                using (var ctx = new Context())
                {
                    ctx.Database.Initialize(true);
                }
    
                TestProxy();
                TestDetectChanges();
                ProxyCreation();
                TrackNonProxy();
    
                ReadKey();
            }
        }
    }

    Chạy chương trình và bạn sẽ thu được kết quả như sau:

    Chúng ta sẽ phân tích chương trình trên để hiểu cách thức làm việc của proxy.

    Yêu cầu đối với model class

    Trong ví dụ trên chúng ta cũng xây dựng hai domain class Teacher và Course. Tuy nhiên nó nhìn hơi khác với những domain class đã từng xây dựng.

    Để sử dụng cơ chế Change Tracking Proxy bạn phải tuân thủ một số quy tắc sau:

    1. Các domain class phải đặt truy cập publickhông được sealed.
    2. Mọi property phải đánh dấu virtual.
    3. Mọi property phải có public getter và setter.
    4. Mọi collection navigation property phải có kiểu ICollection<T>.

    Chúng ta đã vận dụng đầy đủ các yêu cầu này trong Teacher và Course.

    Kiểm tra xem một object có phải là proxy không

    Tính năng Change Tracking chỉ có thể áp dụng được đối với proxy object.

    Để kiểm tra xem một object có phải proxy hay không, chúng ta đã xây dựng một phương thức kiểm tra đơn giản:

    static void TestProxy<T>(T entity) where T : class
    {
        var isProxy = entity is IEntityWithChangeTracker;
        WriteLine($"Object is {(isProxy ? "" : "not")} a proxy");
    }

    Mỗi object thuộc những class xây dựng theo quy tắc trên khi được tải từ cơ sở dữ liệu mặc định sẽ tạo ra một proxy tương ứng. Do vậy phương thức TestProxy

    static void TestProxy()
    {
        ForegroundColor = System.ConsoleColor.Green;
        WriteLine("### Test proxy for loaded objects ###");
        ResetColor();
        using (var ctx = new Context())
        {
            ctx.Configuration.AutoDetectChangesEnabled = false;
    
            var teacher = ctx.Teachers.First();
            TestProxy(teacher);
    
            var course = ctx.Courses.First();
            TestProxy(course);
        }
        WriteLine();
    }

    luôn luôn thông báo hai object teacher và course là các proxy.

    Theo dõi sự thay đổi của các proxy object

    Nếu một object là proxy, Entity Framework tự nhiên có khả năng theo dõi trạng thái của nó. Ở mức độ đơn giản nhất chúng ta có thể kiểm tra xem hiện object đang nằm ở trạng thái nào (Unchanged, Adđe, Modified, Deleted) của proxy object trong phương thức TestDetectChanges.

    static void TestDetectChanges()
    {
        ForegroundColor = System.ConsoleColor.Cyan;
        WriteLine("### Test for state change detection ###");
        ResetColor();
        using (var ctx = new Context())
        {
            ctx.Configuration.AutoDetectChangesEnabled = false;
    
            var teacher = ctx.Teachers.First();
            WriteLine($"After loading: {ctx.Entry(teacher).State}");
    
            teacher.Position = "aProf.";
            WriteLine($"After updating: {ctx.Entry(teacher).State}");
    
            ctx.Teachers.Remove(teacher);
            WriteLine($"After deleting: {ctx.Entry(teacher).State}");
    
            var trump = new Teacher { Name = "Donald Trump" };
            ctx.Teachers.Add(trump);
            WriteLine($"After adding: {ctx.Entry(trump).State}");
        }
        WriteLine();
    }

    Trong phương thức này bạn chỉ đơn giản là thực hiện một số thao tác CRUD trên object và kiểm tra trạng thái của nó qua property State của Entry.

    Tạo proxy object

    Các object của domain class bạn tự tạo bằng constructor sẽ không phải là proxy. Để một object mới tạo ra đồng thời sẽ tạo proxy cho nó, bạn phải sử dụng đến phương thức Create của DbSet:

    static void ProxyCreation()
    {
        ForegroundColor = System.ConsoleColor.Yellow;
        WriteLine("### Test proxy for created objects ###");
        ResetColor();
    
        using (var ctx = new Context())
        {
            ctx.Configuration.AutoDetectChangesEnabled = false;
    
            var trump = new Teacher { Name = "Donald Trump" };
            ctx.Teachers.Add(trump);
            TestProxy(trump);
    
            var obama = ctx.Teachers.Create();
            ctx.Teachers.Add(obama);
            TestProxy(obama);
        }
        WriteLine();
    }

    Ở phương thức này, trump được tạo bằng constructor thông thường, do đó nó không phải là proxy. Obama được tạo ra bằng phương thức Create nên nó sẽ được tạo proxy.

    Nếu object không có proxy, trạng thái của nó sẽ không được Entity Framework theo dõi. Do vậy, trong phương thức

    static void TrackNonProxy()
    {
        ForegroundColor = System.ConsoleColor.Magenta;
        WriteLine("### Test tracking non proxy object ###");
        ResetColor();
    
        using (var ctx = new Context())
        {
            ctx.Configuration.AutoDetectChangesEnabled = false;
    
            var trump = new Teacher { Name = "Donald Trump" };
            ctx.Teachers.Add(trump);
            TestProxy(trump);
            WriteLine($"After adding Trump: {ctx.Entry(trump).State}");
            ctx.SaveChanges();
            trump.Position = "Prof.";
            WriteLine($"After changing Trump: {ctx.Entry(trump).State}");
    
            var obama = ctx.Teachers.Create();
            ctx.Teachers.Add(obama);
            TestProxy(obama);
            WriteLine($"After adding Obama: {ctx.Entry(obama).State}");
            ctx.SaveChanges();
            obama.Position = "Prof.";
            WriteLine($"After changing Obama: {ctx.Entry(obama).State}");
        }
        WriteLine();
    }

    Sau khi được lưu vào cơ sở dữ liệu, nếu bạn tiếp tục thay đổi trump, trạng thái của nó vẫn là Unchanged. Trump không phải là proxy nên trạng thái của nó không tiếp tục được theo dõi.

    Lưu ý rằng, Entity Framework sẽ tự động sử dụng cơ chế proxy nếu nó tạo được proxy từ object. Nếu không, cơ chế snapshot sẽ được sử dụng trên object. Cả hai cơ chế theo dõi này có thể hoạt động song song trong Entity Framework (mixed mode).

    Ở phương thức TrackNonProxy bên trên nếu bạn không tắt chế độ theo dõi snapshot bằng lệnh:

    ctx.Configuration.AutoDetectChangesEnabled = false;

    thì Entity Framework đồng thời sử dụng cả hai cơ chế: proxy để theo dõi obama, snapshot để theo dõi trump.

    Kết luận

    Change Tracking có thể nói là cơ chế hữu dụng hàng đầu đối với việc xử lý dữ liệu của người lập trình. Nhờ có nó, công việc thêm/sửa/xóa dữ liệu trở nên vô cùng đơn giản trong client code.

    Đây là bài học cuối cùng của bài giảng Tự học lập trình ADO.NET và Entity Framework.

    Qua loạt bài học này bạn đã được tiếp xúc một cách bài bản từ những kỹ thuật cơ bản nhất của lập trình cơ sở dữ liệu với ADO.NET, tới những kỹ thuật xây dựng Entity Data Model với tiếp cận code-first, và cách thức truy vấn / xử lý dữ liệu sử dụng LINQ to Entities và DbContext API.

    Mặc dù bài học đã trình bày rất chi tiết các vấn đề quan trọng nhất của lập trình với Entity Framework, còn rất nhiều vấn đề nâng cao khác mà bạn sẽ cần đến khi tiếp xúc với các dự án thực tế. Các nội dung này vượt quá giới hạn kiến thức của bài giảng này. Tuy nhiên, chúng tôi sẽ lần lượt đề cập đến một số vấn đề trong đó ở chuyên mục “Hướng dẫn”.

    Trong quá trình xây dựng các bài học, dù đã rất cẩn trọng nhưng chắc chắn không thể tránh khỏi các sai sót. Chúng tôi chân thành xin lỗi độc giả về bất kỳ sai sót (cả về hình thức và kiến thức) nếu có. Trong trường hợp phát hiện sai sót, kính mong bạn đọc thông báo giúp cho nhóm quản trị thông qua comment ở cuối mỗi bài, qua form liên lạc hoặc qua email.

    Xin trân trọng cảm ơn!

    * Bản quyền bài viết thuộc về Tự học ICT. Đề nghị tôn trọng bản quyền. DMCA.com Protection Status
    Subscribe
    Notify of
    guest
    0 Thảo luận
    Inline Feedbacks
    View all comments