خانه / OOP / بررسی دقیق تر Unit Of Work

بررسی دقیق تر Unit Of Work

Unit of Work

Misconceive یا برداشت غلط یکی از مواردی است که معمولا همه ما با آن مواجه هستیم. تصور و برداشت اشتباه و غلطی که از یک concept معمولا برای هر کدام از ما بوجود میاد و معمولا تمام قضاوت ها و برداشت ها و سایر conceptهای مرتبط با اون مفهوم بد برداشت شده نیز به ناچار دچار مسیر اشتباهی خواهند شد.

Unit Of Work یکی از این conceptهای بسیار مهمی است که تقریبا تمامی توسعه دهنده ها با اون برخورد داشته و استفاده کرده اند. موضوعی که در اینجا وجود داره این هست که Unit Of Work فقط به یک صورت و یک جا آنهم با EF(یا ORMهای مشابه) و فقط به یک فرم بکار برده می شود.(به عنوان یک Facadeای از تمام Repositoryها !!)

در نوشته سعی کردم این الگوی مهم و اهداف و concernای که باعث شد به سمت این الگو برویم رو بررسی کنم. امیدوارم در شناخت و فهم بهتر این concept مهم کمک کنده باشد.

 

Unit of Work جز یکی از الگوهای رفتاری مهم Object Relational محسوب میشه.

Object-Relational Behavioral Pattern

هدف:

لیستی از تمام Objectهایی که توسط تراکنش های Business تحت تاثیر قرار می گیرد را نگه می دارد و مشکلات و مسائل همزمانی –concurrency-  و ثبت تغییرات را هماهنگ می کند.

بهنگام بیرون کشیدن و یا ریختن داده ها به دیتابیس مهم است که قادر باشید مواردی رو که تغییر داده اید track کنید.

این امکان وجود داره که بسادگی بعد از هر تغییر object ها دیتابیس رو تغییر داد, اما این مورد باعث میشه که تعداد Call های دیتابیس غیر ضروری رو بسیار بسیار زیاد کنه, که در نهایت منجر به افت سرعت بسیار محسوس خواهد شد. علاوه بر اون باید برای کل تغییرات مورد نیاز یک transaction باز کنید که این تقریبا برای شرایطی که Business Transactionهای شما شامل درخواست های بسیار زیادی باشد, غیر ممکن  است. و این مورد در شرایطی که نیاز به حفظ objectهایی که خوانده شده اند باشد (منظور objectهایی است که از دیتا سورس fetch می شوند و می خواهم مطمئن باشیم که در حین واکشیدن داده ها, اونها قبلا توسط ترکنش دیگری تغییر داده نشده باشند) بسیار بدتر خواهد شد, بنابراین از خوندن داده های غیر متناقض خود داری کنید.

Unit of Work مسئولیتش اینه که تغییرات تمام مواردی رو که در حین اجرای یک business transaction که می تونه بر روی دیتابیس هم نیز تاثیر بزازه؛ رو پیگیری و کنترل می کنه. به محض اینکه Unit of Work به حالت done بره به عنوان نتیجه ای از کار-work- شما دیتابیس رو به ازای تمام مواردی که نیاز به done شدن رو دارند تغییر می دید.

اما Unit of Work به چه صورت عمل می کنه.

مشهود ترین موردی که در حین کار با دیتابیس باید مورد بررسی قرار بگیره و نگرانش هستیم changes یا تغییرات می باشد. به محض اینکه شروع به انجام اقدامی می کنید که احتمال تغییر بر روی دیتابیس رو داره؛ یه Unit Of Work رو جهت کنترل کردن تغییرات ایجاد می کنید. هر لحظه که objectای ایجاد؛ ویرایش و یا حذف می شه, به Unit Of Work اطلاع داده می شود. همچنین بهنگام خواندن دیتا Unit Of Work می تونه چک کنه و مطمئن شه که هیچکدوم از objectهای خوانده شده در حین business transaction مورد نظر تغییر نکرده اند.

مهمترین نکته در باب Unit Of Work وقتیه که اون به حالت commit می ره و در این حالت Unit Of Work تصمیم می گیره که چه اقدامی رو انجام بده. اون یه transaction رو باز می کنه, تمام چک کردن همزمانی مورد نیاز رو انجام می ده( با استفاده از Pessimistic Offline Lock یا Optimistic Offline Lock) و تغییرات رو به درون دیتابیس اعمال می کنه. نکته ی مهم بهنگام استفاده از Unit Of Work این هست که توسعه دهنده معمولا متدی رو برای بروز رسانی دیتابیس فراخوانی نمی کنند.

Unit Of Work اما به چه صورت می تواند Object هایی که باید تغییراتشون بر روی دیتابیس persist شود رو میتونه تشخیص بده؟

طبیعتا برای انجام این کار Unit Of Work نیازمند این هست که بدونه کدوم object ها نیازمند این هستند که باید تغییراتشون track بشه. به دو طریق این مورد می تونه انجام بشه, یا از طریق یک caller register اینکار انجام میشه یا از طریق Getting the receiver object که به Unit Of Work بگه.

 

  • در روش caller registration کاربر استفاده کننده از یک object مجبور است که register بودن object در Unit Of Work رو بخاطر بسپاره. تمام objectهایی که register نشدند بهنگام commit شدن ثبت و نوشته نمی شن. اگرچه این روش مستعد فراموش شدن هست, اما این انعطاف پذیری رو داره که به افراد اجازه می ده که تغییراتی رو که بصورت in-memory اعمال شده و نیاز به نوشته شدن به درون دیتابیس رو نداره به Unit Of Work معرفی نشه. اما باید گفت که گیچی و سر درگمی که این روش در پی داره بیشتر از مزیت اون هست.
  • در روش object registration تعهد از caller برداشته میشه. Trick اصلی در اینجا قرار دادن registration methodها درون متدهای object هست. Load کردن یک object از دیتابیس؛ register کردن object به عنوان clean؛ در اینجا متدهای مذکور object رو به عنوان dirty درون Unit Of Work ثبت و register می کنند.

 

object registration هم توسعه دهنده ی object رو مجبور می کنه که فراخوانی registration در محل مناسب رو بخاطر بسپاره.

code generationها در اینجا می تونه یک انتخاب بسیار خوب باشه که بتونیم این عملیات رو بصورت اتومات انجام بدیم و از ریزه کاری ها و دور شدن از focus بر روی بیزینس جلوگیری کنیم. در واقع اینجا یک محل طبیعی برای code generation برای ایجاد کردن فراخوانی های مناسب می باشه؛ اما فقط هنگامی عملیه که جداسازیه مناسبی بین کدهای تولید شده و تولید نشده با code generator ایجاد و مهیا کرده باشید. این مشکلات شرایط مناسبی برای وارد شدن aspect-oriented programming رو مهیا می کنه.

رویکرد دیگه کمک گرفتن از خود unit of work controller و سپردن وظیفه به اون هست. در اینجا Unit Of Work تمام readهای از دیتابیس رو کنترل می کنه و objectهایی که read شدن رو به عنوان clean ثبت می کنه. عوض اینکه object ها رو به عنوان dirty مارک کنه, Unit Of Work کپی از داده ها رو در زمان خواندن می گیره و به هنگام commitکردن اون رو با object اصلی مقایسه می کنه. (این روشی است که دات نت از اون استفاده می کنه و به اون بعدا بر می گردیم) هر چند این روش سرباری رو به فرآیند commit اضافه می کنه؛ اما باعث یک رویکرد selective update میشه که فقط فیلدهای تغییر یافته رو واقعا تغییر می ده. همچنین از فراخوانی های درون domain objectها جلوگیری می کنه. یک رویکرد ترکیبی این هست که فقط objectهای تغییر یافته رو کپی بگیره.

که این نیازمند registration هست؛ اما مزیت این رو داره که selective update رو بهمراه کاهش سربار کپی بهمراه داره؛ بخصوص هنگامی که تعداد خیلی زیادی read و update داریم.

 

 

نکته ی دیگری که باید در اینجا مورد بررسی قرار بدیم این هست که معمولا ما با شرایطی مواجه هستیم که object های ایجاد شده نیاز به ثبت شدن ندارند و معمولا بصورت موقت وجود دارند. یکی از این شرایط معمولا در هنگام نوشتن تست ها بروز می کنه, که معمولا تست ها بدون database write ها بسیار سریعتر انجام میشه.

معمولترین زمان جهت اندیشیدن و بررسی کردن caller registration بهنگام ایجاد object می باشه؛ ایجاد objectهایی که بصورت موقت و غیر دائمی فرض و انگاشته می شوند برای افراد غیر معمول نیست. Caller registration می تونه این مورد رو روشن تر و اضع تر کنه. هرچند راه حل های دیگری نیز وجود داره؛ از جمله فراهم نمودن یک transient constructor که با unit of work  ثبت نمیشه, یا حتی یک روش بهتر یعنی Special Case می باشه که در این مورد Unit Of Work بهنگام commit شدن هیچ کاری انجام نمی ده.

Unit Of Work نقطه ی واضع و آشکاری برای بروز رسانی های دسته ای بدست می ده. ایده بروز رسانی دسته ای اینه که چندین دستور SQL رو به عنوان یک واحد که می تونه بصورت یک remote call پردازش شه رو ارسال کنه. این مورد بخصوص وقتی که چندین update و insert و delete بصورت متوالی ارسال می شوند میتوه مفید باشه. البته محیط های متفاوت پشتیابنی مختلفی از بروزرسانی دسته ای رو مهیا می کنند.

پیاده سازی .NET

توی دات نت به کمک مفهوم disconnected data set اقدام به پیاده سازی و پشتیبانی از Unit Of Work شده است. اکثر مواردی که از این الگوی پیروی می کنند معمولا بصورت register کردن و پیگیری کردن تغییرات بر روی object ها استفاده می کنند. دات نت دیتا رو از دیتابیس به درون data set می خواند؛ که data set شامل مجموعه ای از object هایی هست که شبیه جدول ها و سطرها و دیتابیس و ستونها هست. Data set در واقع کپی از داده اصلی بصورت in-memory هست. هر سطر در data set شامل مفهوم Version(current, original, proposed) و همچنین یک state(unchanged, added, deleted, modified) هست که با هم و همچنین با این مورد که data set تقلیدی از ساختار دیتابیس هست؛ براحتی می تونه تغییرات رو در پایان تراکنش به درون دیتابیس اعمال کنه.

چه موقع باید از Unit Of Work استفاده نمود؟

مشکل اصلی و اساسی که Unit Of Work به آن می پردازد؛ track کردن objectهای مختلفی است که شما آنها را احتمالا دستکاری می کنید؛ و در نتیجه می تونید متوجه باشید؛ که کدام یک از این objectهای in-memory نیاز به نوشته شدن به درون دیتابیس رو دارند. اگر شما این امکان و توان رو داشته باشید که تمام کارها رو با یک سیستم transaction انجام بدید؛ تمام نگرانی شما فقط در مورد objectهایی است که تغییر کرده اند. هر چند که Unit Of Work عموما بهترین گزینه برای انجام این کار است؛ اما گزینه های دیگری نیز وجود دارد.

شاید ساده ترین گزینه همین مورد است که تمام objectها رو به محض اینکه تغییر دادید ذخیره کنید. مشکلی که در اینجا پیش می آید این است که ممکنه تعداد فراخوانی های دیتابیس بسیار بسیار بیشتری از اون چیزی که گمان می کردید بشه. همچنین اگر شما object رو در سه نقطه ی متفاوت برنامه تغییر بدید شما به جایی اینکه یک فراخوانی پایانی به دیتابیس داشته باشید؛ سه فراخوانی به دیتابیس دارید.

جهت اجتناب از فراخوانی های چندین باره به دیتابیس؛ می توان تمام بروز رسانی ها رو در پایان کار انجام بدهید. برای داشتن این حالت نیازه که تمام objectهایی که تغییر کردند رو track کرد. برای اینکار از variableهایی در کد استفاده می کنیم, اما با زیاد شدن این متغیرها خیلی زود متوجه می شویم که کنترل این متغیرها از دسترس خارج خواهد شد. از دیگر مشکلات متغیرها این است که هرچند معمولا با الگوهایی دیگری از جمله Transaction Script بهتر کار می کنند؛ اما پیاده سازی آنها با الگویی شبیه Domain Model بسیار مشکل خواهد بود.

بجای نگه داری objectها درون متغیرها می تواند به هر object یک dirty flag اضافه کرد که به محضی که object تغییر کرد آن flag رو ست می کنیم. سپس نیاز هست که تمام objectهایی که dirty هستند رو یافته و در انتهای transaction آنها رو ذخیره کرد. مهمترین ارزش این تکنیک منوط به سادگی در یافتن dirty objectها می باشه.

قدرت اصلی Unit Of Work نگه داری تمام اطلاعات در یک جا و یک محل می باشه.

مثال: Unit Of Work with Object Registration

مثال از آقای David Rice به زبان جاوا

در اینجا مثالی از Unit Of Work آورده شده که تمام تغییرات برای یک business transaction خاص رو track کرده و سپس اونها رو commit می کنه. جهت ذخیره ی تغییرات از سه لیست استفاده میشه؛ شامل new و dirty و removed domain objects.

class UnitOfWork...

private List newObjects = new ArrayList();

private List dirtyObjects = new ArrayList();

private List removedObjects = new ArrayList();

متدهای registration کار نگه داری state این لیست ها رو بر عهده دارند. اونها باید یکسری assertion از جمله چک کردن اینکه ID نال نباشه و یا اینکه dirty object قبلا به عنوان new ثبت نشده باشه.

Class UnitOfWork…

Public void registerNew(DomainObject obj)

{

Assert.notNull(“Id not null”, obj.getId());

Assert.isTrue(“object not dirty”, !dirtyObjects.contains(obj));

Assert.isTrue(“object not removed”, !remodevObjects.contains(obj));

Assert.isTrue(“object not already registered new”, !newObjects.contains(obj));

}

Public void registerDirty(DomainObjects obj){

Assert.notNull(“id not null”,obj.getId());

Assert.isTrue(“Object not removed”, !removedObjects.contains(obj));

If(!dirtyObjects.contains(obj) && !newObjects.contains(obj))

{

dirtyObjects.add(obj);

}

Public void registerClean(DomainObject obj)

{

Assert.notNull(“id not null”, obj.getId());

{

}

توجه کنید که registerClone() در اینجا هیچ کاری انجام نمی ده. یک practice معمول استفاده از الگوی Identity Map بهمراه Unit Of Work هست. Identity Map هنگامی که وضعیت domain object رو درون memory ذخیره می کنید بسیار مهم خواهند بود. چون چندین کپی از یک object باعث رفتار نامتعارف خواهد شد.

Commit() سپس Data Mapper رو یافته و سپس برای هر object mapping method مناسب رو invoke می کنه. در اینجا updateDirty() و deleteDirty() آورده نشده؛ اما اینها هم رفتاری مشابه insertNew() خواهند داشت.

Class UnitOfWork…

Public void commit()

{

insertNew();

updateDirty();

deleteRemoved();

}

Public void insertNew()

{

For(Itrator objects = newObjects.itrator(); objects.hasNext();){

DomainObject obj = (DomainObject) objects.next();

MapperRegistry.getMapper(obj.getClass()).insert(obj);

}

}

در این Unit Of Work از track کردن objectهایی که ما read کردیم و می خواهیم که خواندن غیر سازگار جلوگیری کنیم خبری نیست.

حالا نیازه که object registration رو ساده سازی کنیم. ابتدا هر Domain object نیاز داره که Unit Of Workای که به business transaction جاری سرویس می ده رو بتونه پیدا کنه و بهش دسترسی داشته باشه. از اونجایی که Unit Of Work در سرتاسر domain model ما مورد نیاز می باشه؛ ارسال اون به عنوان پارامتر نمی تونه معقول به نظر برسه.

همونطور که هر تراکنشی با یک thread مستقل و تنها اجرا میشه؛ می تونیم به راحتی Unit Of Work رو به thread در حال اجرای جاری مرتبط کنیم(با استفاده از ThreadLocal موجود در جاوا در java.lang).

Class UnitOfWork…

Private static ThreadLocal current = ThreadLocal();

Public static void NewCurrent()

{

setCurrent(new UnitOfWork);

}

Public static void    setCurrent(UnitOfWork uow){

Current.set(uow);

}

Public static UnitOfWork getCurrent(){

Return (UnitOfWork) current.get();

}

}

خوب حالا می تونیم abstract domain مورد نظرمون رو با marking methodهایی برای register کردن خود به Unit Of Work جاری داشته باشیم.

Class DomainObject ….

Protected void markNew(){

UnitOfWork.getCurrent().registerNew(this);

}

 

 

Protected void markClean(){

UnitOfWork.getCurrent().registerClean(this);

}

 

Protected void markDirty(){

UnitOfWork.getCurrent().registerdirty(this);

}

Protected void markRemoved(){

UnitOfWork.getCurrent().registerRemoded(this);

}

دامین آبجکت هایی که از این کلاس ارث بری می کنن نیازه که بخاطر داشته باشند که خودشون رو به عنوان new و dirty در زمان مناسب معرفی کنند.

Class EditAlbumScript…

Public static void UpdateTitle(Long albumId, string title)

{

UnitOfWork.newCurrent();

Mapper mapper = MapperRegistry.getMapper(Album.class);

Album album = (Album) mapper.find(albumId);

Album.setTitle(title);

UnitOfWork.getCurrent().commit();

}

با تشکر فراوان از آقای مارتین فاولر عزیز

در پستی دیگر درباره Repository و همچنین ارتباط اون با Unit Of Work و نیز خوب یا بد بودن استفاده از این دو الگوی مهم با ORMهای مدرنی از جمله EF صحبت خواهم کرد…

Trying to be Agile
Masoud Bahrami

درباره ی masoud@admin

همچنین ببینید

ِِِِDDD Aggregates

بر طبق تعریف آقای Eric Evans: An AGGREGATE is a cluster of associated objects that …

مقایسه سه Pattern مهم مشتق شده از MV* بنام MVC و MVP و MVVM

مقایسه سه Pattern مهم مشتق شده از MV* بنام MVC و MVP و MVVM MVC …

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

در تلگرام هم همراه شما هستم

اگر علاقمند به معماری نرم افزار و مبحث محبوب مایکروسرویس هستید؛ در کانال با ما همراه باشید. اطلاعات مفید زیادی در این کانال انتظار شما را می کشند. فقط کافیست دکمه ی پیوستن را بفشارید.

پیوستن بستن