مقدمه

در درس‌های گذشته، با مفاهیم پایه‌ای EF Core مانند تعریف DbContext، ایجاد مدل‌ها و انجام عملیات پایه‌ای CRUD (ایجاد، خواندن، به‌روزرسانی، حذف) آشنا شدیم. دیدیم که EF Core از LINQ برای ترجمه‌ی کوئری‌های C# به SQL استفاده می‌کند. این ویژگی، یکی از قدرتمندترین جنبه‌های EF Core است، زیرا به ما اجازه می‌دهد تا با استفاده از یک زبان واحد و امن از نظر نوع (type-safe)، کوئری‌های پیچیده‌ای را بر روی پایگاه داده اجرا کنیم.

در این درس، به بررسی عمیق‌تر نحوه‌ی تعامل LINQ و EF Core می‌پردازیم. خواهیم دید که چگونه می‌توانیم کوئری‌های پیچیده‌تری شامل join، گروه‌بندی و محاسبات تجمعی را با استفاده از LINQ بنویسیم و EF Core چگونه آن‌ها را به SQL بهینه ترجمه می‌کند.

مدل داده‌ی توسعه‌یافته

برای نمایش کوئری‌های پیچیده‌تر، مدل داده‌ی خود را کمی گسترش می‌دهیم. فرض کنید هر محصول به یک "دسته" (Category) تعلق دارد. ابتدا کلاس Category را اضافه می‌کنیم:

Copy Icon Category.cs
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Product> Products { get; set; } = new();
}

سپس، کلاس Product را برای ایجاد رابطه‌ی یک-به-چند با Category به‌روزرسانی می‌کنیم:

Copy Icon Product.cs
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // Foreign Key property
    public int CategoryId { get; set; }
    // Navigation property
    public Category Category { get; set; }
}

در نهایت، DbSet مربوط به دسته‌بندی‌ها را به MyDbContext اضافه کرده و با اجرای دستورات add-migration و update-database، پایگاه داده را به‌روزرسانی می‌کنیم.

کوئری‌های شامل Join

یکی از رایج‌ترین کارها در کار با پایگاه‌های داده‌ی رابطه‌ای، اتصال (join) جداول به یکدیگر است. در EF Core، این کار به صورت بسیار طبیعی و با استفاده از پراپرتی‌های ناوبری (navigation properties) در کوئری LINQ انجام می‌شود.

فرض کنید می‌خواهیم نام محصول و نام دسته‌بندی آن را برای تمام محصولات به دست آوریم.

Copy Icon Program.cs
using (var db = new MyDbContext())
{
    // Using navigation properties automatically translates to a SQL JOIN.
    var productWithCategory = db.Products
        .Select(p => new 
        { 
            ProductName = p.Name, 
            CategoryName = p.Category.Name // Accessing the related Category entity.
        })
        .ToList();

    foreach (var item in productWithCategory)
    {
        Console.WriteLine($"{item.ProductName} is in category '{item.CategoryName}'");
    }
}

EF Core به اندازه‌ی کافی هوشمند است که با دیدن p.Category.Name، بفهمد که شما نیاز به اتصال جدول Products به جدول Categories بر اساس کلید خارجی دارید و یک کوئری SQL بهینه با INNER JOIN تولید می‌کند.

گروه‌بندی و محاسبات تجمعی

ما می‌توانیم از عملگر GroupBy برای گروه‌بندی نتایج و سپس از عملگرهای تجمعی مانند Count, Sum, Average, Max, Min برای انجام محاسبات بر روی هر گروه استفاده کنیم.

برای مثال، بیایید تعداد محصولات در هر دسته‌بندی را محاسبه کنیم.

Copy Icon Program.cs
using (var db = new MyDbContext())
{
    var categoryCounts = db.Products
        .GroupBy(p => p.Category.Name) // Group by the category name.
        .Select(g => new 
        { 
            Category = g.Key,         // The key of the group is the category name.
            ProductCount = g.Count()    // Count the items in each group.
        })
        .ToList();

    Console.WriteLine("\n--- Product Count per Category ---");
    foreach (var item in categoryCounts)
    {
        Console.WriteLine($"{item.Category}: {item.ProductCount}");
    }
}

EF Core این کوئری پیچیده را به یک کوئری SQL کارآمد که از GROUP BY و COUNT(*) استفاده می‌کند، ترجمه خواهد کرد. این قابلیت به ما اجازه می‌دهد تا گزارش‌های آماری پیچیده‌ای را تنها با چند خط کد C# بنویسیم.

محدودیت‌های ترجمه LINQ

هرچند مترجم LINQ در EF Core بسیار قدرتمند است، اما نمی‌تواند تمام متدهای .NET را به SQL ترجمه کند. شما فقط می‌توانید از متدهایی در کوئری LINQ خود استفاده کنید که EF Core یک معادل SQL برای آن‌ها می‌شناسد. برای مثال، شما نمی‌توانید یک متد سفارشی که خودتان نوشته‌اید را مستقیماً در عبارت Where یا Select فراخوانی کنید، زیرا پایگاه داده هیچ درکی از آن متد ندارد.

اگر یک عملیات پیچیده دارید که در SQL قابل پیاده‌سازی نیست، باید ابتدا داده‌های لازم را از پایگاه داده واکشی کرده (مثلاً با ToList()) و سپس عملیات خود را بر روی آن کالکشن در حافظه و با استفاده از LINQ to Objects انجام دهید.