‫‫View به عنوان یک Domain Entity در Castle ActiveRecord

گاهی اوقات تلفیقی از چند جدول با شرایط خاص داریم که می‌خواهیم آن را به چشم یک Domain Entity نگاه کنیم. در حالت عادی انجام این کار خیلی ساده است. کافی است یک View از تلفیق آن جداول بسازیم و View را مطابق نمونه زیر به عنوان یک Domain Entity تعریف کنیم:

[ActiveRecord("SampleView")]
public class SampleDomain : ActiveRecordBase<SampleDomain>
{
[Property]
public virtual string Prop1 { set; get; }

[Property]
public virtual string Prop2 { set; get; }
}
در اینجا SampleView نام View دیتابیسی است و Prop1 و Prop2 نام دو تا از ستون‌های آن است.

اما اگر ما یک Entity پایه داشتیم و حتماً باید همه Entityها از آن Entity پایه ارث‌بری می‌کردند چه باید می‌کردیم؟ Entityهای پایه معمولا دارای Propertyهای مشترکی مثل Id و LastModifyDate هستند. فرض کنید اسم آن Entity پایه EntityBase بود. آن وقت Domain Entity مورد نظر به شکل زیر در می‌آمد:

[ActiveRecord("SampleView")]
public class SampleDomain : BaseEntity<SampleDomain>
{
[Property]
public virtual string Prop1 { set; get; }

[Property]
public virtual string Prop2 { set; get; }
}
تنها تفاوتی که به وجود آمد ارث‌بری از BaseEntity به جای خود ActiveRecordBase بود. البته View دیتابیسی مورد نظر باید شامل همه Propertyهای مشترک تعریف شده در BaseEntity هم باشد مثل Id و LastModifyDate.

‫‫‫رابطه یک به یک در NHibernate و Castle ActiveRecord

بین دو Entity می‌توان رابطه یک به یک برقرار کرد. یعنی به ازای یک instance (رکورد) از یکی، فقط و فقط یک instance (رکورد) در دیگری وجود داشته باشد. هر چند که NH این نوع رابطه را نشانه طراحی بد می‌داند، اما دو راه برای پیاده‌سازی آن مهیا کرده است:

۱- روش primary key associations: در این روش هر دو entity از یک primary key مشترک استفاده می‌کنند.

۲- روش unique foreign key associations: در این روش یکی از طرفین رابطه یک ستون اضافی تعریف کرده و آن را به کلید خارجی مرتبط با primary key طرف دیگر رابطه اختصاص می‌دهد.

هر دوی این روش‌ها به تفصیل در مستندات NH توضیح داده شده‌اند. Castle ActiveRecord هم هر دو این روش‌ها را پشتیبانی می‌کند. مستندات خود ActiveRecord فقط روش اول توضیح داده است اما برای پیاده‌سازی روش دوم هم کار چندان سختی نیست
{لینک به مطلب مرتبط در وبلاگ خودم}.

Unique foreign key associations in Castle ActiveRecord

NHibernate have 2 varieties of one-to-one association, primary key associations and unique foreign key associations. Castle ActiveRecord documentation describes just first varity, primary key associations.

But how about second variety, unique foreign key associations? Well, it can be implemented as follow. Please notice sample:

    [ActiveRecord(Lazy = true)]
    public class User : ActiveRecordBase
    {
        [PrimaryKey]
        public virtual long ID { set; get; }

        [Property]
        public virtual string FirstName { set; get; }

        [OneToOne(PropertyRef = “ContainerUser”)]
        public virtual Profile ContainerProfile { set; get; }
    }

    [ActiveRecord(Lazy = true)]
    public class Profile : ActiveRecordBase    {
        [PrimaryKey]
        public virtual long ID { set; get; }

        [Property]
        public virtual string Field1 { set; get; }

        [BelongsTo(“ContainerUser”, Unique = true, NotNull = true)]
        public virtual User ContainerUser { set; get; }
    }

‫مشکل Stack Overflow در NHibernate

به تجربه دریافته‌ام هر وقت که در NHibernate/Castle AR بدون هیچ دلیل واضحی مشکل Stack Overflow به وجود می‌آید، باید مکانیزم Dirty detection را بررسی کرد. در این مکانیزم، NH خودش objectهایی را که dirty شده‌اند را پیدا کرده و آنها را update می‌کند.

بعضی وقت‌ها پیش می‌آید که update شدن یک آبجکت dirty باعث dirty شدن یک جای دیگر شده و آن هم به نوبه خودش نیاز به update پیدا کرده و این زنجیره آنقدر ادامه پیدا کرده که برنامه در loop افتاده و در اثر Stack Overflow از کار می‌افتد. البته مشکل همیشه دقیقاً به همین حالت نیست اما علت خطا کم و بیش همین است.

به عنوان مثال فرض کنید که برای به روز رسانی یک entity نیاز دارید به دیتابیس مراجعه کرده و چیزی را در آنجا چک کنید. با اولین مراجعه به دیتابیس، NH اول به طور خودکار session را flush کرده و بعد از آن اطلاعات را از دیتابیس فراخوانی می‌کند. اما به هنگام flush شدن، entity مورد نظر ما هنوز dirty است و نیاز به ذخیره در دیتابیس دارد. این روال باعث به روز رسانی زنجیره‌ای entity شده و در نهایت باعث بروز stack overflow می‌گردد.

یکی از راه حل‌های رفع این مشکل استفاده از sessionهای مستقل از هم است. چون dirty checking هر sessionی مستقل از sessionهای دیگر است. در Castle AR می‌توان از روشی مشابه زیر استفاده کرد:

using (new SessionScope())
{
//do something with this separate session
}
پ.ن.: به این لینک هم نگاهی کنید.

‫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 هم همین کار را انجام داد باید از روش‌های متفاوت و جداگانه‌ای استفاده نمود.

Entity auditing in CastleActiveRecord

In my Castle ActiveRecord based application I have a base class named ARSuperClass that all my domain entities except than EntityLogger inherit from it instead of inheriting from ActiveRecordBase. I have ModifyDate and CreateDate fields in ARSuperClass that must be updated each time an entity is created or updated. Additionally each creation/updating operations must be logged in EntityLogger too.

In first solution I overrode OnSave and OnUpdate then changed ModifyDate and CreateDate in them and also added a new record to EntityLogger. This solution was working but with each entity update, two updates were occurring. It was because with changing audit fields, the object went dirty again and so another update was needed.

In second solution I replaced OnSave with BeforeSave and replace OnUpdate with OnFlushDirty. This solved duplicate updates with entity creation but entity updates were still generating two updates.

In third solution I searched for something like BeforeUpdate but Castle ActiveRecord didn’t have such a thing. So I used NHibernate’s IPreUpdateEventListener and all my problems get solved.

For more info about how to use NHibernate event listener in Castle ActiveRecord go here.

‫محدودیت‌های Castle ActiveRecord

Castle ActiveRecord یک کتابخانه مفید برای استفاده از NHibernate است. این کتابخانه هم کار mapping را مشابه Fluent NHibernate ساده می‌کند و هم کار مدیریت session و خیلی کارهای دیگر را خودش مدیریت می‌کند. به طوری که استفاده از NHibernate بسیار ساده‌تر شود. Castle ActiveRecord در واقع پیاده‌سازی Design Patternی به همین نام یعنی Active Record است. با این که Castle ActiveRecord کار با NHibernate را بسیار ساده‌تر کرده ولی به خاطر همین سادگی، محدودیت‌هایی هم نسبت به استفاده مستقیم از NHibernate دارد. از جمله:

۱- الگوی Active Rercord به طور کلی مورد انتقاد است. بعضی‌ها معتقدند الگوی Active Record ناکارآمد است و می‌توان به جای آن از الگوهای بهتری مثل Repository Pattern استفاده کرد. دقت کنید که الگوی پیش‌فرض Castle ActiveRecord همان Active Record است اما می‌توان با الگوهای دیگری از جمله Unit of Work هم با آن کار کرد.

۲- در فرایند Schema Update آن نمی‌توان اسکریپت‌های update را قبل از اجرا دید.

۳- Second Level Cache در بیشتر متودهای کلاس ActiveRecordBase امکان پذیر نیست، از جمله بیشتر overloadهای متود FindAll. برای فعال بودن Second Level Cache باید در Criteriaها از SetCachable(true)‎ استفاده شود.

۴- Second Level Cache ظاهراً در ActiveRecordLinq هم به همان دلیل قبلی غیر فعال است. در LINQ موجود در NHibernate چه در provider قدیمی چه در NH 3 راه حل‌هایی وجود دارد ولی ظاهراً نمی‌توان از آنها در Castle ActiveRecord استفاده کرد.

قاعدتاً غیر از این‌ها محدودیت‌های دیگری هم وجود دارد اما آن چه که مهم است این است که عمده این محدودیت‌ها را می‌توان با ارث‌بری از کلاس ActiveRecordBase یا ActiveRecordMediator و افزودن امکانات مورد نظر به کلاس جدید دور زد. اگر هم پایه بودید می‌توانید یک branch از سورس کد Castle ActiveRecord ایجاد کرده، قابلیت مورد نظر را به آن اضافه کرده و patch آن را به تیم توسعه دهنده Castle ActiveRecord بفرستید تا اگر مورد قبول واقع شد، آن امکان من بعد در نسخه‌های رسمی Castle ActiveRecord اضافه شود.

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.