در این مقاله، به بررسی عمیق ماهیت این مشکل، نحوه شناسایی آن و استراتژیهای مختلف برای حل آن در EF Core میپردازیم.
برای درک این موضوع، فرض کنید دو موجودیت (Entity) دارید: Blog و Post. هر وبلاگ میتواند چندین پست داشته باشد.
اگر بخواهید لیست تمام وبلاگها را به همراه عنوان پستهایشان نمایش دهید، در سناریوی N+1 اتفاق زیر میافتد:
۱ کوئری برای گرفتن تمام وبلاگها اجرا میشود (مثلاً ۱۰ وبلاگ برمیگرداند).
برای هر وبلاگ، یک کوئری جداگانه برای واکشی پستهای آن اجرا میشود.
در مجموع ۱ + ۱۰ کوئری به دیتابیس ارسال میشود.
اگر تعداد وبلاگها ۱۰۰۰ عدد باشد، شما ۱۰۰۱ کوئری خواهید داشت که باعث کندی شدید اپلیکیشن و مصرف بیرویه منابع دیتابیس میشود.
عامل اصلی بروز این مشکل، Lazy Loading (بارگذاری تنبل) است. در این حالت، EF Core دادههای مرتبط را تا زمانی که مستقیماً به آنها دسترسی پیدا نکردهاید، لود نمیکند.
مثال کد مخرب:
using (var context = new MyDbContext())
{
// ۱ کوئری برای گرفتن تمام وبلاگها
var blogs = context.Blogs.ToList();
foreach (var blog in blogs)
{
// به ازای هر تکرار، یک کوئری جدید برای پستها اجرا میشود
foreach (var post in blog.Posts)
{
Console.WriteLine($"{blog.Name} - {post.Title}");
}
}
}
پیش از حل مشکل، باید آن را پیدا کنید. چندین راه برای این کار وجود دارد:
Logging: سادهترین راه، بررسی لاگهای تولید شده توسط EF Core در کنسول است. اگر دیدید تعداد زیادی دستور SELECT مشابه پشت سر هم اجرا میشوند، شما دچار N+1 شدهاید.
SQL Server Profiler: ابزاری برای مشاهده مستقیم کوئریهای ارسالی به SQL Server.
MiniProfiler: یک کتابخانه عالی برای مانیتورینگ عملکرد در محیط توسعه که تعداد کوئریهای هر درخواست را نشان میدهد.
برای عبور از این بحران، سه رویکرد اصلی در EF Core وجود دارد:
الف) Eager Loading (بارگذاری مشتاقانه)
در این روش، شما به EF Core میگویید که دادههای مرتبط را در همان کوئری اول و با استفاده از JOIN واکشی کند. این کار با متد .Include() انجام میشود.
var blogs = context.Blogs
.Include(b => b.Posts) // تمام پستها را در همان ابتدا میآورد
.ToList();
مزیت: تنها یک کوئری به دیتابیس ارسال میشود.
عیب: اگر تعداد جداول مرتبط زیاد باشد، حجم دادههای برگشتی (نتیجه Join) بسیار بزرگ میشود که به آن Cartesian Explosion میگوییم.
ب) استفاده از Projection (انتخاب فیلدها)
این بهترین رویکرد از نظر عملکرد است. به جای لود کردن تمام ستونهای موجودیت، فقط فیلدهایی که نیاز دارید را با .Select() انتخاب کنید.
var blogData = context.Blogs
.Select(b => new
{
BlogName = b.Name,
PostTitles = b.Posts.Select(p => p.Title).ToList()
})
.ToList();
در این حالت، EF Core بهینهترین کوئری ممکن را میسازد و مشکل N+1 کاملاً منتفی میشود.
ج) Explicit Loading (بارگذاری صریح)
اگر دادهها را قبلاً واکشی کردهاید و حالا در شرایط خاصی نیاز به دادههای مرتبط دارید، میتوانید به صورت دستی آنها را لود کنید.
var blog = context.Blogs.First();
context.Entry(blog).Collection(b => b.Posts).Load();
این روش هنوز هم کوئری اضافه ایجاد میکند، اما به شما کنترل میدهد که چه زمانی این اتفاق بیفتد.
از EF Core 5.0 به بعد، قابلیتی به نام Split Queries اضافه شده است. برای حل مشکل Cartesian Explosion در Includeهای متعدد، میتوانید کوئری را بشکنید:
var blogs = context.Blogs
.Include(b => b.Posts)
.AsSplitQuery() // کوئریها را جداگانه اما بهینه اجرا میکند
.ToList();
این کار باعث میشود یک کوئری برای وبلاگها و یک کوئری برای تمام پستهای مربوط به آن وبلاگها اجرا شود (مجموعاً ۲ کوئری به جای N+1).
|
روش |
تعداد کوئری |
پیچیدگی کد |
موارد استفاده |
|
Lazy Loading |
N + 1 |
بسیار پایین |
پروژههای کوچک یا دیتای کم |
|
Eager Loading |
1 |
متوسط |
زمانی که تمام موجودیت نیاز است |
|
Projection |
1 |
متوسط |
بهترین انتخاب عمومی |
|
Split Queries |
1 + \text{Relations} |
پایین |
زمانی که Joinها سنگین هستند |
مشکل N+1 میتواند یک اپلیکیشن سریع را به زانو درآورد. برای جلوگیری از آن:
Lazy Loading را غیرفعال کنید: این کار باعث میشود اگر فراموش کردید دادهای را لود کنید، با خطا مواجه شوید و متوجه مشکل شوید، نه اینکه اپلیکیشن در سکوت کند شود.
همیشه از Select استفاده کنید: سعی کنید تا جای ممکن از View Modelها یا DTOها استفاده کنید تا فقط دیتای لازم واکشی شود.
مانیتورینگ: همیشه تعداد کوئریهای خروجی را در محیط توسعه بررسی کنید.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.