1) رفتار متصل و غیر متصل در EF چیست؟
اولین نکته ای که به ذهنم میرسه اینه که برای استفاده از EF حتما باید درک صحیحی از رفتارها و قابلیتهای اون داشته باشیم. نحوه استفاده ازٍEF رو به دو رفتار متصل و غیر متصل تقسیم میکنیم.
حالت پیش فرضEF بر مبنای رفتار متصل میباشد. در این حالت شما یک موجودیت رو از دیتابیس فرا میخونید EF این موجودیت رو ردگیری میکنه اگه تغییری در اون مشاهده کنه بر روی اون برچسب "تغییر داده شد" میزنه و حتی اونقدر هوشمند هست که وقتی تغییرات رو ذخیره میکنید کوئری آپدیت رو فقط براساس فیلدهای تغییر یافته اجرا کنه. یا مثلا در صورتی که شما بخواهید به یک خاصیت رابطه ای دسترسی پیدا کنید اگر قبلا لود نشده باشه در همون لحظه از دیتابیس فراخوانی میشه، البته این رفتارها هزینه بر خواهد بود و در تعداد زیاد موجودیتها میتونه کارایی رو به شدت پایین بیاره.
رفتار متصل شاید در ویندوز اپلیکیشن کاربرد داشته باشه ولی در حالت وب اپلیکیشن کاربردی نداره چون با هر در خواستی به سرور همه چیز از نو ساخته میشه و پس از پاسخ به درخواست همه چی از بین میره. پس DbContext همیشه از بین میره و ما برحسب نیاز، در درخواستهای جدید به سرور ، دوباره DbContext رو میسازیم. پس از ساخته شدن DbContext باید موجودیت مورد استفاده رو به اون معرفی کنیم و وضعیت اون موجودیت رو هم مشخص کنیم.( جدید ، تغییر یافته، حذف ، بدون تغییر ) در این حالت سیستم ردگیری تغییرات بی استفاده است و ما فقط در حال هدر دادن منابع سیستم هستیم.
در حالت متصل ما باید همیشه از یک DbContext استفاده کنیم و همه موجودیتها در آخر باید تحت نظر این DbContext باشند در یک برنامه واقعی کار خیلی سخت و پیچیده ای است. مثلا بعضی وقتها مجبور هستیم از موجودیت هایی که قبلا در حافظه برنامه بوده اند استفاده کنیم اگر این موجودیت در حافظه DbContext جاری وجود نداشته باشه با معرفی کردن اون از طریق متد attach کار ادامه پیدا میکنه ولی اگر قبلا موجودیتی در سیستم ردگیری DbContext با همین شناسه وجود داشته باشد با خطای زیر مواجه میشویم.
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key
این خطا مفهوم ساده و مشخصی داره ، دو شی با یک شناسه نمیتوانند در یک DbContext وجود داشته باشند. معمولا در این حالت ما بااین اشیا تکراری کاری نداریم و فقط به شناسه اون شی برای نشان دادن روابط نیاز داریم و از دیگر خاصیتهای اون جهت نمایش به کاربر استفاده میکنیم ولی متاسفانه DbContext نمیدونه چی تو سر ما میگذره و فقط حرف خودشو میزنه! البته اگه خواستید با DbContext بر سر این موضوع گفتگو کنید از کدهای زیر استفاده کنید:
T attachedEntity = set.Find(entity.Id); var attachedEntry = dbContext.Entry(attachedEntity); attachedEntry.CurrentValues.SetValues(entity);
خوب با توجه به صحبتهای بالا اگر بخواهیم از رفتار غیر متصل استفاده کنیم باید تنظیمات زیر رو به متد سازنده DbContext اضافه کنیم. از اینجا به بعد همه چیز رو خودمون در اختیار میگیریم و ما مشخص میکنیم که کدوم موجودیت باید چه وضعیتی داشته باشه (افزودن ، بروز رسانی، حذف )و اینکه چه موقع روابط خودش را با دیگر موجودیتها فراخوانی کنه.
public DbContext() { this.Configuration.ProxyCreationEnabled = false; this.Configuration.LazyLoadingEnabled = false; this.Configuration.AutoDetectChangesEnabled = false; }
2) تعیین وضعیت یک موجودیت و راوبط آن در EF چگونه است؟
با کد زیر میتونیم وضعیت یک موجدیت رو مشخص کنیم ، با اجرای هر یک از دستورات زیر موجودیت تحت نظر DbContext قرار میگیره یعنی عمل attach نیز صورت گرفته است :
dbContext.Entry(entity).State = EntityState.Unchanged ; dbContext.Entry(entity).State = EntityState.Added ; //or Dbset.Add(entity) dbContext.Entry(entity).State = EntityState.Modified ; dbContext.Entry(entity).State = EntityState.Deleted ; // or Dbset.Remove(entity)
با اجرای این کد موجودیت از سیستم ردگیری DbContext خارج میشه.
dbContext.Entry(entity).State = EntityState.Detached;
در موجودیتهای ساده با دستورات بالا نحوه ذخیره سازی را مشخص میکنیم در وضعیتی که با موجودیتهای رابطه ای سروکار داریم باید به نکات زیر توجه کنیم.
در نظر بگیرید یک گروه از قبل وجود دارد و ما مشتری جدیدی میسازیم در این حالت انتظار داریم که فقط یک مشتری جدید ذخیره شده باشد:
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);// groupstate=EntityState.Added
اگه از روش بالا استفاده کنید میبینید گروه General جدیدی به همراه مشتری در دیتابیس ساخته میشود.نکته مهمی که اینجا وجود داره اینه که DbContext به id موجودیت گروه توجهی نداره ، برای جلو گیری از این مشکل باید قبل از معرفی موجودیتهای جدید رابطه هایی که از قبل وجود دارند را به صورت بدون تغییر attach کنیم و بعد وضعیت جدید موجودیت رو اعمال کنیم.
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(group).State = EntityState.Unchanged; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);// groupstate=EntityState.Unchanged
در مجموع بهتره که موجودیت ریشه رو attach کنیم و بعد با توجه به نیاز تغییرات رو اعمال کنیم.
// group id=19 Name="General" var customer = new Customer(); customer.Group = group; customer.Name = "mohammadi"; dbContext.Entry(customer).State = EntityState.Unchanged; dbContext.Entry(customer).State = EntityState.Added; var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Added var groupstate = dbContext.Entry(group);//// groupstate=EntityState.Unchanged
3) AsNoTracking و Include دو ابزار مهم در رفتار غیر متصل:
درصورتیکه ما تغییراتی روی دادهها نداشته باشیم و یا از روشهای غیر متصل از موجودیتها استفاده کنیم با استفاده از متد AsNoTracking() در زمان و حافظه سیستم صرف جویی میکنیم در این حالت موجودیتهای فراخوانی شده از دیتابیس در سیستم ردگیری DbContext قرار نمیگیرند و اگر وضعیت آنها را بررسی کنیم در وضعیت Detached قرار دارند.
var customer = dbContext.Customers.FirstOrDefault(); var customerAsNoTracking = dbContext.Customers.AsNoTracking().FirstOrDefault(); var customerstate = dbContext.Entry(customer).State;// customerstate=EntityState.Unchanged var customerstateAsNoTracking = dbContext.Entry(customerAsNoTracking).State;// customerstate=EntityState.Detached
نحوه بررسی کردن موجودیتهای موجود در سیستم ردگیری DbContext :
var Entries = dbContext.ChangeTracker.Entries(); var AddedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Added); var ModifiedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Modified); var UnchangedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Unchanged); var DeletedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Deleted); var DetachedEntries = dbContext.ChangeTracker.Entries().Where(entityEntry => entityEntry.State==EntityState.Detached);//* not working !
* در نظر داشته باشید وضعیت Detached وجود خارجی ندارد و به حالتی گفته میشود که DbContext در سیستم رد گیری خود اطلاعی از موجودیت مورد نظر نداشته باشد.
وقتی که سیستم فراخوانی خودکار رابطهها خاموش باشد باید موقع فراخوانی موجودیتها روابط مورد نیاز را هم با دستور Include در سیستم فراخوانی کنیم.
var CustomersWithGroup = dbContext.Customers.AsNoTracking().Include("Group").ToList(); var CustomerFull = dbContext.Customers.AsNoTracking().Include("Group").Include("Bills").Include("Bills.BillDetails").ToList();
4) از متد AddOrUpdate در در فضای نام System.Data.Entity.Migrations استفاده نکنیم، چرا؟
در صورتی که از فیلد RowVersion و کنترل مسایل همزمانی استفاده کرده باشیم هر وقتی متد AddOrUpdate رو فراخوانی کنیم، تغییر اطلاعات توسط دیگر کاربران نادیده گرفته میشود. با توجه به این که متد AddOrUpdate برای عملیات Migrations در نظر گرفته شده است، این رفتار کاملا طبیعی است. برای حل این مشکل میتونیم این متد رو با بررسی شناسه به سادگی پیاده سازی کنیم:
public virtual void AddOrUpdate(T entity) { if (entity.Id == 0) Add(entity); else Update(entity); }
5) اگر بخواهیم موجودیتهای رابطه ای در دیتا گرید ویو (ویندوز فرم) نشون بدیم باید چه کار کنیم؟
گرید ویو در ویندوز فرم قادر به نشون دادن فیلدهای رابطه ای نیست برای حل این مشکل میتونیم یک نوع ستون جدید برای گرید ویو تعریف کنیم و برای نشون دادن فیلدهای رابطه ای از این نوع ستون استفاده کنیم:
public class DataGridViewChildRelationTextBoxCell : DataGridViewTextBoxCell { protected override object GetValue(int rowIndex) { try { var bs = (BindingSource)DataGridView.DataSource; var cl = (DataGridViewChildRelationTextBoxColumn)DataGridView.Columns[ColumnIndex]; return getChildValue(bs.List[rowIndex], cl.DataPropertyName).ToString(); } catch (Exception) { return ""; } } private object getChildValue(object dataSource, string childMember) { int nextPoint = childMember.IndexOf('.'); if (nextPoint == -1) return dataSource.GetType().GetProperty(childMember).GetValue(dataSource, null); string proName = childMember.Substring(0, nextPoint); object newDs = dataSource.GetType().GetProperty(proName).GetValue(dataSource, null); return getChildValue(newDs, childMember.Substring(nextPoint + 1)); } } public class DataGridViewChildRelationTextBoxColumn : DataGridViewTextBoxColumn { public string DataMember { get; set; } public DataGridViewChildRelationTextBoxColumn() { CellTemplate = new DataGridViewChildRelationTextBoxCell(); } }
نحوه استفاده را در ادامه میبینید. این روش توسط ویزارد گریدویو هم قابل استفاده است. موقع Add کردن Column نوع اون رو روی DataGridViewChildRelationTextBoxColumn تنظیم کنید.
GroupNameColumn= new DataGridViewChildRelationTextBoxColumn(); //from your class GroupNameColumn.HeaderText = "گروه مشتری"; GroupNameColumn.DataPropertyName = "Group.Name"; //EF Property: Customer.Group.Name GroupNameColumn.Visible = true; GroupNameColumn.Width = 300; DataGridView.Columns.Add(GroupNameColumn);