۱۳۹۰/۰۲/۲۴

QueryOver در NHibernate و تفاوت‌هاي آن با LINQ to NH


در NHibernate چندين و چند روش، جهت تهيه كوئري‌ها وجود دارد كه QueryOver يكي از آن‌ها است (+). QueryOver نسبت به LINQ to NH سازگاري بهتري با ساز و كار دروني NHibernate دارد؛ براي مثال امكان يكپارچگي آن با سطح دوم كش. هر چند ظاهر QueryOver با LINQ يكي است، اما در عمل متفاوتند و راه و روش خاص خودش را طلب مي‌كند. براي مثال در LINQ to NH مي‌تواند نوشت x.Property.Contains اما در QueryOver متدي به نام contains قابل استفاده نيست (هر چند در Intellisense ظاهر مي‌شود اما عملا تعريف نشده است و نبايد آن‌را با LINQ اشتباه گرفت) و سعي در استفاده از آن‌ها به استثناهاي زير ختم مي‌شوند:
Unrecognised method call: System.String:Boolean StartsWith(System.String)
Unrecognised method call: System.String:Boolean Contains(System.String)
براي مثال كلاس زير را در نظر بگيريد؛ كوئري‌هاي مطلب جاري بر اين اساس تهيه خواهند شد:
using NHibernate.Validator.Constraints;
using System;

namespace NH3Test.MappingDefinitions.Domain
{
public class Account
{
public virtual int Id { get; set; }

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

[NotNull]
public virtual int Balance { set; get; }

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

1) يافتن ركوردهايي كه در يك مجموعه‌ي مشخص قرار دارند. براي مثال balance آن‌ها مساوي 10 و 12 است:
var list = new[]  { 12,10};
var resultList = session.QueryOver<Account>()
.WhereRestrictionOn(p => p.Balance)
.IsIn(list)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Balance in (
@p0 /* = 10 */, @p1 /* = 12 */
)

2) پياده سازي همان متد Contains ذكر شده، در QueryOver:
var accountsContianX = session.QueryOver<Account>()
.WhereRestrictionOn(x => x.Name)
.IsLike("X", NHibernate.Criterion.MatchMode.Anywhere)
.List();

SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_
FROM
Accounts this_
WHERE
this_.Name like @p0 /* = %X% */

در اينجا بر اساس مقادير مختلف MatchMode مي‌توان متدهاي StartsWith (MatchMode.Start) ، EndsWith (MatchMode.End) ، Equals (MatchMode.Exact) را نيز تهيه نمود.

انجام مثال دوم راه ساده‌تري نيز دارد. قسمت WhereRestrictionOn و IsLike به صورت يك سري extension متد ويژه در فضاي نام NHibernate.Criterion تعريف شده‌اند. ابتدا اين فضاي نام را به كلاس جاري افزوده و سپس مي‌توان نوشت :
using NHibernate.Criterion;
...
var accountsContianX = session.QueryOver<Account>()
.Where(x => x.Name.IsLike("%X%"))
.List();

اين فضاي نام شامل چهار extension method به نام‌هاي IsLike ، IsInsensitiveLike ، IsIn و IsBetween است.


چگونه extension method سفارشي خود را تهيه كنيم؟

بهترين كار اين است كه به سورس NHibernate ، فايل‌هاي RestrictionsExtensions.cs و ExpressionProcessor.cs كه تعاريف متد IsLike در آن‌ها وجود دارد مراجعه كرد. در اينجا مي‌توان با نحوه‌ي تعريف و سپس ثبت آن در رجيستري extension methods مرتبط با QueryOver توسط متد عمومي RegisterCustomMethodCall آشنا شد. در ادامه سه كار را مي‌توان انجام داد:
-متد مورد نظر را در كدهاي خود (نه كدهاي اصلي NH) اضافه كرده و سپس با فراخواني RegisterCustomMethodCall آن‌را قابل استفاده نمائيد.
-متد خود را به سورس اصلي NH اضافه كرده و كامپايل كنيد.
-متد خود را به سورس اصلي NH اضافه كرده و كامپايل كنيد (بهتر است همان روش نامگذاري بكار گرفته شده در فايل‌هاي ذكر شده رعايت شود). يك تست هم براي آن بنويسيد (تست نويسي هم يك سري اصولي دارد (+)). سپس يك patch از آن روي آن ساخته (+) و براي تيم NH ارسال نمائيد (تا جايي كه دقت كردم از كليه ارسال‌هايي كه آزمون واحد نداشته باشند، صرفنظر مي‌شود).

مثال:
مي‌خواهيم extension متد جديدي به نام YearIs را به QueryOver اضافه كنيم. اين متد را هم بر اساس توابع توكار بانك‌هاي اطلاعاتي، تهيه خواهيم نمود. ليست كامل اين نوع متدهاي بومي SQL را در فايل Dialect.cs سورس‌هاي NH مي‌توان يافت (البته به صورت پيش فرض از متد extract براي جداسازي قسمت‌هاي مختلف تاريخ استفاده مي‌كند. اين متد در فايل‌هاي Dialect مربوط به بانك‌هاي اطلاعاتي مختلف، متفاوت است و برحسب بانك اطلاعاتي جاري به صورت خودكار تغيير خواهد كرد).
using System;
using System.Linq.Expressions;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Impl;

namespace NH3Test.ConsoleApplication
{
public static class MyQueryOverExts
{
public static bool YearIs(this DateTime projection, int year)
{
throw new Exception("Not to be used directly - use inside QueryOver expression");
}

public static ICriterion ProcessAnsiYear(MethodCallExpression methodCallExpression)
{
string property = ExpressionProcessor.FindMemberExpression(methodCallExpression.Arguments[0]);
object value = ExpressionProcessor.FindValue(methodCallExpression.Arguments[1]);
return Restrictions.Eq(
Projections.SqlFunction("year", NHibernateUtil.DateTime, Projections.Property(property)),
value);
}
}

public class QueryOverExtsRegistry
{
public static void RegistrMyQueryOverExts()
{
ExpressionProcessor.RegisterCustomMethodCall(
() => MyQueryOverExts.YearIs(DateTime.Now, 0),
MyQueryOverExts.ProcessAnsiYear);
}
}
}

اكنون براي استفاده خواهيم داشت:
QueryOverExtsRegistry.RegistrMyQueryOverExts(); //يكبار در ابتداي اجراي برنامه بايد ثبت شود
...
var data = session.QueryOver<Account>()
.Where(x => x.AddDate.YearIs(2010))
.List();

براي مثال اگر بانك اطلاعاتي انتخابي از نوع SQLite باشد، خروجي SQL مرتبط به شكل زير خواهد بود:
SELECT
this_.AccountId as AccountId0_0_,
this_.Name as Name0_0_,
this_.Balance as Balance0_0_,
this_.AddDate as AddDate0_0_
FROM
Accounts this_
WHERE
strftime("%Y", this_.AddDate) = @p0 /* =2010 */


هر چند ما تابع year را در متد ProcessAnsiYear ثبت كرده‌ايم اما بر اساس فايل SQLiteDialect.cs ، تعاريف مرتبط و مخصوص اين بانك اطلاعاتي (مانند متد strftime فوق) به صورت خودكار دريافت مي‌گردد و كد ما مستقل از نوع بانك اطلاعاتي خواهد بود.


نكته جالب!
LINQ to NH هم قابل بسط است؛ كاري كه در ORM هاي ديگر به اين سادگي نيست. چند مثال در اين زمينه:
چگونه تابع سفارشي SQL Server خود را به صورت يك extension method تعريف و استفاده كنيم: (+) ، يك نمونه ديگر: (+) و نمونه‌اي ديگر: (+).