نکات مهم هنگام کار باخصوصیات ناوبری در Entity Framework CodeFirst
  در این مقاله به معرفی نکاتی مهم هنگام کوئری زدن بر روی خصوصیات ناوبری در Entity Framework CodeFirst پرداخته می شود
   LINQ
   ۱۳۹۲۵
   این مقاله حاوی فایل ضمیمه نمی باشد
   مرتضی صحراگرد
   ۱۳۹۱/۴/۹
ارسال لینک صفحه برای دوستان ارسال لینک صفحه برای دوستان  اضافه کردن به علاقه مندیها اضافه کردن به علاقه مندیها   نسخه قابل چاپ نسخه قابل چاپ

 

این مقاله را با یک مثال ساده شروع می کنیم. فرض کنید دو جدول در پایگاه داده به نام های 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. بنابراین دو نکته بسیار مهم زیر استنتاج می شود که نیاز می باشد حتما مد نظر گرفته شوند.

  1. به دلیل اینکه با استفاده از این روش کل رکورد های مرتبط با کوئری اول، از پایگاه داده بازیابی می شوند، اگر تعداد ستون ها و سطر ها زیاد باشند، این موضوع می تواند به شدت راندمان و کارایی برنامه را تحت تاثیر قرار دهد.
  2. در اینجا باید به تفاوت های پروایدر 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 بوده و سپس مقدار دهی می گردد.