همزمانی (concurrency) در Entity Framework

یکشنبه 8 فروردین 1395

فرض کنید حالتی پیش بیاید که چندین کاربر به صورت همزمان سعی در به روزرسانی یک موجودیت یا entity داشته باشند فکر می کنید واکنش Entity Framework در این حالت چه خواهد بود .در این مقاله به همراه یک نمونه عملی به توضیح روش همزمانی در Entity Framework خواهیم پرداخت .

همزمانی (concurrency) در 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; }

بعد از اجرای برنامه شکل زیر را خواهیم دید

آموزش سی شارپ

فایل های ضمیمه

ایمان مدائنی

نویسنده 1299 مقاله در برنامه نویسان
  • C#.net
  • 4k بازدید
  • 8 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید