معرفی الگوی 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>())
{
var course = courseRepo.Save(new Course() { Teacher = "Shams" });
}
//اکنون یک واحد را به دانشجو انتساب میدهیم
using (var courseRepo = new Repository<Course>())
{
courseRepo.Save(new Course() { Students = new List<Student>() { student } });
}
//سپس شماره دروس استادی خاص را نمایش میدهیم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");
foreach (var course in query)
Console.WriteLine(course.Id);
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور که ملاحظه میکنید در این سطح دیگر برنامه هیچ درکی از ORM مورد استفاده ندارد و پیاده سازی نحوهی تعامل با NHibernate در پس کلاس مخزن مخفی شده است. کار آغاز و پایان تراکنشها به صورت خودکار مدیریت گردیده و همچنین آزاد سازی منابع را نیز توسط اینترفیس IDisposable مدیریت میکند. به این صورت امکان فراموش شدن یک سری از اعمال متداول به حداقل رسیده، میزان کدهای تکراری برنامه کم شده و همچنین هر زمانیکه نیاز بود، صرفا با تغییر پیاده سازی کلاس مخزن میتوان به ORM دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.
دریافت سورس برنامه قسمت هشتم
ادامه دارد ...