【问题标题】:Load filtered hierarchy from self-referencing table in EF Core从 EF Core 中的自引用表加载过滤的层次结构
【发布时间】:2021-05-05 11:45:47
【问题描述】:

我的项目中有一个自引用实体Comment

public class Comment
{
    public Guid CommentId { get; set; }
    
    public string Content { get; set; }
        
    public Guid? ParentCommentId { get; set; }
    public virtual Comment ParentComment { get; set; }
        
    public virtual ICollection<Comment> Children { get; set; }
}

我正在尝试执行以下操作:

await context.Comment
    .Where(c => c.ParentCommentId == null)
    .Include(c => c.Children)
    .ToListAsync();

我想获取所有根 cmets(没有父级的 cmets)并加载 cmets 的整个层次结构。

我想在结果中看到吗?

我在数据库中有以下 cmets:

为了使其更具可读性,我按层次顺序表示它(仅Content 属性):

  • 世界你好!
    • 您是程序员吗?
      • 当然
      • 什么?
  • 我也想去火星!
    • 月球见 :)

执行查询时,如上所示,我想要得到类似这样的东西(在 JSON 中):

[
   {
      "commentId":"be02742a-9170-4335-afe7-3c7c22684424",
      "content":"Hello World!",
      "parentCommentId":null,
      "children":[
         {
            "commentId":"59656765-d1ed-4648-8696-7d576ab7419f",
            "content":"Are you a programmer?",
            "parentCommentId":"be02742a-9170-4335-afe7-3c7c22684424",
            "children":[
               {
                  "commentId":"0bb77a43-c7bb-482f-9bf8-55c4050974da",
                  "content":"Sure",
                  "parentCommentId":"59656765-d1ed-4648-8696-7d576ab7419f",
                  "children":[
                     
                  ]
               },
               {
                  "commentId":"b8d61cfd-d274-4dae-a2be-72e08cfa9066",
                  "content":"What?",
                  "parentCommentId":"59656765-d1ed-4648-8696-7d576ab7419f",
                  "children":[
                     
                  ]
               }
            ]
         }
      ]
   },
   {
      "commentId":"cfe126b3-4601-4432-8c87-445c1362a225",
      "content":"I wanna go to Mars too!",
      "parentCommentId":null,
      "children":[
         {
            "commentId":"ab6d6b49-d772-48cd-9477-8d40f133c37a",
            "content":"See you on the Moon :)",
            "parentCommentId":"cfe126b3-4601-4432-8c87-445c1362a225",
            "children":[
               
            ]
         }
      ]
   }
]

但是我得到了什么?

当我执行这个查询时,我得到以下结果:

[
   {
      "commentId":"be02742a-9170-4335-afe7-3c7c22684424",
      "content":"Hello World!",
      "postId":"69f3ca3a-66fc-4142-873d-01e950d83adf",
      "post":null,
      "parentCommentId":null,
      "parentComment":null,
      "commentRates":[
         
      ],
      "inverseParentComment":[
         {
            "commentId":"59656765-d1ed-4648-8696-7d576ab7419f",
            "content":"Are you a programmer?",
            "postId":"69f3ca3a-66fc-4142-873d-01e950d83adf",
            "post":null,
            "parentCommentId":"be02742a-9170-4335-afe7-3c7c22684424",
            "commentRates":[
               
            ],
            "inverseParentComment":[
               
            ]
         }
      ]
   },
   {
      "commentId":"cfe126b3-4601-4432-8c87-445c1362a225",
      "content":"I wanna go to Mars too!",
      "postId":"69f3ca3a-66fc-4142-873d-01e950d83adf",
      "post":null,
      "parentCommentId":null,
      "parentComment":null,
      "commentRates":[
         
      ],
      "inverseParentComment":[
         {
            "commentId":"ab6d6b49-d772-48cd-9477-8d40f133c37a",
            "content":"See you on the Moon :)",
            "postId":"69f3ca3a-66fc-4142-873d-01e950d83adf",
            "post":null,
            "parentCommentId":"cfe126b3-4601-4432-8c87-445c1362a225",
            "commentRates":[
               
            ],
            "inverseParentComment":[
               
            ]
         }
      ]
   }
]

所以,我得到以下层次结构:

  • 世界你好!
    • 您是程序员吗?
  • 我也想去火星!
    • 月球见 :)

改为:

  • 世界你好!
    • 您是程序员吗?
      • 当然
      • 什么?
  • 我也想去火星!
    • 月球见 :)

为什么会这样?我该如何解决这个问题?

工作,但肮脏的解决方案

List<Comment> allComments = await context.Comment
    .Include(c => c.Children)
    .ToListAsync();

List<Comment> filteredComments = allComments.Where(c => c.ParentCommentId == null);

这可行,但很丑 - 我们将 cmets 的完整层次结构从数据库加载到内存中,然后过滤它们。如果数据库包含例如 100 万个 cmets,它的工作速度会非常慢。

更新

就我而言,不需要使用递归 CTE。请参阅this question 的更新。它看起来像 Greg Ogle 介绍的想法,但更简化了。

【问题讨论】:

  • EF Core 不支持递归 CTE。如果您真的关心性能,请通过 SQL 执行此操作。
  • 采用更强大的 ORM linq2db。你可以在里面使用CTE
  • @SvyatoslavDanyliv,谢谢。我认为这是个好主意。

标签: c# entity-framework-core hierarchy self-referencing-table


【解决方案1】:

如果您只是从 Comment 表中加载平面数据,则速度相当快。为了协助查找,您可以使用Dictionary&lt;{Guid or whatever type makes sense}, {Comment object type}&gt;。 Dictionary 上的查找使用 HashTable(我很确定)并且速度很快。所以,一旦你有了你的根评论,后续的查找就会很快。

以下是用于演示的控制台应用程序代码。它确实需要几秒钟才能运行。或许可以进一步优化。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Timers;

namespace DictionaryHierarchyTest
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();

            var commentsFlat = new List<CommentFlat>();
            var parentLookup = new Dictionary<Guid, List<CommentFlat>>();

            for (var i = 0; i < 100000; i++)
            {
                var id = Guid.NewGuid();
                commentsFlat.Add(new CommentFlat
                {
                    Id = id,
                    ParentId = Guid.Empty,
                    Text = $"Some text for ID:{id}"
                });

                for (var j = 0; j < 5; j++)
                {
                    var childId = Guid.NewGuid();
                    commentsFlat.Add(new CommentFlat
                    {
                        Id = childId,
                        ParentId = id,
                        Text = $"Some text for ID:{childId} Parent ID:{id}"
                    });

                    
                    for (var k = 0; k < 5; k++)
                    {
                        var grandChildId = Guid.NewGuid();
                        commentsFlat.Add(new CommentFlat
                        {
                            Id = grandChildId,
                            ParentId = childId,
                            Text = $"Some text for ID:{grandChildId} Parent ID:{childId}"
                        });
                    }

                }
            }

            foreach (var cf in commentsFlat)
            {
                if (!parentLookup.ContainsKey(cf.ParentId))
                {
                    parentLookup.Add(cf.ParentId, new List<CommentFlat>());
                }

                parentLookup[cf.ParentId].Add(cf);
            }
            stopwatch.Stop();
            var ts = stopwatch.Elapsed;
            Console.WriteLine($"There are {commentsFlat.Count} comments");
            Console.WriteLine($"{String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10)} to randomly populated.");
            stopwatch.Reset();
            stopwatch.Start();

            var commentHierarchy = new List<Comment>();

            foreach (var cf in commentsFlat)
            {
                if (cf.ParentId == Guid.Empty)
                {
                    var comment = new Comment
                    {
                        Id = cf.Id,
                        ParentId = cf.ParentId,
                        Children = BuildHierarchy(cf.Id, parentLookup),
                        Text = cf.Text
                    };
                    commentHierarchy.Add(comment);
                }
            }

            stopwatch.Stop();
            ts = stopwatch.Elapsed;
            Console.WriteLine($"{String.Format("{0:00}:{1:00}:{2:00}.{3:00}", ts.Hours, ts.Minutes, ts.Seconds, ts.Milliseconds / 10)} to make hierarchical");
            //foreach (var c in commentHierarchy) {
            //    PrintHiearchy(c, 1);
            //}
        }
        static string Tabs(int n)
        {
            return new String('\t', n);
        }
        private static void PrintHiearchy(Comment comment, int level) {
            Console.WriteLine($"{Tabs(level)}{comment.Text}");
            foreach(var child in comment.Children){ PrintHiearchy(child, level + 1); }
        }

        private static void OnTimedEvent(Object source, ElapsedEventArgs e)
        {
            Console.WriteLine("The Elapsed event was raised at {0:HH:mm:ss.fff}",
                              e.SignalTime);
        }

        private static List<Comment> BuildHierarchy(Guid parentId, Dictionary<Guid, List<CommentFlat>> flatComments)
        {
            if (flatComments.ContainsKey(parentId))
            {
                return flatComments[parentId].Select(c => new Comment
                {
                    Id = c.Id,
                    ParentId = c.ParentId,
                    Text = c.Text,
                    Children = BuildHierarchy(c.Id, flatComments)
                }).ToList();
            }
            else
            {
                return new List<Comment>();
            }
        }

    }

    public class Comment
    {
        public Guid Id { get; set; }
        public Guid ParentId { get; set; }
        public string Text { get; set; }
        public List<Comment> Children { get; set; }
    }

    public class CommentFlat
    {
        public Guid Id { get; set; }
        public Guid ParentId { get; set; }
        public string Text { get; set; }
    }
}

【讨论】:

  • 感谢您的详细解答。当我们处理中小型收藏时,您的解决方案非常棒。但对于大量数据,最好使用 CTE。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-13
  • 1970-01-01
  • 2013-11-22
  • 2018-01-04
  • 2012-10-02
相关资源
最近更新 更多