‫NHibernate session management در محیط‌های مختلف

مهم‌ترین مسئله‌ای که در Session management در NHibernate وجود دارد، مسئله نگهداری session است. به طور معمول سعی می‌شود برای انجام یک کار فقط یک session باز شود نه بیشتر. اگر طی انجام همان کار مجدداً نیاز به session شد از همان session قبلی استفاده می‌شود نه این که یک session جدید open شود.

این کار در وب خیلی راحت است. session instance مورد نظر در HttpContext قرار داده می‌شود. در مورد ویندوز و WCF هم کار چندان سختی نیست. چون از Thread برای نگهداری session استفاده می‌شود. اما موقعیت‌هایی وجود دارد که می‌خواهیم به طور هم زمان در دو محیط از یک Session Factory و Session استفاده نماییم. مثلاً هم وب را داریم و هم یک سرویس WCF را.

این طور وقت‌ها می‌توان یک SessionContext سفارشی ساده ساخت که هم session را در HttpContext نگاه داشت و هم در جای دیگری که در محیط مورد نظر معنی دار است. اصل این راه حل در اینجا توضیح داده شده.

‫NHibernate Session Management در Winform و WCF

بحث NHibernate Session Management در برنامه‌های وب اصلاً کار سختی نیست. روتین‌ها و نمونه‌های زیادی هم در مورد آن وجود دارد. اما انجام همین بحث در Winform و WCF کمی کار می‌برد. یکی از بهترین روش‌ها برای مدیریت Session در NHibernate استفاده از الگوی Unit Of Work است. تطبیق این الگو با مدل کاری وب خیلی ساده است. فقط کافی است فرض کنید شروع هر unit of workی با شروع Web Request توسط کاربر و پایان آن با رندر صفحه و ارسال آن به کلاینت منطبق است.

برای ویندوز و WCF هم که از بسیاری جهات به هم شبیه هستند چند روش وجود دارد. یکی از این راه‌ها conversation per business transaction یا CpBT است. در CpBT منطق کاری (business transaction) مبنای کار است. یعنی شروع unit of work با شروع عملیات کاری و پایان آن با پایان عملیات کاری اتفاق می‌افتد. به عنوان مثال فرض کنید یک منطق یا روال کاری داریم به نام «محاسبه حقوق ماهیانه شخص فلان». در CpBT شروع unit of work وقتی است که کاربر از طریق winform یا wcf روال «محاسبه حقوق» را شروع می‌کند. به عبارت دیگر شروع روال «محاسبه حقوق» توسط کاربر باعث می‌شود یک session از NHibernate دریافت شده و یک Transaction شروع شود. حال هر وقت که کاربر پایان کار را با مثلاً فشردن دکمه تایید به پایان رساند، پایان unit of work هم فرا رسیده و کارهای مرتبط با آن مثل commit شدن transaction هم انجام می‌شود. دقت شود که اگر از الگوی CpBT استفاده نمی‌شد ممکن بود در حین کاری مثل محاسبه حقوق که چندین و چند مراجعه به دیتابیس و چند modify اطلاعات دارد، مجبور شویم چندین session از NHibernate گرفته و چندین بار commit transation داشته باشیم. که هم کار را خیلی کند می‌کند و هم مدیریت transactionها را غیر ممکن می‌کند.

برای اعمال CpBT در NHibernate و WCF و Winform یک کتابخانه مفید به نام uNhAddins وجود دارد. این کتابخانه تمام این کار را اتوماتیک می‌کند. مثلاً برای آن که بگویید یک متودی از یک کلاس برابر است با نقطه شروع یک CpBT فقط کافی است روی کلاس آن عبارت

[PersistenceConversational(MethodsIncludeMode = MethodsIncludeMode.Implicit)]

و روی متود آن عبارت

[PersistenceConversation(ConversationEndMode = EndMode.End)]

را قرار دهید. البته نیاز به کدها و تنظیمات دیگری هم وجود دارد. از جمله این که اگر WCF کار می‌کنید، از uNhAddIns.SessionEasier.Contexts.ThreadLocalSessionContext به عنوان current_session_context_class در تنظیمات NHibernate استفاده کنید و اگر از WinForm استفاده می‌کنید از uNhAddIns.SessionEasier.Conversations.ThreadLocalConversationalSessionContext استفاده کنید.

به عنوان یک نمونه کامل uNhAddins و CpBT که هم WCF را پوشش می‌دهد، هم WPF را، هم MVP را و هم IoC را، نگاهی به نمونه کد uNHAddins.Examples.SessionManagement در مخزن کد uNhAddins بیندازید.

‫راه‌هایی برای دینامیک کردن entityهای NHibernate

فرض کنید تعدادی کلاس سی‌شارپ را با استفاده از Reflection.Emit در زمان اجرا (Runtime) ساخته‌اید. این یعنی کلاس‌ها دینامیک بوده و هیچ سورس کدی وجود ندارد، بلکه همه چیز فقط در حافظه وجود دارد یا نهایتاً فایل dll اسمبلی آن را بتوان در دیسک ذخیره کرد. حال می‌خواهیم برای این کلاس‌های دینامیک HBM یا همان فایل mapping مربوط به NHibernate را هم داشته باشیم. هدف نهایی این است که این کلاس‌ها از طریق HBM به یک سری جداول دیتابیسی map شده و بتوان با NHibernate با آن‌ها کار کرد. دقت کنید که نمی‌توان HBMها را از قبل داشت چون خود entityها از قبل وجود ندارند. ما هیچ اطلاعاتی راجع به نام آنها، پراپرتی‌ها، نوع آن‌ها یا دیگر مشخصات آن‌ها نداریم.

من برای انجام این امر یعنی داشتن mapping دینامیک برای entityهای دینامیک زمان Runtime چندین راه را امتحان کردم. با اینکه بیشتر آن‌ها به جواب نرسیدند اما هر کدام نکات جالبی داشتند:

۱- استفاده از فیلدهای XML: اگر تعداد جداول خیلی زیاد شوند، هم نگهداری آن‌ها سخت می‌شود و هم نوشتن queryهای آن خیلی خیلی سخت می‌شود.

۲- تولید کد کلاس سی‌شارپ و HBM با ابزارهایی مثل T4 یا CodeDOM و build آن‌ها و خوراندن آن‌ها به AppDomain جاری: سرعت خیلی پایینی خواهد داشت، احتمالاً دسترسی‌های خاص هم نیاز خواهد داشت. هر تغییری در ساختار entityهای دینامیک موجب restart شدن application خواهد شد. تولید کد کار چندان ساده‌ای نیست.

۳- استفاده از آبجکت دینامیک دات‌نت که از نسخه ۴ اضافه شده است: قابلیت توسعه و دینامیک بودن را در حد fieldها و propertyها می‌دهد. نمی‌توان در زمان اجرا mappingهای جدید اضافه کرد.

۴- استفاده از فیلدهای IDictionary: همان مشکلات راه حل آبجکت دینامیک دات‌نت را دارد.

۵- استفاده از قابلیت جدید mapping by code در NHibernate یا ConfORM یا Fluent NHibernate: به هدف خیلی نزدیک است، چون همه چیز بر اساس type است و هیچ HBMی وجود ندارد. اما تولید delegateها و lambda expressionهای مورد نیاز به صورت runtime اگر کار غیر ممکنی باشد حتماً کار سخت و پیچیده‌ای خواهد بود. در مورد Automapping موجود در FNH کار را خوب راه می‌اندازد اما انعطاف پذیری آن با توجه به محدودیت‌های این کار، آن را غیر قابل استفاده می‌سازد.

۶- استفاده از SetResultTransformer موجود در NHibernate برای query زدن روی جداول دیتابیس بدون نیاز به داشتن Entityها. تا اندازه زیادی جواب می‌دهد! ولی زیادی ساده و بی‌ساختار است. نیاز به نوشتن مقادیر زیادی SQL هم دارد.

۷- کلاس‌های تولیدی توسط Reflection.Emit از همان ابتدا به صورتی باشد که روی تعریف کلاس‌ها و memberها از Attributeهای خاص Castle ActiveRecord استفاده شده باشد. این راه چندان بد به نظر نمی‌رسد اما دو تا مشکل دارد: ۱- استفاده اجباری الگوی Active Record ۲- وابستگی به پروژه Castle ActiveRecord.

۸- ساختن کلاس Mapping از Namespace خود NHibernate: این اولین راهی بود که امتحان کردم. مبنای اولیه کار نمونه کد Ayende Rahien بود. اما متأسفانه به هیچ وجه نتوانستم آن کد را اجرا کنیم. با وجود این گزینه هنوز گزینه خوبی به نظر می‌رسد.

۹- ایده گرفتن از کدهای FNH و ConfORM و Castle ActiveRecord و حتی خود NHibernate: این پروژه در داخل خود کدهایی برای کار با Mappingها دارند. اما حجیم بودن کد آن‌ها مانع از استفاده راحت از آن‌ها می‌شود.

۱۰- استفاده از کتابخانه‌های 3rd party تولید HBM از جداول دیتابیسی: این راه را فقط به عنوان رزرو اینجا آورده‌ام و اصلاً نمی‌دانم که آیا چنین کتابخانه‌هایی وجود دارند یا نه.

‫استفاده از Fluent NHibernate

از همان اولین باری که اسم Fluent NHibernate را شنیدم فکر کردم چیز جالبی نیست. نمی‌دانم چرا، شاید به خاطر آن که فکر می‌کردم انعطاف‌پذیری را فدای راحتی کرده یا شاید هم به خاطر غرور low level.

طی مدت اخیر که به دنبال راهی برای dynamic کردن entityهای NHibernate بودم خود به خود مجبور شدم مروری به همه راه‌حل‌های mapping از جمله Fluent NHibernate داشته باشم.

طی این مرور بودم که فهمیدم FNH آنقدر هم که فکر می‌کردم بد نیست، اولاً به خاطر این که Auto Mapping دارد. دوم به خاطر این که Fluent mapping آن سادگی و خوانایی نسبتاً قابل قبولی نسبت به HBM دارد.

FNH سه راه برای mapping دارد: Auto Mapping، Fluent Mapping و Traditional Mapping.‏ Auto mapping کار mapping را در صورتی که نیازمندی‌هایتان با پیش‌فرض‌های FNH خیلی تفاوت نداشته شد به خوبی راه می‌اندازد. در این روش شما فقط یک class ساده سی‌شارپی را به FNH معرفی می‌کنید بدون آن که از attribute یا lambda expression برای بیان تناظر بین کلاس و جدول استفاده کرده باشید. FNH خودش یک mapping پیش فرض در نظر خواهد گرفت. برای فهم سادگی موضوع حتماً نگاهی به نمونه‌های اینجا بیندازید.

‫استفاده بهینه از Lazy Loading

برای Lazy Loading در NHibernate روال زیر را انجام داده بودم. lazy را برای همه mappingها true کرده بودم. در web.config هم همینطور. سپس هر جا که lazy مشکل پیدا می‌کرد و خطای LazyInitializationException اتفاق می‌افتاد، آن association یا collection را با استفاده از کلاس NHibernateUtil.Initialize پیش load می‌کردم تا مشکل حل شود. این راه حل خیلی هم غلط نیست، اما می‌توان برای رفع مشکل از دست رفتن session از روش‌های زیر هم استفاده کرد:

۱- در associationهای مشکل دار از outer-join = true استفاده شود.
۲- در query APIهای مختلف هم می‌توان نوع outer-join را تعیین کرد.

باید دقت کرد که نوع outer-joinی که در سطح mapping یک entity تعریف می‌شود قابل override در query یا هر جای دیگر است.

‫‫‫رابطه یک به یک در 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; }
    }

‫پیغام خطاهای معروف NHibernate – قسمت اول

یکی از معروف‌ترین خطاهای NHibernate خطای زیر است:

No row with the given identifier exists[EntityName#rec_id]
به جای EntityName نام کامل entity و به جای ‎#rec_id شماره رکورد قرار می‌گیرد. این خطا همان طور که پیغامش اشاره می‌کند وقتی به وقوع می‌پیوندد که NH در دیتابیس به دنبال رکورد خاصی می‌گشته ولی آن رکورد در دیتابیس وجود نداشته است. معمولاً می‌توان ردی از Load یا Get را هم در StackTrace پیدا کرد. گاهی اوقات که rec_id چیز عجیب و غریبی باشد، این پیغام خطا گمراه کننده‌تر هم می‌شود. خصوصاً وقتی که از idهای نوع Guid استفاده شده باشد.

این پیغام خطا معمولاً خیلی زیاد به وجود می‌آید. بسیاری از کاربران، خصوصاً آنها که آشنایی کمتری با NH یا ORMها به طور کلی دارند، با دیدن این پیغام خطا دچار اشتباه شده و فکر می‌کنند NH دچار مشکل خاصی شده که باید با تلاش فراوان آن را حل کرد. این خطا در Data Binding صفحات ASP.NET هم خیلی زیاد دیده می‌شود. خصوصاً وقتی که ترتیب Bind کنترل‌ها به هم خورده و کنترلی زودتر از موقع DataBind شده. است. چیزی که خیلی مهم است این است که این پیغام خطا فقط نشانه است که کسی از NH خواسته که رکوردی را از دیتابیس فراخوانی کند. این مشکل NH نیست که آن رکورد وجود ندارد، بلکه خود برنامه باید بررسی گردد تا علت فراخوانی از دیتابیس مشخص گردد.

‫مشکل 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
}
پ.ن.: به این لینک هم نگاهی کنید.

بی‌اطمینانی به مایکروسافت

سال‌های سال است که با محصولات مایکروسافت کار می‌کنم. دقیقاً از داس ۵ به این طرف. البته هیچ وقت هم با این موضوع مشکل خاصی نداشتم. هر وقت مایکروسافت داس را کنار می‌گذاشت و ویندوز را رو می‌کرد ما هم سراغ ویندوز می‌رفتیم، هر وقت ASP Classic را دور می‌انداخت ما هم همین کار را می‌کردیم و سراغ ASP.NET می‌رفتیم. خلاصه این که هر وقت مایکروسافت یکی از فناوری‌هایش را با فناوری جدیدتری جایگزین می‌کرد ما هم بدون هیچ اعتراضی همین کار را می‌کردیم. این تند تند عوض شدن فناوری را هم می‌گذاشتیم به حساب این که در دنیای کامپیوتر همه چیز تند تند عوض می‌شود و این ماییم که باید خودمان را با این موضوع تطبیق دهیم.

اما از وقتی که با NHibernate آشنا شدم و روال کار آن را با LINQ-to-SQL و Entity Framework مقایسه کردم دیدم که یک جای کار می‌لنگد. NHibernate با احستاب سوابق جاوایی‌اش چندین سال است که به خوبی کار کرده و خیلی کم دچار تغییرات شدید شده است. کلیاتی که از NHibernate یاد گرفته‌ام هنوز همان است و مجبور نشده‌ام به جز LINQ-to-NHibernate که یک افزونه جدید حساب می‌شود نه یک تغییر، چیز اساسی یاد بگیرم. اما کسانی که ORM را با مایکروسافت شروع کرده‌اند مثل من راحت نبوده‌اند. چون اول وقت زیادی روی LINQ-to-SQL گذاشتند ولی بعد از مدت کوتاهی به آنها اعلام شد که LINQ-to-SQL دیگر توسعه داده نخواهد شد و به جای آن باید از Entity Framework استفاده کنند. این یعنی چیزی را که یاد گرفته و به آن عادت کرده‌اید را باید به طور کامل دور ریخته و فناوری جدیدتری را با صرف کلی وقت یاد بگیرید.

مشابه این مسئله را در SourceSafe و جایگزینی آن با TFS هم دیده‌ام. کسانی که با SourceSafe کار می‌کردند مجبور شدند آن را با TFS عوض کنند. اما کسانی که از همان اول با SVN کار می‌کردند هیچ وقت دچار همچین اجباری نشدند. حدس می‌زنم همین مسئله را در مورد Web Serviceها از یک سو و ‎.Net Remoting و WCF از دیگر سو داشته باشیم.

مجموع این قضایا باعث شده که اطمینان‌ام را به مایکروسافت از دست بدهم و سعی کنم همیشه جایگزین‌های غیر مایکروسافتی را انتخاب کنم. این طوری دیگر مجبور نیستم تند تند چیزهای جدید یاد بگیرم. به بقیه همکارانم هم توصیه می‌کنم به این مسئله خوب فکر کنند. توجه شود همه این صحبت‌ها بدون توجه به مسائل اخلاقی و Copyright هستند که خود بحث جداگانه‌ای را می‌طلبد.