کوئری نویسی در EF Core - قسمت هشتم - کوئری‌های بازگشتی
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

جدول اعضای این مجموعه، خود ارجاع دهنده طراحی شده‌است:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

       // ...
    }
}
در اینجا RecommendedBy، یک کلید خارجی نال پذیر است که به Id همین جدول اشاره می‌کند. دو خاصیت دیگر تعریف شده، مکمل این خاصیت عددی، جهت سهولت کوئری نویسی‌های EF-Core هستند که در قسمت‌های قبل نیز تعدادی کوئری را در این زمینه مشاهده کردید؛ مانند:
- تولید لیست کاربرانی که کاربر دیگری را توصیه کرده‌اند.
- تولید لیست کاربران، به همراه توصیه کننده‌ی آن‌ها.
- تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها، بدون استفاده از جوین.
- هر کاربر چه تعداد کاربر دیگری را توصیه کرده‌است؟

در این قسمت تعدادی مثال بازگشتی را می‌خواهیم بررسی کنیم.


مثال 1: رنجیره‌ی توصیه کنندگان کاربر با ID مساوی 27 را محاسبه کنید.

می‌خواهیم بدانیم چه کسی کاربر 27 را توصیه کرده و همچنین این کاربر نیز توسط چه شخص دیگری توصیه شده و به همین ترتیب تا بالاترین سطح ممکن.

روش معمول انجام این نوع کوئری‌ها استفاده از «WITH Hierachy» است. اما اگر بخواهیم بدون SQL نویسی مستقیم اینکار را انجام دهیم، می‌توان به صورت زیر عمل کرد:
var id = 27;
var entity27WithAllOfItsParents =
                        context.Members
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree


این کوئری ابتدا تمام رکوردهای جدول کاربران را لیست می‌کند. سپس خاصیت Recommender هر کدام را تا n سطح مقدار دهی می‌کند (خود EF-Core اینکار را انجام می‌دهد). تمام این اتفاقات تا قسمت ToList آن رخ می‌دهند. پس از آن یک FirstOrDefault سمت کاربر را هم داریم (LINQ to Objects). هدف از آن، بازگشت تنها ریشه‌ی مرتبط با ID=27 است و تمام Recommenderهای متصل به آن. این موارد را در تصویر ذیل بهتر می‌توانید مشاهده کنید:


لیست تمام کاربران وجود دارند. سپس سیزدهمین مورد آن، همان کاربر 27 است که توسط کاربر 20 توصیه شده. کاربر 20 توسط کاربر 5 توصیه شده و کاربر 5 توسط کاربر 1 و پس از آن خاصیت Recommender نال است که به معنای پایان پیمودن این زنجیره‌است.
بنابراین مرحله‌ی بعدی پس از یافتن ریشه‌ی کاربر 27، پیمودن خاصیت‌های Recommender به صورت بازگشتی است؛ کاری شبیه به متد FindParents زیر:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindParents(Member member, List<dynamic> actualResult)
        {
            if (member == null || member.Recommender == null)
            {
                return;
            }

            var item = member.Recommender;
            actualResult.Add(new { Recommender = item.MemId, item.FirstName, item.Surname });

            if (item.Recommender != null)
            {
                FindParents(item, actualResult);
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindParents(entity27WithAllOfItsParents, actualResult);


مثال 2: زنجیره‌ی توصیه شده‌های توسط کاربر با ID مساوی 1 را محاسبه کنید.

می‌خواهیم بدانیم کاربر 1، چه کسی را توصیه کرده و این کاربر نیز چه کاربر دیگری را توصیه کرده و به همین ترتیب تا پایین‌ترین سطح ممکن.
var id = 1;
var entity1WithAllOfItsDescendants =
                        context.Members
                            .Include(member => member.Children)
                            .Where(member => member.MemId == id
                                            || member.Children.Any(m => member.MemId == m.RecommendedBy))
                            .ToList() //It's a MUST - get all children from the database
                            .FirstOrDefault(x => x.MemId == id);// then get the root of the tree
این کوئری نیز شبیه به کوئری مثال قبلی است؛ با یک تفاوت. در اینجا Include(member => member.Children) هم ذکر شده‌است. هدف این است که EF-Core، خاصیت Children را تا n سطح ممکن به صورت خودکار مقدار دهی کند و این مورد دقیقا هدف اصلی مثال جاری است.
وجود Include، سبب تولید یک چنین کوئری می‌شود که در آن جدول کاربران با خودش جوین شده‌است:
SELECT   [m].[MemId],
         [m].[Address],
         [m].[FirstName],
         [m].[JoinDate],
         [m].[RecommendedBy],
         [m].[Surname],
         [m].[Telephone],
         [m].[ZipCode],
         [m0].[MemId],
         [m0].[Address],
         [m0].[FirstName],
         [m0].[JoinDate],
         [m0].[RecommendedBy],
         [m0].[Surname],
         [m0].[Telephone],
         [m0].[ZipCode]
FROM     [Members] AS [m]
         LEFT OUTER JOIN
         [Members] AS [m0]
         ON [m].[MemId] = [m0].[RecommendedBy]
WHERE    ([m].[MemId] = 1)
         OR EXISTS (SELECT 1
                    FROM   [Members] AS [m1]
                    WHERE  ([m].[MemId] = [m1].[RecommendedBy])
                           AND ([m].[MemId] = [m1].[RecommendedBy]))
ORDER BY [m].[MemId], [m0].[MemId];
پس از آن باید خاصیت member.Children را تا هر سطح ممکن به صورت بازگشتی پیمود تا به جواب اصلی این مثال رسید:
namespace EFCorePgExercises.Exercises.RecursiveQueries
{
    public static class RecursiveUtils
    {
        public static void FindChildren(Member member, List<dynamic> actualResult)
        {
            if (member == null)
            {
                return;
            }

            foreach (var item in member.Children)
            {
                actualResult.Add(new { item.MemId, item.FirstName, item.Surname });
                if (item.Children != null)
                {
                    FindChildren(item, actualResult);
                }
            }
        }
    }
}
که به صورت زیر می‌تواند مورد استفاده قرار گیرد:
var actualResult = new List<dynamic>();
RecursiveUtils.FindChildren(entity1WithAllOfItsDescendants, actualResult);


کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
  • #
    ‫۴ سال قبل، سه‌شنبه ۲۱ مرداد ۱۳۹۹، ساعت ۲۲:۵۶
    سلام؛ در مثال 1 ، این قطعه کد باعث شده که بتونیم زنجیره‌ی توصیه کنندگان کاربر 27 را بگیریم ؟
    اگر جواب خیر است پس دقیقا چه کاری انجام میدهد ؟
       member.Children.Any(m => member.MemId == m.RecommendedBy)   ||
    • #
      ‫۴ سال قبل، چهارشنبه ۲۲ مرداد ۱۳۹۹، ساعت ۰۰:۲۷
      بله. آن‌را حذف کنید، فقط ردیف با ID مساوی 27 را خواهید داشت (چون حذف آن، سبب عدم مقدار دهی <ICollection<Member توسط EF-Core می‌شود). این ترکیب است که سبب جوین جدول کاربران با خودش می‌شود، بطوریکه زنجیره‌ی رو به بالای توصیه کننده‌ها (m.MemId = m0.RecommendedBy)، توسط EF-Core قابل تشخیص و تشکیل می‌شوند (یا همان امکان دسترسی به خاصیت member.Recommender به صورت بازگشتی در متد FindParents).
      در حین تعریف یک رابطه‌ی خود ارجاعی، خواص Reply (یا Recommender در اینجا) و Children کاملا به هم مرتبط هستند (و زمانیکه یک جدول با خودش جوین می‌شود، به صورت خودکار هر دوی این اشیاء و دو سر رابطه توسط EF-Core تشکیل می‌شوند):
      entity.HasOne(d => d.Reply)
                          .WithMany(p => p.Children)
                          .HasForeignKey(d => d.ReplyId);
  • #
    ‫۴ سال قبل، چهارشنبه ۲۲ مرداد ۱۳۹۹، ساعت ۱۹:۰۱
    راه حل بهتر!
    کتابخانه‌ی « linq2db » از CTEها و recursive CTE پشتیبانی می‌کند. می‌توان این کتابخانه را توسط « linq2db.EntityFrameworkCore » با EF-Core یکی کرد. برای کار با آن ابتدا نیاز است بسته‌ی نیوگت آن‌را نصب کنید:
    dotnet add package linq2db.EntityFrameworkCore
    سپس در ابتدای برنامه یکبار آ‌ن‌را فعال کنید:
    LinqToDB.EntityFrameworkCore.LinqToDBForEFTools.Initialize();
    LinqToDB.Data.DataConnection.TurnTraceSwitchOn();
    پس از آن به صورت زیر می‌توان از CTEها در کوئری‌های معمولی EF-Core استفاده کرد. برای مثال:

    راه حل مثال 1 با استفاده از یک recursive CTE
    می‌خواهیم لیست IDهای parent و childها را توسط یک recursive CTE تولید کنیم. به همین جهت ابتدا مدل معادل آن‌را تهیه می‌کنیم:
    public class MemberHierarchyCTE
    {
       public int ChildId { set; get; }
       public int? ParentId { set; get; }
    }
    سپس CTE زیر، این لیست را تهیه می‌کند:
    var memberHierarchyCte =
                        context.CreateLinqToDbContext().GetCte<MemberHierarchyCTE>(memberHierarchy =>
                        {
                            return
                                (
                                    from member in context.Members
                                    select new MemberHierarchyCTE
                                    {
                                        ChildId = member.MemId,
                                        ParentId = member.RecommendedBy
                                    }
                                )
                                .Concat
                                (
                                    from member in context.Members
                                    from hierarchy in memberHierarchy
                                                .InnerJoin(hierarchy => member.MemId == hierarchy.ParentId)
                                    select new MemberHierarchyCTE
                                    {
                                        ChildId = hierarchy.ChildId,
                                        ParentId = member.RecommendedBy
                                    }
                                );
                        });
    که به این صورت ترجمه خواهد شد:
    WITH [memberHierarchy] ([ChildId], [ParentId])
    AS
    (
        SELECT
            [member_1].[MemId],
            [member_1].[RecommendedBy]
        FROM
            [Members] [member_1]
        UNION ALL
        SELECT
            [hierarchy_1].[ChildId],
            [member_2].[RecommendedBy]
        FROM
            [Members] [member_2]
                INNER JOIN [memberHierarchy] [hierarchy_1] ON [member_2].[MemId] = [hierarchy_1].[ParentId]
    )
    و با کوئری گرفتن از آن برای مثال می‌توان لیست والدهای id=27 را تولید کرد (همان مثال 1):


    راه حل مثال 2 با استفاده از یک recursive CTE 
    و یا می‌توان لیست فرزندان id=1 را با کوئری گرفتن از این CTE تولید کرد (همان مثال 2):

    • #
      ‫۲ سال و ۹ ماه قبل، چهارشنبه ۱۰ آذر ۱۴۰۰، ساعت ۱۶:۱۱
      با سلام
      همینکار و انجام دادم 
      var memberHierarchyCte =
                          _dbContext.CreateLinqToDbContext().GetCte<MemberHierarchyCTE>(memberHierarchy =>
                          {
                              return
                                  (
                                      from navbar in _dbContext.Navbars
                                      select new MemberHierarchyCTE
                                      {
                                          ChildId = navbar.Id,
                                          ParentId = navbar.ParentId,
                                          Title = navbar.Title
                                      }
                                  )
                                  .Concat
                                  (
                                      from nav in _dbContext.Navbars
                                      from hierarchy in memberHierarchy
                                                  .InnerJoin(hierarchy => nav.Id == hierarchy.ParentId)
                                      select new MemberHierarchyCTE
                                      {
                                          ChildId = hierarchy.ChildId,
                                          ParentId = nav.ParentId,
                                          Title = nav.Title
                                      }
                                  );
                          });
      البته خطایی ندارم اما خروجی Result View به شکل زیره :


      • #
        ‫۲ سال و ۹ ماه قبل، پنجشنبه ۱۱ آذر ۱۴۰۰، ساعت ۱۴:۳۸
        این مثال کامل را پیگیری کنید. ذکر تمام قسمت‌های آن ضروری هست. همچنین ساختار داده هم باید یکی باشد. الان مجددا همین آزمایش را با EF-6x و آخرین نگارش وابستگی‌ها، آزمایش کردم و مشکلی نبود.