روشهای مختلف اطلاع رسانی به سیستم ردیابی تغییرات
متد DbSet.Add کار اطلاع رسانی تبدیل وهلههای ثبت شده را به کوئریهای Insert رکوردهای جدید، انجام میدهد:
using (var db = new BloggingContext()) { var blog = new Blog { Url = "http://sample.com" }; db.Blogs.Add(blog); db.SaveChanges(); }
سیستم ردیابی اطلاعات، اگر تغییراتی را در خواص اشیاء تحت نظر خود مشاهده کند، سبب تولید کوئریهای Update میگردد. یک چنین اشیایی تحت نظر Context هستند:
الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.
ب) اشیایی که در طول عمر Context به آن اضافه شدهاند (حالت قبل).
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); blog.Url = "http://sample.com/blog"; db.SaveChanges(); }
و متد DbSet.Remove کار اطلاع رسانی تبدیل وهلههای حذف شده را به کوئریهای Delete معادل، انجام میدهد:
using (var db = new BloggingContext()) { var blog = db.Blogs.First(); db.Blogs.Remove(blog); db.SaveChanges(); }
به علاوه امکان ترکیب متدهای Add، Remove و همچنین به روز رسانی اشیاء در طی یک Context و با فراخوانی یک SaveChanges در انتهای کار نیز وجود دارد. از این جهت که یک Context، الگوی واحد کار را پیاده سازی میکند و بیانگر یک تراکنش است. در این حالت ترکیبی، یا کل تراکنش با موفقیت به پایان میرسد و یا در صورت بروز مشکلی، هیچکدام از تغییرات درخواستی، اعمال نخواهند شد.
عملیات ردیابی، بر روی هر نوع Projections صورت نمیگیرد
اگر توسط LINQ Projections، نتیجهی نهایی کوئری را تغییر دادید، فقط در زمانی سیستم ردیابی بر روی آن فعال خواهد بود که projection نهایی حاوی اصل موجودیت مدنظر باشد. برای مثال در کوئری ذیل چون در Projection صورت گرفتهی در متد Select، هنوز در خاصیت Blog، به اصل موجودیت Blog اشاره میشود، نتیجهی این کوئری نیز تحت نظر سیستم ردیابی خواهد بود:
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Blog = b, Posts = b.Posts.Count() }); }
using (var context = new BloggingContext()) { var blog = context.Blogs .Select(b => new { Id = b.BlogId, Url = b.Url }); }
لغو سیستم ردیابی تغییرات، در زمانیکه به آن نیازی نیست
سیستم ردیابی تغییرات بر اساس مفاهیم AOP و تولید پروکسیهای آن کار میکند. این پروکسیها، اشیایی شفاف هستند که اشیاء شما را احاطه میکنند و هر تغییری را که اعمال میکنید، ابتدا از این غشاء رد شده و در سیستم ردیابی EF ثبت میشوند. سپس به وهلهی اصلی شیء موجود اعمال خواهند شد.
بدیهی است تولید این پروکسیها، دارای سربار است و اگر هدف شما صرفا کوئری گرفتن از اطلاعات، جهت نمایش آنها است، نیازی به تولید خودکار این پروکسیها را ندارید و این مساله سبب کاهش مصرف حافظهی برنامه و بالا رفتن سرعت آن میشود.
در قسمت قبل عنوان شد که «یک چنین اشیایی تحت نظر Context هستند: الف) اشیایی که در طول عمر Context از دیتابیس کوئری گرفته شدهاند.»
اگر میخواهید این حالت پیش فرض را لغو کنید، از متد AsNoTracking استفاده نمائید:
using (var context = new BloggingContext()) { var blogs = context.Blogs.AsNoTracking().ToList(); }
اگر میخواهید متد AsNoTracking را به صورت خودکار به تمام کوئریهای یک context خاص اعمال کنید، روش کار و تنظیم آن به صورت زیر است:
using (var context = new BloggingContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
نکات به روز رسانی ارجاعات موجودیتها
دو حالت زیر را درنظر بگیرید که در اولی، blog از بانک اطلاعاتی واکشی شدهاست و post به صورت مستقیم وهله سازی شدهاست:
using (var context = new BloggingContext()) { var blog = context.Blogs.First(); var post = new Post { Title = "Intro to EF Core" }; blog.Posts.Add(post); context.SaveChanges(); }
using (var context = new BloggingContext()) { var blog = new Blog { Url = "http://blogs.msdn.com/visualstudio" }; var post = context.Posts.First(); blog.Posts.Add(post); context.SaveChanges(); }
در حالت دوم، ابتدا blog در بانک اطلاعاتی ثبت میشود (چون برخلاف حالت اول، تحت نظر context نیست) و سپس این post (که تحت نظر context است) به مجموعه مطالب آن اضافه میشود (بلاگ جدیدی اضافه شده و ارجاع مطلب موجودی به آن اضافه میشود).
وارد کردن یک موجودیت به سیستم ردیابی اطلاعات
در مثال قبل مشاهده کردیم که اگر موجودیتی تحت نظر context نباشد (برای مثال توسط یک کوئری به context وارد نشده باشد)، در حین ذخیره سازی ارجاعات، با آن به صورت یک وهلهی جدید رفتار شده و حتما در بانک اطلاعاتی به صورت یک رکورد جدید ذخیره میشود؛ حتی اگر Id آنرا دستی تنظیم کرده باشید که ندید گرفته خواهد شد.
اگر Id و سایر اطلاعات شیءایی را دارید، نیازی نیست تا حتما توسط یک کوئری ابتدا آنرا از بانک اطلاعاتی دریافت و سپس به صورت خودکار وارد سیستم ردیابی کنید؛ متد Attach نیز یک چنین کاری را انجام میدهد:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Blog.Attach(blog); context.SaveChanges();
علاوه بر متد Attach، متد AttachRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Unchanged، پیش بینی شدهاست.
روش دیگر انجام اینکار به صورت ذیل است:
در اینجا ابتدا یک وهلهی جدید از Blog ایجاد شدهاست و سپس توسط متد Entry به Context وارد شده و همچنین حالت آن به صورت صریح، به تغییر یافته، مشخص گردیدهاست:
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Entry(blog).State = EntityState.Modified ; context.SaveChanges();
var blog = new Blog { Id = 2, Url = "https://www.dntips.ir" }; context.Update(blog); context.SaveChanges();
به علاوه متد UpdateRange نیز برای افزودن لیستی از موجودیتها در حالت EntityState.Modified، پیش بینی شدهاست.
یک نکته: متدهای Attach و Update، هم بر روی یک DbSet و هم بر روی Context، قابل اجرا هستند. اگر بر روی Context اجرا شدند، نوع موجودیت دریافتی به نوع DbSet متناظر به صورت خودکار نگاشت شده و استفاده میشود (context.Set<T>().Attach(entity)). یعنی در حقیقت بین این دو حالت تفاوتی نیست و امکان فراخوانی این متدها بر روی Context، صرفا جهت سهولت کار درنظر گرفته شدهاست.
تفاوت رفتار context.Entry در EF Core با EF 6.x
متد context.Entry در EF 6.x هم وجود دارد. اما در EF core سبب تغییر وضعیت گراف متصل به یک شیء نمیشود و ضعیت روابط آنرا به روز رسانی نمیکند (برخلاف EF 6.x). اگر در EF Core نیاز به یک چنین به روز رسانی گراف مانندی را داشتید، باید از متد جدید context.ChangeTracker.TrackGraph به نحو ذیل استفاده نمائید:
context.ChangeTracker.TrackGraph(blog, e => e.Entry.State = EntityState.Added);
کوئری گرفتن از سیستم ردیابی اطلاعات
این سناریوها را درنظر بگیرید:
- میخواهم سیستمی شبیه به تریگرهای اس کیوال سرور را با EF داشته باشم.
- میخواهم اطلاعات تمام رکوردهای ثبت شده، حذف شده و به روز رسانی شده را لاگ کنم.
- میخواهم پس از ثبت رکوردی در هر جای برنامه، شبیه به مباحث SQL Server Service Broker و SqlDependency بلافاصله مطلع شده و توسط SignalR اطلاع رسانی کنم.
و در حالت کلی میخواهم پیش و یا پس از ثبت اطلاعات، بتوانم به تغییرات صورت گرفته دسترسی داشته باشم و عملیاتی را بر روی آنها انجام دهم. تمام این موارد و سناریوها را با کوئری گرفتن از سیستم ردیابی اطلاعات EF میتوان پیاده سازی کرد.
برای نمونه در مطلب قبل و قسمت «طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط»، یک کلاس پایه را که مقادیر پیش فرض خود را از SQL Server دریافت میکند، طراحی کردیم. در اینجا میخواهیم با استفاده از سیستم ردیابی EF، طراحی این کلاس پایه را عمومی کرده و سازگار با تمام بانکهای اطلاعاتی موجود کنیم.
جهت یادآوری، کلاس پایه موجودیتها، یک چنین شکلی را داشته:
public class BaseEntity { public int Id { set; get; } public DateTime? DateAdded { set; get; } public DateTime? DateUpdated { set; get; } }
public class Person : BaseEntity { public string FirstName { get; set; } public string LastName { get; set; } }
public class ApplicationDbContext : DbContext { // same as before public override int SaveChanges() { this.ChangeTracker.DetectChanges(); var modifiedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Modified); foreach (var modifiedEntry in modifiedEntries) { modifiedEntry.Entity.DateUpdated = DateTime.UtcNow; } var addedEntries = this.ChangeTracker .Entries<BaseEntity>() .Where(x => x.State == EntityState.Added); foreach (var addedEntry in addedEntries) { addedEntry.Entity.DateAdded = DateTime.UtcNow; } return base.SaveChanges(); } }
در اینجا کار با کوئری گرفتن از خاصیت ChangeTracker شروع میشود. سپس باید مشخص کنیم چه نوع موجودیتهایی را مدنظر داریم. چون تمام موجودیتهای ما از کلاس پایهی BaseEntity مشتق میشوند، بنابراین کوئری گرفتن بر روی این نوع، به معنای دسترسی به تمام موجودیتهای برنامه نیز هست. سپس در اینجا اگر حالتی EntityState.Modified بود، فقط مقدار خاصیت DateUpdated را به صورت خودکار مقدار دهی میکنیم و اگر حالتی EntityState.Added بود، تنها مقدار خاصیت DateAdded را به روز رسانی خواهیم کرد.
در یک چنین حالتی دیگر نیازی نیست تا مقادیر این خواص را در حین ثبت اطلاعات برنامه به صورت دستی مشخص کنیم.
یک نکته: اگر به ابتدای متد بازنویسی شده دقت کنید، فراخوانی متد this.ChangeTracker.DetectChanges در آن انجام شدهاست. علت اینجا است که این فراخوانی به صورت خودکار توسط متد base.SaveChanges انجام میشود، اما چون این مرحله را تا انتهای متد بازنویسی شده، به تاخیر انداختهایم، نیاز است خودمان به صورت دستی سبب محاسبهی مجدد تغییرات صورت گرفته شویم.
نکتهای در مورد بهبود کیفیت کدهای متد SaveChanges: استفادهی Change Tracker به این صورت با بازنویسی متد SaveChanges بسیار مرسوم است. اما پس از مدتی به متد SaveChanges ایی خواهید رسید که کنترل آن از دست خارج میشود. به همین جهت برای EF 6.x پروژههایی مانند EFHooks طراحی شدهاند تا کپسوله سازی بهتری را بتوان ارائه داد. انتقال کدهای آن به EF Core کار مشکلی نیست و اصل آن، بازنویسی HookedDbContext آن است که نحوهی مدیریت شکیلتر کوئری گرفتن از ChangeTracker را بیان میکند.
خواص سایهای یا Shadow properties
EF Core به همراه مفهوم کاملا جدیدی است به نام خواص سایهای. این نوع خواص در سمت کدهای ما و در کلاسهای موجودیتهای برنامه وجود خارجی نداشته، اما در سمت جداول بانک اطلاعاتی وجود دارند و اکنون امکان کوئری گرفتن و کار کردن با آنها در EF Core میسر شدهاست.
برای تعریف آنها، بجای افزودن خاصیتی به کلاسهای برنامه، کار از متد OnModelCreating به نحو ذیل شروع میشود:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>().Property<DateTime>("DateAdded");
سپس برای کار کردن و کوئری گرفتن از آن میتوان از متد جدید EF.Property، به نحو ذیل استفاده کرد:
var blogs = context.Blogs.OrderBy(b => EF.Property<DateTime>(b, "DateAdded"));
context.Entry(myBlog).Property("DateAdded").CurrentValue = DateTime.Now;
foreach (var addedEntry in addedEntries) { addedEntry.Property("DateAdded").CurrentValue = DateTime.UtcNow; }