استفاده از الگوي Repository اضافي در EF Code first؛ آري يا خير؟!
اگر در ويژوال استوديو، اشارهگر ماوس را بر روي تعريف DbContext قرار دهيم، راهنماي زير ظاهر ميشود:
A DbContext instance represents a combination of the Unit Of Work and Repository patterns such that it can be used to query from a database and group together changes that will then be written back to the store as a unit. DbContext is conceptually similar to ObjectContext.
در اينجا تيم EF صراحتا عنوان ميكند كه DbContext در EF Code first همان الگوي Unit Of Work را پياده سازي كرده و در داخل كلاس مشتق شده از آن، DbSetها همان Repositories هستند (فقط نامها تغيير كردهاند؛ اصول يكي است).
به عبارت ديگر با نام بردن صريح از اين الگوها، مقصود زير را دنبال ميكنند:
لطفا بر روي اين لايه Abstraction ايي كه ما تهيه ديدهايم، يك لايه Abstraction ديگر را ايجاد نكنيد!
«لايه Abstraction ديگر» يعني پياده سازي الگوهاي Unit Of Work و Repository جديد، برفراز الگوهاي Unit Of Work و Repository توكار موجود!
كار اضافهاي كه در بسياري از سايتها مشاهده ميشود و ... متاسفانه اكثر آنها هم اشتباه هستند! در ذيل روشهاي تشخيص پياده سازيهاي نادرست الگوي Repository را بر خواهيم شمرد:
1) قرار دادن متد Save تغييرات نهايي انجام شده، در داخل كلاس Repository
متد Save بايد داخل كلاس Unit of work تعريف شود نه داخل كلاس Repository. دقيقا همان كاري كه در EF Code first به درستي انجام شده. متد SaveChanges توسط DbContext ارائه ميشود. علت هم اين است كه در زمان Save ممكن است با چندين Entity و چندين جدول مشغول به كار باشيم. حاصل يك تراكنش، بايد نهايتا ذخيره شود نه اينكه هر كدام از اينها، تراكنش خاص خودشان را داشته باشند.
2) نداشتن دركي از الگوي Unit of work
به Unit of work به شكل يك تراكنش نگاه كنيد. در داخل آن با انواع و اقسام موجوديتها از كلاسها و جداول مختلف كار شده و حاصل عمليات، به بانك اطلاعاتي اعمال ميگردد. پياده سازيهاي اشتباه الگوي Repository، تمام امكانات را در داخل همان كلاس Repository قرار ميدهند؛ كه اشتباه است. اين نوع كلاسها فقط براي كار با يك Entity بهينه شدهاند؛ در حاليكه در دنياي واقعي، اطلاعات ممكن است از دو Entity مختلف دريافت و نتيجه محاسبات مفروضي به Entity سوم اعمال شود. تمام اين عمليات يك تراكنش را تشكيل ميدهد، نه اينكه هر كدام، تراكنش مجزاي خود را داشته باشند.
3) وهله سازي از DbContext به صورت مستقيم داخل كلاس Repository
4) Dispose اشياء DbContext داخل كلاس Repository
هر بار وهله سازي DbContext مساوي است با باز شدن يك اتصال به بانك اطلاعاتي و همچنين از آنجائيكه راهنماي ذكر شده فوق را در مورد DbContext مطالعه نكردهاند، زمانيكه در يك متد با سه وهله از سه Repository موجوديتهاي مختلف كار ميكنيد، سه تراكنش و سه اتصال مختلف به بانك اطلاعاتي گشوده شده است. اين مورد ذاتا اشتباه است و سربار بالايي را نيز به همراه دارد.
ضمن اينكه بستن DbContext در يك Repository، امكان اعمال كوئريهاي بعدي LINQ را غيرممكن ميكند. به ظاهر يك شيء IQueryable در اختيار داريم كه ميتوان بر روي آن انواع و اقسام كوئريهاي LINQ را تعريف كرد اما ... در اينجا با LINQ to Objects كه بر روي اطلاعات موجود در حافظه كار ميكند سر و كار نداريم. اتصال به بانك اطلاعاتي با بستن DbContext قطع شده، بنابراين كوئري LINQ بعدي شما كار نخواهد كرد.
همچنين در EF نميتوان يك Entity را از يك Context به Context ديگري ارسال كرد. در پياده سازي صحيح الگوي Repository (دقيقا همان چيزي كه در EF Code first به صورت توكار وجود دارد)، Context بايد بين Repositories كه در اينجا فقط نامش DbSet تعريف شده، به اشتراك گذاشته شود. علت هم اين است كه EF از Context براي رديابي تغييرات انجام شده بر روي موجوديتها استفاده ميكند (همان سطح اول كش كه در قسمتهاي قبل به آن اشاره شد). اگر به ازاي هر Repository يكبار وهله سازي DbContext انجام شود، هر كدام كش جداگانه خاص خود را خواهند داشت.
5) عدم امكان استفاده از تنها يك DbConetext به ازاي يك Http Request
هنگاميكه وهله سازي DbContext به داخل يك Repository منتقل ميشود و الگوي واحد كار رعايت نميگردد، امكان به اشتراك گذاري آن بين Repositoryهاي تعريف شده وجود نخواهد داشت. اين مساله در برنامههاي وب سبب كاهش كارآيي ميگردد (باز و بسته شدن بيش از حد اتصال به بانك اطلاعاتي در حاليكه ميشد تمام اين عمليات را با يك DbContext انجام داد).
نمونهاي از اين پياده سازي اشتباه را در اينجا ميتوانيد پيدا كنيد. متاسفانه شبيه به همين پياده سازي، در پروژه MVC Scaffolding نيز بكارگرفته شده است.
چرا تعريف لايه ديگري بر روي لايه Abstraction موجود در EF Code first اشتباه است؟
يكي از دلايلي كه حين تعريف الگوي Repository دوم بر روي لايه موجود عنوان ميشود، اين است:
«به اين ترتيب به سادگي ميتوان ORM مورد استفاده را تغيير داد» چون پياده سازي استفاده از ORM، در پشت اين لايه مخفي شده و ما هر زمان كه بخواهيم به ORM ديگري كوچ كنيم، فقط كافي است اين لايه را تغيير دهيم و نه كل برنامه را.
ولي سؤال اين است كه هرچند اين مساله از هزار فرسنگ بالاتر درست است، اما واقعا تابحال ديدهايد كه پروژهاي را با يك ORM شروع كنند و بعد سوئيچ كنند به ORM ديگري؟!
ضمنا براي اينكه واقعا لايه اضافي پياده سازي شده انتقال پذير باشد، شما بايد كاملا دست و پاي ORM موجود را بريده و تواناييهاي در دسترس آن را به سطح نازلي كاهش دهيد تا پياده سازي شما قابل انتقال باشد. براي مثال يك سري از قابليتهاي پيشرفته و بسيار جالب در NH هست كه در EF نيست و برعكس. آيا واقعا ميتوان به همين سادگي ORM مورد استفاده را تغيير داد؟ فقط در يك حالت اين امر ميسر است: از قابليتهاي پيشرفته ابزار موجود استفاده نكنيم و از آن در سطحي بسيار ساده و ابتدايي كمك بگيريم تا از قابليتهاي مشترك بين ORMهاي موجود استفاده شود.
ضمن اينكه مباحث نگاشت كلاسها به جداول را چكار خواهيد كرد؟ EF راه و روش خاص خودش را دارد، NH چندين و چند روش خاص خودش را دارد! اينها به اين سادگي قابل انتقال نيستند كه شخصي عنوان كند: «هر زمان كه علاقمند بوديم، ORM مورد استفاده را ميشود عوض كرد!»
دليل دومي كه براي تهيه لايه اضافهتري بر روي DbContext عنوان ميكنند اين است:
«با استفاده از الگوي Repository نوشتن آزمونهاي واحد سادهتر ميشود». زمانيكه برنامه بر اساس Interfaceها كار ميكند ميتوان آنها را بجاي اشاره به بانك اطلاعاتي، به نمونهاي موجود در حافظه، در زمان آزمون تغيير داد.
اين مورد در حالت كلي درست است اما .... نه در مورد بانكهاي اطلاعاتي!
زمانيكه در يك آزمون واحد، پياده سازي جديدي از الگوي Interface مخزن ما تهيه ميشود و اينبار بجاي بانك اطلاعاتي با يك سري شيء قرارگرفته در حافظه سروكار داريم، آيا موارد زير را هم ميتوان به سادگي آزمايش كرد؟
ارتباطات بين جداولرا، cascade delete، فيلدهاي identity، فيلدهاي unique، كليدهاي تركيبي، نوعهاي خاص تعريف شده در بانك اطلاعاتي و مسايلي از اين دست.
پاسخ: خير! تغيير انجام شده، سبب كار برنامه با اطلاعات موجود در حافظه خواهد شد، يعني LINQ to Objects.
شما در حالت استفاده از LINQ to Objects آزادي عمل فوق العادهاي داريد. ميتوانيد از انواع و اقسام متدها حين تهيه كوئريهاي LINQ استفاده كنيد كه هيچكدام معادلي در بانك اطلاعاتي نداشته و ... به ظاهر آزمون واحد شما پاس ميشود؛ اما در عمل بر روي يك بانك اطلاعاتي واقعي كار نخواهد كرد.
البته شايد شخصي عنوان كه بله ميشود تمام اينها نيازمنديها را در حالت كار با اشياء درون حافظه هم پياده سازي كرد ولي ... در نهايت پياده سازي آن بسيار پيچيده و در حد پياده سازي يك بانك اطلاعاتي واقعي خواهد شد كه واقعا ضرورتي ندارد.
و پاسخ صحيح در اينجا و اين مساله خاص اين است:
لطفا در حين كار با بانكهاي اطلاعاتي مباحث mocking را فراموش كنيد. بجاي SQL Server، رشته اتصالي و تنظيمات برنامه را به SQL Server CE تغيير داده و آزمايشات خود را انجام دهيد. پس از پايان كار هم بانك اطلاعاتي را delete كنيد. به اين نوع آزمونها اصطلاحا integration tests گفته ميشود. لازم است برنامه با يك بانك اطلاعاتي واقعي تست شود و نه يك سري شيء ساده قرار گرفته در حافظه كه هيچ قيدي همانند شرايط كار با يك بانك اطلاعاتي واقعي، بر روي آنها اعمال نميشود.
ضمنا بايد درنظر داشت بانكهاي اطلاعاتي كه تنها در حافظه كار كنند نيز وجود دارند. براي مثال SQLite حالت كار كردن صرفا در حافظه را پشتيباني ميكند. زمانيكه آزمون واحد شروع ميشود، يك بانك اطلاعاتي واقعي را در حافظه تشكيل داده و پس از پايان كار هم ... اثري از اين بانك اطلاعاتي باقي نخواهد ماند و براي اين نوع كارها بسيار سريع است.
نتيجه گيري:
حين استفاده از EF code first، الگوي واحد كار، همان DbContext است و الگوي مخزن، همان DbSetها. ضرورتي به ايجاد يك لايه محافظ اضافي بر روي اينها وجود ندارد.
در اينجا بهتر است يك لايه اضافي را به نام مثلا Service ايجاد كرد و تمام اعمال كار با EF را به آن منتقل نمود. سپس در قسمتهاي مختلف برنامه ميتوان از متدهاي اين لايه استفاده كرد. به عبارتي در فايلهاي Code behind برنامه شما نبايد كدهاي EF مشاهده شوند. يا در كنترلرهاي MVC نيز به همين ترتيب. اينها مصرف كننده نهايي لايه سرويس ايجاد شده خواهند بود.
همچنين بجاي نوشتن آزمونهاي واحد، به Integration tests سوئيچ كنيد تا بتوان برنامه را در شرايط كار با يك بانك اطلاعاتي واقعي تست كرد.
براي مطالعه بيشتر: