در این مطلب تعدادی از شایعترین مشکلات حین کار با Entity framework که نهایتا به تولید برنامههایی کند منجر میشوند، بررسی خواهند شد.
مدل مورد بررسی
کوئریهایی که در ادامه بررسی خواهند شد، بر روی رابطهی one-to-many فوق تعریف شدهاند؛ یک کاربر به همراه تعدادی مطلب منتشر شده.
مشکل 1: بارگذاری تعداد زیادی ردیف
در بسیاری از اوقات، در برنامههای خود تنها نیاز به مشاهدهی قسمت خاصی از یک سری از اطلاعات، وجود دارند. به همین جهت بکارگیری متد ToList بدون محدود سازی تعداد ردیفهای بازگشت داده شده، سبب بالا رفتن مصرف حافظهی سرور و همچنین بالا رفتن میزان دادهای که هر بار باید بین سرور و کلاینت منتقل شوند، خواهد شد. یک چنین برنامههایی بسیار مستعد به استثناهایی از نوع out of memory هستند.
راه حل: با استفاده از Skip و Take، مباحث صفحهی بندی را اعمال کنید.
مشکل 2: بازگرداندن تعداد زیادی ستون
فرض کنید View برنامه، در حال نمایش عناوین مطالب ارسالی است. کوئری فوق، علاوه بر عناوین، شامل تمام خواص تعریف شدهی دیگر نیز هست. یک چنین کوئریهایی نیز هربار سبب هدر رفتن منابع سرور میشوند.
راه حل: اگر تنها نیاز به خاصیت Content است، از Select و سپس ToList استفاده کنید؛ البته به همراه نکته 1.
مشکل 3: گزارشگیریهایی که بیشباهت به حملهی به دیتابیس نیستند
فرض کنید قرار است رکوردهای مطالب را نمایش دهید. در حین نمایش این مطالب، در قسمتی از آن باید نام نویسنده نیز درج شود. با توجه به رابطهی تعریف شده، نوشتن post.User.Name به ازای هر مطلب، بسیار ساده به نظر میرسد و بدون مشکل هم کار میکند. اما ... اگر خروجی SQL این گزارش را مشاهده کنیم، به ازای هر ردیف نمایش داده شده، یکبار رفت و برگشت به بانک اطلاعاتی، جهت دریافت نام نویسنده یک مطلب وجود دارد.
این مورد به lazy loading مشهور است و در مواردی که قرار است با یک مطلب و یک نویسنده کار شود، شاید اهمیتی نداشته باشد. اما در حین نمایش لیستی از اطلاعات، بیشباهت به یک حملهی شدید به بانک اطلاعاتی نیست.
راه حل: در گزارشگیریها اگر نیاز به نمایش اطلاعات روابط یک موجودیت وجود دارد، از متد Include استفاده کنید تا Lazy loading لغو شود.
مشکل 4: فعال بودن بیجهت مباحث ردیابی اطلاعات
در اینجا ما فقط قصد داریم که لیستی از اطلاعات را دریافت و سپس نمایش دهیم. در این بین، هدف، ویرایش یا حذف اطلاعات این لیست نیست. یک چنین کوئریهایی مساوی هستند با تشکیل dynamic proxies مخصوص EF جهت ردیابی تغییرات اطلاعات (مباحث AOP توکار). EF توسط این dynamic proxies، محصور کنندههایی را برای تک تک آیتمهای بازگشت داده شده از لیست تهیه میکند. در این حالت اگر خاصیتی را تغییر دهید، ابتدا وارد این محصور کننده (غشاء نامرئی) میشود، در سیستم ردیابی EF ذخیره شده و سپس به شیء اصلی اعمال میگردد. به عبارتی شیء در حال استفاده، هر چند به ظاهر post.User است اما در واقعیت یک User دارای روکشی نامرئی از جنس dynamic proxyهای EF است. تهیه این روکشها، هزینهبر هستند؛ چه از لحاظ میزان مصرف حافظه و چه از نظر سرعت کار.
راه حل: در گزاشگیریها، dynamic proxies را توسط متد AsNoTracking غیرفعال کنید:
مشکل 5: باز کردن تعداد اتصالات زیاد به بانک اطلاعاتی در طول یک درخواست
هر Context دارای اتصال منحصربفرد خود به بانک اطلاعاتی است. اگر در طول یک درخواست، بیش از یک Context مورد استفاده قرار گیرد، بدیهی است به همین تعداد اتصال باز شده به بانک اطلاعاتی، خواهیم داشت. نتیجهی آن فشار بیشتر بر بانک اطلاعاتی و همچنین کاهش سرعت برنامه است؛ از این لحاظ که اتصالات TCP برقرار شده، هزینهی بالایی را به همراه دارند.
روش تشخیص:
داشتن متدهایی که در آنها کار وهله سازی و dispose زمینهی EF انجام میشود (متدهایی که در آنها new Context وجود دارد).
راه حل: برای حل این مساله باید از روشهای تزریق وابستگیها استفاده کرد. یک Context وهله سازی شدهی در طول عمر یک درخواست، باید بین وهلههای مختلف اشیایی که نیاز به Context دارند، زنده نگه داشته شده و به اشتراک گذاشته شود.
مشکل 6: فرق است بین IList و IEnumerable
خروجی کوئری LINQ نوشته شده از نوع IEnumerable است. در EF، هربار مراجعهی مجدد به یک کوئری که خروجی IEnumerable دارد، مساوی است با ارزیابی مجدد آن کوئری. به عبارتی، یکبار دیگر این کوئری بر روی بانک اطلاعاتی اجرا خواهد شد و رفت و برگشت مجددی صورت میگیرد.
زمانیکه در حال تهیهی گزارشی هستید، ابزارهای گزارشگیر ممکن است چندین بار از نتیجهی کوئری شما در حین تهیهی گزارش استفاده کنند. بنابراین برخلاف تصور، data binding انجام شده، تنها یکبار سبب اجرای این کوئری نمیشود؛ بسته به ساز و کار درونی گزارشگیر، چندین بار ممکن است این کوئری فراخوانی شود.
راه حل: یک ToList را به انتهای این کوئری اضافه کنید. به این ترتیب از نتیجهی کوئری، بجای اصل کوئری استفاده خواهد شد و در این حالت تنها یکبار رفت و برگشت به بانک اطلاعاتی را شاهد خواهید بود.
مشکل 7: فرق است بین IQueryable و IEnumerable
خروجی IEnumerable، یعنی این عبارت را محاسبه کن. خروجی IQueryable یعنی این عبارت را درنظر داشته باش. اگر نیاز است نتایج کوئریها با هم ترکیب شوند، مثلا بر اساس رابط کاربری برنامه، کاربر بتواند شرطهای مختلف را با هم ترکیب کند، باید از ترکیب IQueryableها استفاده کرد تا سبب رفت و برگشت اضافی به بانک اطلاعاتی نشویم.
مشکل 8: استفاده از کوئریهای Like دار
این نوع کوئریها که در نهایت به Like در SQL ترجمه میشوند، سبب full table scan خواهند شد که کارآیی بسیار پایینی دارند. در این نوع موارد توصیه شدهاست که از روشهای full text search استفاده کنید.
مشکل 9: استفاده از Count بجای Any
اگر نیاز است بررسی کنید مجموعهای دارای مقداری است یا خیر، از Count>0 استفاده نکنید. کارآیی Any و کوئری SQL ایی که تولید میکند، به مراتب بیشتر و بهینهتر است از Count>0.
مشکل 10: سرعت insert پایین است
ردیابی تغییرات را خاموش کرده و از متد جدید AddRange استفاده کنید. همچنین افزونههایی برای Bulk insert نیز موجود هستند.
مشکل 11: شروع برنامه کند است
میتوان تمام مباحث نگاشتهای پویای کلاسهای برنامه به جداول و روابط بانک اطلاعاتی را به صورت کامپایل شده در برنامه ذخیره کرد. این مورد سبب بالا رفتن سرعت شروع برنامه خصوصا در حالتیکه تعداد جداول بالا است میشود.
مدل مورد بررسی
public class User { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<BlogPost> BlogPosts { get; set; } } public class BlogPost { public int Id { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("UserId")] public virtual User User { get; set; } public int UserId { get; set; } }
مشکل 1: بارگذاری تعداد زیادی ردیف
var data = context.BlogPosts.ToList();
راه حل: با استفاده از Skip و Take، مباحث صفحهی بندی را اعمال کنید.
مشکل 2: بازگرداندن تعداد زیادی ستون
var data = context.BlogPosts.ToList();
راه حل: اگر تنها نیاز به خاصیت Content است، از Select و سپس ToList استفاده کنید؛ البته به همراه نکته 1.
var list = context.BlogPosts.Select(x => x.Content).Skip(15).Take(15).ToList();
مشکل 3: گزارشگیریهایی که بیشباهت به حملهی به دیتابیس نیستند
foreach (var post in context.BlogPosts) { Console.WriteLine(post.User.Name); }
این مورد به lazy loading مشهور است و در مواردی که قرار است با یک مطلب و یک نویسنده کار شود، شاید اهمیتی نداشته باشد. اما در حین نمایش لیستی از اطلاعات، بیشباهت به یک حملهی شدید به بانک اطلاعاتی نیست.
راه حل: در گزارشگیریها اگر نیاز به نمایش اطلاعات روابط یک موجودیت وجود دارد، از متد Include استفاده کنید تا Lazy loading لغو شود.
foreach (var post in context.BlogPosts.Include(x=>x.User))
مشکل 4: فعال بودن بیجهت مباحث ردیابی اطلاعات
var data = context.BlogPosts.ToList();
راه حل: در گزاشگیریها، dynamic proxies را توسط متد AsNoTracking غیرفعال کنید:
var data = context.BlogPosts.AsNoTracking().Skip(15).Take(15).ToList();
مشکل 5: باز کردن تعداد اتصالات زیاد به بانک اطلاعاتی در طول یک درخواست
هر Context دارای اتصال منحصربفرد خود به بانک اطلاعاتی است. اگر در طول یک درخواست، بیش از یک Context مورد استفاده قرار گیرد، بدیهی است به همین تعداد اتصال باز شده به بانک اطلاعاتی، خواهیم داشت. نتیجهی آن فشار بیشتر بر بانک اطلاعاتی و همچنین کاهش سرعت برنامه است؛ از این لحاظ که اتصالات TCP برقرار شده، هزینهی بالایی را به همراه دارند.
روش تشخیص:
private void problem5MoreThan1ConnectionPerRequest() { using (var context = new MyContext()) { var count = context.BlogPosts.ToList(); } }
راه حل: برای حل این مساله باید از روشهای تزریق وابستگیها استفاده کرد. یک Context وهله سازی شدهی در طول عمر یک درخواست، باید بین وهلههای مختلف اشیایی که نیاز به Context دارند، زنده نگه داشته شده و به اشتراک گذاشته شود.
مشکل 6: فرق است بین IList و IEnumerable
DataContext = from user in context.Users where user.Id>10 select user;
زمانیکه در حال تهیهی گزارشی هستید، ابزارهای گزارشگیر ممکن است چندین بار از نتیجهی کوئری شما در حین تهیهی گزارش استفاده کنند. بنابراین برخلاف تصور، data binding انجام شده، تنها یکبار سبب اجرای این کوئری نمیشود؛ بسته به ساز و کار درونی گزارشگیر، چندین بار ممکن است این کوئری فراخوانی شود.
راه حل: یک ToList را به انتهای این کوئری اضافه کنید. به این ترتیب از نتیجهی کوئری، بجای اصل کوئری استفاده خواهد شد و در این حالت تنها یکبار رفت و برگشت به بانک اطلاعاتی را شاهد خواهید بود.
مشکل 7: فرق است بین IQueryable و IEnumerable
خروجی IEnumerable، یعنی این عبارت را محاسبه کن. خروجی IQueryable یعنی این عبارت را درنظر داشته باش. اگر نیاز است نتایج کوئریها با هم ترکیب شوند، مثلا بر اساس رابط کاربری برنامه، کاربر بتواند شرطهای مختلف را با هم ترکیب کند، باید از ترکیب IQueryableها استفاده کرد تا سبب رفت و برگشت اضافی به بانک اطلاعاتی نشویم.
مشکل 8: استفاده از کوئریهای Like دار
var list = context.BlogPosts.Where(x => x.Content.Contains("test"))
مشکل 9: استفاده از Count بجای Any
اگر نیاز است بررسی کنید مجموعهای دارای مقداری است یا خیر، از Count>0 استفاده نکنید. کارآیی Any و کوئری SQL ایی که تولید میکند، به مراتب بیشتر و بهینهتر است از Count>0.
مشکل 10: سرعت insert پایین است
ردیابی تغییرات را خاموش کرده و از متد جدید AddRange استفاده کنید. همچنین افزونههایی برای Bulk insert نیز موجود هستند.
مشکل 11: شروع برنامه کند است
میتوان تمام مباحث نگاشتهای پویای کلاسهای برنامه به جداول و روابط بانک اطلاعاتی را به صورت کامپایل شده در برنامه ذخیره کرد. این مورد سبب بالا رفتن سرعت شروع برنامه خصوصا در حالتیکه تعداد جداول بالا است میشود.