اعمال توابع تجمعی بر روی چند ستون در Entity framework
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: دو دقیقه

فرض کنید که می‌خواهیم معادل کوئری زیر را که اعمال توابع تجمعی به چند ستون است،
 SELECT sum([Rating_TotalRating]), sum([Rating_TotalRaters]), sum([Rating_AverageRating]) FROM [BlogPosts]
در Entity framwork به کمک LINQ to Entities تهیه کنیم.
نکته‌ای که در اینجا وجود دارد، نبود گروه بندی (حداقل به ظاهر) در کوئری نوشته شده است. اما واقعیت این است که یک بانک اطلاعاتی به صورت ضمنی در مورد یک چنین کوئری‌هایی نیز گروه بندی را انجام می‌دهد. برای اینکار، کل رکوردهای مدنظر را یک گروه تصور می‌کند.
اگر سعی کنیم چنین کوئری را توسط عبارات LINQ ایجاد کنیم، در سعی اول به چنین کوئری خواهیم رسید که اصلا کامپایل نمی‌شود:
                context.BlogPost.Select(r =>
                                        new
                                        {
                                            Sum1 = r.Sum(x => x.RatingTotalRating),
                                            Sum2 = r.Sum(x => x.RatingTotalRaters),
                                            Sum3 = r.Sum(x => x.RatingAverageRating)
                                        }).FirstOrDefault();
بنابراین به نظر می‌رسد که شاید بهتر باشد از روش ذیل استفاده کنیم:
 var sum1 = context.BlogPost.Sum(x => x.RatingTotalRating);
var sum2 = context.BlogPost.Sum(x => x.RatingTotalRaters);
var sum2 = context.BlogPost.Sum(x => x.RatingAverageRating);
این روش کار می‌کند و نهایتا معادل نتایج کوئری اول را نیز حاصل خواهد کرد؛ اما با سه بار رفت و برگشت به بانک اطلاعاتی که اصلا بهینه نیست.

راه حل: ایجاد گروه بندی ضمنی SQL به صورت صریح در عبارات LINQ

                context.BlogPost
                       .GroupBy(dummyNumber => 0)
                       .Select(r =>
                                        new
                                        {
                                            Sum1 = r.Sum(x => x.RatingTotalRating),
                                            Sum2 = r.Sum(x => x.RatingTotalRaters),
                                            Sum3 = r.Sum(x => x.RatingAverageRating)
                                        }).FirstOrDefault();
در این کوئری جدید که بر اساس عدد ثابت صفر گروه بندی شده است، یک چنین SQL ایی تولید می‌شود:
SELECT TOP (1) 
                        [Extent1].[K1] AS [K1], 
                        Sum([Extent1].[A1]) AS [A1], 
                        Sum([Extent1].[A2]) AS [A2],
                        Sum([Extent1].[A3]) AS [A3]
                        FROM ( SELECT 
                            0 AS [K1], 
                            [Extent1].[RatingTotalRating] AS [A1], 
                            [Extent1].[RatingTotalRaters] AS [A2],
       [Extent1].[RatingAverageRating] AS [A3]
                            FROM [dbo].[BlogPosts] AS [Extent1]
                        )  AS [Extent1]
                        GROUP BY [K1]
ابتدا یک ستون فرضی با مقدار ثابت صفر به رکوردها اضافه می‌شود. سپس بر اساس این ستون فرضی، کلیه ردیف‌ها گروه بندی شده و در ادامه توابع تجمعی بر روی آن‌ها اعمال می‌گردند. به این ترتیب تعداد رفت و برگشت‌ها به بانک اطلاعاتی به همان یک مورد کاهش خواهد یافت.
  • #
    ‫۱ سال و ۵ ماه قبل، چهارشنبه ۲۴ اسفند ۱۴۰۱، ساعت ۱۷:۰۴
    چگونه توسط EF Core، چندین کوئری را یکجا به بانک اطلاعاتی ارسال کنیم؟

    روشی را که در این مطلب مشاهده کردید، در موارد مشابه دیگری هم قابل استفاده‌است. برای مثال فرض کنید اطلاعات یک مشتری را قرار است به صورت زیر ذخیره کنیم:
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; } = null!;
        public CustomerType Type { get; set; }
    }
    
    public enum CustomerType
    {
        Individual,
        Institution,
    }

    حالت عادی کوئری گرفتن از اطلاعات جدول آن که به همراه صفحه بندی، نمایش تعداد رکوردها و یک کوئری دلخواه دیگر باشد، به صورت زیر است:
    void ManyQueriesManyCalls()
    {
        using var scope = serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();
    
        var baseQuery = context.Customers.Select(customer => new
                                                             {
                                                                 customer.Name,
                                                                 customer.Type,
                                                                 customer.Id,
                                                             });
        var total = baseQuery.Count();
        var types = baseQuery.GroupBy(x => x.Type)
                             .Select(x => x.Key).ToList();
        var pageSize = 10;
        var pageIndex = 0;
        var results = baseQuery
                      .OrderBy(x => x.Id)
                      .Skip(pageSize * pageIndex)
                      .Take(pageSize)
                      .ToList();
        Console.WriteLine($"Total:{total}, First Type: {types.First()}, First Item: {results.First().Name}");
    }
    که سبب می‌شود سه کوئری و سه بار رفت و برگشت را به بانک اطلاعاتی داشته باشیم:
          SELECT COUNT(*)
          FROM [Customers] AS [c]
    
          SELECT [c].[Type]
          FROM [Customers] AS [c]
          GROUP BY [c].[Type]
      
          SELECT [c].[Name], [c].[Type], [c].[Id]
          FROM [Customers] AS [c]
          ORDER BY [c].[Id]
          OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

    اگر بخواهیم این سه کوئری را یکبار به سمت بانک اطلاعاتی ارسال کنیم، می‌توان از همان ترفند گروه بندی مطرح شده‌ی در این مثال برای ترکیب کوئری‌ها استفاده کرد:
    void ManyQueriesOnCall()
    {
        using var scope = serviceProvider.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<CustomerContext>();
        var baseQuery = context.Customers.Select(customer => new
                                                             {
                                                                 customer.Name,
                                                                 customer.Type,
                                                                 customer.Id,
                                                             });
        var pageSize = 10;
        var pageIndex = 0;
        var allTogether = baseQuery
                          .GroupBy(x => 1)
                          .Select(bq => new
                                        {
                                            Total = baseQuery.Count(),
                                            Types = baseQuery.GroupBy(x => x.Type)
                                                             .Select(x => x.Key)
                                                             .ToList(),
                                            Results = baseQuery
                                                      .OrderBy(x => x.Id)
                                                      .Skip(pageSize * pageIndex)
                                                      .Take(pageSize)
                                                      .ToList(),
                                        })
                          .FirstOrDefault();
    
        Console.WriteLine($"Total:{allTogether.Total}, First Type: {allTogether.Types.First()}, First Item: {allTogether.Results.First().Name}");
    }
    که اینبار فقط یک کوئری outer apply دار را تولید می‌کند و فقط یکبار، رفت و برگشت به بانک اطلاعاتی را شاهد خواهیم بود:
          SELECT [t0].[Key], [t1].[Type], [t2].[Name], [t2].[Type], [t2].[Id]
          FROM (
              SELECT TOP(1) [t].[Key]
              FROM (
                  SELECT 1 AS [Key]
                  FROM [Customers] AS [c]
              ) AS [t]
              GROUP BY [t].[Key]
          ) AS [t0]
          OUTER APPLY (
              SELECT [c0].[Type]
              FROM [Customers] AS [c0]
              GROUP BY [c0].[Type]
          ) AS [t1]
          OUTER APPLY (
              SELECT [c1].[Name], [c1].[Type], [c1].[Id]
              FROM [Customers] AS [c1]
              ORDER BY [c1].[Id]
              OFFSET @__p_1 ROWS FETCH NEXT @__pageSize_2 ROWS ONLY
          ) AS [t2]
          ORDER BY [t0].[Key], [t1].[Type], [t2].[Id]

    کدهای این مثال را از اینجا می‌توانید دریافت کنید: EF7ManyQueriesOneCall.zip