همزمانی (concurrency) در Entity Framework
یکشنبه 8 فروردین 1395فرض کنید حالتی پیش بیاید که چندین کاربر به صورت همزمان سعی در به روزرسانی یک موجودیت یا entity داشته باشند فکر می کنید واکنش Entity Framework در این حالت چه خواهد بود .در این مقاله به همراه یک نمونه عملی به توضیح روش همزمانی در Entity Framework خواهیم پرداخت .
فرض کنید حالتی پیش بیاید که چندین کاربر به صورت همزمان سعی در به روزرسانی یک موجودیت یا entity داشته باشند فکر می کنید واکنش Entity Framework در این حالت چه خواهد بود .
تداخلات همزمانی یا concurrency conflict زمانی رخ می دهد که وقتی شما وارد صفحه ویرایش شده اید و تغییرات لازم را داده اید زمانی که می خواهید دکمه ثبت ویرایش را بزنید کاربر دیگری همزمان همان موجودیت را ویرایش کند .در حالت عادی اگر مدیریت خاصی صورت نگیرد تغییرات آخرین کاربر اعمال خواهد شد .و تمام به روز رسانی های دیگر لغو خواهند شد .وقتی تعداد کاربران کم باشد خیلی این مشکلات چشم گیر نخواهد بود اما وقتی تعداد کاربران زیاد باشد این تداخلات، ایجاد مشکل خواهد کرد و باید مدیریت شوند.
یک روش کنترل مشکل همزمانی استفاده از قفل گذاری است .یعنی زمانی که یک دستور در حالت اجراست موجودیت ما قفل خواهد شد و کار دیگری به غیر از کار جاری روی آن انجام نخواهد شد .
زمانی که می خواهید یک سطر از پایگاه داده را بخوانید (به هر منظوری مثلا برای ویرایش ) آن سطر قفل خواهد شد .و کاربران دیگر تنها دسترسی فقط خواندنی خواهند داشت .این روش تنها مشکلی که دارد این است که کار را برای برنامه نویس پیچیده می کند .و شاید کمی افت Performance خواهیم داشت .
روش دیگر کنترل همزمانی استفاده از کنترل همروندی خوش بینانه و یا Optimistic Concurrency است .در این روش به همه حتی به صورت همزمان اجاره دسترسی به موجودیت داده می شود فقط در صورتی که همزمانی رخ دهد واکنش خواهیم داد .
در حالتی که از همزمانی خوش بینانه استفاده می کنیم می توان کارهای مختلفی انجام داد مثلا وقتی شما به عنوان کاربر اول وارد شده اید و یک پراپرتی را تغییر داده اید قبل از اینکه دکمه ذخیره را بزنید کاربر دیگری هم به این موجودیت دسترسی پیدا کرده و پراپرتی دیگری از این موجودیت را تغییر می دهد در این صورت چون این دو پراپرتی متفاوت بوده اند به راحتی تغییرات هر دو کاربر اعمال می شود .مشکلی که این روش دارد این است که برای نگه داری State اینکه کدام پراپرتی توسط کدام کاربر تغییر پیدا کرده است باید حافظه بسیاری را استفاده کنیم .
در روش دوم برای حالت همزمانی خوش بینانه می توان تغییرات کاربر اول را اعمال کرد و تغییرات اعمال شده کاربر دوم را بر روی کاربر اول بازنویسی یا Override کرد .
روش سومی هم وجود دارد که یک پیغام خطا به کاربر دوم نمایش دهیم .و می گوییم اگر تمایل دارد کمی صبر کند و بعد تغییرات خود را اعمال کند .
حالت پیش فرض در EF بر مبنای رفتار متصل است یعنی وقتی شما موجودیتی را از دیتابیس فراخونی می کنید EF این موجودیت را پیگیری می کند اگر در آن تغییری صورت گرفته باشد به آن برچسب تغییر یافته خواهد زد .و وقتی آپدیتی بر روی موجودیت صورت می گیرد تنها مقادیری که تغییر یافته اند را به روزرسانی می کند .
اگر بخواهید فقط به یک پراپرتی از یک موجودیت دسترسی پیدا کنید اگر قبلا لود نشده باشد در همان لحظه فراخوانی میشود .اما این رفتارها ی EF ممکن است کمی از کارایی و سرعت را بگیرد .
رفتار متصل EF در برنامه هایی که به وفور به منابع دسترسی دارند کاربرد دارد و در سطح برنامه های وب این حالت کاربردی ندارد .همان طور که می دانید در وب پس از اینکه پاسخ به درخواستی داده شده همه چیز از بین می رود تا در درخواست بعدی دوباره ساخته شود . وقتی در حالت غیر متصل باشیم خودمان باید وضعیت موجودیت را اعم از اینکه در حالت ویرایش،افزودن و یا حذف است را مشخص کنیم .
برای این کار در متد سازنده DbContext کدهای زیر را می نویسیم
public DbContext() { this.Configuration.ProxyCreationEnabled = false; this.Configuration.LazyLoadingEnabled = false; this.Configuration.AutoDetectChangesEnabled = false; }
و برای تعیین وضعیت یک موجودیت هم می توانیم از کدهای زیر استفاده کنیم .
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)
برای درک بهتر موضوع یک مثال عملی را با هم پیش می بریم .
ابتدا یک دیتابیسی در پایگاه داده خود ایجاد می کنیم . این دیتابیس فقط یک جدول دارد که شمای آن را در زیر می بینید
حال این دیتابیس را به عنوان مدل به پروژه MVC خود اضافه کنید .حال باید در داخل edmx خود را بر روی پراپرتی RowVersion کلیک راست کرده و سپس concurrency mode آن را برابر با fixed قرار دهید .
با این کار EF بر روی سطر RowVersion حساس خواهد شد و زمانی که قصد ذخیره موجودیت student را داشته باشد و متوجه شود که مقدار پراپرتی RowVersion با قبل تغییر کرده است یک خطای DbUpdateConcurrencyExection رخ می دهد .
در ادامه کدهایی که می بینید نمایانگر این است که همزمان User1 و User2 همزمان یک موجودیت را دسترسی دارند و همزمان هم می خواهند یک پراپرتی یکسان از این موجودیت به نام StudentName را تغییر دهند .
Student student1WithUser1 = null; Student student1WithUser2 = null; //User 1 gets student using (var context = new SchoolDBEntities()) { context.Configuration.ProxyCreationEnabled = false; student1WithUser1 = context.Students.Where(s => s.StudentID == 1).Single(); } //User 2 also get the same student using (var context = new SchoolDBEntities()) { context.Configuration.ProxyCreationEnabled = false; student1WithUser2 = context.Students.Where(s => s.StudentID == 1).Single(); } //User 1 updates Student name student1WithUser1.StudentName = "Edited from user1"; //User 2 updates Student name student1WithUser2.StudentName = "Edited from user2";
کاربر اول تغییرات دلخواه خود را ثبت خواهد کرد .اما زمانی که کاربر دوم می خواهد تغییرات خود را ذخیره کند خطای همزمانی را دریافت خواهد کرد .
//User 1 saves changes first using (var context = new SchoolDBEntities()) { try { context.Entry(student1WithUser1).State = EntityState.Modified; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("Optimistic Concurrency exception occured"); } } //User 2 saves changes after User 1. //User 2 will get concurrency exection //because CreateOrModifiedDate is different in the database using (var context = new SchoolDBEntities()) { try { context.Entry(student1WithUser2).State = EntityState.Modified; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("Optimistic Concurrency exception occured"); } }
در ضمیمه ای که همراه مقاله می باشد در داخل Program.cs دموهای مختلفی قرار داده ایم که یکی از آنها EFBasics.OptimisticConcurrencyDemo(); می باشد .در این نقطه یک BreakPoint قرار می دهیم و برنامه را بعد از این خط به صورت مرحله به مرحله اجرا می کنیم .
class Program { static void Main(string[] args) { Console.WriteLine("*** EntityFramework Basic Tutorials Demo Start ***"); Console.WriteLine(""); EFBasics.DynamicProxyDemo(); EFBasics.FindEntityDemo(); EFBasics.LazyLoadingDemo(); EFBasics.ExplicitLoadingDemo(); EFBasics.RawSQLtoEntityTypeDemo(); EFBasics.ReadDataUsingStoredProcedure(); EFBasics.RawSQLCommandDemo(); EFBasics.EntitySQLDemo(); EFBasics.EntitySQLUsingEntityConnectionDemo(); EFBasics.SpatialDataTypeDemo(); EFBasics.GetPropertyValuesDemo(); EFBasics.LocalDemo(); EFBasics.ChangeTrackingDemo(); EFBasics.SetValuesDemo(); EFBasics.ValidationErrorDemo(); EFBasics.CRUDOperationInConnectedScenarioDemo(); EFBasics.OptimisticConcurrencyDemo(); EFBasics.DBEntityEntryDemo(); //Add and map sp_InsertStudentInfo, sp_UpdateStudent //and sp_DeleteStudent into EDM to run following demo //EFBasics.CUDOperationUsingStoredProcedureDemo(); AddDemo.AddSingleEntity(); AddDemo.AddEntityGraph(); UpdateDemo.UpdateSingleEntity(); UpdateDemo.UpdateEntityGraphUsingId(); //Implement IEntityObjectState in Standard & Teacher entity class to run following demo //UpdateDemo.UpdateEntityGraphUsingStateProperty(); Console.WriteLine("*** EntityFramework Basic Tutorials Demo Finished ***"); Console.ReadKey(); }
با زدن f11 وارد تابع OptimisticConcurrencyDemo خواهیم شد .
public static void OptimisticConcurrencyDemo() { Console.WriteLine("///////////////"); Console.WriteLine("*** OptimisticConcurrencyDemo Start ***"); Console.WriteLine("///////////////"); Student student1WithUser1 = null; Student student1WithUser2 = null; //User 1 gets student using (var context = new SchoolDBEntities()) { context.Configuration.ProxyCreationEnabled = false; student1WithUser1 = context.Students.Where(s => s.StudentID == 1).Single(); } //User 2 also get the same student using (var context = new SchoolDBEntities()) { context.Configuration.ProxyCreationEnabled = false; student1WithUser2 = context.Students.Where(s => s.StudentID == 1).Single(); } //User 1 updates Student name student1WithUser1.StudentName = "Edited from user1"; //User 2 updates Student name student1WithUser2.StudentName = "Edited from user2"; //User 1 saves changes first using (var context = new SchoolDBEntities()) { try { context.Entry(student1WithUser1).State = System.Data.Entity.EntityState.Modified; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("Optimistic Concurrency exception occured"); } } //User 2 saves changes after User 1. //User 2 will get concurrency exection because CreateOrModifiedDate is different in the database using (var context = new SchoolDBEntities()) { try { context.Entry(student1WithUser2).State = System.Data.Entity.EntityState.Modified; context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { Console.WriteLine("Optimistic Concurrency exception occured"); } } Console.WriteLine("*** OptimisticConcurrencyDemo Finished ***"); } }
در داخل این تابع ابتدا شی student برای هر دو کاربر ایجاد می شود .ابتدا کاربر اول به شی student دسترسی پیدا می کند .بعد از آن کاربر دوم به همان شی دسترسی پیدا می کند .یعنی شی student با id برابر یک .بعد از آن کاربر اول شی Student با id=1 را ویرایش می کند .کاربر دوم هم همان پراپرتی از همان شی را تغییر می دهد .اولین کسی که در اینجا تغییرات خود را ذخیره می کند کاربر 1 است . حال کاربر دوم قصد دارد که تغییرات خود را ذخیره کند در این حالت یک خطایی را دریافت خواهد کرد .
همزمانی در code-First
شما می توانید در داخل Code_First هم از خاصیت Timestamp استفاده کنید .این کار به صورت کدهای زیر نمایش داده شده است
[Timestamp] public byte[] RowVersion { get; set; }
بعد از اجرای برنامه شکل زیر را خواهیم دید
- C#.net
- 4k بازدید
- 8 تشکر