مطالب
یکسان سازی ی و ک دریافتی حین استفاده از NHibernate


تصویر فوق، یکی از تصویرهایی است که شاید از طریق ایمیل‌هایی تحت عنوان "فقط در ایران!" به دست شما هم رسیده باشد. تصور کاربر نهایی (که این ایمیل را با تعجب ارسال کرده) این است که در اینجا به او گفته شده مثلا "مرتضی" را جستجو نکنید و امثال آن. چون برای او تفاوتی بین ی و ی وجود ندارد. همچنین بکار بردن "اقلامی" هم کمی غلط انداز است و بیشتر ذهن را به سمت کلمه سوق می‌دهد تا حرف.

در ادامه‌ی بحث آلرژی مزمن به وجود انواع "ی" و "ک" در بانک اطلاعاتی (+ و + و +)، اینبار قصد داریم این اطلاعات را به NHibernate بسط دهیم. شاید یک روش اعمال یک دست سازی "ی" و "ک" این باشد که در کل برنامه هر جایی که قرار است update یا insert ایی صورت گیرد، خواص رشته‌ای را یافته و تغییر دهیم. این روش "کار می‌کنه" ولی ایده آل نیست؛ چون حجم کار تکراری در برنامه زیاد خواهد شد و نگهداری آن هم مشکل می‌شود. همچنین امکان فراموش کردن اعمال آن هم وجود دارد.
در NHibernate یک سری EventListener وجود دارند که کارشان گوش فرا دادن به یک سری رخدادها مانند مثلا update یا insert است. این رخدادها می‌توانند پیش یا پس از هرگونه ثبت یا ویرایشی در برنامه صادر شوند. بنابراین بهترین جایی که جهت اعمال این نوع ممیزی (Auditing) بدون بالا بردن حجم برنامه یا اضافه کردن بیش از حد یک سری کد تکراری در حین کار با NHibernate می‌توان یافت، روال‌های مدیریت کننده‌ی همین EventListener ها هستند.

کلاس YeKeAuditorEventListener نهایی با پیاده سازی IPreInsertEventListener و IPreUpdateEventListenerبه شکل زیر خواهد بود:
using NHibernate.Event;

namespace NHYeKeAuditor
{
public class YeKeAuditorEventListener : IPreInsertEventListener, IPreUpdateEventListener
{
// Represents a pre-insert event, which occurs just prior to performing the
// insert of an entity into the database.
public bool OnPreInsert(PreInsertEvent preInsertEvent)
{
var entity = preInsertEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}

// Represents a pre-update event, which occurs just prior to performing the
// update of an entity in the database.
public bool OnPreUpdate(PreUpdateEvent preUpdateEvent)
{
var entity = preUpdateEvent.Entity;
CorrectYeKe.ApplyCorrectYeKe(entity);
return false;
}
}
}
در کدهای فوق روال‌های OnPreInsert و OnPreUpdate پیش از ثبت و ویرایش اطلاعات فراخوانی می‌شوند (همواره و بدون نیاز به نگرانی از فراموش شدن فراخوانی کدهای مربوطه). اینجا است که فرصت داریم تا تغییرات مورد نظر خود را جهت یکسان سازی "ی" و "ک" دریافتی اعمال کنیم (کد کلاس CorrectYeKe را در پیوست خواهید یافت).

تا اینجا فقط تعریف YeKeAuditorEventListener انجام شده است. اما NHibernate چگونه از وجود آن مطلع خواهد شد؟
برای تزریق کلاس YeKeAuditorEventListener به تنظیمات برنامه باید به شکل زیر عمل کرد:
using System;
using System.Linq;
using FluentNHibernate.Cfg;
using NHibernate.Cfg;

namespace NHYeKeAuditor
{
public static class MappingsConfiguration
{
public static FluentConfiguration InjectYeKeAuditorEventListener(this FluentConfiguration fc)
{
return fc.ExposeConfiguration(configListeners());
}

private static Action<Configuration> configListeners()
{
return
c =>
{
var listener = new YeKeAuditorEventListener();
c.EventListeners.PreInsertEventListeners =
c.EventListeners.PreInsertEventListeners
.Concat(new[] { listener })
.ToArray();
c.EventListeners.PreUpdateEventListeners =
c.EventListeners.PreUpdateEventListeners
.Concat(new[] { listener })
.ToArray();
};
}
}
}
به این معنا که FluentConfiguration خود را همانند قبل ایجاد کنید. درست در زمان پایان کار تنها کافی است متد InjectYeKeAuditorEventListener فوق بر روی آن اعمال گردد و بس (یعنی پیش از فراخوانی BuildSessionFactory).

کدهای NHYeKeAuditor را از اینجا می‌توانید دریافت کنید.

مطالب
آشنایی با 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>())
{
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 دیگری کوچ کرد؛ بدون اینکه نیازی به بازنویسی کل برنامه وجود داشته باشد.

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

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


مسیرراه‌ها
NHibernate
      مطالب
      آشنایی با NHibernate - قسمت سوم

      در ادامه، تعاریف سایر موجودیت‌های سیستم ثبت سفارشات و نگاشت آن‌ها را بررسی خواهیم کرد.

      کلاس Product تعریف شده در فایل جدید Product.cs در پوشه domain برنامه:

      namespace NHSample1.Domain
      {
      public class Product
      {
      public int Id { get; set; }
      public string Name { get; set; }
      public decimal UnitPrice { get; set; }
      public bool Discontinued { get; set; }
      }
      }
      کلاس ProductMapping تعریف شده در فایل جدید ProductMapping.cs (توصیه شده است که به ازای هر کلاس یک فایل جداگانه در نظر گرفته شود)، در پوشه Mappings برنامه:

      using FluentNHibernate.Mapping;
      using NHSample1.Domain;

      namespace NHSample1.Mappings
      {
      public class ProductMapping : ClassMap<Product>
      {
      public ProductMapping()
      {
      Not.LazyLoad();
      Id(p => p.Id).GeneratedBy.HiLo("1000");
      Map(p => p.Name).Length(50).Not.Nullable();
      Map(p => p.UnitPrice).Not.Nullable();
      Map(p => p.Discontinued).Not.Nullable();
      }
      }
      }
      همانطور که ملاحظه می‌کنید، روش تعریف آن‌ها همانند شیء Customer است که در قسمت‌های قبل بررسی شد و نکته جدیدی ندارد.
      آزمون واحد بررسی این نگاشت نیز همانند مثال قبلی است.
      کلاس ProductMapping_Fixture را در فایل جدید ProductMapping_Fixture.cs به پروژه UnitTests خود (که ارجاعات آن‌را در قسمت قبل مشخص کردیم) خواهیم افزود:

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

      namespace UnitTests
      {
      [TestFixture]
      public class ProductMapping_Fixture : FixtureBase
      {
      [Test]
      public void can_correctly_map_product()
      {
      new PersistenceSpecification<Product>(Session)
      .CheckProperty(p => p.Id, 1001)
      .CheckProperty(p => p.Name, "Apples")
      .CheckProperty(p => p.UnitPrice, 10.45m)
      .CheckProperty(p => p.Discontinued, true)
      .VerifyTheMappings();
      }
      }
      }
      و پس از اجرای این آزمون واحد، عبارات SQL ایی که به صورت خودکار توسط این ORM جهت بررسی عملیات نگاشت صورت خواهند گرفت به صورت زیر می‌باشند:

      ProductMapping_Fixture.can_correctly_map_product : Passed
      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 "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 10.45, @p2 = True, @p3 = 1001
      NHibernate: SELECT product0_.Id as Id1_0_, product0_.Name as Name1_0_, product0_.UnitPrice as UnitPrice1_0_, product0_.Discontinued as Disconti4_1_0_ FROM "Product" product0_ WHERE product0_.Id=@p0;@p0 = 1001

      در ادامه تعریف کلاس کارمند، نگاشت و آزمون واحد آن به صورت زیر خواهند بود:

      using System;
      namespace NHSample1.Domain
      {
      public class Employee
      {
      public int Id { set; get; }
      public string LastName { get; set; }
      public string FirstName { get; set; }
      }
      }


      using NHSample1.Domain;
      using FluentNHibernate.Mapping;

      namespace NHSample1.Mappings
      {
      public class EmployeeMapping : ClassMap<Employee>
      {
      public EmployeeMapping()
      {
      Not.LazyLoad();
      Id(e => e.Id).GeneratedBy.Assigned();
      Map(e => e.LastName).Length(50);
      Map(e => e.FirstName).Length(50);
      }
      }
      }


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

      namespace UnitTests
      {
      [TestFixture]
      public class EmployeeMapping_Fixture : FixtureBase
      {
      [Test]
      public void can_correctly_map_employee()
      {
      new PersistenceSpecification<Employee>(Session)
      .CheckProperty(p => p.Id, 1001)
      .CheckProperty(p => p.FirstName, "name1")
      .CheckProperty(p => p.LastName, "lname1")
      .VerifyTheMappings();
      }
      }
      }
      خروجی SQL حاصل از موفقیت آزمون واحد آن:

      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 "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 1001
      NHibernate: SELECT employee0_.Id as Id4_0_, employee0_.LastName as LastName4_0_, employee0_.FirstName as FirstName4_0_ FROM "Employee" employee0_ WHERE employee0_.Id=@p0;@p0 = 1001

      همانطور که ملاحظه می‌کنید، این آزمون‌های واحد 4 مرحله را در یک سطر انجام می‌دهند:
      الف) ایجاد یک وهله از کلاس Employee
      ب) ثبت اطلاعات کارمند در دیتابیس
      ج) دریافت اطلاعات کارمند در وهله‌ای جدید از شیء Employee
      د) و در پایان بررسی می‌کند که آیا شیء جدید ایجاد شده با شیء اولیه مطابقت دارد یا خیر

      اکنون در ادامه پیاده سازی سیستم ثبت سفارشات، به قسمت جالب این مدل می‌رسیم. قسمتی که در آن ارتباطات اشیاء و روابط one-to-many تعریف خواهند شد. تعاریف کلاس‌های OrderItem و OrderItemMapping را به صورت زیر در نظر بگیرید:

      کلاس OrderItem تعریف شده در فایل جدید OrderItem.cs واقع شده در پوشه domain پروژه:
      که در آن هر سفارش (order) دقیقا از یک محصول (product) تشکیل می‌شود و هر محصول می‌تواند در سفارشات متعدد و مختلفی درخواست شود.

      namespace NHSample1.Domain
      {
      public class OrderItem
      {
      public int Id { get; set; }
      public int Quantity { get; set; }
      public Product Product { get; set; }
      }
      }
      کلاس OrderItemMapping تعریف شده در فایل جدید OrderItemMapping.cs :

      using FluentNHibernate.Mapping;
      using NHSample1.Domain;

      namespace NHSample1.Mappings
      {
      public class OrderItemMapping : ClassMap<OrderItem>
      {
      public OrderItemMapping()
      {
      Not.LazyLoad();
      Id(oi => oi.Id).GeneratedBy.Assigned();
      Map(oi => oi.Quantity).Not.Nullable();
      References(oi => oi.Product).Not.Nullable();
      }
      }
      }
      نکته جدیدی که در این کلاس نگاشت مطرح شده است، واژه کلیدی References می‌باشد که جهت بیان این ارجاعات و وابستگی‌ها بکار می‌رود. این ارجاع بیانگر یک رابطه many-to-one بین سفارشات و محصولات است. همچنین در ادامه آن Not.Nullable ذکر شده است تا این ارجاع را اجباری نمائید (در غیر اینصورت سفارش غیر معتبر خواهد بود).
      نکته‌ی دیگر مهم آن این مورد است که Id در اینجا به صورت یک کلید تعریف نشده است. یک آیتم سفارش داده شده، موجودیت به حساب نیامده و فقط یک شیء مقداری (value object) است و به خودی خود امکان وجود ندارد. هر وهله از آن تنها توسط یک سفارش قابل تعریف است. بنابراین id در اینجا فقط به عنوان یک index می‌تواند مورد استفاده قرار گیرد و فقط توسط شیء Order زمانیکه یک OrderItem به آن اضافه می‌شود، مقدار دهی خواهد شد.

      اگر برای این نگاشت نیز آزمون واحد تهیه کنیم، به صورت زیر خواهد بود:

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

      namespace UnitTests
      {
      [TestFixture]
      public class OrderItemMapping_Fixture : FixtureBase
      {
      [Test]
      public void can_correctly_map_order_item()
      {
      var product = new Product
      {
      Name = "Apples",
      UnitPrice = 4.5m,
      Discontinued = true
      };

      new PersistenceSpecification<OrderItem>(Session)
      .CheckProperty(p => p.Id, 1)
      .CheckProperty(p => p.Quantity, 5)
      .CheckReference(p => p.Product, product)
      .VerifyTheMappings();
      }
      }
      }

      مشکل! این آزمون واحد با شکست مواجه خواهد شد، زیرا هنوز مشخص نکرده‌ایم که دو شیء Product را که در قسمت CheckReference فوق برای این منظور معرفی کرده‌ایم، چگونه باید با هم مقایسه کرد. در مورد مقایسه نوع‌های اولیه و اصلی مانند int و string و امثال آن مشکلی نیست، اما باید منطق مقایسه سایر اشیاء سفارشی خود را با پیاده سازی اینترفیس IEqualityComparer دقیقا مشخص سازیم:

      using System.Collections;
      using NHSample1.Domain;

      namespace UnitTests
      {
      public class CustomEqualityComparer : IEqualityComparer
      {
      public bool Equals(object x, object y)
      {
      if (ReferenceEquals(x, y)) return true;
      if (x == null || y == null) return false;

      if (x is Product && y is Product)
      return (x as Product).Id == (y as Product).Id;

      if (x is Customer && y is Customer)
      return (x as Customer).Id == (y as Customer).Id;

      if (x is Employee && y is Employee)
      return (x as Employee).Id == (y as Employee).Id;

      if (x is OrderItem && y is OrderItem)
      return (x as OrderItem).Id == (y as OrderItem).Id;


      return x.Equals(y);
      }

      public int GetHashCode(object obj)
      {
      //شاید وقتی دیگر
      return obj.GetHashCode();
      }
      }
      }
      در اینجا فقط Id این اشیاء با هم مقایسه شده است. در صورت نیاز تمامی خاصیت‌های این اشیاء را نیز می‌توان با هم مقایسه کرد (یک سری از اشیاء بکار گرفته شده در این کلاس در ادامه بحث معرفی خواهند شد).
      سپس برای بکار گیری این کلاس جدید، سطر مربوط به استفاده از PersistenceSpecification به صورت زیر تغییر خواهد کرد:

      new PersistenceSpecification<OrderItem>(Session, new CustomEqualityComparer())

      پس از این تغییرات و مشخص سازی نحوه‌ی مقایسه دو شیء سفارشی، آزمون واحد ما پاس شده و خروجی SQL تولید شده آن به صورت زیر می‌باشد:

      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 "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
      NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 5, @p1 = 1001, @p2 = 1
      NHibernate: SELECT orderitem0_.Id as Id0_1_, orderitem0_.Quantity as Quantity0_1_, orderitem0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitem0_ inner join "Product" product1_ on orderitem0_.Product_id=product1_.Id WHERE orderitem0_.Id=@p0;@p0 = 1

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

      using System;
      using System.Collections.Generic;

      namespace NHSample1.Domain
      {
      public class Order
      {
      public int Id { set; get; }
      public DateTime OrderDate { get; set; }
      public Employee Employee { get; set; }
      public Customer Customer { get; set; }
      public IList<OrderItem> OrderItems { get; set; }
      }
      }
      نکته‌ی مهمی که در این کلاس وجود دارد استفاده از IList جهت معرفی مجموعه‌ای از آیتم‌های سفارشی است (بجای List و یا IEnumerable که در صورت استفاده خطای type cast exception در حین نگاشت حاصل می‌شد).

      using NHSample1.Domain;
      using FluentNHibernate.Mapping;

      namespace NHSample1.Mappings
      {
      public class OrderMapping : ClassMap<Order>
      {
      public OrderMapping()
      {
      Not.LazyLoad();
      Id(o => o.Id).GeneratedBy.GuidComb();
      Map(o => o.OrderDate).Not.Nullable();
      References(o => o.Employee).Not.Nullable();
      References(o => o.Customer).Not.Nullable();
      HasMany(o => o.OrderItems)
      .AsList(index => index.Column("ListIndex").Type<int>());
      }
      }
      }
      در تعاریف نگاشت این کلاس نیز دو ارجاع به اشیاء کارمند و مشتری وجود دارد که با References مشخص شده‌اند.
      قسمت جدید آن HasMany است که جهت تعریف رابطه one-to-many بکار گرفته شده است. یک سفارش رابطه many-to-one با یک مشتری و همچنین کارمندی که این رکورد را ثبت می‌کند، دارد. در اینجا مجموعه آیتم‌های یک سفارش به صورت یک لیست بازگشت داده می‌شود و ایندکس آن به ستونی به نام ListIndex در یک جدول دیتابیس نگاشت خواهد شد. نوع این ستون، int می‌باشد.

      using System;
      using System.Collections.Generic;
      using NUnit.Framework;
      using NHSample1.Domain;
      using FluentNHibernate.Testing;

      namespace UnitTests
      {
      [TestFixture]
      public class OrderMapping_Fixture : FixtureBase
      {
      [Test]
      public void can_correctly_map_an_order()
      {
      {
      var product1 =
      new Product
      {
      Name = "Apples",
      UnitPrice = 4.5m,
      Discontinued = true
      };
      var product2 =
      new Product
      {
      Name = "Pears",
      UnitPrice = 3.5m,
      Discontinued = false
      };

      Session.Save(product1);
      Session.Save(product2);

      var items = new List<OrderItem>
      {
      new OrderItem
      {
      Id = 1,
      Quantity = 100,
      Product = product1
      },
      new OrderItem
      {
      Id = 2,
      Quantity = 200,
      Product = product2
      }
      };

      var customer = new Customer
      {
      FirstName = "Vahid",
      LastName = "Nasiri",
      AddressLine1 = "Addr1",
      AddressLine2 = "Addr2",
      PostalCode = "1234",
      City = "Tehran",
      CountryCode = "IR"
      };

      var employee =
      new Employee
      {
      FirstName = "name1",
      LastName = "lname1"
      };



      var order = new Order
      {
      Customer = customer,
      Employee = employee,
      OrderDate = DateTime.Today,
      OrderItems = items
      };

      new PersistenceSpecification<Order>(Session, new CustomEqualityComparer())
      .CheckProperty(o => o.OrderDate, order.OrderDate)
      .CheckReference(o => o.Customer, order.Customer)
      .CheckReference(o => o.Employee, order.Employee)
      .CheckList(o => o.OrderItems, order.OrderItems)
      .VerifyTheMappings();
      }
      }
      }
      }
      همانطور که ملاحظه می‌کنید در این متد آزمون واحد، نیاز به مشخص سازی منطق مقایسه اشیاء سفارش، مشتری و آیتم‌های سفارش داده شده نیز وجود دارد که پیشتر در کلاس CustomEqualityComparer معرفی شدند؛ درغیر اینصورت این آزمون واحد با شکست مواجه می‌شد.
      متد آزمون واحد فوق کمی طولانی است؛ زیرا در آن باید تعاریف انواع و اقسام اشیاء مورد استفاده را مشخص نمود (و ارزش کار نیز دقیقا در همینجا مشخص می‌شود که بجای SQL نوشتن، با اشیایی که توسط کامپایلر تحت نظر هستند سر و کار داریم).
      تنها نکته جدید آن استفاده از CheckList برای بررسی IList تعریف شده در قسمت قبل است.

      خروجی SQL این آزمون واحد پس از اجرا و موفقیت آن به صورت زیر است:

      OrderMapping_Fixture.can_correctly_map_an_order : Passed
      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: select next_hi from hibernate_unique_key
      NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 3, @p1 = 2
      NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Apples', @p1 = 4.5, @p2 = True, @p3 = 1001
      NHibernate: INSERT INTO "Product" (Name, UnitPrice, Discontinued, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 'Pears', @p1 = 3.5, @p2 = False, @p3 = 1002
      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 = 2002
      NHibernate: select next_hi from hibernate_unique_key
      NHibernate: update hibernate_unique_key set next_hi = @p0 where next_hi = @p1;@p0 = 4, @p1 = 3
      NHibernate: INSERT INTO "Employee" (LastName, FirstName, Id) VALUES (@p0, @p1, @p2);@p0 = 'lname1', @p1 = 'name1', @p2 = 3003
      NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 100, @p1 = 1001, @p2 = 1
      NHibernate: INSERT INTO "OrderItem" (Quantity, Product_id, Id) VALUES (@p0, @p1, @p2);@p0 = 200, @p1 = 1002, @p2 = 2
      NHibernate: INSERT INTO "Order" (OrderDate, Employee_id, Customer_id, Id) VALUES (@p0, @p1, @p2, @p3);@p0 = 2009/10/10 12:00:00 ق.ظ, @p1 = 3003, @p2 = 2002, @p3 = 0
      NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 0, @p2 = 1
      NHibernate: UPDATE "OrderItem" SET Order_id = @p0, ListIndex = @p1 WHERE Id = @p2;@p0 = 0, @p1 = 1, @p2 = 2
      NHibernate: SELECT order0_.Id as Id1_2_, order0_.OrderDate as OrderDate1_2_, order0_.Employee_id as Employee3_1_2_, order0_.Customer_id as Customer4_1_2_, employee1_.Id as Id4_0_, employee1_.LastName as LastName4_0_, employee1_.FirstName as FirstName4_0_, customer2_.Id as Id2_1_, customer2_.FirstName as FirstName2_1_, customer2_.LastName as LastName2_1_, customer2_.AddressLine1 as AddressL4_2_1_, customer2_.AddressLine2 as AddressL5_2_1_, customer2_.PostalCode as PostalCode2_1_, customer2_.City as City2_1_, customer2_.CountryCode as CountryC8_2_1_ FROM "Order" order0_ inner join "Employee" employee1_ on order0_.Employee_id=employee1_.Id inner join "Customer" customer2_ on order0_.Customer_id=customer2_.Id WHERE order0_.Id=@p0;@p0 = 0
      NHibernate: SELECT orderitems0_.Order_id as Order4_2_, orderitems0_.Id as Id2_, orderitems0_.ListIndex as ListIndex2_, orderitems0_.Id as Id0_1_, orderitems0_.Quantity as Quantity0_1_, orderitems0_.Product_id as Product3_0_1_, product1_.Id as Id3_0_, product1_.Name as Name3_0_, product1_.UnitPrice as UnitPrice3_0_, product1_.Discontinued as Disconti4_3_0_ FROM "OrderItem" orderitems0_ inner join "Product" product1_ on orderitems0_.Product_id=product1_.Id WHERE orderitems0_.Order_id=@p0;@p0 = 0

      تا اینجای کار تعاریف اشیاء ، نگاشت آن‌ها و همچنین بررسی صحت این نگاشت‌ها به پایان می‌رسد.

      نکته:
      دیتابیس برنامه را جهت آزمون‌های واحد برنامه، از نوع SQLite ساخته شده در حافظه مشخص کردیم. اگر علاقمند باشید که database schema تولید شده توسط NHibernate را مشاهده نمائید، در متد SetupContext کلاس FixtureBase که در قسمت قبل معرفی شد، سطر آخر را به صورت زیر تغییر دهید، تا اسکریپت دیتابیس نیز به صورت خودکار در خروجی اس کیوال آزمون واحد لحاظ شود (پارامتر دوم آن مشخص می‌کند که schema ساخته شده، نمایش داده شود یا خیر):

      SessionSource.BuildSchema(Session, true);
      پس از این تغییر و انجام مجدد آزمون واحد، اسکریپت دیتابیس ما به صورت زیر خواهد بود (که جهت ایجاد یک دیتابیس SQLite می‌تواند مورد استفاده قرار گیرد):

      drop table if exists "OrderItem"

      drop table if exists "Order"

      drop table if exists "Customer"

      drop table if exists "Product"

      drop table if exists "Employee"

      drop table if exists hibernate_unique_key

      create table "OrderItem" (
      Id INTEGER not null,
      Quantity INTEGER not null,
      Product_id INTEGER not null,
      Order_id INTEGER,
      ListIndex INTEGER,
      primary key (Id)
      )

      create table "Order" (
      Id INTEGER not null,
      OrderDate DATETIME not null,
      Employee_id INTEGER not null,
      Customer_id INTEGER not null,
      primary key (Id)
      )

      create table "Customer" (
      Id INTEGER not null,
      FirstName TEXT not null,
      LastName TEXT not null,
      AddressLine1 TEXT not null,
      AddressLine2 TEXT,
      PostalCode TEXT not null,
      City TEXT not null,
      CountryCode TEXT not null,
      primary key (Id)
      )

      create table "Product" (
      Id INTEGER not null,
      Name TEXT not null,
      UnitPrice NUMERIC not null,
      Discontinued INTEGER not null,
      primary key (Id)
      )

      create table "Employee" (
      Id INTEGER not null,
      LastName TEXT,
      FirstName TEXT,
      primary key (Id)
      )

      create table hibernate_unique_key (
      next_hi INTEGER
      )
      البته اگر مستندات SQLite را مطالعه کرده باشید می‌دانید که مفهوم کلید خارجی در این دیتابیس وجود دارد اما اعمال نمی‌شود! (برای اعمال آن باید تریگر نوشت) به همین جهت در این اسکریپت تولیدی خبری از کلید خارجی نیست.

      برای اینکه از دیتابیس اس کیوال سرور استفاده کنیم، در همان متد SetupContext کلاس مذکور، سطر اول را به صورت زیر تغییر دهید (نوع دیتابیس اس کیوال سرور 2008 مشخص شده و سپس رشته اتصالی به دیتابیس ذکر گردیده است):

      var cfg = Fluently.Configure().Database(
      // SQLiteConfiguration.Standard.ShowSql().InMemory
      MsSqlConfiguration
      .MsSql2008
      .ShowSql()
      .ConnectionString("Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true")
      );

      اکنون اگر مجددا آزمون واحد را اجرا نمائیم، اسکریپت تولیدی به صورت زیر خواهد بود (در اینجا مفهوم استقلال برنامه از نوع دیتابیس را به خوبی می‌توان درک کرد):

      if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF88858466CFBF7]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
      alter table [OrderItem] drop constraint FK3EF88858466CFBF7


      if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3EF888589F32DE52]') AND parent_object_id = OBJECT_ID('[OrderItem]'))
      alter table [OrderItem] drop constraint FK3EF888589F32DE52


      if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099B1EBA72BC]') AND parent_object_id = OBJECT_ID('[Order]'))
      alter table [Order] drop constraint FK3117099B1EBA72BC


      if exists (select 1 from sys.objects where object_id = OBJECT_ID(N'[FK3117099BB2F9593A]') AND parent_object_id = OBJECT_ID('[Order]'))
      alter table [Order] drop constraint FK3117099BB2F9593A


      if exists (select * from dbo.sysobjects where id = object_id(N'[OrderItem]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [OrderItem]

      if exists (select * from dbo.sysobjects where id = object_id(N'[Order]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Order]

      if exists (select * from dbo.sysobjects where id = object_id(N'[Customer]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Customer]

      if exists (select * from dbo.sysobjects where id = object_id(N'[Product]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Product]

      if exists (select * from dbo.sysobjects where id = object_id(N'[Employee]') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table [Employee]

      if exists (select * from dbo.sysobjects where id = object_id(N'hibernate_unique_key') and OBJECTPROPERTY(id, N'IsUserTable') = 1) drop table hibernate_unique_key

      create table [OrderItem] (
      Id INT not null,
      Quantity INT not null,
      Product_id INT not null,
      Order_id INT null,
      ListIndex INT null,
      primary key (Id)
      )

      create table [Order] (
      Id INT not null,
      OrderDate DATETIME not null,
      Employee_id INT not null,
      Customer_id INT not null,
      primary key (Id)
      )

      create table [Customer] (
      Id INT not null,
      FirstName NVARCHAR(50) not null,
      LastName NVARCHAR(50) not null,
      AddressLine1 NVARCHAR(50) not null,
      AddressLine2 NVARCHAR(50) null,
      PostalCode NVARCHAR(10) not null,
      City NVARCHAR(50) not null,
      CountryCode NVARCHAR(2) not null,
      primary key (Id)
      )

      create table [Product] (
      Id INT not null,
      Name NVARCHAR(50) not null,
      UnitPrice DECIMAL(19,5) not null,
      Discontinued BIT not null,
      primary key (Id)
      )

      create table [Employee] (
      Id INT not null,
      LastName NVARCHAR(50) null,
      FirstName NVARCHAR(50) null,
      primary key (Id)
      )

      alter table [OrderItem]
      add constraint FK3EF88858466CFBF7
      foreign key (Product_id)
      references [Product]

      alter table [OrderItem]
      add constraint FK3EF888589F32DE52
      foreign key (Order_id)
      references [Order]

      alter table [Order]
      add constraint FK3117099B1EBA72BC
      foreign key (Employee_id)
      references [Employee]

      alter table [Order]
      add constraint FK3117099BB2F9593A
      foreign key (Customer_id)
      references [Customer]

      create table hibernate_unique_key (
      next_hi INT
      )
      که نکات ذیل در مورد آن جالب توجه است:
      الف) جداول مطابق نام کلاس‌های ما تولید شده‌اند.
      ب) نام فیلدها دقیقا مطابق نام خواص کلاس‌های ما تشکیل شده‌اند.
      ج) Id ها به صورت primary key تعریف شده‌اند (از آنجائیکه ما در هنگام تعریف نگاشت‌ها، آن‌ها را از نوع identity مشخص کرده بودیم).
      د) رشته‌ها به نوع nvarchar با اندازه 50 نگاشت شده‌اند.
      ه) کلیدهای خارجی بر اساس نام جدول با پسوند _id تشکیل شده‌اند.




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


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

      آشنایی با کتابخانه NHibernate Validator

      پروژه جدیدی به پروژه NHibernate Contrib در سایت سورس فورج اضافه شده است به نام NHibernate Validator که از آدرس زیر قابل دریافت است:


      این پروژه که توسط Dario Quintana توسعه یافته است، امکان اعتبار سنجی اطلاعات را پیش از افزوده شدن آن‌ها به دیتابیس به دو صورت دستی و یا خودکار و یکپارچه با NHibernate فراهم می‌سازد؛ که امروز قصد بررسی آن‌را داریم.

      کامپایل پروژه اعتبار سنجی NHibernate

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

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

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


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

      namespace NHSample5.Domain
      {
      public class Patient
      {
      public virtual int Id { get; set; }
      public virtual string FirstName { get; set; }
      public virtual string LastName { get; set; }
      }
      }

      using System.Collections.Generic;

      namespace NHSample5.Domain
      {
      public class Doctor
      {
      public virtual int Id { get; set; }
      public virtual string Name { get; set; }
      public virtual IList<Patient> Patients { get; set; }

      public Doctor()
      {
      Patients = new List<Patient>();
      }
      }
      }
      برنامه این قسمت از نوع کنسول با ارجاعاتی به اسمبلی‌های FluentNHibernate.dll ،log4net.dll ،NHibernate.dll ، NHibernate.ByteCode.Castle.dll ،NHibernate.Linq.dll ،NHibernate.Validator.dll و System.Data.Services.dll است.

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


      اطلاعات این برنامه بر مبنای NHRepository و NHSessionManager ایی است که در قسمت‌های قبل توسعه دادیم و پیشنیاز ضروری مطالعه آن می‌باشند (سورس پیوست شده شامل نمونه تکمیل شده این موارد نیز هست). همچنین از قسمت ایجاد دیتابیس از روی مدل نیز صرفنظر می‌شود و همانند قسمت‌های قبل است.


      تعریف اعتبار سنجی دومین با کمک ویژگی‌ها (attributes)

      فرض کنید می‌خواهیم بر روی طول نام و نام خانوادگی بیمار محدودیت قرار داده و آن‌ها را با کمک کتابخانه NHibernate Validator ، اعتبار سنجی کنیم. برای این منظور ابتدا فضای نام NHibernate.Validator.Constraints به کلاس بیمار اضافه شده و سپس با کمک ویژگی‌هایی که در این کتابخانه تعریف شده‌اند می‌توان قیود خود را به خواص کلاس تعریف شده اعمال نمود که نمونه‌ای از آن را مشاهده می‌نمائید:

      using NHibernate.Validator.Constraints;

      namespace NHSample5.Domain
      {
      public class Patient
      {
      public virtual int Id { get; set; }

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

      [Length(Min = 3, Max = 60, Message = "طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد")]
      public virtual string LastName { get; set; }
      }
      }
      اعمال این قیود از این جهت مهم هستند که نباید وقت برنامه و سیستم را با دریافت خطای نهایی از دیتابیس تلف کرد. آیا بهتر نیست قبل از اینکه اطلاعات به دیتابیس وارد شوند و رفت و برگشتی در شبکه صورت گیرد، مشخص گردد که این فیلد حتما نباید خالی باشد یا طول آن باید دارای شرایط خاصی باشد و امثال آن؟

      مثالی دیگر:
      جهت اجباری کردن و همچنین اعمال Regular expressions برای اعتبار سنجی یک فیلد می‌توان دو ویژگی زیر را به بالای آن فیلد مورد نظر افزود:

      [NotNull]
      [Pattern(Regex = "[A-Za-z0-9]+")]

      تعریف اعتبار سنجی با کمک کلاس ValidationDef

      راه دوم تعریف اعتبار سنجی، کمک گرفتن از کلاس ValidationDef این کتابخانه و استفاده از روش fluent configuration است. برای این منظور، پوشه جدیدی را به برنامه به نام Validation اضافه خواهیم کرد و سپس دو کلاس DoctorDef و PatientDef را به آن به صورت زیر خواهیم افزود:

      using NHibernate.Validator.Cfg.Loquacious;
      using NHSample5.Domain;

      namespace NHSample5.Validation
      {
      public class DoctorDef : ValidationDef<Doctor>
      {
      public DoctorDef()
      {
      Define(x => x.Name).LengthBetween(3, 50);
      Define(x => x.Patients).NotNullableAndNotEmpty();
      }
      }
      }

      using NHSample5.Domain;
      using NHibernate.Validator.Cfg.Loquacious;

      namespace NHSample5.Validation
      {
      public class PatientDef : ValidationDef<Patient>
      {
      public PatientDef()
      {
      Define(x => x.FirstName)
      .LengthBetween(3, 20)
      .WithMessage("طول نام باید بین 3 و 20 کاراکتر باشد");

      Define(x => x.LastName)
      .LengthBetween(3, 60)
      .WithMessage("طول نام خانوادگی باید بین 3 و 60 کاراکتر باشد");
      }
      }
      }

      استفاده از قیودات تعریف شده به صورت دستی

      می‌توان از این کتابخانه اعتبار سنجی به صورت مستقیم نیز اضافه کرد. روش انجام آن‌را در متد زیر مشاهده می‌نمائید.

      /// <summary>
      /// استفاده از اعتبار سنجی ویژه به صورت مستقیم
      /// در صورت استفاده از ویژگی‌ها
      /// </summary>
      static void WithoutConfiguringTheEngine()
      {
      //تعریف یک بیمار غیر معتبر
      var patient1 = new Patient() { FirstName = "V", LastName = "N" };
      var ve = new ValidatorEngine();
      var invalidValues = ve.Validate(patient1);
      if (invalidValues.Length == 0)
      {
      Console.WriteLine("patient1 is valid.");
      }
      else
      {
      Console.WriteLine("patient1 is NOT valid!");
      //نمایش پیغام‌های تعریف شده مربوط به هر فیلد
      foreach (var invalidValue in invalidValues)
      {
      Console.WriteLine(
      "{0}: {1}",
      invalidValue.PropertyName,
      invalidValue.Message);
      }
      }

      //تعریف یک بیمار معتبر بر اساس قیودات اعمالی
      var patient2 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
      if (ve.IsValid(patient2))
      {
      Console.WriteLine("patient2 is valid.");
      }
      else
      {
      Console.WriteLine("patient2 is NOT valid!");
      }
      }
      ابتدا شیء ValidatorEngine تعریف شده و سپس متد Validate آن بر روی شیء بیماری غیر معتبر فراخوانی می‌گردد. در صورتیکه این عتبار سنجی با موفقیت روبر نشود، خروجی این متد آرایه‌ای خواهد بود از فیلدهای غیرمعتبر به همراه پیغام‌هایی که برای آن‌ها تعریف کرده‌ایم. یا می‌توان به سادگی همانند بیمار شماره دو، تنها از متد IsValid آن نیز استفاده کرد.

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

      public static ValidatorEngine GetFluentlyConfiguredEngine()
      {
      var vtor = new ValidatorEngine();
      var configuration = new FluentConfiguration();
      configuration
      .Register(
      Assembly
      .GetExecutingAssembly()
      .GetTypes()
      .Where(t => t.Namespace.Equals("NHSample5.Validation"))
      .ValidationDefinitions()
      )
      .SetDefaultValidatorMode(ValidatorMode.UseExternal);
      vtor.Configure(configuration);
      return vtor;
      }

      FluentConfiguration آن مجزا است از نمونه مشابه کتابخانه Fluent NHibernate و نباید با آن اشتباه گرفته شود (در فضای نام NHibernate.Validator.Cfg.Loquacious تعریف شده است).
      در این متد کلاس‌های قرار گرفته در پوشه Validation برنامه که دارای فضای نام NHSample5.Validation هستند، به عنوان کلاس‌هایی که باید اطلاعات لازم مربوط به اعتبار سنجی را از آنان دریافت کرد معرفی شده‌اند.
      همچنین ValidatorMode نیز به صورت External تعریف شده و منظور از External در اینجا هر چیزی بجز استفاده از روش بکارگیری attributes است (علاوه بر امکان تعریف این قیودات در یک پروژه class library مجزا و مشخص ساختن اسمبلی آن در اینجا).

      اکنون جهت دسترسی به این موتور اعتبار سنجی تنظیم شده می‌توان به صورت زیر عمل کرد:

      /// <summary>
      /// استفاده از اعتبار سنجی ویژه به صورت مستقیم
      /// در صورت تعریف آن‌ها با کمک
      /// ValidationDef
      /// </summary>
      static void WithConfiguringTheEngine()
      {
      var ve2 = VeConfig.GetFluentlyConfiguredEngine();
      var doctor1 = new Doctor() { Name = "S" };
      if (ve2.IsValid(doctor1))
      {
      Console.WriteLine("doctor1 is valid.");
      }
      else
      {
      Console.WriteLine("doctor1 is NOT valid!");
      }

      var patient1 = new Patient() { FirstName = "وحید", LastName = "نصیری" };
      if (ve2.IsValid(patient1))
      {
      Console.WriteLine("patient1 is valid.");
      }
      else
      {
      Console.WriteLine("patient1 is NOT valid!");
      }

      var doctor2 = new Doctor() { Name = "شمس", Patients = new List<Patient>() { patient1 } };
      if (ve2.IsValid(doctor2))
      {
      Console.WriteLine("doctor2 is valid.");
      }
      else
      {
      Console.WriteLine("doctor2 is NOT valid!");
      }
      }

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


      استفاده از قیودات تعریف شده و سیستم اعتبار سنجی به صورت یکپارچه با NHibernate

      کتابخانه NHibernate Validator زمانیکه با NHibernate یکپارچه گردد دو رخداد PreInsert و PreUpdate آن‌را به صورت خودکار تحت نظر قرار داده و پیش از اینکه اطلاعات ثبت و یا به روز شوند، ابتدا کار اعتبار سنجی خود را انجام داده و اگر اعتبار سنجی مورد نظر با شکست مواجه شود، با ایجاد یک exception از ادامه برنامه جلوگیری می‌کند. در این حالت استثنای حاصل شده از نوع InvalidStateException خواهد بود.

      برای انجام این مرحله یکپارچه سازی ابتدا متد BuildIntegratedFluentlyConfiguredEngine را به شکل زیر باید فراخوانی نمائیم:

      /// <summary>
      /// از این کانفیگ برای آغاز سشن فکتوری باید کمک گرفته شود
      /// </summary>
      /// <param name="nhConfiguration"></param>
      public static void BuildIntegratedFluentlyConfiguredEngine(ref Configuration nhConfiguration)
      {
      var vtor = new ValidatorEngine();
      var configuration = new FluentConfiguration();
      configuration
      .Register(
      Assembly
      .GetExecutingAssembly()
      .GetTypes()
      .Where(t => t.Namespace.Equals("NHSample5.Validation"))
      .ValidationDefinitions()
      )
      .SetDefaultValidatorMode(ValidatorMode.UseExternal)
      .IntegrateWithNHibernate
      .ApplyingDDLConstraints()
      .And
      .RegisteringListeners();
      vtor.Configure(configuration);

      //Registering of Listeners and DDL-applying here
      ValidatorInitializer.Initialize(nhConfiguration, vtor);
      }
      این متد کار دریافت Configuration مرتبط با NHibernate را جهت اعمال تنظیمات اعتبار سنجی به آن انجام می‌دهد. سپس از nhConfiguration تغییر یافته در این متد جهت ایجاد سشن فکتوری استفاده خواهیم کرد (در غیر اینصورت سشن فکتوری درکی از اعتبار سنجی‌های تعریف شده نخواهد داشت). اگر قسمت‌های قبل را مطالعه کرده باشید، کلاس SingletonCore را جهت مدیریت بهینه‌ی سشن فکتوری به خاطر دارید. این کلاس اکنون باید به شکل زیر وصله شود:

      SingletonCore()
      {
      Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
      VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
      //با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود
      _sessionFactory = cfg.BuildSessionFactory();
      }

      از این لحظه به بعد، نیاز به فراخوانی متدهای Validate و یا IsValid نبوده و کار اعتبار سنجی به صورت خودکار و یکپارچه با NHibernate انجام می‌شود. لطفا به مثال زیر دقت بفرمائید:

      /// <summary>
      /// استفاده از اعتبار سنجی یکپارچه و خودکار
      /// </summary>
      static void tryToSaveInvalidPatient()
      {
      using (Repository<Patient> repo = new Repository<Patient>())
      {
      try
      {
      var patient1 = new Patient() { FirstName = "V", LastName = "N" };
      repo.Save(patient1);
      }
      catch (InvalidStateException ex)
      {
      Console.WriteLine("Validation failed!");
      foreach (var invalidValue in ex.GetInvalidValues())
      Console.WriteLine(
      "{0}: {1}",
      invalidValue.PropertyName,
      invalidValue.Message);
      log4net.LogManager.GetLogger("NHibernate.SQL").Error(ex);
      }
      }
      }

      /// <summary>
      /// استفاده از اعتبار سنجی یکپارچه و خودکار
      /// </summary>
      static void tryToSaveValidPatient()
      {
      using (Repository<Patient> repo = new Repository<Patient>())
      {
      var patient1 = new Patient() { FirstName = "Vahid", LastName = "Nasiri" };
      repo.Save(patient1);
      }
      }
      در اینجا از کلاس Repository که در قسمت‌های قبل توسعه دادیم، استفاده شده است. در متد tryToSaveInvalidPatient ، بدلیل استفاده از تعریف بیماری غیرمعتبر، پیش از انجام عملیات ثبت، استثنایی حاصل شده و پیش از هرگونه رفت و برگشتی به دیتابیس، سیستم از بروز این مشکل مطلع خواهد شد. همچنین پیغام‌هایی را که هنگام تعریف قیودات مشخص کرده بودیم را نیز توسط آرایه ex.GetInvalidValues می‌توان دریافت کرد.

      نکته:
      اگر کار ساخت database schema را با کمک کانفیگ تنظیم شده توسط کتابخانه اعتبار سنجی آغاز کنیم، طول فیلدها دقیقا مطابق با حداکثر طول مشخص شده در قسمت تعاریف قیود هر یک از فیلدها تشکیل می‌گردد (حاصل از اعمال متد ApplyingDDLConstraints در متد BuildIntegratedFluentlyConfiguredEngine ذکر شده می‌باشد).

      public static void CreateValidDb()
      {
      bool script = false;//آیا خروجی در کنسول هم نمایش داده شود
      bool export = true;//آیا بر روی دیتابیس هم اجرا شود
      bool dropTables = false;//آیا جداول موجود دراپ شوند

      Configuration cfg = DbConfig.GetConfig().BuildConfiguration();
      VeConfig.BuildIntegratedFluentlyConfiguredEngine(ref cfg);
      //با همان کانفیگ تنظیم شده برای اعتبار سنجی باید کار شروع شود

      new SchemaExport(cfg).Execute(script, export, dropTables);
      }


      دریافت سورس کامل قسمت دهم


      مطالب
      استفاده از فیلدهای XML در NHibernate

      در مورد طراحی یک برنامه "فرم ساز" در مطلب قبلی بحث شد ... حدودا سه سال قبل اینکار را برای شرکتی انجام دادم. یک برنامه درخواست خدمات نوشته شده با ASP.NET که مدیران برنامه می‌توانستند برای آن فرم طراحی کنند؛ فرم درخواست پرینت، درخواست نصب نرم افزار، درخواست وام، درخواست پیک، درخواست آژانس و ... فرم‌هایی که تمامی نداشتند! آن زمان برای حل این مساله از فیلدهای XML استفاده کردم.
      فیلدهای XML قابلیت نه چندان جدیدی هستند که از SQL Server 2005 به بعد اضافه شده‌اند. مهم‌ترین مزیت آن‌ها‌ هم امکان ذخیره سازی اطلاعات هر نوع شیء‌ایی به عنوان یک فیلد XML است. یعنی همان زیرساختی که برای ایجاد یک برنامه فرم ساز نیاز است. ذخیره سازی آن هم آداب خاصی را طلب نمی‌کند. به ازای هر فیلد مورد نظر کاربر، یک نود جدید به صورت رشته معمولی باید اضافه شود و نهایتا رشته تولیدی باید ذخیره گردد. از دید ما یک رشته‌ است، از دید SQL Server یک نوع XML واقعی؛ به همراه این مزیت مهم که به سادگی می‌توان با T-SQL/XQuery/XPath از جزئیات اطلاعات این نوع فیلدها کوئری گرفت و سرعت کار هم واقعا بالا است؛ به علاوه بر خلاف مطلب قبلی در مورد dynamic components ، اینبار نیازی نیست تا به ازای هر یک فیلد درخواستی کاربر، واقعا یک فیلد جدید را به جدول خاصی اضافه کرد. داخل این فیلد XML هر نوع ساختار دلخواهی را می‌توان ذخیره کرد. به عبارتی به کمک فیلدهایی از نوع XML می‌توان داخل یک سیستم بانک اطلاعاتی رابطه‌ای، schema-less کار کرد (un-typed XML) و همچنین از این اطلاعات ویژه، کوئری‌های پیچیده هم گرفت.
      تا جایی که اطلاع دارم، چند شرکت دیگر هم در ایران دقیقا از همین ایده فیلدهای XML برای ساخت برنامه فرم ساز استفاده کرده‌اند ...؛ البته مطلب جدیدی هم نیست؛ برنامه‌های فرم ساز اوراکل و IBM هم سال‌ها است که از XML برای همین منظور استفاده می‌کنند. مایکروسافت هم به همین دلیل (شاید بتوان گفت مهم‌ترین دلیل وجودی فیلدهای XML در SQL Server)، پشتیبانی توکاری از XML به عمل آورده‌ است.
      یا روش دیگری را که برای طراحی سیستم‌های فرم ساز پیشنهاد می‌کنند استفاده از بانک‌های اطلاعاتی مبتنی بر key-value‌ مانند Redis یا RavenDb است؛ یا استفاده از بانک‌های اطلاعاتی schema-less واقعی مانند CouchDb.


      خوب ... اکنون سؤال این است که NHibernate برای کار با فیلدهای XML چه تمهیداتی را درنظر گرفته است؟
      برای این منظور خاصیتی را که قرار است به یک فیلد از نوع XML نگاشت شود، با نوع XDocument مشخص خواهیم ساخت:
      using System.Xml.Linq;

      namespace TestModel
      {
      public class DynamicTable
      {
      public virtual int Id { get; set; }
      public virtual XDocument Document { get; set; }
      }
      }

      سپس باید جهت معرفی این نوع ویژه، به صورت صریح از XDocType استفاده کرد؛ یعنی نکته‌ی اصلی، استفاده از CustomType مرتبط است:
      using FluentNHibernate.Automapping;
      using FluentNHibernate.Automapping.Alterations;
      using NHibernate.Type;

      namespace TestModel
      {
      public class DynamicTableMapping : IAutoMappingOverride<DynamicTable>
      {
      public void Override(AutoMapping<DynamicTable> mapping)
      {
      mapping.Id(x => x.Id);
      mapping.Map(x => x.Document).CustomType<XDocType>();
      }
      }
      }

      البته لازم به ذکر است که دو نوع NHibernate.Type.XDocType و NHibernate.Type.XmlDocType برای کار با فیلد‌های XML در NHibernate وجود دارند. XDocType برای کار با نوع System.Xml.Linq.XDocument طراحی شده است و XmlDocType مخصوص نگاشت نوع System.Xml.XmlDocument است.

      اکنون اگر به کمک کلاس SchemaExport ، اسکریپت تولید جدول متناظر با اطلاعات فوق را ایجاد کنیم به حاصل زیر خواهیم رسید:
         if exists (select * from dbo.sysobjects
      where id = object_id(N'[DynamicTable]') and OBJECTPROPERTY(id, N'IsUserTable') = 1)
      drop table [DynamicTable]

      create table [DynamicTable] (
      Id INT IDENTITY NOT NULL,
      Document XML null,
      primary key (Id)
      )

      یک سری اعمال متداول ذخیره سازی اطلاعات و تهیه کوئری نیز در ادامه ذکر شده‌اند:
      //insert
      object savedId = 0;
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var obj = new DynamicTable
      {
      Document = System.Xml.Linq.XDocument.Parse(
      @"<Doc><Node1>Text1</Node1><Node2>Text2</Node2></Doc>"
      )
      };
      savedId = session.Save(obj);
      tx.Commit();
      }
      }

      //simple query
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var entity = session.Get<DynamicTable>(savedId);
      if (entity != null)
      {
      Console.WriteLine(entity.Document.Root.ToString());
      }

      tx.Commit();
      }
      }

      //advanced query
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var list = session.CreateSQLQuery("select [Document].value('(//Doc/Node1)[1]','nvarchar(255)') from [DynamicTable] where id=:p0")
      .SetParameter("p0", savedId)
      .List();

      if (list != null)
      {
      Console.WriteLine(list[0]);
      }

      tx.Commit();
      }
      }

      و در پایان بدیهی است که جهت کار با امکانات پیشرفته‌تر موجود در SQL Server در مورد فیلد‌های XML ( برای نمونه: + و +) باید مثلا رویه ذخیره شده تهیه کرد (یا مستقیما از متد CreateSQLQuery همانند مثال فوق کمک گرفت) و آن‌را در NHibernate مورد استفاده قرار داد. البته به این صورت کار شما محدود به SQL Server خواهد شد و باید در نظر داشت که در کل تعداد کمی بانک اطلاعاتی وجود دارند که نوع‌های XML را به صورت توکار پشتیبانی می‌کنند.

      مطالب
      آشنایی با 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 نیز باید حذف شوند.

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


      مطالب
      فیلدهای پویا در NHibernate

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

      در اینجا کلاسی که قرار است توانایی افزودن فیلدهای سفارشی را داشته باشد به صورت زیر تعریف می‌شود:
      using System.Collections;

      namespace TestModel
      {
      public class DynamicEntity
      {
      public virtual int Id { get; set; }
      public virtual IDictionary Attributes { set; get; }
      }
      }

      Attributes در عمل همان فیلدهای سفارشی مورد نظر خواهند بود. جهت معرفی صحیح این قابلیت نیاز است تا نگاشت آن‌را از نوع dynamic component تعریف کنیم:
      using FluentNHibernate.Automapping;
      using FluentNHibernate.Automapping.Alterations;

      namespace TestModel
      {
      public class DynamicEntityMapping : IAutoMappingOverride<DynamicEntity>
      {
      public void Override(AutoMapping<DynamicEntity> mapping)
      {
      mapping.Table("tblDynamicEntity");
      mapping.Id(x => x.Id);
      mapping.IgnoreProperty(x => x.Attributes);
      mapping.DynamicComponent(x => x.Attributes,
      c =>
      {
      c.Map(x => (string)x["field1"]);
      c.Map(x => (string)x["field2"]).Length(300);
      c.Map(x => (int)x["field3"]);
      c.Map(x => (double)x["field4"]);
      });
      }
      }
      }
      و مهم‌ترین نکته‌ی این بحث هم همین نگاشت فوق است.
      ابتدا از IgnoreProperty جهت ندید گرفتن Attributes استفاده کردیم. زیرا درغیراینصورت در حالت Auto mapping ، یک رابطه چند به یک به علت وجود IDictionary به صورت خودکار ایجاد خواهد شد که نیازی به آن نیست (یافتن این نکته نصف روز کار برد ....! چون مرتبا خطای An association from the table DynamicEntity refers to an unmapped class: System.Collections.Idictionary ظاهر می‌شد و مشخص نبود که مشکل از کجاست).
      سپس Attributes به عنوان یک DynamicComponent معرفی شده است. در اینجا چهار فیلد سفارشی را اضافه کرده‌ایم. به این معنا که اگر نیاز باشد تا فیلد سفارشی دیگری به سیستم اضافه شود باید یکبار Session factory ساخته شود و SchemaUpdate فراخوانی گردد و خوشبختانه با وجود Fluent NHibernate ، تمام این تغییرات در کدهای برنامه قابل انجام است (بدون نیاز به سر و کار داشتن با فایل‌های XML نگاشت‌ها و ویرایش دستی آن‌ها). از تغییر نام جدول که برای مثال در اینجا tblDynamicEntity در نظر گرفته شده تا افزودن فیلدهای دیگر در قسمت DynamicComponent فوق.
      همچنین باتوجه به اینکه این نوع تغییرات (ساخت دوبار سشن فکتوری) مواردی نیستند که قرار باشد هر ساعت انجام شوند، بنابراین سربار آنچنانی را به سیستم تحمیل نمی‌کنند.
         drop table tblDynamicEntity

      create table tblDynamicEntity (
      Id INT IDENTITY NOT NULL,
      field1 NVARCHAR(255) null,
      field2 NVARCHAR(300) null,
      field3 INT null,
      field4 FLOAT null,
      primary key (Id)
      )

      اگر با SchemaExport، اسکریپت خروجی معادل با نگاشت فوق را تهیه کنیم به جدول فوق خواهیم رسید. نوع و طول این فیلدهای سفارشی بر اساس نوعی که برای اشیاء دیکشنری مشخص می‌کنید، تعیین خواهند شد.

      چند مثال جهت کار با این فیلدهای سفارشی یا پویا :

      نحوه‌ی افزودن رکوردهای جدید بر اساس خاصیت‌های سفارشی:
      //insert
      object savedId = 0;
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var obj = new DynamicEntity();
      obj.Attributes = new Hashtable();
      obj.Attributes["field1"] = "test1";
      obj.Attributes["field2"] = "test2";
      obj.Attributes["field3"] = 1;
      obj.Attributes["field4"] = 1.1;

      savedId = session.Save(obj);
      tx.Commit();
      }
      }

      با خروجی
      INSERT
      INTO
      tblDynamicEntity
      (field1, field2, field3, field4)
      VALUES
      (@p0, @p1, @p2, @p3);
      @p0 = 'test1' [Type: String (0)], @p1 = 'test2' [Type: String (0)], @p2 = 1
      [Type: Int32 (0)], @p3 = 1.1 [Type: Double (0)]

      نحوه‌ی کوئری گرفتن از این اطلاعات (فعلا پایدارترین روشی را که برای آن یافته‌ام استفاده از HQL می‌باشد ...):
      //query
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      //using HQL
      var list = session
      .CreateQuery("from DynamicEntity d where d.Attributes.field1=:p0")
      .SetString("p0", "test1")
      .List<DynamicEntity>();

      if (list != null && list.Any())
      {
      Console.WriteLine(list[0].Attributes["field2"]);
      }
      tx.Commit();
      }
      }
      با خروجی:
          select
      dynamicent0_.Id as Id1_,
      dynamicent0_.field1 as field2_1_,
      dynamicent0_.field2 as field3_1_,
      dynamicent0_.field3 as field4_1_,
      dynamicent0_.field4 as field5_1_
      from
      tblDynamicEntity dynamicent0_
      where
      dynamicent0_.field1=@p0;
      @p0 = 'test1' [Type: String (0)]

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

      نحوه‌ی به روز رسانی و حذف اطلاعات بر اساس فیلدهای پویا:
      //update
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var entity = session.Get<DynamicEntity>(savedId);
      if (entity != null)
      {
      entity.Attributes["field2"] = "new-val";
      tx.Commit(); // Persist modification
      }
      }
      }

      //delete
      using (var session = sessionFactory.OpenSession())
      {
      using (var tx = session.BeginTransaction())
      {
      var entity = session.Get<DynamicEntity>(savedId);
      if (entity != null)
      {
      session.Delete(entity);
      tx.Commit();
      }
      }
      }