آشنايي با Automapping در فريم ورك Fluent NHibernate
اگر قسمتهاي قبل را دنبال كرده باشيد، احتمالا به پروسه طولاني ساخت نگاشتها توجه كردهايد. با كمك فريم ورك Fluent NHibernate ميتوان پروسه نگاشت domain model خود را به data model متناظر آن به صورت خودكار نيز انجام داد و قسمت عمدهاي از كار به اين صورت حذف خواهد شد. (اين مورد يكي از تفاوتهاي مهم NHibernate با نمونههاي مشابهي است كه مايكروسافت تا تاريخ نگارش اين مقاله ارائه داده است. براي مثال در نگارشهاي فعلي LINQ to SQL يا Entity framework ، اول ديتابيس مطرح است و بعد ساخت كد از روي آن، در حاليكه در اينجا ابتدا كد و طراحي سيستم مطرح است و بعد نگاشت آن به سيستم دادهاي و ديتابيس)
امروز قصد داريم يك سيستم ساده ثبت خبر را از صفر با NHibernate پياده سازي كنيم و همچنين مروري داشته باشيم بر قسمتهاي قبلي.
مطابق كلاس دياگرام فوق، اين سيستم از سه كلاس خبر، كاربر ثبت كنندهي خبر و گروه خبري مربوطه تشكيل شده است.
ابتدا يك پروژه كنسول جديد را به نام NHSample2 آغاز كنيد. سپس ارجاعاتي را به اسمبليهاي زير به آن اضافه نمائيد:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و ارجاعي به اسمبلي استاندارد System.Data.Services.dll دات نت فريم ورك سه و نيم
سپس پوشهاي را به نام Domain به اين پروژه اضافه نمائيد (كليك راست روي نام پروژه در VS.Net و سپس مراجعه به منوي Add->New folder). در اين پوشه تعاريف موجوديتهاي برنامه را قرار خواهيم داد. سه كلاس جديد Category ، User و News را در اين پوشه ايجاد نمائيد. محتويات اين سه كلاس به شرح زير هستند:
namespace NHSample2.Domain
{
public class User
{
public virtual int Id { get; set; }
public virtual string UserName { get; set; }
public virtual string Password { get; set; }
}
}
namespace NHSample2.Domain
{
public class Category
{
public virtual int Id { get; set; }
public virtual string CategoryName { get; set; }
}
}
using System;
namespace NHSample2.Domain
{
public class News
{
public virtual Guid Id { get; set; }
public virtual string Subject { get; set; }
public virtual string NewsText { get; set; }
public virtual DateTime DateEntered { get; set; }
public virtual Category Category { get; set; }
public virtual User User { get; set; }
}
}
اكنون كلاس جديد Config را به برنامه اضافه نمائيد:
using FluentNHibernate.Automapping;
using FluentNHibernate.Cfg;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHibernate.Cfg;
using NHibernate.Tool.hbm2ddl;
namespace NHSample2
{
class Config
{
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly).Configure(cfg);
return cfg;
}
public static void GenerateDbScript(Configuration config, string filePath)
{
bool script = true;//فقط اسكريپت ديتابيس توليد گردد
bool export = false;//نيازي نيست بر روي ديتابيس هم اجرا شود
new SchemaExport(config).SetOutputFile(filePath).Create(script, export);
}
public static void BuildDbSchema(Configuration config)
{
bool script = false;//آيا خروجي در كنسول هم نمايش داده شود
bool export = true;//آيا بر روي ديتابيس هم اجرا شود
bool drop = false;//آيا اطلاعات موجود دراپ شوند
new SchemaExport(config).Execute(script, export, drop);
}
public static void CreateSQL2008DbPlusScript(string connectionString, string filePath)
{
Configuration cfg =
GenerateMapping(
MsSqlConfiguration
.MsSql2008
.ConnectionString(connectionString)
.ShowSql()
);
GenerateDbScript(cfg, filePath);
BuildDbSchema(cfg);
}
public static ISessionFactory CreateSessionFactory(IPersistenceConfigurer dbType)
{
return
Fluently.Configure().Database(dbType)
.Mappings(m => m.AutoMappings
.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly))
)
.BuildSessionFactory();
}
}
}
در متد GenerateMapping از قابليت Automapping موجود در فريم ورك Fluent Nhibernate استفاده شده است (بدون نوشتن حتي يك سطر جهت تعريف اين نگاشتها). اين متد نوع ديتابيس مورد نظر را جهت ساخت تنظيمات خود دريافت ميكند. سپس با كمك كلاس AutoPersistenceModel اين فريم ورك، به صورت خودكار از اسمبلي برنامه نگاشتهاي لازم را به كلاسهاي موجود در پوشه Domain ما اضافه ميكند (مرسوم است كه اين پوشه در يك پروژه Class library مجزا تعريف شود كه در اين برنامه جهت سهولت كار در خود برنامه قرار گرفته است). قسمت Where ذكر شده به اين جهت معرفي گرديده است تا Fluent Nhibernate براي تمامي كلاسهاي موجود در اسمبلي جاري، سعي در تعريف نگاشتهاي لازم نكند. اين نگاشتها تنها به كلاسهاي موجود در پوشه دومين ما محدود شدهاند.
سه متد بعدي آن، جهت ايجاد اسكريپت ديتابيس از روي اين نگاشتهاي تعريف شده و سپس اجراي اين اسكريپت بر روي ديتابيس جاري معرفي شده، تهيه شدهاند. براي مثال CreateSQL2008DbPlusScript يك مثال ساده از استفاده دو متد قبلي جهت ايجاد اسكريپت و ديتابيس متناظر اس كيوال سرور 2008 بر اساس نگاشتهاي برنامه است.
با متد CreateSessionFactory در قسمتهاي قبل آشنا شدهايد. تنها تفاوت آن در اين قسمت، استفاده از كلاس AutoPersistenceModel جهت توليد خودكار نگاشتها است.
در ادامه ديتابيس متناظر با موجوديتهاي برنامه را ايجاد خواهيم كرد:
using System;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
Config.CreateSQL2008DbPlusScript(
"Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true",
"db.sql");
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
پس از اجراي برنامه، ابتدا فايل اسكريپت ديتابيس به نام db.sql در پوشه اجرايي برنامه تشكيل خواهد شد و سپس اين اسكريپت به صورت خودكار بر روي ديتابيس معرفي شده اجرا ميگردد. ديتابيس دياگرام حاصل را در شكل زير ميتوانيد ملاحظه نمائيد:
همچنين اسكريپت توليد شده آن، صرفنظر از عبارات drop اوليه، به صورت زير است:
create table [Category] (
Id INT IDENTITY NOT NULL,
CategoryName NVARCHAR(255) null,
primary key (Id)
)
create table [User] (
Id INT IDENTITY NOT NULL,
UserName NVARCHAR(255) null,
Password NVARCHAR(255) null,
primary key (Id)
)
create table [News] (
Id UNIQUEIDENTIFIER not null,
Subject NVARCHAR(255) null,
NewsText NVARCHAR(255) null,
DateEntered DATETIME null,
Category_id INT null,
User_id INT null,
primary key (Id)
)
alter table [News]
add constraint FKE660F9E1C9CF79
foreign key (Category_id)
references [Category]
alter table [News]
add constraint FKE660F95C1A3C92
foreign key (User_id)
references [User]
اكنون يك سري گروه خبري، كاربر و خبر را به ديتابيس خواهيم افزود:
using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using NHSample2.Domain;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
//با توجه به كليدهاي خارجي تعريف شده ابتدا بايد گروهها را اضافه كرد
Category ca = new Category() { CategoryName = "Sport" };
session.Save(ca);
Category ca2 = new Category() { CategoryName = "IT" };
session.Save(ca2);
Category ca3 = new Category() { CategoryName = "Business" };
session.Save(ca3);
//سپس يك كاربر را به ديتابيس اضافه ميكنيم
User u = new User() { Password = "123$5@1", UserName = "VahidNasiri" };
session.Save(u);
//اكنون ميتوان يك خبر جديد را ثبت كرد
News news = new News()
{
Category = ca,
User = u,
DateEntered = DateTime.Now,
Id = Guid.NewGuid(),
NewsText = "متن خبر جديد",
Subject = "عنواني دلخواه"
};
session.Save(news);
transaction.Commit(); //پايان تراكنش
}
}
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
و يا ميتوان از LINQ استفاده كرد:
براي مثال كاربر VahidNasiri تعريف شده را يافته، اطلاعات آنرا نمايش دهيد؛ سپس نام او را به Vahid ويرايش كرده و ديتابيس را به روز كنيد.
براي اينكه كوئريهاي LINQ ما شبيه به LINQ to SQL شوند، كلاس NewsContext را به صورت ذيل تشكيل ميدهيم. اين كلاس از كلاس پايه NHibernateContext مشتق شده و سپس به ازاي تمام موجوديتهاي برنامه، يك متد از نوع IOrderedQueryable را تشكيل خواهيم داد.
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample2.Domain;
namespace NHSample2
{
class NewsContext : NHibernateContext
{
public NewsContext(ISession session)
: base(session)
{ }
public IOrderedQueryable<News> News
{
get { return Session.Linq<News>(); }
}
public IOrderedQueryable<Category> Categories
{
get { return Session.Linq<Category>(); }
}
public IOrderedQueryable<User> Users
{
get { return Session.Linq<User>(); }
}
}
}
using System;
using FluentNHibernate.Cfg.Db;
using NHibernate;
using System.Linq;
using NHSample2.Domain;
namespace NHSample2
{
class Program
{
static void Main(string[] args)
{
using (ISessionFactory sessionFactory = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{
using (ISession session = sessionFactory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
using (NewsContext db = new NewsContext(session))
{
var query = from x in db.Users
where x.UserName == "VahidNasiri"
select x;
//اگر چيزي يافت شد
if (query.Any())
{
User vahid = query.First();
//نمايش اطلاعات كاربر
Console.WriteLine("Id: {0}, UserName: {0}", vahid.Id, vahid.UserName);
//به روز رساني نام كاربر
vahid.UserName = "Vahid";
session.Update(vahid);
transaction.Commit(); //پايان تراكنش
}
}
}
}
}
Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
اگر به اسكريپت ديتابيس توليد شده دقت كرده باشيد، عمليات AutoMapping يك سري پيش فرضهايي را اعمال كرده است. براي مثال فيلد Id را از نوع identity و به صورت كليد تعريف كرده، يا رشتهها را به صورت nvarchar با طول 255 ايجاد نموده است. امكان سفارشي سازي اين موارد نيز وجود دارد.
مثال:
using FluentNHibernate.Conventions.Helpers;
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add(
PrimaryKey.Name.Is(x => "ID"),
DefaultLazy.Always(),
ForeignKey.EndsWith("ID"),
Table.Is(t => "tbl" + t.EntityType.Name)
)
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);
return cfg;
}
تابع GenerateMapping معرفي شده را اينجا با قسمت Conventions.Add تكميل كردهايم. به اين صورت دقيقا مشخص شده است كه فيلدهايي با نام ID بايد primary key در نظر گرفته شوند، همواره lazy loading صورت گيرد و نام كليد خارجي به ID ختم شود. همچنين نام جداول با tbl شروع گردد.
روش ديگري نيز براي معرفي اين قرار دادها و پيش فرضها وجود دارد. فرض كنيد ميخواهيم طول رشته پيش فرض را از 255 به 500 تغيير دهيم. براي اينكار بايد اينترفيس IPropertyConvention را پياده سازي كرد:
using FluentNHibernate.Conventions;
using FluentNHibernate.Conventions.Instances;
namespace NHSample2.Conventions
{
class MyStringLengthConvention : IPropertyConvention
{
public void Apply(IPropertyInstance instance)
{
instance.Length(500);
}
}
}
public static Configuration GenerateMapping(IPersistenceConfigurer dbType)
{
var cfg = dbType.ConfigureProperties(new Configuration());
new AutoPersistenceModel()
.Conventions.Add()
.Where(x => x.Namespace.EndsWith("Domain"))
.Conventions.Add<MyStringLengthConvention>()
.AddEntityAssembly(typeof(NHSample2.Domain.News).Assembly)
.Configure(cfg);
return cfg;
}
نكته:
اگر براي يافتن اطلاعات بيشتر در اين مورد در وب جستجو كنيد، اكثر مثالهايي را كه مشاهده خواهيد كرد بر اساس نگارش بتاي fluent NHibernate هستند و هيچكدام با نگارش نهايي اين فريم ورك كار نميكنند. در نگارش رسمي نهايي ارائه شده، تغييرات بسياري صورت گرفته كه آنها را در اين آدرس ميتوان مشاهده كرد.
دريافت سورس برنامه قسمت ششم
ادامه دارد ...