۱۳۸۸/۰۷/۲۸

آشنايي با 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);
}


دريافت سورس كامل قسمت دهم