این مقاله را با یک مثال ساده شروع می کنیم. فرض کنید دو جدول در پایگاه داده به
نام های Customer و Phone داریم. که جدول Phone تلفن های مشتریان را نگهداری می کند.
بنابراین یک ارتباط "یک به چند" بین جدول Customer و Phone وجود دارد.
کلاس های مورد نیاز جهت کار با این جداول نیز به شکل زیر طراحی شده اند.
public
class CustomerDbContext :
DbContext
{
public
DbSet<Customer>
Customers { get;
set; }
public
DbSet<Phone>
Phones { get;
set; }
}
[Table("Customer")]
public
class Customer
{
public
Customer()
{
Phones
= new HashSet<Phone>();
}
public
int CustomerId {
get; set;
}
public
string FirstName {
get; set;
}
public
string LastName {
get; set;
}
public
virtual
ICollection<Phone>
Phones { get;
set;
}
}
[Table("Phone")]
public
class Phone
{
public
int PhoneId {
get; set;
}
public
int CustomerId {
get; set;
}
public
string PhoneNumber {
get; set;
}
public
virtual
Customer Customer
{ get;
set;
}
}
قطعه کد بالا از سه کلاس تشکل شده است. کلاس CustomerDbContext
که همان DbContext برنامه است و دو کلاس دیگر معادل جداول Customer و Phone در پایگاه
داده می باشند.
یاد آوری:
به خصوصیت Phones در کلاس Customer و خصوصیت
Customer در کلاس Phone، خصوصیات ناوبری (Navigation
Properties) گفته می شود. زیرا این خصوصیات نمایانگر ارتباط بین کلاس ها و در نتیجه
جداول در پایگاه داده می باشند. خصوصیت Phones در کلاس
Customer از نوع کالکشن (Collection)
می باشد زیرا این خصوصیات مجموعه ای از تلفن های مربوط به یک مشتری را نگهداری می کند.
خصوصیت Customer در کلاس Phone از نوع
ارجاع (Reference) می باشد زیرا این خصوصیت یک نمونه از
نوع کلاس Customer که در حقیقت صاحب این تلفن
می باشد را نگهداری می کند.
خوب اکنون قصد داریم چند کوئری ساده بنویسیم و دستورت SQL تولید شده را مقایسه کنیم.
ابتدا قصد داریم تعداد مشتریان موجود را بدست آوریم.
تذکر:
نیازی به بررسی جزئیات کوئری ها نیست و فقط با توضیحات مقاله همراه باشید.
using (var
context=new
CustomerDbContext())
{
var
count = context.Customers.Count();
}
دستور SQL تولید شده را توسط SQL Server Profiler مشاهده می کنیم و نتیجه اجرای
این دستور به شکل زیر می باشد:
همانطور که انتظار داشتیم تابع COUNT در کوئری
تولید شده و پس از اجرای این دستور، تعداد مشتریان (3) بازیابی شده است و این موضوع
کاملا مطابق پیشبینی ما بود.
اکنون یک مثال دیگر انجام می دهیم. این بار قصد داریم مشخصات تمامی مشتریانی که
نام آن ها Morteza می باشد را بازیابی کنیم.
using (var
context=new
CustomerDbContext())
{
var
result = context.Customers.Where(m => m.FirstName ==
"Morteza").ToList();
}
دستور تولید شده و نتیجه اجرای آن بر روی پایگاه داده بدین شکل می باشد.
مشاهده می کنید که باز هم مطابق انتظار ما فرمان WHERE
N'Morteza'تولید شده است و نتیجه دستور
که شامل 1 رکورد می باشد نیز از پایگاه داده بازیابی شده است.
تاکنون ما بر روی موجودیت مشتری فرمان های خود را اجرا نموده ایم. اکنون به سراغ
خصوصیات ناوبری می رویم.
در این مثال قصد داریم تعداد شماره های تماس اولین مشتری را بدست آوریم.
using (var
context=new
CustomerDbContext())
{
var
count = context.Customers.First().Phones.Count;
}
دستورات SQL تولید شده به شکل زیر می باشند.
اولین دستور تولید شده مطابق پیشبینی ما می باشد و با استفاده از تابع
Top(1) مشخصات اولین مشتری
را بازیابی می کند. اما دستور دوم بر خلاف انتظار ما به جای اینکه تعداد
شماره تماس ها را برگرداند، کلیه شماره تماس های اوین مشتری را برگردانده است!!! داستان
از چه قرار است؟
قبل از اینکه به این سوال جواب دهیم، اجازه بدهید یک مثال دیگر را نیز امتحان بکنیم.
این بار قصد داریم که کلیه شماره های تماس اولین مشتری که با 0912 آغاز شده اند
را بازیابی کنیم.
using (var
context = new
CustomerDbContext())
{
var
result = context.Customers.First().Phones.Where(m => m.PhoneNumber.StartsWith("0912"));
}
دستورات SQL تولید شده به شکل زیر می باشند.
همانطور که ملاحظه می کنید، دستورات تولید شده دقیقا همانند مثال قبلی می باشند
در حالی که فرمان ما کاملا متفاوت بوده است و اینجا نیز کل شماره های تماس اولین مشتری
برگردانده شده است!
قضیه از این قرار است که لااقل تا زمان نگارش این مقاله (در حال حاضر نسخه 4.3.1.0
آخرین نسخه ارائه شده می باشد) برنامه Entity Framework به شکل بالا قادر به ایجاد
کوئری های مناسب (مرتبط با خصوصیات ناوبری)برای پایگاه داده نمی باشد و تنها کاری که
می کند این است که تعداد کل رکورد های مرتبط با مشتری اول را از پایگاه داده بر می
گرداند و بقیه کارها یعنی شمارش تعداد رکورد ها یا اعمال شرط Where در حافظه برنامه
انجام می شود.
اگر بخواهیم به شکل دقیق تری این موضوع را بیان کنیم باید بگوییم که عملیات بازیابی
تمامی رکورد ها از پایگاه داده (باز هم تاکیید می کنیم که این موضوع مرتبط با کوئری
های انجام شده بر روی خصوصیات ناوبری می باشد) توسط پروایدر LINQ to Entity انجام شده
و سایر فرامین توسط پروایدر LINQ to Object. بنابراین دو نکته بسیار مهم زیر استنتاج
می شود که نیاز می باشد حتما مد نظر گرفته شوند.
- به دلیل اینکه با استفاده از این روش کل رکورد های مرتبط با کوئری اول، از
پایگاه داده بازیابی می شوند، اگر تعداد ستون ها و سطر ها زیاد باشند، این موضوع
می تواند به شدت راندمان و کارایی برنامه را تحت تاثیر قرار دهد.
- در اینجا باید به تفاوت های پروایدر LINQ to Entity و LINQ to Object نیز توجه
داشت. به طور مثال هنگام جستجوی کلمه Ali، در پروایدر LINQ to Entity تفاوتی میان
Aliو ali و خلاصه اینکه کوچک و بزرگ بودن حروف وجود ندارد ولی در مورد LINQ to
Object این چنین نیست و کوچک و بزرگ بودن حروف مهم می باشد.
اکنون روشی جهت رفع مشکل بالا ارائه می دهیم.
در این روش ابتدا با استفاده از متد Entry از کلاس DbContext به توابع Collection
و Reference دسترسی پیدا می کنیم و سپس اپراتور Query را فراخوانی می کنیم و بر روی
نتیجه اعمال این دستور، سایر کار ها را انجام می دهید.
در مثال زیر قصد داریم تعداد شماره های تماس اولین مشتری را بدیت آوریم.
using (var
context = new
CustomerDbContext())
{
var
firstCustomer = context.Customers.First();
var
resultOfQuery = context.Entry(firstCustomer).Collection(m
=> m.Phones).Query();
var
phonesCount = resultOfQuery.Count();
}
به منظور خوانایی بیشتر، فرامین را در چند سطر نوشته ایم. ابتدا مشخصات اولین مشتری
بازیابی شده است. در سطر دوم از متد Entry استفاده نموده ایم تا به توابع Collection
و Reference دسترسی پیدا کنیم. با توجه به اینکه خصوصیت ناوبری ما در این مثال همانطور
که در ابتدای مقاله توضیح دادیم از نوع کالکشن می باشد، ما نیز از تابع Collection
استفاده نموده ایم. اگر خصوصیت ناوبری ما از نوع ارجاع بود، به جای متد Collection
از متد Reference می کردیم که عملکرد مشابهی دارد. سپس متد
Query را بر روی نتیجه کار اعمال کردیم و کوئری مورد نظر ما ساخته
شده است.
در خط سوم، دستور مورد نظر خود، یعنی Count را فرا خوانده ایم. به نتیجه اجرای این
کوئری ها بر روی پایگاه داده دقت کنید.
دستور اول یعنی بازیابی مشتری دقیقا همانند مثال های قبل می باشد اما در دستور دوم
این بار طبق انتظار ما دیگر فقط تعداد رکورد ها بازگردانده شده است.
در مثال بعدی قصد داریم خواسته قبلی خود یعنی تمامی شماره تماس های اولین مشتری
که با 0912 شروع می شود را بازیابی کنیم.
using (var
context = new
CustomerDbContext())
{
var firstCustomer = context.Customers.First();
var resultOfQuery = context.Entry(firstCustomer).Collection(m
=> m.Phones).Query();
var
phones = resultOfQuery.Where(m => m.PhoneNumber.StartsWith("0912")).ToList();
}
خوب، تنها تغییر در خط آخر می باشد که این بار دستور داده ایم که شماره تماس های
اولین مشتری که با 0912 شروع شده اند، بازیابی شوند.
نتیجه اجرای این دستور بر روی پایگاه داده:
خوب این دفعه نیز مطابق انتظار ما فقط شماره تماس هایی که با 0912 شروع شده اند،
نتیجه اجرای دستور می باشند و در خود دستور SQL دوم نیز عبارات Where و Like را مشاهده
می کنید.
در پایان خالی از لطف نیست به این موضوع اشاره کنیم که در صورتی که کلاس های شما
به هر دلیلی ویژگی Lazy Loading را حمایت نکنند (به طور مثال به هر دلیلی امکان
virtual نمودن خصوصیات ناوبری موجود نباشد) با
استفاده از توابع Collection و Reference و اجرای متد Load، می توانیم
مشابه این ویژگی را ایجاد نماییم. این تکنیک Explicit Loading نامیده می شود.
به قطعه کد زیر دقیت کنید:
using (var
context = new
CustomerDbContext())
{
var
firstCustomer = context.Customers.First();
context.Entry(firstCustomer).Collection(m
=> m.Phones).Load();
}
اکنون دو تصویر گرفته شده توسط دیباگر برنامه ویژوال استودیو قبل و بعد از خط فرمان
تابع Load را مشاهده می کنید.
قبل:
بعد:
مشاهده می کنید که مقدار خصوصیت Phones ابتدا null
بوده و سپس مقدار دهی می گردد.