عنوان:

‫نحوه‌ی اعمال فیلتر بر روی روی داده‌های مرتبط (جوین‌شده) در Entity Framework Core


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۱/۲۴ ۱۹:۰۱
آدرس: www.dntips.ir
در ادامه‌ی مطلب «بهینه‌سازی عملکرد کوئری‌ها در EF Core و SQL با فیلتر به‌هنگام اطلاعات» یکی از دلایل اصلی معرفی Filtered Includes در Entity Framework Core (از نسخه ۵ به بعد) این است که بتوانید روی داده‌های مرتبط (جوین‌شده) فیلتر اعمال کنید، بدون اینکه نیاز به استفاده از کوئری‌های پیچیده یا چندین مرحله‌ای باشد.

جزئیات Filtered Includes
  • هدف: Filtered Includes به شما اجازه می‌دهد در زمان لود داده‌های مرتبط (مثل روابط یک‌به‌چند یا چندبه‌چند) شرط‌هایی را اعمال کنید تا فقط داده‌های موردنظر بارگذاری شوند.
  • مزیت: این کار باعث کاهش حجم داده‌های لودشده از دیتابیس و بهبود عملکرد کوئری می‌شود. همچنین کد را خواناتر و تمیزتر می‌کند.

تفاوت باWhereمعمولی:
متد Where روی کل مجموعه داده‌های اصلی (root entity) اعمال می‌شود، اما Filtered Includes روی داده‌های مرتبط داخل Include کار می‌کند.

سینتکس Filtered Includes
سینتکس کلی به این صورت است:
var query = dbContext.Entity
    .Include(e => e.RelatedEntities.Where(condition));
در اینجا:
  • Entity: جدول یا موجودیت اصلی.
  • RelatedEntities: مجموعه داده‌های مرتبط (مثل یک لیست در رابطه یک‌به‌چند).
  • condition: شرط فیلتری که روی داده‌های مرتبط اعمال می‌شود.

مثال عملی
فرض کنید دو مدل زیر را داریم:
public class Order
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
    public List<OrderItem> OrderItems { get; set; }
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

سناریو: لود سفارشات با آیتم‌های خاص
می‌خواهیم تمام سفارشات را به همراه آیتم‌های سفارشی که تعدادشان (Quantity) بیشتر از ۲ است لود کنیم.
بدون Filtered Includes، باید به این صورت عمل می‌کردیم:
var query = dbContext.Orders
    .Select(o => new
    {
        Order = o,
        OrderItems = o.OrderItems.Where(oi => oi.Quantity > 2).ToList()
    });
این روش چند مشکل دارد:
  • نتیجه به صورت یک شیء ناشناس (anonymous type) برگردانده می‌شود، نه خود مدل Order.
  • ممکن است نیاز به پردازش اضافی در سمت کلاینت داشته باشد.
با Filtered Includes، می‌توانیم به سادگی بنویسیم:
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 2));
  • نتیجه: تمام سفارشات لود می‌شوند، اما فقط آیتم‌هایی که Quantity > 2 دارند در مجموعه OrderItems هر سفارش قرار می‌گیرند.
  • خروجی SQL: EF Core یک کوئری SQL تولید می‌کند که جوین را با شرط فیلتر ترکیب می‌کند، بنابراین فقط داده‌های موردنظر از دیتابیس لود می‌شوند.

نکات مهم درباره Filtered Includes
  1. پشتیبانی از روابط یک‌به‌چند و چندبه‌چند:
  • Filtered Includes در روابط یک‌به‌چند (مثل Order و OrderItems) و چندبه‌چند (با استفاده از جدول واسطه) کار می‌کند.
  • برای روابط یک‌به‌یک، معمولاً نیازی به فیلتر نیست، چون فقط یک موجودیت مرتبط وجود دارد.

ترکیب با Include‌های دیگر: می‌توانید Filtered Includes را با Includeهای دیگر ترکیب کنید:
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 2))
    .Include(o => o.Customer);
در اینجا، آیتم‌های فیلترشده و اطلاعات مشتری به طور همزمان لود می‌شوند.

ترکیب با ThenInclude: برای روابط عمیق‌تر، می‌توانید از ThenInclude استفاده کنید:
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 2))
        .ThenInclude(oi => oi.Product);
این کوئری آیتم‌های فیلترشده را همراه با اطلاعات محصول مرتبطشان لود می‌کند.

تأثیر بر عملکرد:
  • Filtered Includes معمولاً کوئری‌های بهینه‌تری تولید می‌کند، چون فیلتر در سطح SQL اعمال می‌شود و داده‌های غیرضروری از دیتابیس لود نمی‌شوند.
  • با این حال، باید مراقب باشید که شرط‌های پیچیده در فیلتر باعث تولید کوئری‌های سنگین نشوند.

محدودیت‌ها:
  • Filtered Includes فقط روی مجموعه‌ها (collections) کار می‌کند، نه روی روابط تک‌موجودیتی (single entity).
  • در برخی موارد پیچیده، ممکن است همچنان نیاز به استفاده از Select یا کوئری‌های خام SQL باشد.

مقایسه با روش‌های دیگر
قبل از Filtered Includes، برای فیلتر کردن داده‌های مرتبط، معمولاً از روش‌های زیر استفاده می‌شد:
  • Select با پروجکشن: همان‌طور که در مثال بالا دیدیم، این روش انعطاف‌پذیر است، اما ممکن است مدل‌های اصلی را تغییر دهد یا خوانایی کد را کاهش دهد.
  • Explicit Loading: لود کردن داده‌های مرتبط به صورت دستی با Load، که نیاز به چندین درخواست به دیتابیس دارد.
  • کوئری‌های خام SQL: نوشتن کوئری SQL به صورت دستی، که کنترل کامل می‌دهد، اما نگهداری کد را سخت‌تر می‌کند.

Filtered Includes این مشکلات را تا حد زیادی حل کرده و تعادل خوبی بین خوانایی، عملکرد و انعطاف‌پذیری ارائه می‌دهد.

خروجی SQL نمونه
برای کوئری زیر:
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 2));
EF Core کوئری SQL مشابه این تولید می‌کند:
SELECT o.*, oi.*
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId AND oi.Quantity > 2
این کوئری تضمین می‌کند که فقط آیتم‌های موردنظر در زمان جوین لود شوند.

برای توضیح پیشرفته‌تر درباره Filtered Includes در Entity Framework Core، چند مثال پیچیده‌تر ارائه می‌دهم که سناریوهای واقعی‌تر و کاربردی را پوشش می‌دهند. این مثال‌ها نشان می‌دهند چگونه می‌توان از Filtered Includes در روابط چندسطحی، ترکیب با سایر متدها، فیلترهای پیچیده، و سناریوهای خاص استفاده کرد. فرض می‌کنیم مدل‌های زیر را داریم:
public class Order
{
    public int Id { get; set; }
    public decimal TotalAmount { get; set; }
    public DateTime OrderDate { get; set; }
    public int CustomerId { get; set; }
    public Customer Customer { get; set; }
    public List<OrderItem> OrderItems { get; set; }
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
    public Product Product { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Order> Orders { get; set; }
}

public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}
مثال ۱: فیلتر چندسطحی با Filtered Includes و ThenInclude
سناریو: می‌خواهیم تمام سفارشات را به همراه آیتم‌هایی که تعدادشان بیشتر از ۳ است لود کنیم، و برای هر آیتم، فقط محصولاتی که در دسته‌بندی خاصی (مثلاً "Electronics") هستند بارگذاری شوند.
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 3))
        .ThenInclude(oi => oi.Product.Where(p => p.Category.Name == "Electronics"));
توضیح:
  • Include اول فقط آیتم‌های سفارش با Quantity > 3 را لود می‌کند.
  • ThenInclude روی محصولات مرتبط با آیتم‌های فیلترشده اعمال می‌شود و فقط محصولاتی که در دسته‌بندی "Electronics" هستند را شامل می‌شود.
کاربرد: این نوع کوئری برای گزارش‌گیری یا نمایش داده‌های فیلترشده در داشبوردهای پیچیده مفید است.
  • خروجی SQL تقریبی:
SELECT o.*, oi.*, p.*
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId AND oi.Quantity > 3
LEFT JOIN Products p ON oi.ProductId = p.Id
INNER JOIN Categories c ON p.CategoryId = c.Id AND c.Name = 'Electronics'

مثال ۲: ترکیب Filtered Includes با فیلتر روی موجودیت اصلی
سناریو: می‌خواهیم فقط سفارشات سال ۲۰۲۴ را لود کنیم، و برای هر سفارش، فقط آیتم‌هایی که قیمت واحدشان بیشتر از ۵۰ است بارگذاری شوند، به همراه اطلاعات مشتری.
var query = dbContext.Orders
    .Where(o => o.OrderDate.Year == 2024)
    .Include(o => o.OrderItems.Where(oi => oi.Price > 50))
    .Include(o => o.Customer);
توضیح:
  • Where اول روی جدول اصلی (Orders) اعمال می‌شود و فقط سفارشات سال ۲۰۲۴ را انتخاب می‌کند.
  • Include اول آیتم‌های سفارش با قیمت بیش از ۵۰ را لود می‌کند.
  • Include دوم اطلاعات مشتری را برای هر سفارش اضافه می‌کند.
کاربرد: این نوع کوئری برای سناریوهایی مثل تحلیل فروش در یک بازه زمانی خاص با تمرکز روی محصولات گران‌قیمت مناسب است.
نکته عملکردی: قرار دادن Where قبل از Include باعث می‌شود داده‌های کمتری از جدول اصلی لود شوند، که بهینه‌تر است.

مثال ۳: Filtered Includes با چندین شرط ترکیبی
سناریو: می‌خواهیم سفارشاتی را لود کنیم که حداقل یک آیتم با تعداد بیشتر از ۵ و قیمت بالاتر از ۱۰۰ داشته باشند، و فقط آن آیتم‌ها را در نتایج بیاوریم.
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 5 && oi.Price > 100))
    .Where(o => o.OrderItems.Any(oi => oi.Quantity > 5 && oi.Price > 100));
توضیح:
  • Include آیتم‌هایی را لود می‌کند که هم Quantity > 5 و هم Price > 100 باشند.
  • Where روی جدول اصلی تضمین می‌کند که فقط سفارشاتی برگردانده شوند که حداقل یک آیتم با این شرایط داشته باشند (بدون این شرط، سفارشات بدون آیتم هم ممکن است برگردانده شوند).
کاربرد: این برای سناریوهایی مثل شناسایی سفارشات با آیتم‌های خاص (مثلاً اقلام لوکس با تعداد بالا) مفید است.
خروجی SQL تقریبی:
SELECT o.*, oi.*
FROM Orders o
LEFT JOIN OrderItems oi ON o.Id = oi.OrderId AND oi.Quantity > 5 AND oi.Price > 100
WHERE EXISTS (
    SELECT 1
    FROM OrderItems oi2
    WHERE oi2.OrderId = o.Id AND oi2.Quantity > 5 AND oi2.Price > 100
)

مثال ۴: Filtered Includes در روابط چندبه‌چند
سناریو: فرض کنید یک رابطه چندبه‌چند بین Customer و Order از طریق یک جدول واسطه (مثلاً CustomerOrder) داریم. می‌خواهیم مشتریانی را لود کنیم که سفارشاتشان در سال ۲۰۲۴ باشد، و فقط آن سفارشات را شامل کنیم.
var query = dbContext.Customers
    .Include(c => c.Orders.Where(o => o.OrderDate.Year == 2024));
توضیح:
  • Include فقط سفارشاتی را لود می‌کند که در سال ۲۰۲۴ ثبت شده‌اند.
  • این کار مستقیماً روی رابطه چندبه‌چند اعمال می‌شود.
کاربرد: این برای تحلیل رفتار مشتریان در یک بازه زمانی خاص مفید است.
نکته: اگر رابطه چندبه‌چند از طریق یک جدول واسطه صریح تعریف شده باشد، ممکن است نیاز به مدل‌سازی دقیق‌تر یا استفاده از ThenInclude برای دسترسی به ویژگی‌های جدول واسطه داشته باشید.

مثال ۵: Filtered Includes با مرتب‌سازی و پروجکشن
سناریو: می‌خواهیم سفارشات را به همراه آیتم‌هایشان که تعدادشان بیشتر از ۲ است لود کنیم، آیتم‌ها را بر اساس قیمت مرتب کنیم، و فقط برخی فیلدها را در خروجی بیاوریم.
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 2))
    .Select(o => new
    {
        OrderId = o.Id,
        TotalAmount = o.TotalAmount,
        OrderItems = o.OrderItems.OrderBy(oi => oi.Price).Select(oi => new
        {
            oi.Id,
            oi.Quantity,
            oi.Price
        })
    });
توضیح:
  • Include آیتم‌های با Quantity > 2 را لود می‌کند.
  • OrderBy آیتم‌ها را بر اساس قیمت مرتب می‌کند.
  • Select خروجی را به یک مدل سبک‌تر محدود می‌کند تا فقط فیلدهای موردنیاز برگردانده شوند.
کاربرد: این نوع کوئری برای APIهایی که نیاز به خروجی‌های سبک و مرتب‌شده دارند مناسب است.
نکته: اگرچه اینجا از Select استفاده کردیم، Filtered Includes همچنان نقش اصلی را در فیلتر کردن داده‌های مرتبط ایفا می‌کند.

نکات پیشرفته و محدودیت‌ها
ترکیب با AsNoTracking: اگر نیازی به ردیابی تغییرات ندارید، می‌توانید از AsNoTracking برای بهبود عملکرد استفاده کنید:
var query = dbContext.Orders
    .AsNoTracking()
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 3));
محدودیت در فیلترهای پیچیده: Filtered Includes از عبارات ساده (مثل &&, ||) پشتیبانی می‌کند، اما برای شرایط بسیار پیچیده (مثل زیرکوئری‌های تودرتو)، ممکن است نیاز به استفاده از کوئری‌های خام یا روش‌های دیگر داشته باشید.
ترکیب با Split Queries: برای کوئری‌های سنگین با چندین Include، می‌توانید از AsSplitQuery استفاده کنید تا کوئری به چندین درخواست SQL تقسیم شود:
var query = dbContext.Orders
    .AsSplitQuery()
    .Include(o => o.OrderItems.Where(oi => oi.Quantity > 3))
    .Include(o => o.Customer);
کار با نال‌ها: اگر داده‌های مرتبط ممکن است نال باشند، باید مراقب باشید که شرط‌های فیلتر باعث خطا نشوند. مثلاً:
var query = dbContext.Orders
    .Include(o => o.OrderItems.Where(oi => oi.Product != null && oi.Product.Price > 100));

نتیجه‌گیری
Filtered Includes در سناریوهای پیشرفته مثل فیلترهای چندسطحی، ترکیب با مرتب‌سازی، پروجکشن، و روابط پیچیده بسیار قدرتمند است. این قابلیت به شما امکان می‌دهد کوئری‌های بهینه و خوانا بنویسید و از لود داده‌های غیرضروری جلوگیری کنید. با این حال، برای کوئری‌های بسیار پیچیده، ممکن است همچنان نیاز به ترکیب آن با سایر تکنیک‌ها مثل Select، کوئری‌های خام، یا Split Queries داشته باشید.


مشاهده مطلب اصلی