۱۳۸۸/۰۸/۰۹

آموزش مقدماتي Silverlight


يك سري فايل كارگاه آموزشي معرفي Silverlight را از آدرس زير مي‌توانيد دريافت نمائيد.


براي صرفه جويي در وقت، لينكي به نام Download all files associated with related objects را در آدرس فوق يافته و تمام 5 قسمت را در طي يك بسته دريافت كنيد.

۱۳۸۸/۰۸/۰۷

CopyPasteKiller


افزونه‌اي به نام clone detective چندي قبل در اين سايت معرفي شد. اكنون پروژه ديگري به صورت يك برنامه مستقل ارائه شده است به نام copy/paste killer كه آن هم تمام فايل‌هاي پروژه شما را بررسي كرده، خطوط مشابه (نه الزاما دقيقا يكسان) را يافته و گزارش مي‌دهد. به اين صورت كار refactoring جهت حذف قسمت‌هاي تكراري و مديريت اين امر با سهولت بيشتري امكان پذير خواهد شد. اين برنامه در حال حاضر از VB.Net و سي شارپ پشتيباني مي‌كند.



جهت دريافت آن مي‌توان به آدرس زير مراجعه كرد:


Vote on iDevCenter

۱۳۸۸/۰۸/۰۶

پيدا كردن آيتم‌هاي تكراري در يك ليست به كمك LINQ


گاهي از اوقات نياز مي‌شود تا در يك ليست، آيتم‌هاي تكراري موجود را مشخص كرد. به صورت پيش فرض متد Distinct براي حذف مقادير تكراري در يك ليست با استفاده از LINQ موجود است كه البته آن‌هم اما و اگرهايي دارد كه در ادامه به آن پرداخته خواهد شد، اما باز هم اين مورد پاسخ سؤال اصلي نيست (نمي‌خواهيم موارد تكراري را حذف كنيم).

براي حذف آيتم‌هاي تكراري از يك ليست جنريك مي‌توان متد زير را نوشت:
public static List<T> RemoveDuplicates<T>(List<T> items)
{
return (from s in items select s).Distinct().ToList();
}
براي مثال:
public static void TestRemoveDuplicates()
{
List<string> sampleList =
new List<string>() { "A1", "A2", "A3", "A1", "A2", "A3" };
sampleList = RemoveDuplicates(sampleList);
foreach (var item in sampleList)
Console.WriteLine(item);
}
اين متد بر روي ليست‌هايي با نوع‌هاي اوليه مانند string‌ و int و امثال آن درست كار مي‌كند. اما اكنون مثال زير را در نظر بگيريد:
public class Employee
{
public int ID { get; set; }
public string FName { get; set; }
public int Age { get; set; }
}

public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates<Employee>(lstEmp);

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
اگر متد TestRemoveDuplicates را اجرا نمائيد، ركورد تكراري اين ليست جنريك حذف نخواهد شد؛ زيرا متد distinct بكارگرفته شده نمي‌داند اشيايي از نوع كلاس سفارشي Employee را چگونه بايد با هم مقايسه نمايد تا بتواند موارد تكراري آن‌ها را حذف كند.
براي رفع اين مشكل بايد از آرگومان دوم متد distinct جهت معرفي وهله‌اي از كلاسي كه اينترفيس IEqualityComparer را پياده سازي مي‌كند، كمك گرفت.
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
كه نمونه‌اي از پياده سازي آن به شرح زير مي‌تواند باشد:

public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee x, Employee y)
{
//آيا دقيقا يك وهله هستند؟
if (Object.ReferenceEquals(x, y)) return true;

//آيا يكي از وهله‌ها نال است؟
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;

return x.Age == y.Age && x.FName == y.FName && x.ID == y.ID;
}

public int GetHashCode(Employee obj)
{
if (Object.ReferenceEquals(obj, null)) return 0;
int hashTextual = obj.FName == null ? 0 : obj.FName.GetHashCode();
int hashDigital = obj.Age.GetHashCode();
return hashTextual ^ hashDigital;
}
}
اكنون اگر يك overload براي متد RemoveDuplicates با درنظر گرفتن IEqualityComparerتهيه كنيم، به شكل زير خواهد بود:
public static List<T> RemoveDuplicates<T>(List<T> items, IEqualityComparer<T> comparer)
{
return (from s in items select s).Distinct(comparer).ToList();
}
به اين صورت متد آزمايشي ما به شكل زير (كه وهله‌اي از كلاس EmployeeComparer‌ به آن ارسال شده) تغيير خواهد كرد:
public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates(lstEmp, new EmployeeComparer());

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
پس از اين تغيير، حاصل اين متد تنها دو ركورد غيرتكراري مي‌باشد.

سؤال: براي يافتن آيتم‌هاي تكراري يك ليست چه بايد كرد؟
احتمالا مقاله "روش‌هايي براي حذف ركوردهاي تكراري" را به خاطر داريد. اينجا هم مي‌توان كوئري LINQ ايي را نوشت كه ركوردها را بر اساس سن، گروه بندي كرده و سپس گروه‌هايي را كه بيش از يك ركورد دارند، انتخاب نمايد.
public static void FindDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

var query = from c in lstEmp
group c by c.Age into g
where g.Count() > 1
select new { Age = g.Key, Count = g.Count() };

foreach (var item in query)
{
Console.WriteLine("Age {0} has {1} records", item.Age, item.Count);
}
}


Vote on iDevCenter

۱۳۸۸/۰۸/۰۴

استفاده از LINQ جهت تهيه كدهايي كوتاه‌تر و خواناتر


با كمك امكانات ارائه شده توسط LINQ ، مي‌توان بسياري از اعمال برنامه نويسي را در حجمي كمتر، خواناتر و در نتيجه با قابليت نگهداري بهتر، انجام داد كه تعدادي از آن‌ها را در ادامه مرور خواهيم كرد.

الف) تهيه يك يك رشته، حاوي عناصر يك آرايه، جدا شده با كاما.

using System.Linq;

public class CLinq
{
public static string GetCommaSeparatedListNormal(string[] data)
{
string items = string.Empty;

foreach (var item in data)
{
items += item + ", ";
}

return items.Remove(items.Length - 2, 1).Trim();
}

public static string GetCommaSeparatedList(string[] data)
{
return data.Aggregate((s1, s2) => s1 + ", " + s2);
}
}
همانطور كه ملاحظه مي‌كنيد در روش دوم با استفاده از LINQ Aggregate extension method ، كد جمع و جورتر و خواناتري نسبت به روش اول حاصل شده است.

ب) پيدا كردن تعداد عناصر يك آرايه حاوي مقداري مشخص
براي مثال آرايه زير را در نظر بگيريد:

var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
قصد داريم تعداد عناصر حاوي name را مشخص سازيم.
در تابع GetCountNormal زير، اين كار به شكلي متداول انجام شده و در GetCount از LINQ Count extension method كمك گرفته شده است.

using System.Linq;

public class CLinq
{
public static int GetCountNormal()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
var count = 0;
foreach (var name in names)
{
if (name.Contains("name"))
count += 1;
}
return count;
}

public static int GetCount()
{
var names = new[] { "name1", "name2", "name3", "name4", "name5", "name6", "name7" };
return names.Count(name => name.Contains("name"));
}
}
به نظر شما كدام روش خواناتر بوده و نگهداري و يا تغيير آن در آينده ساده‌تر مي‌باشد؟

ج) دريافت ليستي از عناصر شروع شده با يك عبارت
در اينجا نيز دو روش متداول و استفاده از LINQ بررسي شده است.

using System.Linq;
using System.Collections.Generic;

public class CLinq
{
public static List<string> GetListNormal()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
List<string> result = new List<string>();
foreach (var item in sampleList)
{
if (item.StartsWith("P"))
result.Add(item);
}
return result;
}

public static List<string> GetList()
{
List<string> sampleList = new List<string>() { "A1", "A2", "P1", "P10", "B1", "B@", "J30", "P12" };
return sampleList.Where(x => x.StartsWith("P")).ToList();
}
}

و در حالت كلي، اكثر حلقه‌هاي foreach متداول را مي‌توان با نمونه‌هاي خواناتر كوئري‌هاي LINQ معادل، جايگزين كرد.

Vote on iDevCenter

۱۳۸۸/۰۸/۰۳

نمايش خودكار مقدار يكDropDownList با كمك jQuery


نياز بود هنگام انتخاب يك آيتم دراپ داون ليست در كل برنامه و تمامي دراپ داون‌هاي آن، مقدار آن‌ها نيز به صورت يك برچسب در كنار آن نمايش داده شود.
براي مثال در ليست زير:

<asp:DropDownList ID="ddlActive" runat="server">
<asp:ListItem Value="Active">فعال</asp:ListItem>
<asp:ListItem Value="Inactive">غيرفعال</asp:ListItem>
</asp:DropDownList>
اگر آيتم فعال انتخاب شد، مقدار active نيز كنار آن نمايش داده شود و الي آخر.

راه حل اول:
در تمام صفحات به ازاي تك تك دراپ داون‌ها يك label اضافه كنيم و همچنين كدهاي تمام قسمت‌هاي برنامه را نيز اصلاح كنيم تا اين مورد را لحاظ كند.

راه دوم:
يك كنترل دراپ داون سفارشي را با خاصيت مورد نظر (همراه بودن با يك ليبل) ايجاد كرده و سپس تمام فرم‌ها را بايد اصلاح كرد تا از اين كنترل جديد استفاده كنند.

راه سوم:
استفاده از jQuery براي اعمال اين مهم به كل برنامه بدون نياز به تغييرات اساسي در آن (و همچنين سازگاري با تمام مرورگرها):

//فقط در اين محدوده
$("#mainFormReq select").change(function() {
var currentId = $(this).attr("id"); //آي دي شيء جاري
var val = $(this).val(); //مقدار
var text = $('#' + currentId + ' option:selected').text(); //متن
$("#lbl" + currentId).remove(); //اگر نمونه‌ي قبلي موجود است حذف شود
if (val && (val.length > 0) && (text != val)) {
//اگر متن و مقدار يكي نيست نمايش داده شود
$(this).after('<label id="lbl' + currentId + '">' + val + '</label>');
}
});
توضيحات:
در يك محدوده مشخص شده با ID مساوي mainFormReq (مثلا استفاده از master page ها و نسبت دادن اين ID به content آن)، به دنبال تمام select هاي موجود در آن ناحيه مي‌گرديم (اگر mainFormReq حذف شود، اين جستجو در كل صفحه صورت خواهد گرفت) و تغييرات آن‌ها را تحت نظر قرار خواهيم داد.
سپس آي دي اين كنترل انتخابي را دريافت مي‌كنيم (از اين ID براي توليد ID برچسب مورد نظر استفاده خواهيم كرد).
در ادامه مقدارهاي text و value گزينه انتخابي دريافت مي‌شوند (+).
سپس بررسي خواهيم كرد كه آيا برچسبي با ID مشخص شده ما وجود دارد (در صورت انتخاب آيتم‌هاي ديگر، نبايد برچسبي غير منحصربفرد و تكراري در صفحه ايجاد كرد)
در ادامه اگر اين مقدار null نبود و همچنين مقدار text و value هم يكي نبودند (اگر يكي بودند لزوم وجود اين برچسب بي معنا است)، با استفاده از متد after كتابخانه jQuery يك برچسب را توليد و مقدار مورد نظر را پس از محل نمايش دراپ داون خود، نمايش خواهيم داد.

بهبود كد:
صورت مساله: اكنون نياز است بجز ناحيه mainFormReq، به سه ناحيه ديگر نيز اين تغييرات اعمال گردد. آيا بايد همين مقدار كد را سه بار ديگر copy/paste كرد؟
روش صحيح انجام اينكار در jQuery ، نوشتن يك افزونه بر اساس كدهاي فوق است كه روش انجام آن به صورت زير مي‌باشد (+):

//<![CDATA[
(function($) {
$.fn.dropdownlabel = function() {
return this.change(function() {
var obj = $(this);
var currentId = obj.attr("id"); //آي دي شيء جاري
var val = obj.val(); //مقدار
var text = $('#' + currentId + ' option:selected').text(); //متن
$("#lbl" + currentId).remove(); //اگر نمونه‌ي قبلي موجود است حذف شود
if (val && (val.length > 0) && (text != val)) {
//اگر متن و مقدار يكي نيست نمايش داده شود
obj.after('<label id="lbl' + currentId + '">' + val + '</label>');
}
});
};
})(jQuery);
//]]>
و در نهايت نحوه استفاده از آن (فايلي به نام jquery.dropdownlabel.js ) به صورت زير خواهد بود:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="TestDropdownlabel.aspx.cs"
Inherits="testWebForms87.TestDropdownlabel" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="jquery.min.js" type="text/javascript"></script>
<script src="jquery.dropdownlabel.js" type="text/javascript"></script>

<script type="text/javascript">
$(document).ready(function() {
$("#mainFormReq select").dropdownlabel();
});
</script>

</head>
<body>
<form id="form1" runat="server">
<div id="mainFormReq">
<asp:DropDownList ID="ddlActive" runat="server">
<asp:ListItem Value=""></asp:ListItem>
<asp:ListItem Value="Active">فعال</asp:ListItem>
<asp:ListItem Value="Inactive">غيرفعال</asp:ListItem>
</asp:DropDownList>
</div>
</form>
</body>
</html>

۱۳۸۸/۰۷/۳۰

به روز رساني فيلدهاي XML در SQL Server - قسمت دوم


قسمت اول را در اين آدرس مي‌توانيد مطالعه نمائيد.

در ادامه قسمت اول، اگر بخواهيم نود جديدي را به فيلد XML موجود اضافه كنيم، روش انجام آن به صورت زير است (يكي از روش‌ها البته):

DECLARE @tblTest AS TABLE (xmlField XML)

INSERT INTO @tblTest
(
xmlField
)
VALUES
(
'<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3>OldValue</Node3>
</Sample>'
)

DECLARE @Name NVARCHAR(50)
SELECT @Name = 'Vahid'

UPDATE @tblTest
SET xmlField.modify(
'insert element Node4 {sql:variable("@Name")} as last into
(/Sample)[1]'
)

SELECT tt.xmlField
FROM @tblTest tt

كه حاصل آن (افزوده شدن يك المان جديد به نام Node4 بر اساس مقدار متغير Name در انتهاي ليست) به صورت زير مي‌باشد:

<Sample>
<Node1>Value1</Node1>
<Node2>Value2</Node2>
<Node3>OldValue</Node3>
<Node4>Vahid</Node4>
</Sample>

سؤال 1 :
اگر بخواهيم نام Node4 نيز متغير باشد به چه صورتي بايد مساله را حل كرد؟
در اين حالت بايد از كوئري‌هاي دايناميك استفاده كرد. بايد يك رشته را ايجاد (كل عبارت update بايد يك رشته شود) و سپس از دستور exec كمك گرفت و البته بايد دقت داشت در اين حالت كار encoding كاركترهاي غيرمجاز در XML را بايد خودمان انجام دهيم.

سؤال 2:
اگر بخواهيم نام نودها و مقادير آن‌ها را به صورت يك جدول معمولي بازگشت دهيم به چه صورتي بايد عمل كرد؟

DECLARE @XML AS XML

SELECT @XML = tt.xmlField
FROM @tblTest tt

SELECT t.c.value('local-name(..)', 'varchar(max)') AS ParentNodeName,
t.c.value('local-name(.)', 'varchar(max)') AS NodeName,
t.c.value('text()[1]', 'varchar(max)') AS NodeText

FROM @XML.nodes('/*/*') AS t(c)

كه پس از اجراي آن خواهيم داشت:

ParentNodeName - NodeName - NodeText
Sample Node1 Value1
Sample Node2 Value2
Sample Node3 OldValue
Sample Node4 Vahid

۱۳۸۸/۰۷/۲۹

متفرقه - طنز


يكي از توانايي‌هاي سايت‌هاي جمع كننده آمار بازديدكنندگان يك سايت، مشخص كردن كلمات و جملاتي است كه از طريق موتورهاي جستجو به سايت مورد نظر ختم شده‌اند كه تعدادي از آن‌ها را در مورد وبلاگ جاري ملاحظه مي‌كنيد!


شرمنده. اين يك قلم موجود نيست!



سؤال خوبيه. من خودم هم موندم كه چه بايد بكنيم!؟



از يك حرفه‌اي. گوگل زمانيكه يك حرف فارسي در كنار جمله جستجوي شما باشد، در ابتدا فقط سايت‌هاي فارسي را ليست مي‌كند.



از ياهو بيشتر از اين انتظار نميره!



؟؟!



ياد غول چراغ جادو نيفتاديد؟! متاسفانه هنوز گوگل به اين درجه ارتقاء پيدا نكرده!



هنوز منتشر نشده!


۱۳۸۸/۰۷/۲۸

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


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


۱۳۸۸/۰۷/۲۶

آشنايي با NHibernate - قسمت نهم


استفاده از Log4Net جهت ثبت خروجي‌هاي SQL حاصل از NHibernate

هنگام استفاده از NHibernate، پس از افزودن ارجاعات لازم به اسمبلي‌هاي مورد نياز آن به برنامه، يكي از اسمبلي‌هايي كه به پوشه build برنامه به صورت خودكار كپي مي‌شود، فايل log4net.dll است (حتي اگر ارجاعي را به آن اضافه نكرده باشيم) كه جهت ثبت وقايع مرتبط با NHibernate مورد استفاده قرار مي‌گيرد. خوب اگر مجبوريم كه اين وابستگي كتابخانه NHibernate را نيز در پروژه‌هاي خود داشته باشيم، چرا از آن استفاده نكنيم؟!
شرح مفصل استفاده از اين كتابخانه سورس باز را در سايت اصلي آن مي‌توان مشاهده كرد:


براي اينكه از اين كتابخانه در برنامه خود جهت ثبت عبارات SQL توليدي توسط NHibernate استفاده كنيم، بايد مراحل زير طي شوند:
الف) ارجاعي را به اسمبلي log4net.dll اضافه نمائيد (كنار اسمبلي NHibernate در پوشه build برنامه بايد موجود باشد)
ب) فايل app.config برنامه را (برنامه ويندوزي) به صورت زير ويرايش كرده و چند سطر مربوطه را اضافه نمائيد (در مورد برنامه‌هاي وب هم به همين شكل است. configSections فايل web.config تنظيم شده و سپس تنظيمات log4net را قبل از بسته شدن تگ configuration اضافه نمائيد ) :

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="log4net"
type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
</configSections>

<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true"/>
</connectionStrings>

<log4net>
<appender name="rollingFile"
type="log4net.Appender.RollingFileAppender,log4net" >
<param name="File" value="NHibernate_Log.txt" />
<param name="AppendToFile" value="true" />
<param name="DatePattern" value="yyyy.MM.dd" />
<rollingStyle value="Size" />
<maxSizeRollBackups value="10" />
<maximumFileSize value="500KB" />
<staticLogFileName value="true" />
<layout type="log4net.Layout.PatternLayout,log4net">
<conversionPattern value="%d %p %m%n" />
</layout>
</appender>
<logger name="NHibernate.SQL">
<level value="ALL" />
<appender-ref ref="rollingFile" />
</logger>
</log4net>

</configuration>
ج) سپس بايد فراخواني زير نيز در ابتداي كار برنامه صورت گيرد:

log4net.Config.XmlConfigurator.Configure();
در يك برنامه ASP.Net اين فراخواني بايد در Application_Start فايل Global.asax.cs صورت گيرد.
يا در يك برنامه از نوع WinForms تنها كافي است سطر زير را به فايل AssemblyInfo.cs برنامه اضافه كرد:

// Configure log4net using the .config file
[assembly: log4net.Config.XmlConfigurator(Watch = true)]
يا اين سطر را به فايل Global.asax.cs يك برنامه ASP.Net نيز مي‌توان اضافه كرد. Watch=true آن، با كمك FileSystemWatcher تغييرات فايل كانفيگ را تحت نظر داشته و هر بار كه تغيير كند بلافاصله، تغييرات جديد را اعمال خواهد كرد.

د) هنگام استفاده از كتابخانه Fluent NHibernate حتما بايد متد ShowSql در جايي كه ديتابيس برنامه را تنظيم مي‌كنيم (Fluently.Configure().Database) ذكر گردد (كه نمونه آن‌را در مثال‌هاي قسمت‌هاي قبل ملاحظه‌ كرده‌ايد).

توضيحاتي در مورد تنظيمات فوق:
configSections حتما بايد در ابتداي فايل app.config‌ ذكر شود و گرنه برنامه كار نخواهد كرد.
سپس كانكشن استرينگ مورد استفاده در قسمت كانفيگ برنامه ذكر شده است.
در ادامه تنظيمات استاندارد مربوط به log4net را مشاهده مي‌كنيد.
در تنظيمات اين كتابخانه، appender مشخص كننده محل ثبت وقايع است. زمانيكه كه از RollingFileAppender استفاده كنيم، اطلاعات را در يك سري فايل ذخيره خواهد كرد (امكان ثبت وقايع در EventLog ويندوز، ارسال از طريق ايميل و غيره نيز ميسر است كه جهت توضيحات بيشتر مي‌توان به مستندات آن رجوع نمود).
سپس نام فايلي كه اطلاعات وقايع در آن ثبت خواهند شد ذكر شده است (براي مثال NHibernate_Log.txt)، در ادامه مشخص گرديده كه اطلاعات بايد هر بار به اين فايل Append و اضافه شوند. سطرهاي بعدي مشخص مي‌كنند كه هر زمانيكه اين لاگ فايل به 10 مگابايت رسيد، يك فايل جديد توليد كن و هر بار 10 فايل آخر را نگه دار و مابقي فايل‌هاي قديمي را حذف كن.
در قسمت PatternLayout مشخصات مي‌كنيم كه خروجي ثبت شده با چه فرمتي باشد. براي مثال يك سطر خروجي مطابق با تنظيمات فوق به شكل زير خواهد بود:

2009-10-18 20:03:54,187 DEBUG INSERT INTO [Student] (Name) VALUES (@p0); select SCOPE_IDENTITY();@p0 = 'Vahid'
در قسمت Logger يك نام دلخواه ذكر شده و ميزان اطلاعاتي كه بايد درج شود، از طريق مقدار level مورد نظر، قابل تنظيم است كه مي‌تواند يكي از مقادير ALL ،DEBUG ،INFO ،WARN ،ERROR ،FATAL و يا OFF باشد. اينجا level در نظر گرفته شده ALL است كه تمامي اطلاعات مرتبط با اعمال پشت صحنه NHibernate را لاگ خواهد كرد.
توسط appender-ref آن appender ايي را كه در ابتداي كار تعريف و تنظيم كرديم، مشخص خواهيم كرد.

اگر هم با برنامه نويسي بخواهيم اطلاعاتي را به اين لاگ فايل اضافه كنيم تنها كافي است بنويسيم:

log4net.LogManager.GetLogger("NHibernate.SQL").Info("test1");

اطلاعات بيشتر

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

۱۳۸۸/۰۷/۲۵

آشنايي با 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>())
{
courseRepo.Save(new Course()
{
Teacher = "Shams",
Students = new List<Student>() { student }
});
}

//سپس شماره دروس استادي خاص را نمايش مي‌دهيم
using (var courseRepo = new Repository<Course>())
{
var query = courseRepo.Find(t => t.Teacher == "Shams");

foreach (var courseItem in query)
Console.WriteLine(courseItem.Id);
}

Console.WriteLine("Press a key...");
Console.ReadKey();
}
}
}
همانطور كه ملاحظه مي‌كنيد در اين سطح ديگر برنامه هيچ دركي از ORM مورد استفاده ندارد و پياده سازي نحوه‌ي تعامل با NHibernate در پس كلاس مخزن مخفي شده است. كار آغاز و پايان تراكنش‌ها به صورت خودكار مديريت گرديده و همچنين آزاد سازي منابع را نيز توسط اينترفيس IDisposable مديريت مي‌كند. به اين صورت امكان فراموش شدن يك سري از اعمال متداول به حداقل رسيده، ميزان كدهاي تكراري برنامه كم شده و همچنين هر زمانيكه نياز بود، صرفا با تغيير پياده سازي كلاس مخزن مي‌توان به ORM ديگري كوچ كرد؛ بدون اينكه نيازي به بازنويسي كل برنامه وجود داشته باشد.

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

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


۱۳۸۸/۰۷/۲۴

خلاصه‌اي از آغاز به كار با NHibernate


اگر شش يا هفت قسمت قبل را بخواهيم به صورت سريع مرور كنيم مي‌توان به ويديوي زير مراجعه كرد:


در طي يك ربع، خيلي سريع به دريافت فايل‌هاي لازم، ايجاد يك پروژه جديد، افزودن ارجاعات لازم، استفاده از fluent NHibernate براي ساخت نگاشت‌ها و سپس استفاده از LINQ to NHibernate براي كوئري گرفتن از اطلاعات ديتابيس اشاره كرده است (كه از اين لحاظ كاملا به روز است).


۱۳۸۸/۰۷/۲۳

آشنايي با NHibernate - قسمت هفتم


مديريت بهينه‌ي سشن فكتوري

ساخت يك شيء SessionFactory بسيار پر هزينه و زمانبر است. به همين جهت لازم است كه اين شيء يكبار حين آغاز برنامه ايجاد شده و سپس در پايان كار برنامه تخريب شود. انجام اينكار در برنامه‌هاي معمولي ويندوزي (WinForms ،WPF و ...)، ساده است اما در محيط Stateless وب و برنامه‌هاي ASP.Net ، نياز به راه حلي ويژه وجود خواهد داشت و تمركز اصلي اين مقاله حول مديريت صحيح سشن فكتوري در برنامه‌هاي ASP.Net است.

براي پياده سازي شيء سشن فكتوري به صورتي كه يكبار در طول برنامه ايجاد شود و بارها مورد استفاده قرار گيرد بايد از يكي از الگوهاي معروف طراحي برنامه نويسي شيء گرا به نام Singleton Pattern استفاده كرد. پياده سازي نمونه‌ي thread safe آن كه در برنامه‌هاي ذاتا چند ريسماني وب و همچنين برنامه‌هاي معمولي ويندوزي مي‌تواند مورد استفاده قرار گيرد، در آدرس ذيل قابل مشاهده است:



از پنجمين روش ذكر شده در اين مقاله جهت ايجاد يك lazy, lock-free, thread-safe singleton استفاده خواهيم كرد.

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

در اين مدل ساده ما يك يا چند پاركينگ داريم كه در هر پاركينگ يك يا چند خودرو مي‌توانند پارك شوند.


يك برنامه ASP.Net را آغاز كرده و ارجاعاتي را به اسمبلي‌هاي زير به آن اضافه نمائيد:
FluentNHibernate.dll
NHibernate.dll
NHibernate.ByteCode.Castle.dll
NHibernate.Linq.dll
و همچنين ارجاعي به اسمبلي استاندارد System.Data.Services.dll دات نت فريم ورك سه و نيم

تصوير نهايي پروژه ما به شكل زير خواهد بود:



پروژه ما داراي يك پوشه domain ، تعريف كننده موجوديت‌هاي برنامه جهت تهيه نگاشت‌هاي لازم از روي ‌آن‌ها است. سپس يك پوشه جديد را به نام NHSessionManager به آن جهت ايجاد يك Http module مديريت كننده سشن‌هاي NHibernate در برنامه اضافه خواهيم كرد.

ساختار دومين برنامه (مطابق كلاس دياگرام فوق):

namespace NHSample3.Domain
{
public class Car
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Color { get; set; }
}
}

using System.Collections.Generic;

namespace NHSample3.Domain
{
public class Parking
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual string Location { get; set; }
public virtual IList<Car> Cars { get; set; }

public Parking()
{
Cars = new List<Car>();
}
}
}
مديريت سشن فكتوري در برنامه‌هاي وب

در اين قسمت قصد داريم Http Module ايي را جهت مديريت سشن‌هاي NHibernate ايجاد نمائيم.

در ابتدا كلاس Config را در پوشه مديريت سشن NHibernate با محتويات زير ايجاد كنيد:

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"))
)
.ExposeConfiguration(
x => x.SetProperty("current_session_context_class", "managed_web")
)
.Mappings(
m => m.AutoMappings.Add(
new AutoPersistenceModel()
.Where(x => x.Namespace.EndsWith("Domain"))
.AddEntityAssembly(typeof(NHSample3.Domain.Car).Assembly))
);
}

public static void CreateDb()
{
bool script = false;//آيا خروجي در كنسول هم نمايش داده شود
bool export = true;//آيا بر روي ديتابيس هم اجرا شود
bool dropTables = false;//آيا جداول موجود دراپ شوند
new SchemaExport(GetConfig().BuildConfiguration()).Execute(script, export, dropTables);
}
}
}
با اين كلاس در قسمت‌هاي قبل آشنا شده‌ايد. در اين كلاس با كمك امكانات Auto mapping موجود در Fluent Nhibernate (مطلب قسمت قبلي اين سري آموزشي) اقدام به تهيه نگاشت‌هاي خودكار از كلاس‌هاي قرار گرفته در پوشه دومين خود خواهيم كرد (فضاي نام اين پوشه به دومين ختم مي‌شود كه در متد GetConfig مشخص است).
دو نكته جديد در متد GetConfig وجود دارد:
الف) استفاده از متد FromConnectionStringWithKey ، بجاي تعريف مستقيم كانكشن استرينگ در متد مذكور كه روشي است توصيه شده. به اين صورت فايل وب كانفيگ ما بايد داراي تعريف كليد مشخص شده در متد GetConfig به نام DbConnectionString باشد:

<connectionStrings>
<!--NHSessionManager-->
<add name="DbConnectionString"
connectionString="Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true" />
</connectionStrings>
ب) قسمت ExposeConfiguration آن نيز جديد است.
در اينجا به AutoMapper خواهيم گفت كه قصد داريم از امكانات مديريت سشن مخصوص وب فريم ورك NHibernate استفاده كنيم. فريم ورك NHibernate داراي كلاسي است به نام NHibernate.Context.ManagedWebSessionContext كه جهت مديريت سشن‌هاي خود در پروژه‌هاي وب ASP.Net پيش بيني كرده است و از اين متد در Http module ايي كه ايجاد خواهيم كرد جهت ردگيري سشن جاري آن كمك خواهيم گرفت.

اگر متد CreateDb را فراخواني كنيم، جداول نگاشت شده به كلاس‌هاي پوشه دومين برنامه، به صورت خودكار ايجاد خواهند شد كه ديتابيس دياگرام آن به صورت زير مي‌باشد:



سپس كلاس SingletonCore را جهت تهيه تنها و تنها يك وهله از شيء سشن فكتوري در كل برنامه ايجاد خواهيم كرد (همانطور كه عنوان شده، ايده پياده سازي اين كلاس thread safe ، از مقاله معرفي شده در ابتداي بحث گرفته شده است):

using NHibernate;

namespace NHSessionManager
{
/// <summary>
/// lazy, lock-free, thread-safe singleton
/// </summary>
public class SingletonCore
{
private readonly ISessionFactory _sessionFactory;

SingletonCore()
{
_sessionFactory = Config.GetConfig().BuildSessionFactory();
}

public static SingletonCore Instance
{
get
{
return Nested.instance;
}
}

public static ISession GetCurrentSession()
{
return Instance._sessionFactory.GetCurrentSession();
}

public static ISessionFactory SessionFactory
{
get { return Instance._sessionFactory; }
}

class Nested
{
// Explicit static constructor to tell C# compiler
// not to mark type as beforefieldinit
static Nested()
{
}

internal static readonly SingletonCore instance = new SingletonCore();
}
}
}
اكنون مي‌توان از اين Singleton object جهت تهيه يك Http Module كمك گرفت. براي اين منظور كلاس SessionModule را به برنامه اضافه كنيد:

using System;
using System.Web;
using NHibernate;
using NHibernate.Context;

namespace NHSessionManager
{
public class SessionModule : IHttpModule
{
public void Dispose()
{ }

public void Init(HttpApplication context)
{
if (context == null)
throw new ArgumentNullException("context");

context.BeginRequest += Application_BeginRequest;
context.EndRequest += Application_EndRequest;
}

private void Application_BeginRequest(object sender, EventArgs e)
{
ISession session = SingletonCore.SessionFactory.OpenSession();
ManagedWebSessionContext.Bind(HttpContext.Current, session);
session.BeginTransaction();
}

private void Application_EndRequest(object sender, EventArgs e)
{
ISession session = ManagedWebSessionContext.Unbind(
HttpContext.Current, SingletonCore.SessionFactory);
if (session == null) return;

try
{
if (session.Transaction != null &&
!session.Transaction.WasCommitted &&
!session.Transaction.WasRolledBack)
{
session.Transaction.Commit();
}
else
{
session.Flush();
}
}
catch (Exception)
{
session.Transaction.Rollback();
}
finally
{
if (session != null && session.IsOpen)
{
session.Close();
session.Dispose();
}
}
}
}
}
كلاس فوق كار پياده سازي اينترفيس IHttpModule را جهت دخالت صريح در request handling pipeline برنامه ASP.Net جاري انجام مي‌دهد. در اين كلاس مديريت متدهاي استاندارد Application_BeginRequest و Application_EndRequest به صورت خودكار صورت مي‌گيرد.
در متد Application_BeginRequest ، در ابتداي هر درخواست يك سشن جديد ايجاد و به مديريت سشن وب NHibernate بايند مي‌شود، همچنين يك تراكنش نيز آغاز مي‌گردد. سپس در پايان درخواست، اين انقياد فسخ شده و تراكنش كامل مي‌شود، همچنين كار پاكسازي اشياء نيز صورت خواهد گرفت.

با توجه به اين موارد، ديگر نيازي به ذكر using جهت dispose كردن سشن جاري در كدهاي ما نخواهد بود، زيرا در پايان هر درخواست اينكار به صورت خودكار صورت مي‌گيرد. همچنين نيازي به ذكر تراكنش نيز نمي‌باشد، چون مديريت آن‌را خودكار كرده‌ايم.

جهت استفاده از اين Http module تهيه شده بايد چند سطر زير را به وب كانفيگ برنامه اضافه كرد:

<httpModules>
<!--NHSessionManager-->
<add name="SessionModule" type="NHSessionManager.SessionModule"/>
</httpModules>
بديهي است اگر نخواهيد از Http module استفاده كنيد بايد اين كدها را در فايل Global.asax برنامه قرار دهيد.

اكنون مثالي از نحوه‌ي استفاده از امكانات فراهم شده فوق به صورت زير مي‌تواند باشد:
ابتدا كلاس ParkingContext را جهت مديريت مطلوب‌تر LINQ to NHibernate تشكيل مي‌دهيم.

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample3.Domain;

namespace NHSample3
{
public class ParkingContext : NHibernateContext
{
public ParkingContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Car> Cars
{
get { return Session.Linq<Car>(); }
}

public IOrderedQueryable<Parking> Parkings
{
get { return Session.Linq<Parking>(); }
}
}
}
سپس در فايل Default.aspx.cs برنامه ، براي نمونه تعدادي ركورد را افزوده و نتيجه را در يك گريد ويوو نمايش خواهيم داد:

using System;
using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample3.Domain;
using NHSessionManager;

namespace NHSample3
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//ايجاد ديتابيس در صورت نياز
//Config.CreateDb();

//ثبت يك سري ركورد در ديتابيس
ISession session = SingletonCore.GetCurrentSession();

Car car1 = new Car() { Name = "رنو", Color = "مشكلي" };
session.Save(car1);
Car car2 = new Car() { Name = "پژو", Color = "سفيد" };
session.Save(car2);

Parking parking1 = new Parking()
{
Location = "آدرس پاركينگ مورد نظر",
Name = "پاركينگ يك",
Cars = new List<Car> { car1, car2 }
};

session.Save(parking1);

//نمايش حاصل در يك گريد ويوو
ParkingContext db = new ParkingContext(session);
var query = from x in db.Cars select new { CarName = x.Name, CarColor = x.Color };
GridView1.DataSource = query.ToList();
GridView1.DataBind();
}
}
}
مديريت سشن فكتوري در برنامه‌هاي غير وب

در برنامه‌هاي ويندوزي مانند WinForms ، WPF و غيره، تا زمانيكه يك فرم باز باشد، كل فرم و اشياء مرتبط با آن به يكباره تخريب نخواهند شد، اما در يك برنامه ASP.Net جهت حفظ منابع سرور در يك محيط چند كاربره، پس از پايان نمايش يك صفحه وب، اثري از آثار اشياء تعريف شده در كدهاي آن صفحه در سرور وجود نداشته و همگي بلافاصله تخريب مي‌شوند. به همين جهت بحث‌هاي ويژه state management در ASP.Net در اينباره مطرح است و مديريت ويژه‌اي بايد روي آن صورت گيرد كه در قسمت قبل مطرح شد.
از بحث فوق، تنها استفاده از كلاس‌هاي Config و SingletonCore ، جهت استفاده و مديريت بهينه‌ي سشن فكتوري در برنامه‌هاي ويندوزي كفايت مي‌كنند.

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

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

۱۳۸۸/۰۷/۲۲

آشنايي با NHibernate - قسمت ششم


آشنايي با 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; }
}
}
همانطور كه در قسمت‌هاي قبل نيز ذكر شد، تمام خواص پابليك كلاس‌هاي Domain ما به صورت virtual تعريف شده‌اند تا lazy loading را در NHibernate فعال سازيم. در حالت lazy loading ، اطلاعات تنها زمانيكه به آن‌ها نياز باشد بارگذاري خواهند شد. اين مورد در حالتيكه نياز به نمايش اطلاعات تنها يك شيء وجود داشته باشد بسيار مطلوب مي‌باشد، يا هنگام ثبت و به روز رساني اطلاعات نيز يكي از بهترين روش‌ها است. اما زمانيكه با ليستي از اطلاعات سروكار داشته باشيم باعث كاهش افت كارآيي خواهد شد زيرا براي مثال نمايش آن‌ها سبب خواهد شد كه 100 ها كوئري ديگر جهت دريافت اطلاعات هر ركورد در حال نمايش اجرا شود (مفهوم دسترسي به اطلاعات تنها در صورت نياز به آن‌ها). Lazy loading و eager loading (همانند مثال‌هاي قبلي) هر دو در NHibernate به سادگي قابل تنظيم هستند (براي مثال LINQ to SQL به صورت پيش فرض همواره lazy load است و تا اين تاريخ راه استانداردي براي امكان تغيير و تنظيم اين مورد پيش بيني نشده است).

اكنون كلاس جديد 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

اگر به اسكريپت ديتابيس توليد شده دقت كرده باشيد، عمليات 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 هستند و هيچكدام با نگارش نهايي اين فريم ورك كار نمي‌كنند. در نگارش رسمي نهايي ارائه شده، تغييرات بسياري صورت گرفته كه آن‌ها را در اين آدرس مي‌توان مشاهده كرد.

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


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

۱۳۸۸/۰۷/۲۱

آشنايي با NHibernate - قسمت پنجم


استفاده از LINQ جهت انجام كوئري‌ها توسط NHibernate

نگارش نهايي 1.0 كتابخانه‌ي LINQ to NHibernate اخيرا (حدود سه ماه قبل) منتشر شده است. در اين قسمت قصد داريم با كمك اين كتابخانه، اعمال متداول انجام كوئري‌ها را بر روي ديتابيس قسمت قبل انجام دهيم.
توسط اين نگارش ارائه شده، كليه اعمال قابل انجام با criteria API اين فريم ورك را مي‌توان از طريق LINQ نيز انجام داد (NHibernate براي كار با داده‌ها و جستجوهاي پيشرفته بر روي آن‌ها، HQL : Hibernate Query Language و Criteria API را سال‌ها قبل توسعه داده است).

جهت دريافت پروايدر LINQ مخصوص NHibernate به آدرس زير مراجعه نمائيد:


پس از دريافت آن، به همان برنامه كنسول قسمت قبل، دو ارجاع را بايد افزود:
الف) ارجاعي به اسمبلي NHibernate.Linq.dll
ب) ارجاعي به اسمبلي استاندارد System.Data.Services.dll دات نت فريم ورك سه و نيم

در ابتداي متد Main برنامه قصد داريم تعدادي مشتري را به ديتابيس اضافه نمائيم. به همين منظور متد AddNewCustomers را به كلاس CDbOperations برنامه كنسول قسمت قبل اضافه نمائيد. اين متد ليستي از مشتري‌ها را دريافت كرده و آن‌ها را در طي يك تراكنش به ديتابيس اضافه مي‌كند:

public void AddNewCustomers(params Customer[] customers)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
foreach (var data in customers)
session.Save(data);

session.Flush();

transaction.Commit();
}
}
}
در اينجا استفاده از واژه كليدي params سبب مي‌شود كه بجاي تعريف الزامي يك آرايه از نوع مشتري‌ها، بتوانيم تعداد دلخواهي پارامتر از نوع مشتري را به اين متد ارسال كنيم.

پس از افزودن اين ارجاعات، كلاس جديدي را به نام CLinqTest به برنامه كنسول اضافه نمائيد. ساختار كلي اين كلاس كه قصد استفاده از پروايدر LINQ مخصوص NHibernate را دارد بايد به شكل زير باشد (به كلاس پايه NHibernateContext دقت نمائيد):

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{ }
}
اكنون پس از مشخص شدن context يا زمينه، نحوه ايجاد يك كوئري ساده LINQ to NHibernate به صورت زير مي‌تواند باشد:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{
ISessionFactory _factory;

public CLinqTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetAllCustomers()
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>() select x;
return query.ToList();
}
}
}
}
ابتدا علاوه بر ساير فضاهاي نام مورد نياز، فضاي نام NHibernate.Linq به پروژه افزوده مي‌شود. سپس از extension متدي به نام Linq بر روي اشياء ISession از نوع يكي از موجوديت‌هاي تعريف شده در برنامه در قسمت‌هاي قبل، مي‌توان جهت تهيه كوئري‌هاي Linq مورد نظر بهره برد.
در اين كوئري، ليست تمامي مشتري‌ها بازگشت داده مي‌شود.

سپس جهت استفاده و بررسي آن در متد Main برنامه خواهيم داشت:

static void Main(string[] args)
{
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{

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

var customer2 = new Customer()
{
FirstName = "Ali",
LastName = "Hasani",
AddressLine1 = "Addr..1",
AddressLine2 = "Addr..2",
PostalCode = "4321",
City = "Shiraz",
CountryCode = "IR"
};

var customer3 = new Customer()
{
FirstName = "Mohsen",
LastName = "Shams",
AddressLine1 = "Addr...1",
AddressLine2 = "Addr...2",
PostalCode = "5678",
City = "Ahwaz",
CountryCode = "IR"
};

CDbOperations db = new CDbOperations(session);
db.AddNewCustomers(customer1, customer2, customer3);

CLinqTest lt = new CLinqTest(session);
foreach (Customer customer in lt.GetAllCustomers())
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();

}
در اين متد ابتدا تعدادي ركورد تعريف و سپس به ديتابيس اضافه شدند. در ادامه ليست تمامي آن‌ها از ديتابيس دريافت و نمايش داده مي‌شود.

مهمترين مزيت استفاده از LINQ در اين نوع كوئري‌ها نسبت به روش‌هاي ديگر، استفاده از كدهاي strongly typed دات نتي تحت نظر كامپايلر است، نسبت به رشته‌هاي معمولي SQL كه كامپايلر كنترلي را بر روي آن‌ها نمي‌تواند داشته باشد (براي مثال اگر نوع يك ستون تغيير كند يا نام آن‌، در حالت استفاده از LINQ بلافاصله يك خطا را از كامپايلر جهت تصحيح مشكلات دريافت خواهيم كرد كه اين مورد در زمان استفاده از يك رشته معمولي صادق نيست). همچنين مزيت فراهم بودن Intellisense را حين نوشتن كوئري‌هايي از اين دست نيز نمي‌توان نديد گرفت.

مثالي ديگر:
ليست تمام مشتري‌هاي شيرازي را نمايش دهيد:
ابتدا متد GetCustomersByCity را به كلاس CLinqTest فوق اضافه مي‌كنيم:

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>()
where x.City == city
select x;
return query.ToList();
}
}
سپس براي استفاده از آن، چند سطر ساده زير به ادامه متد Main اضافه مي‌شوند:

foreach (Customer customer in lt.GetCustomersByCity("Shiraz"))
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
يكي ديگر از مزاياي استفاده از LINQ to NHibernate ، امكان بكارگيري LINQ بر روي تمامي ديتابيس‌هاي پشتيباني شده توسط NHibernate است؛ براي مثال ماي اس كيوال، اوراكل و ....
ليست كامل ديتابيس‌هاي پشتيباني شده توسط NHibernate را در اين آدرس مي‌توانيد مشاهده نمائيد. (البته به نظر ليست آن، آنچنان هم به روز نيست؛ چون در نگارش آخر NHibernate ، پشتيباني از اس كيوال سرور 2008 هم اضافه شده است)


نكته:
در كوئري‌هاي مثال‌هاي فوق همواره بايد session.Linq<T> را ذكر كرد. اگر علاقمند بوديد شبيه به روشي كه در LINQ to SQL موجود است مثلا db.TableName بجاي session.Linq<T> در كوئري‌ها ذكر گردد، مي‌توان اصلاحاتي را به صورت زير اعمال كرد:
يك كلاس جديد را به نام SampleContext به برنامه كنسول جاري با محتويات زير اضافه نمائيد:

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class SampleContext : NHibernateContext
{
public SampleContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Customer> Customers
{
get { return Session.Linq<Customer>(); }
}

public IOrderedQueryable<Employee> Employees
{
get { return Session.Linq<Employee>(); }
}

public IOrderedQueryable<Order> Orders
{
get { return Session.Linq<Order>(); }
}

public IOrderedQueryable<OrderItem> OrderItems
{
get { return Session.Linq<OrderItem>(); }
}

public IOrderedQueryable<Product> Products
{
get { return Session.Linq<Product>(); }
}
}
}
در اين كلاس به ازاي تمام موجوديت‌هاي تعريف شده در پوشه domain برنامه اصلي خود (همان NHSample1 قسمت‌هاي اول و دوم)، يك متد از نوع IOrderedQueryable را بايد تشكيل دهيم كه پياده سازي آن‌را ملاحظه مي‌نمائيد.
سپس بازنويسي متد GetCustomersByCity بر اساس SampleContext فوق به صورت زير خواهد بود كه به كوئري‌هاي LINQ to SQL بسيار شبيه است:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CSampleContextTest
{
ISessionFactory _factory;

public CSampleContextTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
using (SampleContext db = new SampleContext(session))
{
var query = from x in db.Customers
where x.City == city
select x;
return query.ToList();
}
}
}
}
}
دريافت سورس برنامه تا اين قسمت

و در تكميل اين بحث، مي‌توان به ليستي از 101 مثال LINQ ارائه شده در MSDN اشاره كرد كه يكي از بهترين و سريع ترين مراجع يادگيري مبحث LINQ است.


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