۱۳۸۹/۱۰/۰۶

مقدار دهي كليدهاي خارجي در NHibernate و Entity framework


ORM هاي NHibernate و Entity framework روش‌هاي متفاوتي را براي به روز رساني كليد خارجي با حداقل رفت و برگشت به ديتابيس ارائه مي‌دهند كه در ادامه معرفي خواهند شد.

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

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



در اينجا جهت تعريف ويژگي‌ها يا Attributes تعريف شده در اين كلاس‌ها از NHibernate validator استفاده شده (+). مزيت اينكار هم علاوه بر اعتبارسنجي سمت كلاينت (پيش از تبادل اطلاعات با بانك اطلاعاتي)، توليد جداولي با همين مشخصات است. براي مثال Fluent NHibernate بر اساس ويژگي Length تعريف شده با طول حداكثر 120 ، يك فيلد nvarchar با همين طول را ايجاد مي‌كند.

public class Account
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام بايد بين 3 و 120 كاراكتر باشد")]
public virtual string Name { get; set; }
}

public class Category
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 130, Message = "طول نام بايد بين 3 و 130 كاراكتر باشد")]
public virtual string Name { get; set; }
}

public class Payee
{
public virtual int Id { get; set; }

[NotNullNotEmpty]
[Length(Min = 3, Max = 120, Message = "طول نام بايد بين 3 و 120 كاراكتر باشد")]
public virtual string Name { get; set; }
}

public class Bill
{
public virtual int Id { get; set; }

[NotNull]
public virtual Account Account { get; set; }

[NotNull]
public virtual Category Category { get; set; }

[NotNull]
public virtual Payee Payee { get; set; }

[NotNull]
public virtual decimal Amount { set; get; }

[NotNull]
public virtual DateTime BillDate { set; get; }

[NotNullNotEmpty]
[Length(Min = 1, Max = 500, Message = "طول توضيحات بايد بين 1 و 500 كاراكتر باشد")]
public virtual string Description { get; set; }
}




ب) ساختار جداول متناظر (توليد شده به صورت خودكار توسط Fluent NHibernate در اينجا)


در مورد نحوه‌ي استفاده از ويژگي AutoMapping و همچنين توليد خودكار ساختار بانك اطلاعاتي از روي جداول در NHibernate قبلا توضيح داده شده است. البته بديهي است كه تركيب مقاله‌ي Validation و آشنايي با AutoMapping در اينجا جهت اعمال ويژگي‌ها بايد بكار گرفته شود كه در همان مقاله‌ي Validation مفصل توضيح داده شده است.
نكته‌ي مهم database schema توليدي، كليد‌هاي خارجي (foreign key) تعريف شده بر روي جدول Bills است (همان AccountId، CategoryId و PayeeId تعريف شده) كه به primary key جداول متناظر اشاره مي‌كند.
    create table Accounts (
AccountId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (AccountId)
)

create table Bills (
BillId INT IDENTITY NOT NULL,
Amount DECIMAL(19,5) not null,
BillDate DATETIME not null,
Description NVARCHAR(500) not null,
AccountId INT not null,
CategoryId INT not null,
PayeeId INT not null,
primary key (BillId)
)

create table Categories (
CategoryId INT IDENTITY NOT NULL,
Name NVARCHAR(130) not null,
primary key (CategoryId)
)

create table Payees (
PayeeId INT IDENTITY NOT NULL,
Name NVARCHAR(120) not null,
primary key (PayeeId)
)

alter table Bills
add constraint fk_Account_Bill
foreign key (AccountId)
references Accounts

alter table Bills
add constraint fk_Category_Bill
foreign key (CategoryId)
references Categories

alter table Bills
add constraint fk_Payee_Bill
foreign key (PayeeId)
references Payees

ج) صفحه‌ي ثبت صورتحساب‌ها

صفحات ثبت گروه‌هاي اقلام، حساب‌ها و فروشنده‌ها، نكته‌ي خاصي ندارند. چون اين جداول وابستگي خاصي به جايي نداشته و به سادگي اطلاعات آن‌ها را مي‌توان ثبت يا به روز كرد.
صفحه‌ي مشكل در اين مثال، همان صفحه‌ي ثبت صورتحساب‌ها است كه از سه كليد خارجي به سه جدول ديگر تشكيل شده است.
عموما براي طراحي اين نوع صفحات، كليدهاي خارجي را با drop down list نمايش مي‌دهند و اگر در جهت سهولت كار كاربر قدم برداشته شود، بايد از يك Auto complete drop down list استفاده كرد تا كاربر برنامه جهت يافتن آيتم‌هاي از پيش تعريف شده كمتر سختي بكشد.



اگر از Silverlight يا WPF استفاده شود، امكان بايند يك ليست كامل از اشياء با تمام خواص مرتبط به آن‌ها وجود دارد (هر ركورد نمايش داده شده در دراپ داون ليست، دقيقا معادل است با يك شيء متناظر با كلاس‌هاي تعريف شده است). اگر از ASP.NET استفاده شود (يعني يك محيط بدون حالت كه پس از نمايش يك صفحه ديگر خبري از ليست اشياء بايند شده وجود نخواهد داشت و همگي توسط وب سرور جهت صرفه جويي در منابع تخريب شده‌اند)، بهتر است datatextfield را با فيلد نام و datavaluefield را با فيلد Id مقدار دهي كرد تا كاربر نهايي، نام را جهت ثبت اطلاعات مشاهده كند و برنامه از Id موجود در ليست جهت ثبت كليدهاي خارجي استفاده نمايد.
و نكته‌ي اصلي هم همينجا است كه چگونه؟! چون ما زمانيكه با يك ORM سر و كار داريم، براي ثبت يك ركورد در جدول Bills بايد يك وهله از كلاس Bill را ايجاد كرده و خواص آن‌را مقدار دهي كنيم. اگر به تعريف كلاس Bill مراجعه كنيد، سه خاصيت آن از نوع سه كلاس مجزا تعريف شده است. به به عبارتي با داشتن فقط يك id از ركوردهاي اين كلاس‌ها بايد بتوان سه وهله‌ي متناظر آن‌ها را از بانك اطلاعاتي خواند و سپس به اين خواص انتساب داد:

var newBill = new Bill
{
Account = accountRepository.GetByKey(1),
Amount = 1,
BillDate = DateTime.Now,
Category = categoryRepository.GetByKey(1),
Description = "testestest...",
Payee = payeeRepository.GetByKey(1)
};
يعني براي ثبت يك ركورد در جدول Bills فوق، چهار بار رفت و برگشت به ديتابيس خواهيم داشت:
- يكبار براي دريافت ركورد متناظر با گروه‌ها بر اساس كليد اصلي آن (كه از دراپ داون ليست مربوطه دريافت مي‌شود)
- يكبار براي دريافت ركورد متناظر با فروشند‌ه‌ها بر اساس كليد اصلي آن (كه از دراپ داون ليست مربوطه دريافت مي‌شود)
- يكبار براي دريافت ركورد متناظر با حساب‌ها بر اساس كليد اصلي آن (كه از دراپ داون ليست مربوطه دريافت مي‌شود)
- يكبار هم ثبت نهايي اطلاعات در بانك اطلاعاتي

متد GetByKey فوق همان متد session.Get استاندارد NHibernate است (چون به primary key ها از طريق drop down list دسترسي داريم، به سادگي مي‌توان بر اساس متد Get استاندارد ذكر شده عمل كرد).

SQL نهايي توليدي هم به صورت واضحي اين مشكل را نمايش مي‌دهد (4 بار رفت و برگشت؛ سه بار select يكبار هم insert نهايي):
SELECT account0_.AccountId as AccountId0_0_, account0_.Name as Name0_0_
FROM Accounts account0_ WHERE account0_.AccountId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT category0_.CategoryId as CategoryId2_0_, category0_.Name as Name2_0_
FROM Categories category0_ WHERE category0_.CategoryId=@p0;@p0 = 1 [Type: Int32 (0)]

SELECT payee0_.PayeeId as PayeeId3_0_, payee0_.Name as Name3_0_
FROM Payees payee0_ WHERE payee0_.PayeeId=@p0;@p0 = 1 [Type: Int32 (0)]

INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:48:33 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

كساني كه قبلا با رويه‌هاي ذخيره شده كار كرده باشند (stored procedures) احتمالا الان خواهند گفت؛ ما كه گفتيم اين روش كند است! سربار زيادي دارد! فقط كافي است يك SP بنويسيد و كل عمليات را با يك رفت و برگشت انجام دهيد.
اما در ORMs نيز براي انجام اين مورد در طي يك حركت يك ضرب راه حل‌هايي وجود دارد كه در ادامه بحث خواهد شد:

د) پياده سازي با NHibernate
براي حل اين مشكل در NHibernate با داشتن primary key (براي مثال از طريق datavaluefield ذكر شده)، بجاي session.Get از session.Load استفاده كنيد.
session.Get يعني همين الان برو به بانك اطلاعاتي مراجعه كن و ركورد متناظر با كليد اصلي ذكر شده را بازگشت بده و يك شيء از آن را ايجاد كن (حالت‌هاي ديگر دسترسي به اطلاعات مانند استفاده از LINQ يا Criteria API يا هر روش مشابه ديگري نيز در اينجا به همين معنا خواهد بود).
session.Load يعني فعلا دست نگه دار! مگر در جدول نهايي نگاشت شده، اصلا چيزي به نام شيء مثلا گروه وجود دارد؟ مگر اين مورد واقعا يك فيلد عددي در جدول Bills بيشتر نيست؟ ما هم كه الان اين عدد را داريم (به كمك عناصر دراپ داون ليست)، پس لطفا در پشت صحنه يك پروكسي براي ايجاد شيء مورد نظر ايجاد كن (uninitialized proxy to the entity) و سپس عمليات مرتبط را در حين تشكيل SQL نهايي بر اساس اين عدد موجود انجام بده. يعني نيازي به رفت و برگشت به بانك اطلاعاتي نيست. در اين حالت اگر SQL نهايي را بررسي كنيم فقط يك سطر زير خواهد بود (سه select ذكر شده حذف خواهند شد):
INSERT INTO Bills (Amount, BillDate, Description, AccountId, CategoryId, PayeeId)
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);
select SCOPE_IDENTITY();
@p0 = 1 [Type: Decimal (0)],
@p1 = 2010/12/27 11:58:22 ق.ظ [Type: DateTime (0)],
@p2 = 'testestest...' [Type: String (500)],
@p3 = 1 [Type: Int32 (0)],
@p4 = 1 [Type: Int32 (0)],
@p5 = 1 [Type: Int32 (0)]

ه) پياده سازي با Entity framework

Entity framework زمانيكه بانك اطلاعاتي فوق را (به روش database first) به كلاس‌هاي متناظر تبديل/نگاشت مي‌كند، حاصل نهايي مثلا در مورد كلاس Bill به صورت خلاصه به شكل زير خواهد بود:
public partial class Bill : EntityObject
{
public global::System.Int32 BillId {set;get;}
public global::System.Decimal Amount {set;get;}
public global::System.DateTime BillDate {set;get;}
public global::System.String Description {set;get;}
public global::System.Int32 AccountId {set;get;}
public global::System.Int32 CategoryId {set;get;}
public global::System.Int32 PayeeId {set;get;}
public Account Account {set;get;}
public Category Category {set;get;}
}
به عبارتي فيلدهاي كليدهاي خارجي، در تعريف نهايي اين كلاس هم مشاهده مي‌شوند. در اينجا فقط كافي است سه كليد خارجي، از نوع int مقدار دهي شوند (و نيازي به مقدار دهي سه شيء متناظر نيست). در اين حالت نيز براي ثبت اطلاعات، فقط يكبار رفت و برگشت به بانك اطلاعاتي خواهيم داشت.