۱۳۸۸/۰۷/۲۰

آشنايي با NHibernate - قسمت چهارم


در اين قسمت يك مثال ساده از insert ، load و delete را بر اساس اطلاعات قسمت‌هاي قبل با هم مرور خواهيم كرد. براي سادگي كار از يك برنامه Console استفاده خواهد شد (هر چند مرسوم شده است كه براي نوشتن آزمايشات از آزمون‌هاي واحد بجاي اين نوع پروژه‌ها استفاده شود). همچنين فرض هم بر اين است كه database schema برنامه را مطابق قسمت قبل در اس كيوال سرور ايجاد كرده ايد (نكته آخر بحث قسمت سوم).

يك پروژه جديد از نوع كنسول را به solution برنامه (همان NHSample1 كه در قسمت‌هاي قبل ايجاد شد)، اضافه نمائيد.
سپس ارجاعاتي را به اسمبلي‌هاي زير به آن اضافه كنيد:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHSample1.dll : در قسمت‌هاي قبل تعاريف موجوديت‌ها و نگاشت‌ آن‌ها را در اين پروژه class library ايجاد كرده بوديم و اكنون قصد استفاده از آن را داريم.

اگر ديتابيس قسمت قبل را هنوز ايجاد نكرده‌ايد، كلاس CDb را به برنامه افزوده و سپس متد CreateDb آن‌را به برنامه اضافه نمائيد.

using FluentNHibernate;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class CDb
{
public static void CreateDb(IPersistenceConfigurer dbType)
{
var cfg = Fluently.Configure().Database(dbType);

PersistenceModel pm = new PersistenceModel();
pm.AddMappingsFromAssembly(typeof(CustomerMapping).Assembly);
var sessionSource = new SessionSource(
cfg.BuildConfiguration().Properties,
pm);

var session = sessionSource.CreateSession();
sessionSource.BuildSchema(session, true);
}
}
}
اكنون براي ايجاد ديتابيس اس كيوال سرور بر اساس نگاشت‌هاي قسمت قبل، تنها كافي است دستور ذيل را صادر كنيم:

CDb.CreateDb(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql());

تمامي جداول و ارتباطات مرتبط در ديتابيسي كه در كانكشن استرينگ فوق ذكر شده است، ايجاد خواهد شد.

در ادامه يك كلاس جديد به نام Config را به برنامه كنسول ايجاد شده اضافه كنيد:

using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample1.Mappings;

namespace ConsoleTestApplication
{
class Config
{
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType
).Mappings(m => m.FluentMappings.AddFromAssembly(typeof(CustomerMapping).Assembly))
.BuildSessionFactory();
}
}
}
اگر بحث را دنبال كرده باشيد، اين كلاس را پيشتر در كلاس FixtureBase آزمون واحد خود، به نحوي ديگر ديده بوديم. براي كار با NHibernate‌ نياز به يك سشن مپ شده به موجوديت‌هاي برنامه مي‌باشد كه توسط متد CreateSessionFactory كلاس فوق ايجاد خواهد شد. اين متد را به اين جهت استاتيك تعريف كرده‌ايم كه هيچ نوع وابستگي به كلاس جاري خود ندارد. در آن نوع ديتابيس مورد استفاده ( براي مثال اس كيوال سرور 2008 يا هر مورد ديگري كه مايل بوديد)، به همراه اسمبلي حاوي اطلاعات نگاشت‌هاي برنامه معرفي شده‌اند.

اكنون سورس كامل مثال برنامه را در نظر بگيريد:

كلاس CDbOperations جهت اعمال ثبت و حذف اطلاعات:

using System;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CDbOperations
{
ISessionFactory _factory;

public CDbOperations(ISessionFactory factory)
{
_factory = factory;
}

public int AddNewCustomer()
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer vahid = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

Console.WriteLine("Saving a customer...");

session.Save(vahid);
session.Flush();//چندين عمليات با هم و بعد

transaction.Commit();

return vahid.Id;
}
}
}

public void DeleteCustomer(int id)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
Customer customer = session.Load<Customer>(id);
Console.WriteLine("Id:{0}, Name: {1}", customer.Id, customer.FirstName);

Console.WriteLine("Deleting a customer...");
session.Delete(customer);

session.Flush();//چندين عمليات با هم و بعد

transaction.Commit();
}
}
}
}
}
و سپس استفاده از آن در برنامه

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

namespace ConsoleTestApplication
{
class Program
{
static void Main(string[] args)
{
//CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
//return;

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
CDbOperations db = new CDbOperations(session);
int id = db.AddNewCustomer();
Console.WriteLine("Loading a customer and delete it...");
db.DeleteCustomer(id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
توضيحات:
نياز است تا ISessionFactory را براي ساخت سشن‌هاي دسترسي به ديتابيس ذكر شده در تنظميات آن جهت استفاده در تمام تردهاي برنامه، ايجاد نمائيم. لازم به ذكر است كه تا قبل از فراخواني BuildSessionFactory اين تنظيمات بايد معرفي شده باشند و پس از آن ديگر اثري نخواهند داشت.
ايجاد شيء ISessionFactory هزينه بر است و گاهي بر اساس تعداد كلاس‌هايي كه بايد مپ شوند، ممكن است تا چند ثانيه به طول انجامد. به همين جهت نياز است تا يكبار ايجاد شده و بارها مورد استفاده قرار گيرد. در برنامه به كرات از using استفاده شده تا اشياء IDisposable را به صورت خودكار و حتمي، معدوم نمايد.

بررسي متد AddNewCustomer :
در ابتدا يك سشن را از ISessionFactory موجود درخواست مي‌كنيم. سپس يكي از بهترين تمرين‌هاي كاري جهت كار با ديتابيس‌ها ايجاد يك تراكنش جديد است تا اگر در حين اجراي كوئري‌ها مشكلي در سيستم، سخت افزار و غيره پديد آمد، ديتابيسي ناهماهنگ حاصل نشود. زمانيكه از تراكنش استفاده شود، تا هنگاميكه دستور transaction.Commit آن با موفقيت به پايان نرسيده باشد، اطلاعاتي در ديتابيس تغيير نخواهد كرد و از اين لحاظ استفاده از تراكنش‌ها جزو الزامات يك برنامه اصولي است.
در ادامه يك وهله از شيء Customer را ايجاد كرده و آن‌را مقدار دهي مي‌كنيم (اين شيء در قسمت‌هاي قبل ايجاد گرديد). سپس با استفاده از session.Save دستور ثبت را صادر كرده، اما تا زمانيكه transaction.Commit فراخواني و به پايان نرسيده باشد، اطلاعاتي در ديتابيس ثبت نخواهد شد.
نيازي به ذكر سطر فلاش در اين مثال نبود و NHibernate اينكار را به صورت خودكار انجام مي‌دهد و فقط از اين جهت عنوان گرديد كه اگر چندين عمليات را با هم معرفي كرديد، استفاده از session.Flush سبب خواهد شد كه رفت و برگشت‌ها به ديتابيس حداقل شود و فقط يكبار صورت گيرد.
در پايان اين متد، Id ثبت شده در ديتابيس بازگشت داده مي‌شود.

چون در متد CreateSessionFactory ، متد ShowSql را نيز ذكر كرده بوديم، هنگام اجراي برنامه، عبارات SQL ايي كه در پشت صحنه توسط NHibernate توليد مي‌شوند را نيز مي‌توان مشاهده نمود:



بررسي متد DeleteCustomer :
ايجاد سشن و آغاز تراكنش آن همانند متد AddNewCustomer است. سپس در اين سشن، يك شيء از نوع Customer با Id ايي مشخص load‌ خواهد گرديد. براي نمونه، نام اين مشتري نيز در كنسول نمايش داده مي‌شود. سپس اين شيء مشخص و بارگذاري شده را به متد session.Delete ارسال كرده و پس از فراخواني transaction.Commit ، اين مشتري از ديتابيس حذف مي‌شود.

براي نمونه خروجي SQL پشت صحنه اين عمليات كه توسط NHibernate مديريت مي‌شود، به صورت زير است:

Saving a customer...
NHibernate: select next_hi from hibernate_unique_key with (updlock, rowlock)
NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 17, @p1 = 16
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 = 16016
Loading a customer and delete it...
NHibernate: SELECT customer0_.Id as Id2_0_, customer0_.FirstName as FirstName2_0_, customer0_.LastName as LastName2_0_, customer0_.AddressLine1 as AddressL4_2_0_, customer0_.AddressLine2 as AddressL5_2_0_, customer0_.PostalCode as PostalCode2_0_, customer0_.City as City2_0_, customer0_.CountryCode as CountryC8_2_0_ FROM [Customer] customer0_ WHERE customer0_.Id=@p0;@p0 = 16016
Id:16016, Name: Vahid
Deleting a customer...
NHibernate: DELETE FROM [Customer] WHERE Id = @p0;@p0 = 16016
Press a key...
استفاده از ديتابيس SQLite بجاي SQL Server در مثال فوق:

فرض كنيد از هفته آينده قرار شده است كه نسخه سبك و تك كاربره‌اي از برنامه ما تهيه شود. بديهي است SQL server براي اين منظور انتخاب مناسبي نيست (هزينه بالا براي يك مشتري، مشكلات نصب، مشكلات نگهداري و امثال آن براي يك كاربر نهايي و نه يك سازمان بزرگ كه حتما ادميني براي اين مسايل در نظر گرفته مي‌شود).
اكنون چه بايد كرد؟ بايد برنامه را از صفر بازنويسي كرد يا قسمت دسترسي به داده‌هاي آن‌را كلا مورد باز بيني قرار داد؟ اگر برنامه اسپاگتي ما اصلا لايه دسترسي به داده‌ها را نداشت چه؟! همه جاي برنامه پر است از SqlCommand و Open و Close ! و عملا استفاده از يك ديتابيس ديگر يعني باز نويسي كل برنامه.
همانطور كه ملاحظه مي‌كنيد، زمانيكه با NHibernate كار شود، مديريت لايه دسترسي به داده‌ها به اين فريم ورك محول مي‌شود و اكنون براي استفاده از ديتابيس SQLite تنها بايد تغييرات زير صورت گيرد:
ابتدا ارجاعي را به اسمبلي System.Data.SQLite.dll اضافه نمائيد (تمام اين اسمبلي‌هاي ذكر شده به همراه مجموعه FluentNHibernate ارائه مي‌شوند). سپس:
الف) ايجاد يك ديتابيس خام بر اساس كلاس‌هاي domain و mapping تعريف شده در قسمت‌هاي قبل به صورت خودكار

CDb.CreateDb(SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql());
ب) تغيير آرگومان متد CreateSessionFactory

//todo: Read ConnectionString from app.config or web.config
using (ISessionFactory session = Config.CreateSessionFactory(
SQLiteConfiguration.Standard.ConnectionString("data source=sample.sqlite").ShowSql()
))
{
...

نمايي از ديتابيس SQLite تشكيل شده پس از اجراي متد قسمت الف ، در برنامه Lita :




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

نكته:
در سه قسمت قبل، تمام خواص پابليك كلاس‌هاي پوشه domain را به صورت معمولي و متداول معرفي كرديم. اگر نياز به lazy loading در برنامه وجود داشت، بايد تمامي كلاس‌ها را ويرايش كرده و واژه كليدي virtual را به كليه خواص پابليك آن‌ها اضافه كرد. علت هم اين است كه براي عمليات lazy loading ، فريم ورك NHibernate بايد يك سري پروكسي را به صورت خودكار جهت كلاس‌هاي برنامه ايجاد نمايد و براي اين امر نياز است تا بتواند اين خواص را تحريف (override) كند. به همين جهت بايد آن‌ها را به صورت virtual تعريف كرد. همچنين تمام سطرهاي Not.LazyLoad نيز بايد حذف شوند.

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