یکی از پراپرتی های کلاس DbContext، پراپرتی ChangeTracker می باشد که وظیفه آن مدیریت تغییرات entityها و اعمال تغییرات در دیتابیس می باشد.
وقتی entityهای ما تحت کنترل Entity Framework قرار می گیرند، یک پراپرتی به نام EntityState از نوع Enum به آنها اضافه می شود که با توجه به وضعیت entity می تواند مقادیر مختلفی داشته باشد.
برای مثال وقتی که یک entity جهت ویرایش کردن از دیتابیس خوانده می شود، برای کل پراپرتی های آن اسکریپت ویرایش نوشته می شود در حالی که ممکن است فقط یک پراپرتی از آن را تغییر داده باشیم. با استفاده از مفهومی به نام IsModified، می توانیم تشخیص دهیم مقدار کدام پراپرتی تغییر کرده است. دستورات ویرایش برای پراپرتی هایی در نظر گرفته می شود که مقدار IsModified آنها true می باشد.
زمانی که ما قصد ویرایش یک entity را داشته باشیم، entity از دیتابیس خوانده می شود، پراپرتی های آن تغییر می کند و آن entity در همان context جاری ذخیره می شود. در این حالت DetectChange اتفاق می افتد. تابع DetectChange با استفاده از Tracking snapshot می تواند پراپرتی های entity را مقایسه کند. به محض اینکه به یک پراپرتی برسد که مقدار آن تغییر کرده باشد، IsModified آن پراپرتی را true می کند و وضعیت خود entity را نیز برابر Modified قرار می دهد و اگر تغییر نکرده باشد وضعیت Unchanged باقی می ماند. این حالت برای اپلیکیشن هایی که دیتای زیادی در حافظه load کرده اند می تواند مشکل ایجاد کند. هر بار که تابع SaveChanges را صدا می زنیم تمام پراپرتی های کل entityهای موجود بررسی می شوند که متوجه شود تغییر کرده اند یا خیر.
برای آبجکت هایی که با تعداد بالا در حافظه load شده اند، می توانیم اینترفیسی به نام INotifyPropertyChanged را پیاده سازی کنیم و عملیات تشخیص تغییرات را خود ما انجام می دهیم.
public class Person : INotifyPropertyChanged { private int _personId; public int PersonId { get { return _personId; } set { SetWithNotify(value, ref _personId); } } private string _personName; public string PersonName { get { return _personName; } set { SetWithNotify(value, ref _personName); } } private string _personLastName; public string PersonLastName { get { return _personLastName; } set { SetWithNotify(value, ref _personLastName); } } public event PropertyChangedEventHandler PropertyChanged; protected void SetWithNotify<T> (T value, ref T field, [CallerMemberName] string propertyName = "") { if (Equals(field, value) == false) { field = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }
در مرحله اول در یک تابع به نام SetWithNotify مقدار پراپرتی را قرار می دهیم و PropertyChanged event را raze می کنیم.
در مرحله بعد باید در configuration اعلام کنیم که برای این entity از این روش برای تشخیص تغییرات پراپرتی ها استفاده شود.
public class PersonContext : DbContext { public DbSet<Person> Persons { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>().HasChangeTrackingStrategy (ChangeTrackingStrategy.ChangedNotifications); base.OnModelCreating(modelBuilder); } }
از طریق متد HasChangeTrackingStrategy، استراتژی خود که در این حالت ChangedNotifications می باشد را قرار می دهیم.
اگر قصد داشته باشیم برای کل entityها به شرطی که همه از جنس INotifyPropertyChanged باشند، این استراتژی را قرار دهیم، از دستور زیر استفاده می کنیم.
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangedNotifications);
گاهی اوقات اشیاء از context ما detached می شوند. در حالتی مثل استفاده از تابع AsNoTracking یا حالتی را در نظر بگیرید که در فرم ویرایش عملیات submit انجام می شود و آبجتی که ارسال می شود در آن context نمی باشد. در این مواقع برای ویرایش این گونه اشیاء، می توانیم یک بار آن شی را از context بخوانیم و هر پراپرتی که می خواهیم را تغییر دهیم و ذخیره کنیم و یا می توانیم از تابع Attach استفاده کنیم. در لحظه Attach، وضعیت آبجکت نسبت به حالت قبلی Unchanged می باشد.
var person = new Person { PersonId = 2, PersonName = "John" }; using (var ctx = new PersonContext()) { ctx.Attach(person); ctx.Entry(person).Property(nameof(Person.PersonName)).IsModified = true; ctx.SaveChanges(); }
در این مثال فقط مقدار PersonName ویرایش می شود.
در شرایطی که با یک گراف روبرو می شویم، از ChangeTracker استفاده می کنیم.
Book book = new Book { BookId = 1, BookName = "EF Core", Authors = new List<Author> { new Author{Name = "Bob"}, new Author{Name = "John"}, } }; using (var ctx = new PersonContext()) { ctx.ChangeTracker.TrackGraph(book, c => { if (c is Book) { } else if (c is Author) { } }); }
تابع TrackGraph، روت گراف به همراه یک اکشن را به عنوان ورودی دریافت می کند و تمام گراف را برای ما پیمایش می کند و ما باید در هر پیمایش کاری که می خواهیم را انجام دهیم.
با استفاده از ChangeTracker، می توانیم برخی از کارهای تکراری را در لحظه Save انجام دهیم.
public override int SaveChanges() { foreach (var entity in ChangeTracker.Entries() .Where(e => (e.State == EntityState.Added || e.State == EntityState.Modified))) { var myentity = entity.Entity as BaseEntity; if (entity.State == EntityState.Added) { myentity.InsertDate = DateTime.Now; } myentity.UpdateDate = DateTime.Now; } return base.SaveChanges(); }
متد SaveChanges را override کردیم و برای هر entity که قرار است عملیات SaveChanges برای آن اتفاق بیافتد، پراپرتی های InsertDate و UpdateDate آن ها را مقدار دهی می کنیم و از این طریق کد های تکراری در سطح اپلیکیشن را می توانیم مجتمع کنیم.
پایان