مطالب
فعال سازی سطح دوم کش در Fluent NHibernate

سطح اول کش در NHibernate در یک تراکنش معنا پیدا می‌کند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنش‌های جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.

الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریم‌ها امکان دریافت فایل‌های مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، می‌خواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامه‌های وب و دسکتاپ بدون مشکل کار می‌کند).

ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate‌ می‌باشد. این‌کار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعریف نوع کش در هنگام ایجاد نگاشت‌ها
اگر از ClassMap‌ها برای تعریف نگاشت‌ها استفاده می‌کنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده می‌کنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعریف یک سطر فوق هم مهم است؛ زیرا در غیراینصورت فقط primary key حاصل از بار اول فراخوانی کوئری‌های مرتبط کش می‌شوند؛ نه نتیجه عملیات. هرچند این مورد هم یک قدم مثبت به شمار می‌رود از این لحاظ که برای مثال تهیه نتایج کوئری بر روی فیلدی که ایندکس بر روی آن تعریف نشده است همیشه از حالت تهیه کوئری بر روی فیلد دارای ایندکس کندتر است. اما هدف ما در اینجا این است که پس از بار اول فراخوانی کوئری، بار‌های دوم و بعدی دیگر کوئری خاصی را به بانک اطلاعاتی ارسال نکرده و نتایج از کش خوانده شوند (جهت استفاده عموم کاربران در کلیه تراکنش‌های جاری برنامه).

د) اعمال متد Cacheable به کوئر‌ی‌ها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان می‌کنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنش‌های جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینه‌تر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).

مطالب
آشنایی با NHibernate - قسمت دوم

آزمون واحد کلاس نگاشت تهیه شده

در مورد آشنایی با آزمون‌های واحد لطفا به برچسب مربوطه در سمت راست سایت مراجعه بفرمائید. همچنین در مورد اینکه چرا به این نوع API کلمه Fluent اطلاق می‌شود، می‌توان به تعریف آن جهت مطالعه بیشتر مراجعه نمود.

در این قسمت قصد داریم برای بررسی وضعیت کلاس نگاشت تهیه شده یک آزمون واحد تهیه کنیم. برای این منظور ارجاعی را به اسمبلی nunit.framework.dll به پروژه UnitTests که در ابتدای کار به solution جاری در VS.Net افزوده بودیم، اضافه نمائید (همچنین ارجاع‌هایی به اسمبلی‌های پروژه NHSample1 ، FluentNHibernate ، System.Data.SQLite ، NHibernate.ByteCode.Castle و Nhibernate نیز نیاز هستند). تمام اسمبلی‌های این فریم ورک‌ها از پروژه FluentNHibernate قابل استخراج هستند.

سپس سه کلاس زیر را به پروژه آزمون واحد اضافه خواهیم کرد.
کلاس TestModel : (جهت مشخص سازی محل دریافت اطلاعات نگاشت)

using FluentNHibernate;
using NHSample1.Domain;

namespace UnitTests
{
public class TestModel : PersistenceModel
{
public TestModel()
{
AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
}
}
}

کلاس FixtureBase : (جهت ایجاد سشن NHibernate در ابتدای آزمون واحد و سپس پاکسازی اشیاء در پایان کار)

using NUnit.Framework;
using NHibernate;
using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;

namespace UnitTests
{
public class FixtureBase
{
protected SessionSource SessionSource { get; set; }
protected ISession Session { get; private set; }

[SetUp]
public void SetupContext()
{
var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.InMemory);

SessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
new TestModel());

Session = SessionSource.CreateSession();
SessionSource.BuildSchema(Session);
}

[TearDown]
public void TearDownContext()
{
Session.Close();
Session.Dispose();
}
}
}

و کلاس CustomerMapping_Fixture.cs : (جهت بررسی صحت نگاشت تهیه شده با کمک دو کلاس قبل)

using NUnit.Framework;
using FluentNHibernate.Testing;
using NHSample1.Domain;

namespace UnitTests
{
[TestFixture]
public class CustomerMapping_Fixture : FixtureBase
{
[Test]
public void can_correctly_map_customer()
{
new PersistenceSpecification<Customer>(Session)
.CheckProperty(c => c.Id, 1001)
.CheckProperty(c => c.FirstName, "Vahid")
.CheckProperty(c => c.LastName, "Nasiri")
.CheckProperty(c => c.AddressLine1, "Addr1")
.CheckProperty(c => c.AddressLine2, "Addr2")
.CheckProperty(c => c.PostalCode, "1234")
.CheckProperty(c => c.City, "Tehran")
.CheckProperty(c => c.CountryCode, "IR")
.VerifyTheMappings();
}
}
}

توضیحات:
اکنون به عنوان یک برنامه نویس متعهد نیاز است تا کار صورت گرفته در قسمت قبل را آزمایش کنیم.
کار بررسی صحت نگاشت تعریف شده در قسمت قبل توسط کلاس استاندارد PersistenceSpecification فریم ورک FluentNHibernate انجام خواهد شد (در کلاس CustomerMapping_Fixture). این کلاس برای انجام عملیات آزمون واحد نیاز به کلاس پایه دیگری به نام FixtureBase دارد که در آن کار ایجاد سشن NHibernate (در قسمت استاندارد SetUp آزمون واحد) و سپس آزاد سازی آن را در هنگام خاتمه کار ، انجام می‌دهد (در قسمت TearDown آزمون واحد).
این ویژگی‌ها که در مباحث آزمون واحد نیز به آن‌ها اشاره شده است، سبب اجرای متدهایی پیش از اجرا و بررسی هر آزمون واحد و سپس آزاد سازی خودکار منابع خواهند شد.
برای ایجاد یک سشن NHibernate نیاز است تا نوع دیتابیس و همچنین رشته اتصالی به آن (کانکشن استرینگ) مشخص شوند. فریم ورک Fluent NHibernate با ایجاد کلاس‌های کمکی برای این امر، به شدت سبب ساده‌ سازی انجام آن شده است. در این مثال، نوع دیتابیس به SQLite و در حالت دیتابیس در حافظه (in memory)، تنظیم شده است (برای انجام امور آزمون واحد با سرعت بالا).
جهت اجرای هر دستوری در NHibernate نیاز به یک سشن می‌باشد. برای تعریف شیء سشن، نه تنها نیاز به مشخص سازی نوع و حالت دیتابیس مورد استفاده داریم، بلکه نیاز است تا وهله‌ای از کلاس استاندارد PersistanceModel را نیز جهت مشخص سازی کلاس نگاشت مورد استفاده مشخص نمائیم. برای این منظور کلاس TestModel فوق تعریف شده است تا این نگاشت را از اسمبلی مربوطه بخواند و مورد استفاده قرار دهد (بر پایی اولیه این مراحل شاید در ابتدای امر کمی زمانبر باشد اما در نهایت یک پروسه استاندارد است). توسط این کلاس به سیستم اعلام خواهیم کرد که اطلاعات نگاشت را باید از کدام کلاس دریافت کند.
تا اینجای کار شیء SessionSource را با معرفی نوع دیتابیس و همچنین محل دریافت اطلاعات نگاشت اشیاء معرفی کردیم. در دو سطر بعدی متد SetupContext کلاس FixtureBase ، ابتدا یک سشن را از این منبع سشن تهیه می‌کنیم. شیء منبع سشن در این فریم ورک در حقیقت یک factory object است (الگوهای طراحی برنامه نویسی شیءگرا) که امکان دسترسی به انواع و اقسام دیتابیس‌ها را فراهم می‌سازد. برای مثال اگر روزی نیاز بود از دیتابیس اس کیوال سرور استفاده شود، می‌توان از کلاس MsSqlConfiguration بجای SQLiteConfiguration استفاده کرد و همینطور الی آخر.
در ادامه توسط شیء SessionSource کار ساخت database schema را نیز به صورت پویا انجام خواهیم داد. بله، همانطور که متوجه شده‌اید، کار ساخت database schema نیز به صورت پویا توسط فریم ورک NHibernate با توجه به اطلاعات کلاس‌های نگاشت، صورت خواهد گرفت.

این مراحل، نحوه ایجاد و بر پایی یک آزمایشگاه آزمون واحد فریم ورک Fluent NHibernate را مشخص ساخته و در پروژه‌های شما می‌توانند به کرات مورد استفاده قرار گیرند.

در ادامه اگر آزمون واحد را اجرا نمائیم (متد can_correctly_map_customer در کلاس CustomerMapping_Fixture)، نتیجه باید شبیه به شکل زیر باشد:



توسط متد CheckProperty کلاس PersistenceSpecification ، امکان بررسی نگاشت تهیه شده میسر است. اولین پارامتر آن، یک lambda expression خاصیت مورد نظر جهت بررسی است و دومین آرگومان آن، مقداری است که در حین آزمون به خاصیت تعریف شده انتساب داده می‌شود.

نکته:
شاید سؤال بپرسید که در تابع can_correctly_map_customer عملا چه اتفاقاتی رخ داده است؟ برای بررسی آن در متد SetupContext کلاس FixtureBase ، اولین سطر آن‌را به صورت زیر تغییر دهید تا عبارات SQL نهایی تولید شده را نیز بتوانیم در حین عملیات تست مشاهده نمائیم:

var cfg = Fluently.Configure().Database(SQLiteConfiguration.Standard.ShowSql().InMemory);




مطابق متد تست فوق، عبارات تولید شده به شرح زیر هستند:

NHibernate: select next_hi from hibernate_unique_key
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 2, @p1 = 1
NHibernate: INSERT INTO "Customer" (FirstName, LastName, AddressLine1, AddressLine2, PostalCode, City, CountryCode, Id) VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6, @p7);@p0 = 'Vahid', @p1 = 'Nasiri', @p2 = 'Addr1', @p3 = 'Addr2', @p4 = '1234', @p5 = 'Tehran', @p6 = 'IR', @p7 = 1001
NHibernate: SELECT customer0_.Id as Id0_0_, customer0_.FirstName as FirstName0_0_, customer0_.LastName as LastName0_0_, customer0_.AddressLine1 as AddressL4_0_0_, customer0_.AddressLine2 as AddressL5_0_0_, customer0_.PostalCode as PostalCode0_0_, customer0_.City as City0_0_, customer0_.CountryCode as CountryC8_0_0_ FROM "Customer" customer0_ WHERE customer0_.Id=@p0;@p0 = 1001

نکته جالب این عبارات، استفاده از کوئری‌های پارامتری است به صورت پیش فرض که در نهایت سبب بالا رفتن امنیت بیشتر برنامه (یکی از راه‌های جلوگیری از تزریق اس کیوال در ADO.Net که در نهایت توسط تمامی این فریم ورک‌ها در پشت صحنه مورد استفاده قرار خواهند گرفت) و همچنین سبب بکار افتادن سیستم‌های کش دیتابیس‌های پیشرفته مانند اس کیوال سرور می‌شوند (execution plan کوئری‌های پارامتری در اس کیوال سرور جهت بالا رفتن کارآیی سیستم کش می‌شوند و اهمیتی هم ندارد که حتما رویه ذخیره شده باشند یا خیر).

ادامه دارد ...

نظرات مطالب
لیستی از بانک‌های اطلاعاتی قابل استفاده در دات نت
- البته روشی که به صورت پیش فرض در NHibernate استفاده می‌شود سیستم اول کد و طراحی و بعد دیتابیس است که Entity framework 4 برای نمونه به این سمت هم گرایش پیدا کرده.
- ضمنا یک Visual NHibernate تجاری هم موجود است:
Visual NHibernate
مطالب
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 نیز لغو خواهند شد.

مطالب
مدیریت Join در NHibernate 3.0

مباحث eager fetching/loading (واکشی حریصانه) و lazy loading/fetching (واکشی در صورت نیاز، با تاخیر، تنبل) جزو نکات کلیدی کار با ORM های پیشرفته بوده و در صورت عدم اطلاع از آن‌ها و یا استفاده‌ی ناصحیح از هر کدام، باید منتظر از کار افتادن زود هنگام سیستم در زیر بار چند کاربر همزمان بود. به همین جهت تصور اینکه "با استفاده از ORMs دیگر از فراگیری SQL راحت شدیم!" یا اینکه "به من چه که پشت صحنه چه اتفاقی می‌افته!" بسی مهلک و نادرست است!
در ادامه به تفصیل به این موضوع پرداخته خواهد شد.

ابزار مورد نیاز

در این مطلب از برنامه‌ی NHProf استفاده خواهد شد.
اگر مطالب NHibernate این سایت را دنبال کرده باشید، در مورد لاگ کردن SQL تولیدی به اندازه‌ی کافی توضیح داده شده یا حتی یک ماژول جمع و جور هم برای مصارف دم دستی نوشته شده است. این موارد شاید این ایده را به همراه داشته باشند که چقدر خوب می‌شد یک برنامه‌ی جامع‌تر برای این نوع بررسی‌ها تهیه می‌شد. حداقل SQL نهایی فرمت می‌شد (یعنی برنامه باید مجهز به یک SQL Parser تمام عیار باشد که کار چند ماهی هست ...؛ با توجه به اینکه مثلا NHibernate از افزونه‌های SQL ویژه بانک‌های اطلاعاتی مختلف هم پشتیبانی می‌کند، مثلا T-SQL مایکروسافت با یک سری ریزه کاری‌های منحصر به MySQL متفاوت است)، یا پس از فرمت شدن، syntax highlighting به آن اضافه می‌شد، در ادامه مشخص می‌کرد کدام کوئری‌ها سنگین‌تر هستند، کدامیک نشانه‌ی عدم استفاده‌ی صحیح از ORM مورد استفاده است، چه مشکلی دارد و از این موارد.
خوشبختانه این ایده‌ها یا آرزوها با برنامه‌ی NHProf محقق شده است. این برنامه برای استفاده‌ی یک ماه اول آن رایگان است (آدرس ایمیل خود را وارد کنید تا یک فایل مجوز رایگان یک ماهه برای شما ارسال گردد) و پس از یک ماه، باید حداقل 300 دلار هزینه کنید.


واکشی حریصانه و غیرحریصانه چیست؟

رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص می‌سازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).


چگونه واکشی حریصانه و غیرحریصانه را در NHibernate 3.0 تنظیم کنیم؟

در NHibernate اگر تنظیم خاصی را تدارک ندیده و خواص جداول خود را به صورت virtual معرفی کرده باشید، تنظیم پیش فرض دریافت اطلاعات همان lazy loading است. به مثالی در این زمینه توجه بفرمائید:

مدل برنامه:
مدل برنامه همان مثال کلاسیک مشتری و سفارشات او می‌باشد. هر مشتری چندین سفارش می‌تواند داشته باشد. هر سفارش به یک مشتری وابسته است. هر سفارش نیز از چندین قلم جنس تشکیل شده است. در این خرید، هر جنس نیز به یک سفارش وابسته است.


using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
}

using System;
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Order
{
public virtual int Id { get; set; }
public virtual DateTime OrderDate { set; get; }
public virtual Customer Customer { get; set; }
public virtual IList<OrderItem> OrderItems { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class OrderItem
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
public virtual int Quntity { get; set; }
public virtual Order Order { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class Product
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual decimal UnitPrice { get; set; }
}
}

که جداول متناظر با آن به صورت زیر خواهند بود:
    create table Customers (
CustomerId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (CustomerId)
)

create table Orders (
OrderId INT IDENTITY NOT NULL,
OrderDate DATETIME null,
CustomerId INT null,
primary key (OrderId)
)

create table OrderItems (
OrderItemId INT IDENTITY NOT NULL,
Quntity INT null,
ProductId INT null,
OrderId INT null,
primary key (OrderItemId)
)

create table Products (
ProductId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
UnitPrice NUMERIC(19,5) null,
primary key (ProductId)
)

alter table Orders
add constraint fk_Customer_Order
foreign key (CustomerId)
references Customers

alter table OrderItems
add constraint fk_Product_OrderItem
foreign key (ProductId)
references Products

alter table OrderItems
add constraint fk_Order_OrderItem
foreign key (OrderId)
references Orders

همچنین یک سری اطلاعات آزمایشی زیر را هم در نظر بگیرید: (بانک اطلاعاتی انتخاب شده SQL CE است)

SET IDENTITY_INSERT [Customers] ON;
GO
INSERT INTO [Customers] ([CustomerId],[Name]) VALUES (1,N'Customer1');
GO
SET IDENTITY_INSERT [Customers] OFF;
GO
SET IDENTITY_INSERT [Products] ON;
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (1,N'Product1',1000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (2,N'Product2',2000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (3,N'Product3',3000.00000);
GO
SET IDENTITY_INSERT [Products] OFF;
GO
SET IDENTITY_INSERT [Orders] ON;
GO
INSERT INTO [Orders] ([OrderId],[OrderDate],[CustomerId]) VALUES (1,{ts '2011-01-07 11:25:20.000'},1);
GO
SET IDENTITY_INSERT [Orders] OFF;
GO
SET IDENTITY_INSERT [OrderItems] ON;
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (1,10,1,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (2,5,2,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (3,20,3,1);
GO
SET IDENTITY_INSERT [OrderItems] OFF;
GO

دریافت اطلاعات :
می‌خواهیم نام کلیه محصولات خریداری شده توسط مشتری‌ها را به همراه نام مشتری و زمان خرید مربوطه، نمایش دهیم (دریافت اطلاعات از 4 جدول بدون join نویسی):

var list = session.QueryOver<Customer>().List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

خروجی به صورت زیر خواهد بود:
Customer1:2011/01/07 11:25:20 :Product1
Customer1:2011/01/07 11:25:20 :Product2
Customer1:2011/01/07 11:25:20 :Product3
اما بهتر است نگاهی هم به پشت صحنه عملیات داشته باشیم:



همانطور که مشاهده می‌کنید در اینجا اطلاعات از 4 جدول مختلف دریافت می‌شوند اما ما Join ایی را ننوشته‌ایم. ORM هرجایی که به اطلاعات فیلدهای جداول دیگر نیاز داشته، به صورت مستقیم به آن جدول مراجعه کرده و یک کوئری، حاصل این عملیات خواهد بود (مطابق تصویر جمعا 6 کوئری در پشت صحنه برای نمایش سه سطر خروجی فوق اجرا شده است).
این حالت فقط و فقط با تعداد رکورد کم بهینه است (و به همین دلیل هم تدارک دیده شده است). بنابراین اگر برای مثال قصد نمایش اطلاعات حاصل از 4 جدول فوق را در یک گرید داشته باشیم، بسته به تعداد رکوردها و تعداد کاربران همزمان برنامه (خصوصا در برنامه‌های تحت وب)، بانک اطلاعاتی باید بتواند هزاران هزار کوئری رسیده حاصل از lazy loading را پردازش کند و این یعنی مصرف بیش از حد منابع (IO بالا، مصرف حافظه بالا) به همراه بالا رفتن CPU usage و از کار افتادن زود هنگام سیستم.
کسانی که پیش از این با SQL نویسی خو گرفته‌اند احتمالا الان منابع موجود را در مورد نحوه‌ی نوشتن Join در NHibernate زیر و رو خواهند کرد؛ زیرا پیش از این آموخته‌اند که برای دریافت اطلاعات از دو یا چند جدول مرتبط باید Join نوشت. اما همانطور که پیشتر نیز عنوان شد، اگر با جزئیات کار با NHibernate آشنا شویم، نیازی به Join نویسی نخواهیم داشت. اینکار را خود ORM در پشت صحنه باید و می‌تواند مدیریت کند. اما چگونه؟
در NHibernate 3.0 با معرفی QueryOver که جایگزینی از نوع strongly typed همان ICriteria API قدیمی است، یا با معرفی Query که همان LINQ to NHibernate می‌باشد، متدی به نام Fetch نیز تدارک دیده شده است که استراتژی‌های lazy loading و eager loading را به سادگی توسط آن می‌توان مشخص نمود.

مثال: دریافت اطلاعات با استفاده از QueryOver

var list = session
.QueryOver<Customer>()
.Fetch(c => c.Orders).Eager
.Fetch(c => c.Orders.First().OrderItems).Eager
.Fetch(c => c.Orders.First().OrderItems.First().Product).Eager
.List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

پشت صحنه:



اینبار فقط یک کوئری حاصل عملیات بوده و join ها به صورت خودکار با توجه به متدهای Fetch ذکر شده که حالت eager loading آن‌ها صریحا مشخص شده است، تشکیل شده‌اند (6 بار رفت و برگشت به بانک اطلاعاتی به یکبار تقلیل یافت).

نکته 1: نتایج تکراری
اگر حاصل join آخر را نمایش دهیم، نتایجی تکراری خواهیم داشت که مربوط است به مقدار دهی customer با سه وهله از شیء مربوطه تا بتواند واکشی حریصانه‌ی مجموعه اشیاء فرزند آن‌را نیز پوشش دهد. برای رفع این مشکل یک سطر TransformUsing باید اضافه شود:
...
.TransformUsing(NHibernate.Transform.Transformers.DistinctRootEntity)
.List();


دریافت اطلاعات با استفاده از LINQ to NHibernate3.0
برای اینکه بتوان متدهای Fetch ذکر شده را به LINQ to NHibernate 3.0 اعمال نمود، ذکر فضای نام NHibernate.Linq ضروری است. پس از آن خواهیم داشت:
var list = session
.Query()
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.OrderItems)
.ThenFetch(p => p.Product)
.ToList();

اینبار از FetchMany، سپس ThenFetchMany (برای واکشی حریصانه مجموعه‌های فرزند) و در آخر از ThenFetch استفاده خواهد شد.

همانطور که ملاحظه می‌کنید حاصل این کوئری، با کوئری قبلی ذکر شده یکسان است. هر دو، اطلاعات مورد نیاز از دو جدول مختلف را نمایش می‌دهند. اما یکی در پشت صحنه شامل چندین و چند کوئری برای دریافت اطلاعات است، اما دیگری تنها از یک کوئری Join دار تشکیل شده است.


نکته 2: خطاهای ممکن
ممکن است حین تعریف متدهای Fetch در زمان اجرا به خطاهای Antlr.Runtime.MismatchedTreeNodeException و یا Specified method is not supported و یا موارد مشابهی برخورد نمائید. تنها کاری که باید انجام داد جابجا کردن مکان بکارگیری extension methods است. برای مثال متد Fetch باید پس از Where در حالت استفاده از LINQ ذکر شود و نه قبل از آن.

مطالب
مقدار دهی کلیدهای خارجی در NHibernate و Entity framework

ORM های NHibernate و Entity framework روش‌های متفاوتی را برای به روز رسانی کلید خارجی با حداقل رفت و برگشت به دیتابیس ارائه می‌دهند که در ادامه معرفی خواهند شد.

صورت مساله:
فرض کنید می‌خواهیم برنامه‌ای را بنویسیم که ریز پرداخت‌های روزانه‌ی ما را ثبت کند. برای اینکار حداقل به یک جدول گروه‌های اقلام خریداری شده، یک جدول حساب‌های تامین کننده‌ی مخارج، یک جدول فروشنده‌ها و نهایتا یک جدول صورتحساب‌های پرداختی بر اساس جداول ذکر شده نیاز خواهد بود.

الف) بررسی مدل برنامه



در اینجا جهت تعریف ویژگی‌ها یا Attributes تعریف شده در این کلاس‌ها از NHibernate validator استفاده شده (+). مزیت اینکار هم علاوه بر اعتبارسنجی سمت کلاینت (پیش از تبادل اطلاعات با بانک اطلاعاتی)، تولید جداولی با همین مشخصات است. برای مثال Fluent NHibernate بر اساس ویژگی Length تعریف شده با طول حداکثر 120 ، یک فیلد nvarchar با همین طول را ایجاد می‌کند.

public class Account
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Category
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 130, Message = "طول نام باید بین 3 و 130 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Payee
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام باید بین 3 و 120 کاراکتر باشد")]
public virtual string Name { get; set; }
}

public class Bill
{
public virtual int Id { get; set; }

[NotNull]
public virtual Account Account { get; set; }

[NotNull]
public virtual Category Category { get; set; }

[NotNull]
public virtual Payee Payee { get; set; }

[NotNull]
public virtual decimal Amount { set; get; }

[NotNull]
public virtual DateTime BillDate { set; get; }

[NotNullNotEmpty]
[Length(Min = 1, Max = 500, Message = "طول توضیحات باید بین 1 و 500 کاراکتر باشد")]
public virtual string Description { get; set; }
}




ب) ساختار جداول متناظر (تولید شده به صورت خودکار توسط Fluent NHibernate در اینجا)


در مورد نحوه‌ی استفاده از ویژگی AutoMapping و همچنین تولید خودکار ساختار بانک اطلاعاتی از روی جداول در NHibernate قبلا توضیح داده شده است. البته بدیهی است که ترکیب مقاله‌ی Validation و آشنایی با AutoMapping در اینجا جهت اعمال ویژگی‌ها باید بکار گرفته شود که در همان مقاله‌ی Validation مفصل توضیح داده شده است.
نکته‌ی مهم database schema تولیدی، کلید‌های خارجی (foreign key) تعریف شده بر روی جدول Bills است (همان AccountId، CategoryId و PayeeId تعریف شده) که به primary key جداول متناظر اشاره می‌کند.
    create table Accounts (
AccountId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (AccountId)
)

create table Bills (
BillId INT IDENTITY NOT NULL,
Amount DECIMAL(19,5) not null,
BillDate DATETIME not null,
Description NVARCHAR(500) not null,
AccountId INT not null,
CategoryId INT not null,
PayeeId INT not null,
primary key (BillId)
)

create table Categories (
CategoryId INT IDENTITY NOT NULL,
Name NVARCHAR(130) not null,
primary key (CategoryId)
)

create table Payees (
PayeeId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (PayeeId)
)

alter table Bills
add constraint fk_Account_Bill
foreign key (AccountId)
references Accounts

alter table Bills
add constraint fk_Category_Bill
foreign key (CategoryId)
references Categories

alter table Bills
add constraint fk_Payee_Bill
foreign key (PayeeId)
references Payees

ج) صفحه‌ی ثبت صورتحساب‌ها

صفحات ثبت گروه‌های اقلام، حساب‌ها و فروشنده‌ها، نکته‌ی خاصی ندارند. چون این جداول وابستگی خاصی به جایی نداشته و به سادگی اطلاعات آن‌ها را می‌توان ثبت یا به روز کرد.
صفحه‌ی مشکل در این مثال، همان صفحه‌ی ثبت صورتحساب‌ها است که از سه کلید خارجی به سه جدول دیگر تشکیل شده است.
عموما برای طراحی این نوع صفحات، کلیدهای خارجی را با drop down list نمایش می‌دهند و اگر در جهت سهولت کار کاربر قدم برداشته شود، باید از یک Auto complete drop down list استفاده کرد تا کاربر برنامه جهت یافتن آیتم‌های از پیش تعریف شده کمتر سختی بکشد.



اگر از Silverlight یا WPF استفاده شود، امکان بایند یک لیست کامل از اشیاء با تمام خواص مرتبط به آن‌ها وجود دارد (هر رکورد نمایش داده شده در دراپ داون لیست، دقیقا معادل است با یک شیء متناظر با کلاس‌های تعریف شده است). اگر از ASP.NET استفاده شود (یعنی یک محیط بدون حالت که پس از نمایش یک صفحه دیگر خبری از لیست اشیاء بایند شده وجود نخواهد داشت و همگی توسط وب سرور جهت صرفه جویی در منابع تخریب شده‌اند)، بهتر است datatextfield را با فیلد نام و datavaluefield را با فیلد Id مقدار دهی کرد تا کاربر نهایی، نام را جهت ثبت اطلاعات مشاهده کند و برنامه از Id موجود در لیست جهت ثبت کلیدهای خارجی استفاده نماید.
و نکته‌ی اصلی هم همینجا است که چگونه؟! چون ما زمانیکه با یک ORM سر و کار داریم، برای ثبت یک رکورد در جدول Bills باید یک وهله از کلاس Bill را ایجاد کرده و خواص آن‌را مقدار دهی کنیم. اگر به تعریف کلاس Bill مراجعه کنید، سه خاصیت آن از نوع سه کلاس مجزا تعریف شده است. به به عبارتی با داشتن فقط یک id از رکوردهای این کلاس‌ها باید بتوان سه وهله‌ی متناظر آن‌ها را از بانک اطلاعاتی خواند و سپس به این خواص انتساب داد:

var newBill = new Bill
{
Account = accountRepository.GetByKey(1),
Amount = 1,
BillDate = DateTime.Now,
Category = categoryRepository.GetByKey(1),
Description = "testestest...",
Payee = payeeRepository.GetByKey(1)
};
یعنی برای ثبت یک رکورد در جدول Bills فوق، چهار بار رفت و برگشت به دیتابیس خواهیم داشت:
- یکبار برای دریافت رکورد متناظر با گروه‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار برای دریافت رکورد متناظر با فروشند‌ه‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار برای دریافت رکورد متناظر با حساب‌ها بر اساس کلید اصلی آن (که از دراپ داون لیست مربوطه دریافت می‌شود)
- یکبار هم ثبت نهایی اطلاعات در بانک اطلاعاتی

متد GetByKey فوق همان متد session.Get استاندارد NHibernate است (چون به primary key ها از طریق drop down list دسترسی داریم، به سادگی می‌توان بر اساس متد Get استاندارد ذکر شده عمل کرد).

SQL نهایی تولیدی هم به صورت واضحی این مشکل را نمایش می‌دهد (4 بار رفت و برگشت؛ سه بار select یکبار هم insert نهایی):
SELECT account0_.AccountId as AccountId0_0_, account0_.Name as Name0_0_
FROM Accounts account0_ WHERE account0_.AccountId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT category0_.CategoryId as CategoryId2_0_, category0_.Name as Name2_0_
FROM Categories category0_ WHERE category0_.CategoryId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT payee0_.PayeeId as PayeeId3_0_, payee0_.Name as Name3_0_
FROM Payees payee0_ WHERE payee0_.PayeeId=@p0;@p0 = 1 [Type: Int32 (0)]

INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:48:33 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

کسانی که قبلا با رویه‌های ذخیره شده کار کرده باشند (stored procedures) احتمالا الان خواهند گفت؛ ما که گفتیم این روش کند است! سربار زیادی دارد! فقط کافی است یک SP بنویسید و کل عملیات را با یک رفت و برگشت انجام دهید.
اما در ORMs نیز برای انجام این مورد در طی یک حرکت یک ضرب راه حل‌هایی وجود دارد که در ادامه بحث خواهد شد:

د) پیاده سازی با NHibernate
برای حل این مشکل در NHibernate با داشتن primary key (برای مثال از طریق datavaluefield ذکر شده)، بجای session.Get از session.Load استفاده کنید.
session.Get یعنی همین الان برو به بانک اطلاعاتی مراجعه کن و رکورد متناظر با کلید اصلی ذکر شده را بازگشت بده و یک شیء از آن را ایجاد کن (حالت‌های دیگر دسترسی به اطلاعات مانند استفاده از LINQ یا Criteria API یا هر روش مشابه دیگری نیز در اینجا به همین معنا خواهد بود).
session.Load یعنی فعلا دست نگه دار! مگر در جدول نهایی نگاشت شده، اصلا چیزی به نام شیء مثلا گروه وجود دارد؟ مگر این مورد واقعا یک فیلد عددی در جدول Bills بیشتر نیست؟ ما هم که الان این عدد را داریم (به کمک عناصر دراپ داون لیست)، پس لطفا در پشت صحنه یک پروکسی برای ایجاد شیء مورد نظر ایجاد کن (uninitialized proxy to the entity) و سپس عملیات مرتبط را در حین تشکیل SQL نهایی بر اساس این عدد موجود انجام بده. یعنی نیازی به رفت و برگشت به بانک اطلاعاتی نیست. در این حالت اگر SQL نهایی را بررسی کنیم فقط یک سطر زیر خواهد بود (سه select ذکر شده حذف خواهند شد):
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:58:22 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

ه) پیاده سازی با Entity framework

Entity framework زمانیکه بانک اطلاعاتی فوق را (به روش database first) به کلاس‌های متناظر تبدیل/نگاشت می‌کند، حاصل نهایی مثلا در مورد کلاس Bill به صورت خلاصه به شکل زیر خواهد بود:
public partial class Bill : EntityObject
{
public global::System.Int32 BillId {set;get;}
public global::System.Decimal Amount {set;get;}
public global::System.DateTime BillDate {set;get;}
public global::System.String Description {set;get;}
public global::System.Int32 AccountId {set;get;}
public global::System.Int32 CategoryId {set;get;}
public global::System.Int32 PayeeId {set;get;}
public Account Account {set;get;}
public Category Category {set;get;}
}
به عبارتی فیلدهای کلیدهای خارجی، در تعریف نهایی این کلاس هم مشاهده می‌شوند. در اینجا فقط کافی است سه کلید خارجی، از نوع int مقدار دهی شوند (و نیازی به مقدار دهی سه شیء متناظر نیست). در این حالت نیز برای ثبت اطلاعات، فقط یکبار رفت و برگشت به بانک اطلاعاتی خواهیم داشت.

نظرات مطالب
ASP.NET MVC #11
آیا برای insert یا update باید از viewmodel‌های متفاوتی استفاده کرد؟
مطالب
آشنایی با NHibernate - قسمت هفتم

مدیریت بهینه‌ی سشن فکتوری

ساخت یک شیء SessionFactory بسیار پر هزینه و زمانبر است. به همین جهت لازم است که این شیء یکبار حین آغاز برنامه ایجاد شده و سپس در پایان کار برنامه تخریب شود. انجام اینکار در برنامه‌های معمولی ویندوزی (WinForms ،WPF و ...)، ساده است اما در محیط Stateless وب و برنامه‌های ASP.Net ، نیاز به راه حلی ویژه وجود خواهد داشت و تمرکز اصلی این مقاله حول مدیریت صحیح سشن فکتوری در برنامه‌های ASP.Net است.

برای پیاده سازی شیء سشن فکتوری به صورتی که یکبار در طول برنامه ایجاد شود و بارها مورد استفاده قرار گیرد باید از یکی از الگوهای معروف طراحی برنامه نویسی شیء گرا به نام Singleton Pattern استفاده کرد. پیاده سازی نمونه‌ی thread safe آن که در برنامه‌های ذاتا چند ریسمانی وب و همچنین برنامه‌های معمولی ویندوزی می‌تواند مورد استفاده قرار گیرد، در آدرس ذیل قابل مشاهده است:



از پنجمین روش ذکر شده در این مقاله جهت ایجاد یک lazy, lock-free, thread-safe singleton استفاده خواهیم کرد.

بررسی مدل برنامه

در این مدل ساده ما یک یا چند پارکینگ داریم که در هر پارکینگ یک یا چند خودرو می‌توانند پارک شوند.


یک برنامه ASP.Net را آغاز کرده و ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و همچنین ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

تصویر نهایی پروژه ما به شکل زیر خواهد بود:



پروژه ما دارای یک پوشه domain ، تعریف کننده موجودیت‌های برنامه جهت تهیه نگاشت‌های لازم از روی ‌آن‌ها است. سپس یک پوشه جدید را به نام NHSessionManager به آن جهت ایجاد یک Http module مدیریت کننده سشن‌های NHibernate در برنامه اضافه خواهیم کرد.

ساختار دومین برنامه (مطابق کلاس دیاگرام فوق):

namespace NHSample3.Domain
{
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Color { get; set; }
}
}

using System.Collections.Generic;

namespace NHSample3.Domain
{
public class Parking
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Location { get; set; }
public virtual IList<Car> Cars { get; set; }

public Parking()
{
Cars = new List<Car>();
}
}
}
مدیریت سشن فکتوری در برنامه‌های وب

در این قسمت قصد داریم Http Module ایی را جهت مدیریت سشن‌های NHibernate ایجاد نمائیم.

در ابتدا کلاس Config را در پوشه مدیریت سشن NHibernate با محتویات زیر ایجاد کنید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate.Tool.hbm2ddl;

namespace NHSessionManager
{
public class Config
{
public static FluentConfiguration GetConfig()
{
return
Fluently.Configure()
.Database(
MsSqlConfiguration
.MsSql2008
.ConnectionString(x => x.FromConnectionStringWithKey("DbConnectionString"))
)
.ExposeConfiguration(
x => x.SetProperty("current_session_context_class", "managed_web")
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample3.Domain.Car).Assembly))
);
}

public static void CreateDb()
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool dropTables = false;//آیا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
با این کلاس در قسمت‌های قبل آشنا شده‌اید. در این کلاس با کمک امکانات Auto mapping موجود در Fluent Nhibernate (مطلب قسمت قبلی این سری آموزشی) اقدام به تهیه نگاشت‌های خودکار از کلاس‌های قرار گرفته در پوشه دومین خود خواهیم کرد (فضای نام این پوشه به دومین ختم می‌شود که در متد GetConfig مشخص است).
دو نکته جدید در متد GetConfig وجود دارد:
الف) استفاده از متد FromConnectionStringWithKey ، بجای تعریف مستقیم کانکشن استرینگ در متد مذکور که روشی است توصیه شده. به این صورت فایل وب کانفیگ ما باید دارای تعریف کلید مشخص شده در متد GetConfig به نام DbConnectionString باشد:

<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true" />
</connectionStrings>
ب) قسمت ExposeConfiguration آن نیز جدید است.
در اینجا به AutoMapper خواهیم گفت که قصد داریم از امکانات مدیریت سشن مخصوص وب فریم ورک NHibernate استفاده کنیم. فریم ورک NHibernate دارای کلاسی است به نام NHibernate.Context.ManagedWebSessionContext که جهت مدیریت سشن‌های خود در پروژه‌های وب ASP.Net پیش بینی کرده است و از این متد در Http module ایی که ایجاد خواهیم کرد جهت ردگیری سشن جاری آن کمک خواهیم گرفت.

اگر متد CreateDb را فراخوانی کنیم، جداول نگاشت شده به کلاس‌های پوشه دومین برنامه، به صورت خودکار ایجاد خواهند شد که دیتابیس دیاگرام آن به صورت زیر می‌باشد:



سپس کلاس SingletonCore را جهت تهیه تنها و تنها یک وهله از شیء سشن فکتوری در کل برنامه ایجاد خواهیم کرد (همانطور که عنوان شده، ایده پیاده سازی این کلاس thread safe ، از مقاله معرفی شده در ابتدای بحث گرفته شده است):

using NHibernate;

namespace NHSessionManager
{
/// <summary>
/// lazy, lock-free, thread-safe singleton
/// </summary>
public class SingletonCore
{
private readonly ISessionFactory _sessionFactory;

SingletonCore()
{
_sessionFactory = Config.GetConfig().BuildSessionFactory();
}

public static SingletonCore Instance
{
get
{
return Nested.instance;
}
}

public static ISession GetCurrentSession()
{
return Instance._sessionFactory.GetCurrentSession();
}

public static ISessionFactory SessionFactory
{
get { return Instance._sessionFactory; }
}

class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}

internal static readonly SingletonCore instance = new SingletonCore();
}
}
}
اکنون می‌توان از این Singleton object جهت تهیه یک Http Module کمک گرفت. برای این منظور کلاس SessionModule را به برنامه اضافه کنید:

using System;
using System.Web;
using NHibernate;
using NHibernate.Context;

namespace NHSessionManager
{
public class SessionModule : IHttpModule
{
public void Dispose()
{ }

public void Init(HttpApplication context)
{
if (context == null)
throw new ArgumentNullException("context");

context.BeginRequest += Application_BeginRequest;
context.EndRequest += Application_EndRequest;
}

private void Application_BeginRequest(object sender, EventArgs e)
{
ISession session = SingletonCore.SessionFactory.OpenSession();
ManagedWebSessionContext.Bind(HttpContext.Current, session);
session.BeginTransaction();
}

private void Application_EndRequest(object sender, EventArgs e)
{
ISession session = ManagedWebSessionContext.Unbind(
HttpContext.Current, SingletonCore.SessionFactory);
if (session == null) return;

try
{
if (session.Transaction != null &&
!session.Transaction.WasCommitted &&
!session.Transaction.WasRolledBack)
{
session.Transaction.Commit();
}
else
{
session.Flush();
}
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
if (session != null && session.IsOpen)
{
session.Close();
session.Dispose();
}
}
}
}
}
کلاس فوق کار پیاده سازی اینترفیس IHttpModule را جهت دخالت صریح در request handling pipeline برنامه ASP.Net جاری انجام می‌دهد. در این کلاس مدیریت متدهای استاندارد Application_BeginRequest و Application_EndRequest به صورت خودکار صورت می‌گیرد.
در متد Application_BeginRequest ، در ابتدای هر درخواست یک سشن جدید ایجاد و به مدیریت سشن وب NHibernate بایند می‌شود، همچنین یک تراکنش نیز آغاز می‌گردد. سپس در پایان درخواست، این انقیاد فسخ شده و تراکنش کامل می‌شود، همچنین کار پاکسازی اشیاء نیز صورت خواهد گرفت.

با توجه به این موارد، دیگر نیازی به ذکر using جهت dispose کردن سشن جاری در کدهای ما نخواهد بود، زیرا در پایان هر درخواست اینکار به صورت خودکار صورت می‌گیرد. همچنین نیازی به ذکر تراکنش نیز نمی‌باشد، چون مدیریت آن‌را خودکار کرده‌ایم.

جهت استفاده از این Http module تهیه شده باید چند سطر زیر را به وب کانفیگ برنامه اضافه کرد:

<httpModules>
<!--NHSessionManager-->
<add name="SessionModule" type="NHSessionManager.SessionModule"/>
</httpModules>
بدیهی است اگر نخواهید از Http module استفاده کنید باید این کدها را در فایل Global.asax برنامه قرار دهید.

اکنون مثالی از نحوه‌ی استفاده از امکانات فراهم شده فوق به صورت زیر می‌تواند باشد:
ابتدا کلاس ParkingContext را جهت مدیریت مطلوب‌تر LINQ to NHibernate تشکیل می‌دهیم.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample3.Domain;

namespace NHSample3
{
public class ParkingContext : NHibernateContext
{
public ParkingContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Car> Cars
{
get { return Session.Linq<Car>(); }
}

public IOrderedQueryable<Parking> Parkings
{
get { return Session.Linq<Parking>(); }
}
}
}
سپس در فایل Default.aspx.cs برنامه ، برای نمونه تعدادی رکورد را افزوده و نتیجه را در یک گرید ویوو نمایش خواهیم داد:

using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample3.Domain;
using NHSessionManager;

namespace NHSample3
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//ایجاد دیتابیس در صورت نیاز
//Config.CreateDb();

//ثبت یک سری رکورد در دیتابیس
ISession session = SingletonCore.GetCurrentSession();

Car car1 = new Car() { Name = "رنو", Color = "مشکلی" };
session.Save(car1);
Car car2 = new Car() { Name = "پژو", Color = "سفید" };
session.Save(car2);

Parking parking1 = new Parking()
{
Location = "آدرس پارکینگ مورد نظر",
Name = "پارکینگ یک",
Cars = new List<Car> { car1, car2 }
};

session.Save(parking1);

//نمایش حاصل در یک گرید ویوو
ParkingContext db = new ParkingContext(session);
var query = from x in db.Cars select new { CarName = x.Name, CarColor = x.Color };
GridView1.DataSource = query.ToList();
GridView1.DataBind();
}
}
}
مدیریت سشن فکتوری در برنامه‌های غیر وب

در برنامه‌های ویندوزی مانند WinForms ، WPF و غیره، تا زمانیکه یک فرم باز باشد، کل فرم و اشیاء مرتبط با آن به یکباره تخریب نخواهند شد، اما در یک برنامه ASP.Net جهت حفظ منابع سرور در یک محیط چند کاربره، پس از پایان نمایش یک صفحه وب، اثری از آثار اشیاء تعریف شده در کدهای آن صفحه در سرور وجود نداشته و همگی بلافاصله تخریب می‌شوند. به همین جهت بحث‌های ویژه state management در ASP.Net در اینباره مطرح است و مدیریت ویژه‌ای باید روی آن صورت گیرد که در قسمت قبل مطرح شد.
از بحث فوق، تنها استفاده از کلاس‌های Config و SingletonCore ، جهت استفاده و مدیریت بهینه‌ی سشن فکتوری در برنامه‌های ویندوزی کفایت می‌کنند.

دریافت سورس برنامه قسمت هفتم

ادامه دارد ....

مطالب
آشنایی با NHibernate - قسمت ششم

آشنایی با Automapping در فریم ورک Fluent NHibernate

اگر قسمت‌های قبل را دنبال کرده باشید، احتمالا به پروسه طولانی ساخت نگاشت‌ها توجه کرده‌اید. با کمک فریم ورک Fluent NHibernate می‌توان پروسه نگاشت domain model خود را به data model متناظر آن به صورت خودکار نیز انجام داد و قسمت عمده‌ای از کار به این صورت حذف خواهد شد. (این مورد یکی از تفاوت‌های مهم NHibernate با نمونه‌های مشابهی است که مایکروسافت تا تاریخ نگارش این مقاله ارائه داده است. برای مثال در نگار‌ش‌های فعلی LINQ to SQL یا Entity framework ، اول دیتابیس مطرح است و بعد ساخت کد از روی آن، در حالیکه در اینجا ابتدا کد و طراحی سیستم مطرح است و بعد نگاشت آن به سیستم داده‌ای و دیتابیس)

امروز قصد داریم یک سیستم ساده ثبت خبر را از صفر با NHibernate پیاده سازی کنیم و همچنین مروری داشته باشیم بر قسمت‌های قبلی.

مطابق کلاس دیاگرام فوق، این سیستم از سه کلاس خبر، کاربر ثبت کننده‌ی خبر و گروه خبری مربوطه تشکیل شده است.

ابتدا یک پروژه کنسول جدید را به نام NHSample2 آغاز کنید. سپس ارجاعاتی را به اسمبلی‌های زیر به آن اضافه نمائید:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

سپس پوشه‌ای را به نام Domain به این پروژه اضافه نمائید (کلیک راست روی نام پروژه در VS.Net و سپس مراجعه به منوی Add->New folder). در این پوشه تعاریف موجودیت‌های برنامه را قرار خواهیم داد. سه کلاس جدید Category ، User و News را در این پوشه ایجاد نمائید. محتویات این سه کلاس به شرح زیر هستند:

namespace NHSample2.Domain
{
public class User
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
}


namespace NHSample2.Domain
{
public class Category
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
}


using System;

namespace NHSample2.Domain
{
public class News
{
public virtual Guid Id { get; set; }
public virtual string Subject { get; set; }
public virtual string NewsText { get; set; }
public virtual DateTime DateEntered { get; set; }
public virtual Category Category { get; set; }
public virtual User User { get; set; }
}
}
همانطور که در قسمت‌های قبل نیز ذکر شد، تمام خواص پابلیک کلاس‌های Domain ما به صورت virtual تعریف شده‌اند تا lazy loading را در NHibernate فعال سازیم. در حالت lazy loading ، اطلاعات تنها زمانیکه به آن‌ها نیاز باشد بارگذاری خواهند شد. این مورد در حالتیکه نیاز به نمایش اطلاعات تنها یک شیء وجود داشته باشد بسیار مطلوب می‌باشد، یا هنگام ثبت و به روز رسانی اطلاعات نیز یکی از بهترین روش‌ها است. اما زمانیکه با لیستی از اطلاعات سروکار داشته باشیم باعث کاهش افت کارآیی خواهد شد زیرا برای مثال نمایش آن‌ها سبب خواهد شد که 100 ها کوئری دیگر جهت دریافت اطلاعات هر رکورد در حال نمایش اجرا شود (مفهوم دسترسی به اطلاعات تنها در صورت نیاز به آن‌ها). Lazy loading و eager loading (همانند مثال‌های قبلی) هر دو در NHibernate به سادگی قابل تنظیم هستند (برای مثال LINQ to SQL به صورت پیش فرض همواره lazy load است و تا این تاریخ راه استانداردی برای امکان تغییر و تنظیم این مورد پیش بینی نشده است).

اکنون کلاس جدید Config را به برنامه اضافه نمائید:

using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;

namespace NHSample2
{
class Config
{
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly).Configure(cfg);

return cfg;
}

public static void GenerateDbScript(Configuration config, string filePath)
{
bool script = true;//فقط اسکریپت دیتابیس تولید گردد
bool export = false;//نیازی نیست بر روی دیتابیس هم اجرا شود
new SchemaExport(config).SetOutputFile(filePath).Create(script, export);
}

public static void BuildDbSchema(Configuration config)
{
bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
bool export = true;//آیا بر روی دیتابیس هم اجرا شود
bool drop = false;//آیا اطلاعات موجود دراپ شوند
new SchemaExport(config).Execute(script, export, drop);
}

public static void CreateSQL2008DbPlusScript(string connectionString, string filePath)
{
Configuration cfg =
GenerateMapping(
MsSqlConfiguration
.MsSql2008
.ConnectionString(connectionString)
.ShowSql()
);
GenerateDbScript(cfg, filePath);
BuildDbSchema(cfg);
}

public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType)
.Mappings(m => m.AutoMappings
.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly))
)
.BuildSessionFactory();
}
}
}

در متد GenerateMapping از قابلیت Automapping موجود در فریم ورک Fluent Nhibernate استفاده شده است (بدون نوشتن حتی یک سطر جهت تعریف این نگاشت‌ها). این متد نوع دیتابیس مورد نظر را جهت ساخت تنظیمات خود دریافت می‌کند. سپس با کمک کلاس AutoPersistenceModel این فریم ورک، به صورت خودکار از اسمبلی برنامه نگاشت‌های لازم را به کلاس‌های موجود در پوشه Domain ما اضافه می‌کند (مرسوم است که این پوشه در یک پروژه Class library مجزا تعریف شود که در این برنامه جهت سهولت کار در خود برنامه قرار گرفته است). قسمت Where ذکر شده به این جهت معرفی گردیده است تا Fluent Nhibernate برای تمامی کلاس‌های موجود در اسمبلی جاری، سعی در تعریف نگاشت‌های لازم نکند. این نگاشت‌ها تنها به کلاس‌های موجود در پوشه دومین ما محدود شده‌اند.
سه متد بعدی آن، جهت ایجاد اسکریپت دیتابیس از روی این نگاشت‌های تعریف شده و سپس اجرای این اسکریپت بر روی دیتابیس جاری معرفی شده، تهیه شده‌اند. برای مثال CreateSQL2008DbPlusScript یک مثال ساده از استفاده دو متد قبلی جهت ایجاد اسکریپت و دیتابیس متناظر اس کیوال سرور 2008 بر اساس نگاشت‌های برنامه است.
با متد CreateSessionFactory در قسمت‌های قبل آشنا شده‌اید. تنها تفاوت آن در این قسمت، استفاده از کلاس AutoPersistenceModel جهت تولید خودکار نگاشت‌ها است.

در ادامه دیتابیس متناظر با موجودیت‌های برنامه را ایجاد خواهیم کرد:

using System;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
Config.CreateSQL2008DbPlusScript(
"Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true",
"db.sql");

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}

پس از اجرای برنامه، ابتدا فایل اسکریپت دیتابیس به نام db.sql در پوشه اجرایی برنامه تشکیل خواهد شد و سپس این اسکریپت به صورت خودکار بر روی دیتابیس معرفی شده اجرا می‌گردد. دیتابیس دیاگرام حاصل را در شکل زیر می‌توانید ملاحظه نمائید:



همچنین اسکریپت تولید شده آن، صرفنظر از عبارات drop اولیه، به صورت زیر است:

create table [Category] (
Id INT IDENTITY NOT NULL,
CategoryName NVARCHAR(255) null,
primary key (Id)
)

create table [User] (
Id INT IDENTITY NOT NULL,
UserName NVARCHAR(255) null,
Password NVARCHAR(255) null,
primary key (Id)
)

create table [News] (
Id UNIQUEIDENTIFIER not null,
Subject NVARCHAR(255) null,
NewsText NVARCHAR(255) null,
DateEntered DATETIME null,
Category_id INT null,
User_id INT null,
primary key (Id)
)

alter table [News]
add constraint FKE660F9E1C9CF79
foreign key (Category_id)
references [Category]

alter table [News]
add constraint FKE660F95C1A3C92
foreign key (User_id)

references [User]

اکنون یک سری گروه خبری، کاربر و خبر را به دیتابیس خواهیم افزود:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
//با توجه به کلیدهای خارجی تعریف شده ابتدا باید گروه‌ها را اضافه کرد
Category ca = new Category() { CategoryName = "Sport" };
session.Save(ca);
Category ca2 = new Category() { CategoryName = "IT" };
session.Save(ca2);
Category ca3 = new Category() { CategoryName = "Business" };
session.Save(ca3);

//سپس یک کاربر را به دیتابیس اضافه می‌کنیم
User u = new User() { Password = "123$5@1", UserName = "VahidNasiri" };
session.Save(u);

//اکنون می‌توان یک خبر جدید را ثبت کرد

News news = new News()
{
Category = ca,
User = u,
DateEntered = DateTime.Now,
Id = Guid.NewGuid(),
NewsText = "متن خبر جدید",
Subject = "عنوانی دلخواه"
};
session.Save(news);

transaction.Commit(); //پایان تراکنش
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
جهت بررسی انجام عملیات ثبت هم می‌توان به دیتابیس مراجعه کرد، برای مثال:



و یا می‌توان از LINQ استفاده کرد:
برای مثال کاربر VahidNasiri تعریف شده را یافته، اطلاعات آن‌را نمایش دهید؛ سپس نام او را به Vahid ویرایش کرده و دیتابیس را به روز کنید.

برای اینکه کوئری‌های LINQ ما شبیه به LINQ to SQL شوند، کلاس NewsContext را به صورت ذیل تشکیل می‌دهیم. این کلاس از کلاس پایه NHibernateContext مشتق شده و سپس به ازای تمام موجودیت‌های برنامه، یک متد از نوع IOrderedQueryable را تشکیل خواهیم داد.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class NewsContext : NHibernateContext
{
public NewsContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<News> News
{
get { return Session.Linq<News>(); }
}

public IOrderedQueryable<Category> Categories
{
get { return Session.Linq<Category>(); }
}

public IOrderedQueryable<User> Users
{
get { return Session.Linq<User>(); }
}
}
}
اکنون جهت یافتن کاربر و به روز رسانی اطلاعات او در دیتابیس خواهیم داشت:

using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using System.Linq;
using NHSample2.Domain;

namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
using (NewsContext db = new NewsContext(session))
{
var query = from x in db.Users
where x.UserName == "VahidNasiri"
select x;

//اگر چیزی یافت شد
if (query.Any())
{
User vahid = query.First();
//نمایش اطلاعات کاربر
Console.WriteLine("Id: {0}, UserName: {0}", vahid.Id, vahid.UserName);
//به روز رسانی نام کاربر
vahid.UserName = "Vahid";
session.Update(vahid);

transaction.Commit(); //پایان تراکنش
}
}
}
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
مباحث تکمیلی AutoMapping

اگر به اسکریپت دیتابیس تولید شده دقت کرده باشید، عملیات AutoMapping یک سری پیش فرض‌هایی را اعمال کرده است. برای مثال فیلد Id را از نوع identity و به صورت کلید تعریف کرده، یا رشته‌ها را به صورت nvarchar با طول 255 ایجاد نموده است. امکان سفارشی سازی این موارد نیز وجود دارد.

مثال:

using FluentNHibernate.Conventions.Helpers;

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add(
PrimaryKey.Name.Is(x => "ID"),
DefaultLazy.Always(),
ForeignKey.EndsWith("ID"),
Table.Is(t => "tbl" + t.EntityType.Name)
)
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

تابع GenerateMapping معرفی شده را اینجا با قسمت Conventions.Add تکمیل کرده‌ایم. به این صورت دقیقا مشخص شده است که فیلدهایی با نام ID باید primary key در نظر گرفته شوند، همواره lazy loading صورت گیرد و نام کلید خارجی به ID ختم شود. همچنین نام جداول با tbl شروع گردد.
روش دیگری نیز برای معرفی این قرار دادها و پیش فرض‌ها وجود دارد. فرض کنید می‌خواهیم طول رشته پیش فرض را از 255 به 500 تغییر دهیم. برای اینکار باید اینترفیس IPropertyConvention را پیاده سازی کرد:

using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;

namespace NHSample2.Conventions
{
class MyStringLengthConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
instance.Length(500);
}
}
}
سپس نحوه‌ی معرفی آن به صورت زیر خواهد بود:

public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());

new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add<MyStringLengthConvention>()
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);

return cfg;
}

نکته:
اگر برای یافتن اطلاعات بیشتر در این مورد در وب جستجو کنید، اکثر مثال‌هایی را که مشاهده خواهید کرد بر اساس نگارش بتای fluent NHibernate هستند و هیچکدام با نگارش نهایی این فریم ورک کار نمی‌کنند. در نگارش رسمی نهایی ارائه شده، تغییرات بسیاری صورت گرفته که آن‌ها را در این آدرس می‌توان مشاهده کرد.

دریافت سورس برنامه قسمت ششم


ادامه دارد ...