۱۳۸۸/۰۷/۲۵

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


معرفي الگوي Repository

روش متداول كار با فناوري‌هاي مختلف دسترسي به داده‌ها عموما بدين شكل است:
الف) يافتن رشته اتصالي رمزنگاري شده به ديتابيس از يك فايل كانفيگ (در يك برنامه اصولي البته!)
ب) باز كردن يك اتصال به ديتابيس
ج) ايجاد اشياء Command براي انجام عمليات مورد نظر
د) اجرا و فراخواني اشياء مراحل قبل
ه) بستن اتصال به ديتابيس و آزاد سازي اشياء

اگر در برنامه‌هاي يك تازه كار به هر محلي از برنامه او دقت كنيد اين 5 مرحله را مي‌توانيد مشاهده كنيد. همه جا! قسمت ثبت، قسمت جستجو، قسمت نمايش و ...
مشكلات اين روش:
1- حجم كارهاي تكراري انجام شده بالا است. اگر قسمتي از فناوري دسترسي به داده‌ها را به اشتباه درك كرده باشد، پس از مطالعه بيشتر و مشخص شدن نحوه‌ي رفع مشكل، قسمت عمده‌اي از برنامه را بايد اصلاح كند (زيرا كدهاي تكراري همه جاي آن پراكنده‌اند).
2- برنامه نويس هر بار بايد اين مراحل را به درستي انجام دهد. اگر در يك برنامه بزرگ تنها قسمت آخر در يكي از مراحل كاري فراموش شود دير يا زود برنامه تحت فشار كاري بالا از كار خواهد افتاد (و متاسفانه اين مساله بسيار شايع است).
3- برنامه منحصرا براي يك نوع ديتابيس خاص تهيه خواهد شد و تغيير اين رويه جهت استفاده از ديتابيسي ديگر (مثلا كوچ برنامه از اكسس به اس كيوال سرور)، نيازمند بازنويسي كل برنامه مي‌باشد.
و ...

همين برنامه نويس پس از مدتي كار به اين نتيجه مي‌رسد كه بايد براي اين‌كارهاي متداول، يك لايه و كلاس دسترسي به داده‌ها را تشكيل دهد. اكنون هر قسمتي از برنامه براي كار با ديتابيس بايد با اين كلاس مركزي كه انجام كارهاي متداول با ديتابيس را خلاصه مي‌كند، كار كند. به اين صورت كد نويسي يك نواختي با حذف كدهاي تكراري از سطح برنامه و همچنين بدون فراموش شدن قسمت مهمي از مراحل كاري، حاصل مي‌گردد. در اينجا اگر روزي قرار شد از يك ديتابيس ديگر استفاده شود فقط كافي است يك كلاس برنامه تغيير كند و نيازي به بازنويسي كل برنامه نخواهد بود.

اين روزها تشكيل اين لايه دسترسي به داده‌ها (data access layer يا DAL) نيز مزموم است! و دلايل آن در مباحث چرا به يك ORM نيازمنديم برشمرده شده است. جهت كار با ORM ها نيز نيازمند يك لايه ديگر مي‌باشيم تا يك سري اعمال متداول با آن‌هارا كپسوله كرده و از حجم كارهاي تكراري خود بكاهيم. براي اين منظور قبل از اينكه دست به اختراع بزنيم، بهتر است به الگوهاي طراحي برنامه نويسي شيء گرا رجوع كرد و از رهنمودهاي آن استفاده نمود.

الگوي Repository يكي از الگوهاي برنامه‌ نويسي با مقياس سازماني است. با كمك اين الگو لايه‌اي بر روي لايه نگاشت اشياء برنامه به ديتابيس تشكيل شده و عملا برنامه را مستقل از نوع ORM مورد استفاه مي‌كند. به اين صورت هم از تشكيل يك سري كدهاي تكراري در سطح برنامه جلوگيري شده و هم از وابستگي بين مدل برنامه و لايه دسترسي به داده‌ها (كه در اينجا همان NHibernate مي‌باشد) جلوگيري مي‌شود. الگوي Repository (مخزن)، كار ثبت،‌ حذف، جستجو و به روز رساني داده‌ها را با ترجمه آن‌ها به روش‌هاي بومي مورد استفاده توسط ORM‌ مورد نظر، كپسوله مي‌كند. به اين شكل شما مي‌توانيد يك الگوي مخزن عمومي را براي كارهاي خود تهيه كرده و به سادگي از يك ORM به ORM ديگر كوچ كنيد؛ زيرا كدهاي برنامه شما به هيچ ORM خاصي گره نخورده و اين عمليات بومي كار با ORM توسط لايه‌اي كه توسط الگوي مخزن تشكيل شده، صورت گرفته است.

طراحي كلاس مخزن بايد شرايط زير را برآورده سازد:
الف) بايد يك طراحي عمومي داشته باشد و بتواند در پروژه‌هاي متعددي مورد استفاده مجدد قرار گيرد.
ب) بايد با سيستمي از نوع اول طراحي و كد نويسي و بعد كار با ديتابيس، سازگاري داشته باشد.
ج) بايد امكان انجام آزمايشات واحد را سهولت بخشد.
د) بايد وابستگي كلاس‌هاي دومين برنامه را به زير ساخت ORM مورد استفاده قطع كند (اگر سال بعد به اين نتيجه رسيديد كه ORM ايي به نام XYZ براي كار شما بهتر است، فقط پياده سازي اين كلاس بايد تغيير كند و نه كل برنامه).
ه) بايد استفاده از كوئري‌هايي از نوع strongly typed را ترويج كند (مثل كوئري‌هايي از نوع LINQ).


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

مدل اين قسمت (برنامه NHSample4 از نوع كنسول با همان ارجاعات متداول ذكر شده در قسمت‌هاي قبل)، از نوع many-to-many مي‌باشد. در اينجا يك واحد درسي توسط چندين دانشجو مي‌تواند اخذ شود يا يك دانشجو مي‌تواند چندين واحد درسي را اخذ نمايد كه براي نمونه كلاس دياگرام و كلاس‌هاي متشكل آن به شكل زير خواهند بود:



using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Course
{
public virtual int Id { get; set; }
public virtual string Teacher { get; set; }
public virtual IList<Student> Students { get; set; }

public Course()
{
Students = new List<Student>();
}
}
}


using System.Collections.Generic;

namespace NHSample4.Domain
{
public class Student
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Course> Courses { get; set; }

public Student()
{
Courses = new List<Course>();
}
}
}

كلاس كانفيگ برنامه جهت ايجاد نگاشت‌ها و سپس ساخت ديتابيس متناظر

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"))
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample4.Domain.Course).Assembly))
.ExportTo(System.Environment.CurrentDirectory)
);
}

public static void CreateDb()
{
bool script = false;//آيا خروجي در كنسول هم نمايش داده شود
bool export = true;//آيا بر روي ديتابيس هم اجرا شود
bool dropTables = false;//آيا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
چند نكته در مورد اين كلاس:
الف) با توجه به اينكه برنامه از نوع ويندوزي است، براي مديريت صحيح كانكشن استرينگ، فايل App.Config را به برنامه افروده و محتويات آن‌را به شكل زير تنظيم مي‌كنيم (تا كليد DbConnectionString توسط متد GetConfig مورد استفاده قرارگيرد ):

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>
</configuration>

ب) در NHibernate سنتي (!) كار ساخت نگاشت‌ها توسط يك سري فايل xml صورت مي‌گيرد كه با معرفي فريم ورك Fluent NHibernate و استفاده از قابليت‌هاي Auto Mapping آن، اين‌كار با سهولت و دقت هر چه تمام‌تر قابل انجام است كه توضيحات نحوه‌ي انجام ‌آن‌را در قسمت‌هاي قبل مطالعه فرموديد. اگر نياز بود تا اين فايل‌هاي XML نيز جهت بررسي شخصي ايجاد شوند، تنها كافي است از متد ExportTo آن همانگونه كه در متد GetConfig استفاده شده، كمك گرفته شود. به اين صورت پس از ايجاد خودكار نگاشت‌ها، فايل‌هاي XML متناظر نيز در مسيري كه به عنوان آرگومان متد ExportTo مشخص گرديده است، توليد خواهند شد (دو فايل NHSample4.Domain.Course.hbm.xml و NHSample4.Domain.Student.hbm.xml را در پوشه‌اي كه محل اجراي برنامه است خواهيد يافت).

با فراخواني متد CreateDb اين كلاس، پس از ساخت خودكار نگاشت‌ها، database schema متناظر، در ديتابيسي كه توسط كانكشن استرينگ برنامه مشخص شده، ايجاد خواهد شد كه ديتابيس دياگرام آن‌را در شكل ذيل مشاهده مي‌نمائيد (جداول دانشجويان و واحدها هر كدام به صورت موجوديتي مستقل ايجاد شده كه ارجاعات آن‌ها در جدولي سوم نگهداري مي‌شود).



پياده سازي الگوي مخزن

اينترفيس عمومي الگوي مخزن به شكل زير مي‌تواند باشد:

using System;
using System.Linq;
using System.Linq.Expressions;

namespace NHSample4.NHRepository
{
//Repository Interface
public interface IRepository<T>
{
T Get(object key);

T Save(T entity);
T Update(T entity);
void Delete(T entity);

IQueryable<T> Find();
IQueryable<T> Find(Expression<Func<T, bool>> predicate);
}
}

سپس پياده سازي آن با توجه به كلاس SingletonCore ايي كه در قسمت قبل تهيه كرديم (جهت مديريت صحيح سشن فكتوري)، به صورت زير خواهد بود.
اين كلاس كار آغاز و پايان تراكنش‌ها را نيز مديريت كرده و جهت سهولت كار اينترفيس IDisposable را نيز پياده سازي مي‌كند :

using System;
using System.Linq;
using NHSessionManager;
using NHibernate;
using NHibernate.Linq;

namespace NHSample4.NHRepository
{
public class Repository<T> : IRepository<T>, IDisposable
{
private ISession _session;
private bool _disposed = false;

public Repository()
{
_session = SingletonCore.SessionFactory.OpenSession();
BeginTransaction();
}

~Repository()
{
Dispose(false);
}

public T Get(object key)
{
if (!isSessionSafe) return default(T);

return _session.Get<T>(key);
}

public T Save(T entity)
{
if (!isSessionSafe) return default(T);

_session.Save(entity);
return entity;
}

public T Update(T entity)
{
if (!isSessionSafe) return default(T);

_session.Update(entity);
return entity;
}

public void Delete(T entity)
{
if (!isSessionSafe) return;

_session.Delete(entity);
}

public IQueryable<T> Find()
{
if (!isSessionSafe) return null;

return _session.Linq<T>();
}

public IQueryable<T> Find(System.Linq.Expressions.Expression<Func<T, bool>> predicate)
{
if (!isSessionSafe) return null;

return Find().Where(predicate);
}

void Commit()
{
if (!isSessionSafe) return;

if (_session.Transaction != null &&
_session.Transaction.IsActive &&
!_session.Transaction.WasCommitted &&
!_session.Transaction.WasRolledBack)
{
_session.Transaction.Commit();
}
else
{
_session.Flush();
}
}

void Rollback()
{
if (!isSessionSafe) return;

if (_session.Transaction != null && _session.Transaction.IsActive)
{
_session.Transaction.Rollback();
}
}

private bool isSessionSafe
{
get
{
return _session != null && _session.IsOpen;
}
}

void BeginTransaction()
{
if (!isSessionSafe) return;

_session.BeginTransaction();
}


public void Dispose()
{
Dispose(true);
// tell the GC that the Finalize process no longer needs to be run for this object.
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposeManagedResources)
{
if (_disposed) return;
if (!disposeManagedResources) return;
if (!isSessionSafe) return;

try
{
Commit();
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Rollback();
}
finally
{
if (isSessionSafe)
{
_session.Close();
_session.Dispose();
}
}

_disposed = true;
}
}
}
اكنون جهت استفاده از اين كلاس مخزن به شكل زير مي‌توان عمل كرد:

using System;
using System.Collections.Generic;
using NHSample4.Domain;
using NHSample4.NHRepository;

namespace NHSample4
{
class Program
{
static void Main(string[] args)
{
//ايجاد ديتابيس در صورت نياز
//NHSessionManager.Config.CreateDb();

//ابتدا يك دانشجو را اضافه مي‌كنيم
Student student = null;
using (var studentRepo = new Repository<Student>())
{
student = studentRepo.Save(new Student() { Name = "Vahid" });
}

//اكنون يك واحد را افزوده و ارجاع آن‌را با دانشجو برقرار مي كنيم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course()
{
Teacher = "Shams",
Students = new List<Student>() { student }
});
}

//سپس شماره دروس استادي خاص را نمايش مي‌دهيم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");

foreach (var courseItem in query)
Console.WriteLine(courseItem.Id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور كه ملاحظه مي‌كنيد در اين سطح ديگر برنامه هيچ دركي از ORM مورد استفاده ندارد و پياده سازي نحوه‌ي تعامل با NHibernate در پس كلاس مخزن مخفي شده است. كار آغاز و پايان تراكنش‌ها به صورت خودكار مديريت گرديده و همچنين آزاد سازي منابع را نيز توسط اينترفيس IDisposable مديريت مي‌كند. به اين صورت امكان فراموش شدن يك سري از اعمال متداول به حداقل رسيده، ميزان كدهاي تكراري برنامه كم شده و همچنين هر زمانيكه نياز بود، صرفا با تغيير پياده سازي كلاس مخزن مي‌توان به ORM ديگري كوچ كرد؛ بدون اينكه نيازي به بازنويسي كل برنامه وجود داشته باشد.

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

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