۱۳۸۹/۱۱/۰۱

NHibernate و سطح اول cache آن


اين روزها هيچكدام از فناوري‌هاي دسترسي به داده بدون امكان يكپارچگي آن‌ها با سيستم‌ها و روش‌هاي متفاوت caching ، مطلوب شمرده نمي‌شوند. ايده اصلي caching هم به زبان ساده به اين صورت است :‌ فراهم آوردن روش‌هايي جهت ميسر ساختن دسترسي سريعتر به داده‌هايي كه به صورت متناوب در برنامه مورد استفاده قرار مي‌گيرند، بجاي مراجعه مستقيم به بانك اطلاعاتي و خواندن اطلاعات از ديسك سخت.
يكي از تفاوت‌هاي مهم NHibernate با اكثر ORM هاي موجود داشتن دو سطح متفاوت cache است : first level cache & second level cache .
براي نمونه Entity framework (در زمان نگارش اين مطلب) تنها first level caching را پشتيباني مي‌كند و پروايدر توكار و يكپارچه‌اي را جهت second level caching ارائه نمي‌دهد.
در اين قسمت قصد داريم First Level Cache را بررسي كنيم.

سطح اول caching در NHibernate چيست؟

سطح اول caching در تمام ORM هايي كه آن‌را پشتيباني مي‌كنند مانند NHibernate ، در طول عمر يك تراكنش تعريف مي‌گردد. در اين حالت در طي يك تراكنش و طول عمر يك سشن، دريافت اطلاعات هر ركورد از بانك اطلاعاتي، تنها يكبار انجام خواهد شد؛ صرفنظر از اينكه كوئري دريافت اطلاعات آن چندبار فراخواني مي‌‌گردد. يكي از دلايل اين روش هم آن است كه هيچ دو شيء متفاوتي كه هم اكنون در حافظه قرار دارند نبايد بيانگر يك ركورد واحد از بانك اطلاعاتي باشند.
در NHibernate به صورت پيش فرض هر زمانيكه از شيء استاندارد session استفاده مي‌كنيد، سطح اول caching نيز فعال است. درست در زمانيكه سشن خاتمه مي‌يابد، اين سطح از caching نيز به صورت خودكار تخليه خواهد گرديد.
به first level caching اصطلاحا thought-out cache system يا Cache Through pattern و يا identity map هم گفته مي‌شود.

مثال:

روش متداول و استاندارد كار با NHibernate عموما به صورت زير است:

الف) دريافت شيء Session از Session Factory
ب) شروع يك تراكنش با فراخواني متد BeginTransaction شيء Session
ج) براي مثال دريافت اطلاعات ركوردي با ID مساوي يك به كمك متد Get مرتبط با شيء Session : اين اطلاعات مستقيما از بانك اطلاعاتي دريافت خواهد شد.
د) سپس مجددا سعي در دريافت ركوردي با ID مساوي يك. اينبار اطلاعات اين شيء مستقيما از cache خوانده مي‌شود و رفت و برگشتي به بانك اطلاعاتي نخواهيم داشت. به همين جهت به اين روش identity map هم گفته مي‌شود، زيرا NHibernate بر اساس ID منحصربفرد اين اشياء ، identity map خود را تشكيل مي‌دهد.
ه) خاتمه‌ي سشن با فراخواني متد Close آن
بلافاصله
الف) دريافت شيء Session از Session Factory
ب) شروع يك تراكنش با فراخواني متد BeginTransaction شيء Session
ج) براي مثال دريافت اطلاعات ركوردي با ID مساوي يك به كمك متد Get مرتبط با شيء Session : اين اطلاعات مستقيما از بانك اطلاعاتي دريافت خواهد شد (زيرا در يك سشن جديد قرار داريم و همچنين سشن قبلي بسته شده و كش آن تخليه گشته است).
د) خاتمه‌ي سشن با فراخواني متد Close آن


سؤال: آيا استفاده از يك سشن سراسري در برنامه صحيح است؟
پاسخ: خير!
توضيحات: زمانيكه از يك سشن سراسري استفاده مي‌كنيد، كش NHibernate را در اختيار تمام كاربران همزمان سيستم قرار داده‌ايد. در طي يك سشن، همانطور كه عنوان شد، بر اساس IDهاي اشياء، يك identity map تشكيل مي‌شود و در اين حالت به ازاي هر ركورد بانك اطلاعاتي فقط و فقط يك شيء در حافظه وجود خواهد داشت كه اين روش در محيط‌هاي چندكاربره مانند برنامه‌هاي وب به زودي تبديل به نشت اطلاعات و يا تخريب اطلاعات مي‌گردد. به همين جهت در اين نوع برنامه‌ها روش session-per-request بهترين حالت كاري است.

سؤال: حين به روز رساني اشياء جديد، به خطا بر مي‌خورم. مشكل در كجاست؟
فرض كنيد شيء مفروض Customer را توسط متد session.Get از بانك اطلاعاتي دريافت و تعدادي از خواص آن‌را جهت ساخت شيء جديدي از كلاس Customer استفاده كرده‌ايم. اكنون اگر بخواهيم اين شيء جديد را در بانك اطلاعاتي ذخيره يا به روز رساني كنيم، NHibernate اين اجازه را نمي‌دهد! چرا؟
پاسخ:
خطاي متداول اين حالت عموما به صورت زير است:
a different object with the same identifier value was already associated with the session
اگر شخصي با مكانيزم سطح اول caching در NHibernate آشنايي نداشته باشد، شايد ساعاتي را در انجمن‌هاي مرتبط، جهت يافتن روش حل خطاي فوق سپري كند.
همانطور كه عنوان شد، در طول يك سشن، نمي‌توان دو شيء با يك ID را به عنوان يك ركورد بانك اطلاعاتي مورد استفاده قرار داد. اولين فراخواني Get ، سبب كش شدن آن شيء در identity map سطح اول caching مي‌گردد.
راه حل:
الف) از چندين و چند شيء استفاده نكنيد. هر ركورد بايد تنها با يك وهله از شيء‌ايي متناظر باشد.
ب) مي‌توان پيش از update‌، كش سطح اول را به صورت دستي خالي كرد. براي اين منظور از متد Clear شيء سشن استفاده كنيد.
ج) بجاي استفاده از متد saveOrUpdate شيء سشن، از متد Merge آن استفاده كنيد. به اين صورت شيء جديد ايجاد شده با شيء موجود در كش يكي خواهد شد.
د) مي‌توان بجاي تخليه كل كش (حالت ب)، كش مرتبط با شيء Customer را به صورت دستي خالي كرد. براي اين منظور از متد Evict شيء سشن استفاده نمائيد.

و لازم به ذكر است كه متد Flush سبب تخليه كش نمي‌گردد. كار اين متد اعمال كليه تغييرات اعمالي موجود در كش به بانك اطلاعاتي است و بيشتر جهت هماهنگ سازي اين دو مورد استفاده قرار مي‌گيرد.

سؤال: آيا مي‌توان سطح اول caching را غيرفعال كرد؟
پاسخ:بله.
توضيحات:
عموما كليه ORMs جهت Batching يا Bulk data operations (براي مثال ثبت تعداد زيادي ركورد يا به روز رساني تعداد بالايي از آن‌ها، يا نمايش فقط خواندني تعداد زيادي ركورد و گزارشگيري از آن‌ها) كارآيي مطلوبي ندارند. نمونه‌اي از آن‌را در مبحث جاري ملاحظه كرده‌ايد. هر شيءايي كه به نحوي به سشن جاري وارد مي‌شود تحت نظر قرار مي‌گيرد و اين مورد در تعداد بالاي ثبت يا به روز رساني ركوردها، يعني كاهش سرعت و كارآيي، به علاوه مصرف بالاي حافظه. به همين جهت بايد به خاطر داشت كه ORMs جهت سناريوهاي OLTP مناسب هستند و كساني كه سرعت و كارآيي ORMs را با Batch processing اندازه گيري مي‌كنند، كلا دركي از فلسفه‌ي وجودي ORMs و ساختار دروني آن‌ها ندارند!
خوشبختانه NHibernate با معرفي Stateless Sessions بر اين مشكل فائق آمده است. در اينجا بجاي ISession تنها كافي است از IStatelessSession استفاده گردد:
using (IStatelessSession statelessSession = sessionFactory.OpenStatelessSession())
using (ITransaction transaction = statelessSession.BeginTransaction())
{
//now insert 1,000,000 records!
}
در اين حالت سيستم دو مزيت عمده را تجربه خواهد كرد: سرعت بالاي ثبت اطلاعات با تعداد زياد ركورد و همچنين مصرف پايين حافظه از آنجائيكه يك IStatelessSession ارجاعي را به اشيايي كه بارگذاري مي‌كند، در خود نگهداري نخواهد كرد.
تنها بايد به خاطر داشت كه در اين حالت lazy loading پشتيباني نمي‌شود و همچنين رخدادهاي دروني NHibernate نيز لغو خواهند شد.