‫تعیین نوع fetch در Search APIهای NHibernate

برای آن که NHibernate بتواند به طور بهینه از Caching استفاده نماید باید Fetch در Entityها و Queryها به طور مناسبی تعریف شده باشد. Fetch به طور کلی به NHibernate می‌گوید sql دریافت اطلاعات از دیتابیس را چطور تولید کند. یک select کلی از همه جداول با استفاده از outer joinهای متعدد یا selectهای جداگانه به ازای هر جدول. مزیت عمده select جداگانه وقتی است که آن اطلاعات از قبل در Cache موجود باشند.

Fetch در Queryهای دیتابیسی NHibernate و APIهای مختلف جستجوی آن روش‌های متفاوتی دارد:

  • در HQL با استفاده join fetch انجام می‌شود. ±
  • در Linq-to-NH با استفاده از Expand و Fetch انجام می‌شود. ±
  • در QueryOver با Fetch انجام می‌شود (مشابه linq-to-nh). ±
  • در ICriteria با SetFetchMode انجام می‌شود. ±

‫NHibernate Caching و نوع Fetch در Entityها

موقع فعال‌سازی Cache در NHibernate متوجه شدم که فراخوانی بعضی Entityها و بعضی از Queryها از دیتابیس شامل چند join است و عملاً از آن چیزی که در Cache ممکن است موجود باشد استفاده‌ای نمی‌شود. به عنوان مثال فرض کنید کلاس Teacher و Student موجود باشد به نحوی که از کلاس Student یک Association به کلاس Teacher موجود باشد به اسم MyTeacher. حال اگر با متود Load یا Get یک رکورد از Student را از دیتابیس فراخوانی کنیم به طور معمول با چنین چیزی مواجه می‌شویم:

select col1, col2, … from student s left outer join teacher t on s.myteacher_id = t.id where …
همان طور که مشاهده می‌شود اگر تمام Teacherها هم در Cache موجود باشد باز هم به آنها مراجعه نشده و مورد استفاده قرار نخواهند گرفت چون query بالا خودش راساً اقدام به load رکورد teacher مربوطه می‌کند.

این موضوع جای نگرانی ندارد چون می‌توان NHibernate را مجبور کرد که از join استفاده نکرده و query بالا را به دو query زیر بشکند:

select col1, col2, … from student s where …

select col1, col2, … from teacher t where id = …

در این حالت اگر teacher مورد نظر ما در Cache موجود باشد، query دوم اطلاعاتش را به جای دیتابیس از Cache می‌خواند.

برای مجبور کردن NHibernate به عدم استفاده از join در سطح Load/Get مربوط به Entityها باید از property خاصی به اسم fetch در تعریف associationها در mapping کلاس مربوطه استفاده کرده و مقدار آن را برابر select قرار داد. مقدار پیش‌فرض این property برابر join است. برای انجام همین کار در Castle ActiveRecord باید از property خاصی به اسم Fetch استفاده کرد.

برای آن که در queryهای دیتابیسی مثل LINQ-to-NHibernate، HQL، ICriteria یا QueryOver هم همین کار را انجام داد باید از روش‌های متفاوت و جداگانه‌ای استفاده نمود.

‫‫استفاده از Stored Procedure و Trigger در NHibernate و تاثیر آنها بر Caching

در رابطه با NHibernate رایج است که هر جا حس کردیم NHibernate یا مهارت خودمان در استفاده از آن دچار محدودیت است فوراً دست به کار شده و ضمن دور زدن مکانیزم NHibernate session managment مستقیماً از SQL در برنامه‌مان استفاده کنیم. مثلاً وقتی که نمی‌توانیم از eventهای مرتبط با Save/Update در NHibernate به درستی استفاده کنیم یک تریگر روی جدول مورد نظر می‌گذاریم. یا مثلا وقتی که سرعت update جدول یا فیلدی خیلی پایین است، حالا یا به علت محدودیت‌های خود NHibernate یا به علت عدم استفاده صحیح خودمان، آن وقت یک Stored Procedure نوشته و عملیات مورد نظر را به جای NHibernate از طریق آن انجام می‌دهیم.

این نوع دور زدن NHibernate به غیر از این که نرم‌افزار را از لحاظ فناوری دو تیکه و ناخوانا می‌کند، مشکل دیگری نیز دارد. چون این طور کارها از دروازه NHibernate session management رد نمی‌شوند، NHibernate هم از تغییرات داده‌ای آنها بی‌خبر مانده و نمی‌تواند Cache را (چه سطح اول و چه سطح دوم) به خوبی به روز رسانی کند. در نتیجه مقادیر موجود در Cache نامعتبر شده و آنچه که از Cache دریافت می‌داریم ممکن است قدیمی و به درد نخور باشد. چون یک تریگر یا sp دور از چشم NHibernate آنها را تغییر داده است. در این طور مواقع تنها کاری که می‌توان کرد عدم استفاده از روش‌های این چنینی یا چشم پوشی از Caching می‌باشد.

Second Level Cache in Castle ActiveRecord

Second level cache is different than first level cache. It acts on session factory level not each individual session. In order to setup second level cache in a Castle ActiveRecord application consider following points:

1. Settings of second level cache in Castle ActiveRecord is basically as same as NHibernate.

2. In addition of web.config/app.config settings, caching must enabled in entities too.

3. Caching in collections and queries must be considered too.

4. Each operation must be wrapped in a transaction.

5. FindAll series of Castle ActiveRecord does not allow query caching. You must use your own custom build of Castle ActiveRecord.

6. For more info go here, here and here.

پیغام خطای کشنده

بعضی پیغام خطاها آنقدر سمج هستند و آن قدر منبع نامعلومی دارند که می‌توان به آنها لقب لعنتی، کشنده یا حتی دهن صاف کن داد.

به تازگی یکی از این پیغام خطاهای کشنده برای من اتفاق افتاد. زمانی که در حال فعال‌سازی Caching سطح دوم برای یک برنامه‌ی مبتنی بر Castle ActiveRecord بودم دچار خطای زیر شدم:

Unable to cast object of type ‘NHibernate.Caches.SysCache.SysCacheProvider’ to type ‘NHibernate.Cache.ICacheProvider’.

NHibernate.Caches.SysCache نام اسمبلی است که قابلیت Second Level Cache را به NHibernate و Castle Active Record اضافه می‌کند. ظاهر امر نشان می‌داد که بین اسمبلی‌ها مورد استفاده ناهمخوانی نسخه‌ای وجود دارد. مثلا NHibernate من نسخه 2.1 است ولی SysCache مورد استفاده نسخه 3.0 است. به همین دلیل بارها و بارها نسخه‌های متفاوت NHibernate و Castle و SysCache را از SourceForge دریافت کردم ولی مشکل حل نشد. سعی کردم به جای SysCache از دیگر Cache Providerها استفاده کنم ولی آن هم نشد. در ادامه این سعی و تلاش‌ها چندین بار تنظیمات Caching مورد استفاده را در web.config و mappingها چک کردم، سورس‌های Castle و SysCache را خودم کامپایل کردم و حتی به فکر افتادم خودم یک Cache Provider ساده بنویسم اما مشکل حل نشد که نشد.

خلاصه این که در اوج ناامیدی بودم که در یکی از آن «لحظات طلایی» متوجه علت خطا شدم. چون تعداد Assemblyهای مورد استفاده در ارتباط با NHibernate و Castle زیاد بود، یکی از دوستان آنها را با هم merge کرده بود تا یک اسمبلی واحد تشکیل گردد. من هم اسمبلی NHibernate.Caches.SysCache را در کنار اسمبلی فوق‌الذکر گذاشته بودم و در تنظیم cache.provider_class نام اسمبلی NHibernate.Caches.SysCache را ذکر کرده بودم، در حالی که باید اسمبلی NHibernate.Caches.SysCache را با مجموعه قبلی merge (با استفاده از ilmerge.exe) کرده و در cache.provider_class نام اسمبلی حاصل از merge را ذکر می‌کردم!

First level cache in Castle ActiveRecord

While I was trying to use first level cache in a Castle ActiveRecord based application I found some points that like to share:

1. First level cache is alive within a single session (NHibernate’s ISession) only.

2. First level cache is enabled by default and can not be disabled.

3. If you don’t use SessionScope properly you may have more than one session per ASP.NET web request. So you will not profit from first level cache. SessionScope is Castle ActiveRecord’s way for NHibernate’s ISession. Correct way to use SessionScopt in an ASP.NET web application is to use SessionScopeWebModule as a httpModule. More info could be found here.

4. First level cache can be used only by NHibernate Load and Get methods. So with Castle ActiveRecord you can only use Find and TryFind method. FindAll and other methods can not benefit from first level cache because they use ICriteria internally.

More info about NHibernate caching is available here.