public static class ConcurrentDictionaryExtensions { public static TValue GetOrAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.GetOrAdd(key, (k) => new Lazy<TValue>(() => valueFactory(k)) ).Value; } public static TValue AddOrUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory ) { return @this.AddOrUpdate(key, (k) => new Lazy<TValue>(() => addValueFactory(k)), (k, currentValue) => new Lazy<TValue>( () => updateValueFactory(k, currentValue.Value) ) ).Value; } public static bool TryGetValue<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); var result = @this.TryGetValue(key, out Lazy<TValue> v); if (result) value = v.Value; return result; } // this overload may not make sense to use when you want to avoid // the construction of the value when it isn't needed public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, TValue value ) { return @this.TryAdd(key, new Lazy<TValue>(() => value)); } public static bool TryAdd<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue> valueFactory ) { return @this.TryAdd(key, new Lazy<TValue>(() => valueFactory(key)) ); } public static bool TryRemove<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, out TValue value ) { value = default(TValue); if (@this.TryRemove(key, out Lazy<TValue> v)) { value = v.Value; return true; } return false; } public static bool TryUpdate<TKey, TValue>( this ConcurrentDictionary<TKey, Lazy<TValue>> @this, TKey key, Func<TKey, TValue, TValue> updateValueFactory ) { if (!@this.TryGetValue(key, out Lazy<TValue> existingValue)) return false; return @this.TryUpdate(key, new Lazy<TValue>( () => updateValueFactory(key, existingValue.Value) ), existingValue ); } }
ASP.NET MVC #18
- سپس دو کلاس Role و User را باید تعریف کنید. این دو رابطه many-to-many با هم دارند؛ یعنی هر کدام با یک ICollection به دیگری ارتباط پیدا میکنند. سپس این دو کلاس را در کلاس Context برنامه مطابق معمول توسط DbSetها در معرض دید EF قرار میدهید. مابقی آن کارکردن معمولی با این دو جدول اضافه شده به برنامه است:
public class EfRolesService : IRolesService { readonly IUnitOfWork _uow; readonly IDbSet<Role> _roles; public EfRolesService(IUnitOfWork uow) { _uow = uow; _roles = _uow.Set<Role>(); } public IList<Role> FindUserRoles(int userId) { var query = from role in _roles from user in role.Users where user.Id == userId select role; return query.OrderBy(x => x.Name).ToList(); } public string[] GetRolesForUser(int userId) { var roles = FindUserRoles(userId); if (roles == null || !roles.Any()) { return new string[] { }; } return roles.Select(x => x.Name).ToArray(); } public bool IsUserInRole(int userId, string roleName) { var query = from role in _roles where role.Name == roleName from user in role.Users where user.Id == userId select role; var userRole = query.FirstOrDefault(); return userRole != null; } }
public class CustomRoleProvider : RoleProvider { public override bool IsUserInRole(string username, string roleName) { // Since the role provider, in this case the CustomRoleProvider is instantiated by // the ASP.NET framework the best solution is to use the service locator pattern. // The service locator pattern is normally considered to be an anti-pattern but // sometimes you have to be pragmatic and accept the limitation on the framework // that is being used (in this case the ASP.NET framework). var rolesService = ObjectFactory.GetInstance<IRolesService>(); return rolesService.IsUserInRole(username.ToInt(), roleName); } public override string[] GetRolesForUser(string username) { var rolesService = ObjectFactory.GetInstance<IRolesService>(); return rolesService.GetRolesForUser(username.ToInt()); } // مابقی نیازی نیست پیاده سازی شوند
مثال 1: تمام اطلاعات یک جدول را دریافت کنید.
هدف دریافت تمام اطلاعات جدول facilities است.
برای انجام اینکار فقط کافیاست بر روی DbSet متناظر با آن، متد ToList فراخوانی شود:
var facilities = context.Facilities.ToList();
یک نکته: به فراخوانی متد ToList، اصطلاحا materialization گفته میشود و هدف آن تبدیل یک IQueryable، به یک IEnumerable است. اطلاعات بیشتر
مثال 2: اطلاعات ستونهای خاصی از یک جدول را دریافت کنید.
میخواهیم لیست نام امکانات مجموعه را به همراه هزینهی مرتبط با آنها، نمایش دهیم:
var facilities = context.Facilities.Select(x => new { x.Name, x.MemberCost }).ToList();
مثال 3: نحوهی بازگشت ردیفها را کنترل کنید.
چگونه میتوان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند؟
var facilities = context.Facilities.Where(x => x.MemberCost > 0).ToList();
مثال 4: نحوهی بازگشت ردیفها را کنترل کنید؛ قسمت دوم.
چگونه میتوان لیست امکاناتی را بازگشت داد که برای کاربران رایگان نیستند و همچنین هزینهی آنها، 1/50 ام هزینهی نگهداری ماهیانهی آنها است؟ خروجی این کوئری باید تنها به همراه ستونهای FacId, Name, MemberCost, MonthlyMaintenance باشد.
var facilities = context.Facilities.Where(x => x.MemberCost > 0 && x.MemberCost < (x.MonthlyMaintenance / 50)) .Select(x => new { x.FacId, x.Name, x.MemberCost, x.MonthlyMaintenance }).ToList();
در این مثال نحوهی ترکیب چند شرط را با هم در قسمت Where، مشاهده میکنید و همچنین با استفاده از متد Select، تعداد ستونهای بازگشتی نیز کنترل شدهاند.
مثال 5: جستجوهای سادهی رشتهای
لیستی از امکاناتی را تهیه کنید که واژهی «Tennis» در نام آنها بکار رفتهاست.
var facilities = context.Facilities.Where(x => x.Name.Contains("Tennis")).ToList();
مثال 6: ردیفهایی را که با چندین مقدار ممکن تطابق دارند، بازگشت دهید.
چگونه میتوان امکانات دارای ID مساوی 1 و 5 را بازگشت داد؟ برای اینکار از ترکیب شرطها با استفاده از OR استفاده نکنید.
int[] ids = { 1, 5 }; var facilities = context.Facilities.Where(x => ids.Contains(x.FacId)).ToList();
مثال 7: نتایج بازگشت داده شده را طبقه بندی کنید.
گزارشی از امکانات را تهیه کنید که در آن اگر هزینهی نگهداری ماهیانهی امکاناتی بیشتر از 100 دلار بود، به صورت expensive و در غیراینصورت cheap، طبقه بندی شوند.
var facilities = context.Facilities .Select(x => new { x.Name, Cost = x.MonthlyMaintenance > 100 ? "expensive" : "cheap" }).ToList();
مثال 8: کار با تاریخ و زمان
لیست کاربرانی را بازگشت دهید که پس از September 2012 عضو این مجموعه شدهاند. این گزارش باید تنها به همراه ستونهای MemId, Surname, FirstName, JoinDate باشد.
var date = new DateTime(2012, 09, 01); var members = context.Members.Where(x => x.JoinDate >= date) .Select(x => new { x.MemId, x.Surname, x.FirstName, x.JoinDate }).ToList();
مثال 9: نتایج تکراری را از اطلاعات بازگشتی حذف کرده و آنها را مرتب کنید.
گزارشی را تهیه کنید که در آن تنها فیلد Surname مرتب شدهی کاربران وجود دارد. از لیست Surnameها، تنها 10 مورد غیر تکراری را بازگشت دهید.
var members = context.Members.OrderBy(x => x.Surname) .Select(x => new { x.Surname }) .Distinct() .Take(10) .ToList();
مثال 10: نتایج چند کوئری را با هم ترکیب کنید.
لیست نامهای امکانات و نامهای اشخاص را با هم ترکیب کنید.
var names = context.Members.Select(m => m.Surname).ToList() .Union(context.Facilities.Select(f => f.Name).ToList()) // For now we have to use `.ToList()` here .ToList();
SELECT surname FROM members UNION SELECT name FROM facilities;
یعنی در مثال فوق، دوبار رفت و برگشت به بانک اطلاعاتی صورت گرفته (به ازای هر ToList ذکر شده) و سپس نتیجهی حاصل، در سمت کلاینت با هم Union شدهاند و نه در سمت دیتابیس.
مثال 11: محاسبات تجمعی ابتدایی
زمان ثبت نام آخرین عضو مجموعه چیست؟
برای حل این مثال میتوان از روشهای مختلفی استفاده کرد:
الف) استفاده از متد تجمعی Max برای یافتن بزرگترین مقدار JoinDate
var latest = context.Members.Max(x => x.JoinDate);
متد Max برای خواص nullable میتواند null را بازگشت دهد و همچنین اگر این مجموعه دارای مقداری نباشد و آن خاصیت نیز nullable نباشد، استثنای Sequence contains no element را صادر میکند. میتوان این استثناء را به صورت زیر با استفاده از متد DefaultIfEmpty کنترل کرد:
var latest2 = context.Members.Select(m => m.JoinDate).DefaultIfEmpty().Max();
SELECT MAX([m].[JoinDate]) FROM (SELECT NULL AS [empty]) AS [empty] LEFT OUTER JOIN [Members] AS [m] ON 1 = 1;
var latest3 = context.Members.Max(m => (DateTime?)m.JoinDate) ?? DateTime.Now;
ب) بجای استفاده از متد Max میتوان ابتدا رکوردها را بر اساس JoinDate به صورت نزولی مرتب کرد و سپس اولین عضو حاصل را بازگشت داد؛ چون اکنون بر اساس مرتب سازی صورت گرفته، در بالای لیست قرار دارد:
var latest4 = context.Members.OrderByDescending(m => m.JoinDate).Select(m => m.JoinDate).FirstOrDefault();
مثال 12: مثالی دیگر از محاسبات تجمعی ابتدایی
در مثال قبلی، نام و نام خانوادگی آخرین شخص ثبت نام شده را نیز به گزارش اضافه کنید؛ یعنی Select انجام شده شامل x.FirstName, x.Surname, x.JoinDate باشد.
یک روش انجام اینکار، همان کوئری ب مثال قبلی است که اینبار فقط Select آن فرق میکند:
var lastMember = context.Members.OrderByDescending(m => m.JoinDate) .Select(x => new { x.FirstName, x.Surname, x.JoinDate }) .FirstOrDefault();
روش دیگر آن نوشتن یک sub-query در قسمت Where است:
var members = context.Members.Select(x => new { x.FirstName, x.Surname, x.JoinDate }) .Where(x => x.JoinDate == context.Members.Max(x => x.JoinDate)) .ToList();
کدهای کامل این قسمت را در اینجا میتوانید مشاهده کنید.
روش های مقایسه اشیاء با null
Check | Code | Description |
Is Null | if(variable is null) return true; |
|
Is Not Null | if(variable is { }) return false |
|
Is Not Null | if(variable is object) return false |
|
Is Null | if(variable == null) return true |
|
Is Not Null | if(variable != null) return false |
|
طراحی یک Interceptor برای یک دست سازی ی و ک
در اینجا کدهای کلاس YeKeInterceptor را ملاحظه میکنید. در متدهایی که به کلمهی Executing ختم میشوند، میتوان به دستورات SQL تولید شده توسط EF، پیش از اعمال بر روی بانک اطلاعاتی دسترسی داشت:
public class YeKeInterceptor : IDbCommandInterceptor { public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { command.ApplyCorrectYeKe(); } public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { } public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { command.ApplyCorrectYeKe(); } public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { } public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { } public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { command.ApplyCorrectYeKe(); } }
public static class YeKe { public const char ArabicYeChar = (char)1610; public const char PersianYeChar = (char)1740; public const char ArabicKeChar = (char)1603; public const char PersianKeChar = (char)1705; public static string ApplyCorrectYeKe(this object data) { return data == null ? null : ApplyCorrectYeKe(data.ToString()); } public static string ApplyCorrectYeKe(this string data) { return string.IsNullOrWhiteSpace(data) ? string.Empty : data.Replace(ArabicYeChar, PersianYeChar).Replace(ArabicKeChar, PersianKeChar).Trim(); } public static void ApplyCorrectYeKe(this DbCommand command) { command.CommandText = command.CommandText.ApplyCorrectYeKe(); foreach (DbParameter parameter in command.Parameters) { switch (parameter.DbType) { case DbType.AnsiString: case DbType.AnsiStringFixedLength: case DbType.String: case DbType.StringFixedLength: case DbType.Xml: parameter.Value = parameter.Value is DBNull ? parameter.Value : parameter.Value.ApplyCorrectYeKe(); break; } } } }
در آن، CommandText و همچنین parameter.Valueها در صورت رشتهای بودن، اصلاح میشوند.
سربار این روش نسبت به روشهای پیشین استفاده از Reflection کمتر است. همچنین اشیاء پیچیده و تو در تو را نیز بهتر پشتیبانی میکند؛ چون در مرحله Executing، کار پردازش این اشیاء پایان یافته و SQL خام نهایی آن در اختیار ما است.
نحوهی استفاده از YeKeInterceptor
در آغاز برنامه (برای مثال متد Application_Start فایل Global.asax.cs برنامههای MVC )، سطر زیر را فراخوانی کنید:
DbInterception.Add(new YeKeInterceptor());
یک مثال کامل برای دریافت
Sample32.cs
Every day millions of users are commuting on the electronic highway. For you as a web developer you want to ensure that your website is adapted to the needs of the modern user and that you're not putting up road blocks, forcing users to take side roads.
Using modern web standards you can remove these road blocks and optimise your website to accommodate all users regardless of the browser they're using.
Cloud Computing is currently the hot topic in the developer world these days, and it seems all anyone wants to talk about is the cloud. If you're like me you signed up for something like Windows Azure just to see what the hype was all about. There are a lot of good reasons to move an app to the cloud, but it's still not for everyone. There are some things you need to think about before taking this gamble with your app.
مباحث eager fetching/loading (واکشی حریصانه) و lazy loading/fetching (واکشی در صورت نیاز، با تاخیر، تنبل) جزو نکات کلیدی کار با ORM های پیشرفته بوده و در صورت عدم اطلاع از آنها و یا استفادهی ناصحیح از هر کدام، باید منتظر از کار افتادن زود هنگام سیستم در زیر بار چند کاربر همزمان بود. به همین جهت تصور اینکه "با استفاده از ORMs دیگر از فراگیری SQL راحت شدیم!" یا اینکه "به من چه که پشت صحنه چه اتفاقی میافته!" بسی مهلک و نادرست است!
در ادامه به تفصیل به این موضوع پرداخته خواهد شد.
ابزار مورد نیاز
در این مطلب از برنامهی NHProf استفاده خواهد شد.
اگر مطالب NHibernate این سایت را دنبال کرده باشید، در مورد لاگ کردن SQL تولیدی به اندازهی کافی توضیح داده شده یا حتی یک ماژول جمع و جور هم برای مصارف دم دستی نوشته شده است. این موارد شاید این ایده را به همراه داشته باشند که چقدر خوب میشد یک برنامهی جامعتر برای این نوع بررسیها تهیه میشد. حداقل SQL نهایی فرمت میشد (یعنی برنامه باید مجهز به یک SQL Parser تمام عیار باشد که کار چند ماهی هست ...؛ با توجه به اینکه مثلا NHibernate از افزونههای SQL ویژه بانکهای اطلاعاتی مختلف هم پشتیبانی میکند، مثلا T-SQL مایکروسافت با یک سری ریزه کاریهای منحصر به MySQL متفاوت است)، یا پس از فرمت شدن، syntax highlighting به آن اضافه میشد، در ادامه مشخص میکرد کدام کوئریها سنگینتر هستند، کدامیک نشانهی عدم استفادهی صحیح از ORM مورد استفاده است، چه مشکلی دارد و از این موارد.
خوشبختانه این ایدهها یا آرزوها با برنامهی NHProf محقق شده است. این برنامه برای استفادهی یک ماه اول آن رایگان است (آدرس ایمیل خود را وارد کنید تا یک فایل مجوز رایگان یک ماهه برای شما ارسال گردد) و پس از یک ماه، باید حداقل 300 دلار هزینه کنید.
واکشی حریصانه و غیرحریصانه چیست؟
رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص میسازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).
چگونه واکشی حریصانه و غیرحریصانه را در NHibernate 3.0 تنظیم کنیم؟
در NHibernate اگر تنظیم خاصی را تدارک ندیده و خواص جداول خود را به صورت virtual معرفی کرده باشید، تنظیم پیش فرض دریافت اطلاعات همان lazy loading است. به مثالی در این زمینه توجه بفرمائید:
مدل برنامه:
مدل برنامه همان مثال کلاسیک مشتری و سفارشات او میباشد. هر مشتری چندین سفارش میتواند داشته باشد. هر سفارش به یک مشتری وابسته است. هر سفارش نیز از چندین قلم جنس تشکیل شده است. در این خرید، هر جنس نیز به یک سفارش وابسته است.
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
}
using System;
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Order
{
public virtual int Id { get; set; }
public virtual DateTime OrderDate { set; get; }
public virtual Customer Customer { get; set; }
public virtual IList<OrderItem> OrderItems { set; get; }
}
}
namespace CustomerOrdersSample.Domain
{
public class OrderItem
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
public virtual int Quntity { get; set; }
public virtual Order Order { set; get; }
}
}
namespace CustomerOrdersSample.Domain
{
public class Product
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual decimal UnitPrice { get; set; }
}
}
که جداول متناظر با آن به صورت زیر خواهند بود:
create table Customers (
CustomerId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (CustomerId)
)
create table Orders (
OrderId INT IDENTITY NOT NULL,
OrderDate DATETIME null,
CustomerId INT null,
primary key (OrderId)
)
create table OrderItems (
OrderItemId INT IDENTITY NOT NULL,
Quntity INT null,
ProductId INT null,
OrderId INT null,
primary key (OrderItemId)
)
create table Products (
ProductId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
UnitPrice NUMERIC(19,5) null,
primary key (ProductId)
)
alter table Orders
add constraint fk_Customer_Order
foreign key (CustomerId)
references Customers
alter table OrderItems
add constraint fk_Product_OrderItem
foreign key (ProductId)
references Products
alter table OrderItems
add constraint fk_Order_OrderItem
foreign key (OrderId)
references Orders
همچنین یک سری اطلاعات آزمایشی زیر را هم در نظر بگیرید: (بانک اطلاعاتی انتخاب شده SQL CE است)
SET IDENTITY_INSERT [Customers] ON;
GO
INSERT INTO [Customers] ([CustomerId],[Name]) VALUES (1,N'Customer1');
GO
SET IDENTITY_INSERT [Customers] OFF;
GO
SET IDENTITY_INSERT [Products] ON;
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (1,N'Product1',1000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (2,N'Product2',2000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (3,N'Product3',3000.00000);
GO
SET IDENTITY_INSERT [Products] OFF;
GO
SET IDENTITY_INSERT [Orders] ON;
GO
INSERT INTO [Orders] ([OrderId],[OrderDate],[CustomerId]) VALUES (1,{ts '2011-01-07 11:25:20.000'},1);
GO
SET IDENTITY_INSERT [Orders] OFF;
GO
SET IDENTITY_INSERT [OrderItems] ON;
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (1,10,1,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (2,5,2,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (3,20,3,1);
GO
SET IDENTITY_INSERT [OrderItems] OFF;
GO
دریافت اطلاعات :
میخواهیم نام کلیه محصولات خریداری شده توسط مشتریها را به همراه نام مشتری و زمان خرید مربوطه، نمایش دهیم (دریافت اطلاعات از 4 جدول بدون join نویسی):
var list = session.QueryOver<Customer>().List();
foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}
خروجی به صورت زیر خواهد بود:
Customer1:2011/01/07 11:25:20 :Product1
Customer1:2011/01/07 11:25:20 :Product2
Customer1:2011/01/07 11:25:20 :Product3
همانطور که مشاهده میکنید در اینجا اطلاعات از 4 جدول مختلف دریافت میشوند اما ما Join ایی را ننوشتهایم. ORM هرجایی که به اطلاعات فیلدهای جداول دیگر نیاز داشته، به صورت مستقیم به آن جدول مراجعه کرده و یک کوئری، حاصل این عملیات خواهد بود (مطابق تصویر جمعا 6 کوئری در پشت صحنه برای نمایش سه سطر خروجی فوق اجرا شده است).
این حالت فقط و فقط با تعداد رکورد کم بهینه است (و به همین دلیل هم تدارک دیده شده است). بنابراین اگر برای مثال قصد نمایش اطلاعات حاصل از 4 جدول فوق را در یک گرید داشته باشیم، بسته به تعداد رکوردها و تعداد کاربران همزمان برنامه (خصوصا در برنامههای تحت وب)، بانک اطلاعاتی باید بتواند هزاران هزار کوئری رسیده حاصل از lazy loading را پردازش کند و این یعنی مصرف بیش از حد منابع (IO بالا، مصرف حافظه بالا) به همراه بالا رفتن CPU usage و از کار افتادن زود هنگام سیستم.
کسانی که پیش از این با SQL نویسی خو گرفتهاند احتمالا الان منابع موجود را در مورد نحوهی نوشتن Join در NHibernate زیر و رو خواهند کرد؛ زیرا پیش از این آموختهاند که برای دریافت اطلاعات از دو یا چند جدول مرتبط باید Join نوشت. اما همانطور که پیشتر نیز عنوان شد، اگر با جزئیات کار با NHibernate آشنا شویم، نیازی به Join نویسی نخواهیم داشت. اینکار را خود ORM در پشت صحنه باید و میتواند مدیریت کند. اما چگونه؟
در NHibernate 3.0 با معرفی QueryOver که جایگزینی از نوع strongly typed همان ICriteria API قدیمی است، یا با معرفی Query که همان LINQ to NHibernate میباشد، متدی به نام Fetch نیز تدارک دیده شده است که استراتژیهای lazy loading و eager loading را به سادگی توسط آن میتوان مشخص نمود.
مثال: دریافت اطلاعات با استفاده از QueryOver
var list = session
.QueryOver<Customer>()
.Fetch(c => c.Orders).Eager
.Fetch(c => c.Orders.First().OrderItems).Eager
.Fetch(c => c.Orders.First().OrderItems.First().Product).Eager
.List();
foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}
پشت صحنه:
اینبار فقط یک کوئری حاصل عملیات بوده و join ها به صورت خودکار با توجه به متدهای Fetch ذکر شده که حالت eager loading آنها صریحا مشخص شده است، تشکیل شدهاند (6 بار رفت و برگشت به بانک اطلاعاتی به یکبار تقلیل یافت).
نکته 1: نتایج تکراری
اگر حاصل join آخر را نمایش دهیم، نتایجی تکراری خواهیم داشت که مربوط است به مقدار دهی customer با سه وهله از شیء مربوطه تا بتواند واکشی حریصانهی مجموعه اشیاء فرزند آنرا نیز پوشش دهد. برای رفع این مشکل یک سطر TransformUsing باید اضافه شود:
...
.TransformUsing(NHibernate.Transform.Transformers.DistinctRootEntity)
.List();
دریافت اطلاعات با استفاده از LINQ to NHibernate3.0
برای اینکه بتوان متدهای Fetch ذکر شده را به LINQ to NHibernate 3.0 اعمال نمود، ذکر فضای نام NHibernate.Linq ضروری است. پس از آن خواهیم داشت:
var list = session
.Query()
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.OrderItems)
.ThenFetch(p => p.Product)
.ToList();
اینبار از FetchMany، سپس ThenFetchMany (برای واکشی حریصانه مجموعههای فرزند) و در آخر از ThenFetch استفاده خواهد شد.
همانطور که ملاحظه میکنید حاصل این کوئری، با کوئری قبلی ذکر شده یکسان است. هر دو، اطلاعات مورد نیاز از دو جدول مختلف را نمایش میدهند. اما یکی در پشت صحنه شامل چندین و چند کوئری برای دریافت اطلاعات است، اما دیگری تنها از یک کوئری Join دار تشکیل شده است.
نکته 2: خطاهای ممکن
ممکن است حین تعریف متدهای Fetch در زمان اجرا به خطاهای Antlr.Runtime.MismatchedTreeNodeException و یا Specified method is not supported و یا موارد مشابهی برخورد نمائید. تنها کاری که باید انجام داد جابجا کردن مکان بکارگیری extension methods است. برای مثال متد Fetch باید پس از Where در حالت استفاده از LINQ ذکر شود و نه قبل از آن.