【问题标题】:Loading related data - recursive comments加载相关数据——递归注释
【发布时间】:2021-04-23 22:02:19
【问题描述】:

上下文

我有一个使用 Razor Pages 构建的简单链接聚合器 ASP.NET Core 应用程序。

每个链接的详细信息页面显示了 cmets:

用户可以评论链接并回复 cmets。

型号

这是Link 类:

    public class Link
    {
        public int Id { get; set; }
        public string UserId { get; set; }

        [DataType(DataType.Url)]
        public string Url { get; set; }

        public string Title { get; set; }

        [Display(Name = "Date")]
        [DataType(DataType.Date)]
        public DateTime DateTime { get; set; }

        [ForeignKey("UserId")]
        public IdentityUser User { get; set; }

        public List<Vote> Votes { get; set; }

        public int Score() => Votes.Sum(vote => vote.Score);

        public Vote UserVote(string userId) => 
            Votes.FirstOrDefault(vote => vote.UserId == userId);

        public int UserScore(string userId)
        {
            var vote = UserVote(userId);

            return vote == null ? 0 : vote.Score;
        }

        public async Task Vote(int score, string voterUserId)
        {
            var vote = UserVote(voterUserId);

            if (vote == null)
            {
                vote = new Vote()
                {
                    UserId = voterUserId,
                    LinkId = Id,
                    Score = score,
                    DateTime = DateTime.Now
                };

                Votes.Add(vote);
            }
            else
            {
                vote.Score = vote.Score == score ? 0 : score;
            }
        }
              
        public List<Comment> Comments { get; set; }

        public async Task AddComment(string text, string commenterUserId)
        {
            var comment = new Comment()
            {
                UserId = commenterUserId,
                LinkId = Id,
                Text = text,
                DateTime = DateTime.Now
            };

            Comments.Add(comment);
        }
    }

这是Comment 类:

    public class Comment
    {
        public int Id { get; set; }

        public int? LinkId { get; set; }
        //[ForeignKey("LinkId")]
        public Link Link { get; set; }

        public int? ParentCommentId { get; set; }
        //[ForeignKey("ParentCommentId")]
        public Comment ParentComment { get; set; }

        public List<Comment> Comments { get; set; }

        public string Text { get; set; }
        public DateTime DateTime { get; set; }

        public string UserId { get; set; }
        [ForeignKey("UserId")]
        public IdentityUser User { get; set; }

        public List<CommentVote> Votes { get; set; }

        public int Score() =>
            Votes
                .Where(vote => vote.CommentId == Id)
                .Sum(vote => vote.Score);

        public CommentVote UserVote(string userId) =>
            Votes.FirstOrDefault(vote => vote.UserId == userId);

        public int UserScore(string userId)
        {
            var vote = UserVote(userId);

            return vote == null ? 0 : vote.Score;
        }

        public async Task Vote(int score, string voterUserId)
        {
            var vote = UserVote(voterUserId);

            if (vote == null)
            {
                vote = new CommentVote()
                {
                    UserId = voterUserId,
                    CommentId = Id,
                    Score = score,
                    DateTime = DateTime.Now
                };

                Votes.Add(vote);
            }
            else
            {
                vote.Score = vote.Score == score ? 0 : score;
            }
        }

        public async Task AddComment(string text, string commenterUserId)
        {
            var comment = new Comment()
            { 
                UserId = commenterUserId,
                ParentCommentId = Id,
                Text = text,
                DateTime = DateTime.Now
            };

            Comments.Add(comment);
        }
    }

Details剃刀页面

这是Details 页面的OnGetAsync 方法。

public async Task<IActionResult> OnGetAsync(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    Link = await _context.Link
        .Include(link => link.User)
        .Include(link => link.Votes)
        .Include(link => link.Comments)
        .FirstOrDefaultAsync(m => m.Id == id);

    void load_comments(List<Comment> comments)
    {
        foreach (var comment in comments)
        {
            _context.Entry(comment).Reference(comment => comment.User).Load();
            
            _context.Entry(comment).Collection(comment => comment.Comments).Load();

            _context.Entry(comment).Collection(comment => comment.Votes).Load();

            load_comments(comment.Comments);
        }
    }

    load_comments(Link.Comments);

    if (Link == null)
    {
        return NotFound();
    }
    return Page();
}

如您所见,我使用IncludeLink 的各种导航属性。

当用户向Link 添加评论时,CommentLinkId 具有值。当用户向Comment 添加评论时,CommentParentCommentId 具有值。

由于cmets可以有cmets,所以我使用递归内部函数load_comments通过explicit loading检索cmets树。

建议的方法

通过 Google 搜索 ef core include recursive 可以找到各种加载递归实体的方法。

  • stackoverflow 上的This answer 推荐使用递归函数方法,有点类似于我上面展示的。

  • 但是,有经验的用户this popular answer 建议可以通过Include (eager loading) 检索整个树,而无需使用显式加载。

问题

有没有办法使用急切加载而不是显式加载来获取 Link 的 cmets?看起来这将是一个比显式递归方法更简单的实现。

上面的第二个答案表明它应该可以工作。但是,在加载 cmets 页面时,实施该建议(及其一些变体)会导致运行时错误。

完整项目

完整的项目可在 github 上找到:LinkAggregatorComments。这只是项目的快照,用于此类问题。

上面讨论的一些部分的链接:

【问题讨论】:

  • 您所指的答案中的关键点是whole这个词,即没有任何过滤的完整表格。由于您要按Link 过滤,因此不适用。这意味着您又回到了 EF Core 中缺乏分层 (CTE) 支持的状态。顺便说一句,如果您为子 cmets 存储了 LinkId - 是的,我知道它是多余的并且来自父节点,但它会给您一个简单的非分层过滤条件,用于所有链接 cmets,因此使用简单的 @987654356 的技术@ 会起作用的。
  • 感谢您的 cmets @IvanStoev。我已经在gist 中发布了我的回复,因为这里的 cmets 太长了。只是想确保我理解您的建议。
  • 是的,你没看错。确实,通过这种方法,Link.Comments 将包含 Link 的所有 cmets - 类似于包含所有 cmets 的 DbSet&lt;Comment&gt;,而不仅仅是顶部的 cmets。但是您总是可以在需要时使用Link.Comments.Where(c =&gt; c,ParentCommentId == null) 轻松过滤掉排名靠前的那些。并确保在保存现有 Link 时不要删除 Comments 集合中缺少的 cmets。该解决方案有其缺点 - 虽然它解决了负载问题,但它使其他一些操作变得复杂,包括序列化/反序列化......
  • ...所以这取决于您对涉及的实体的所有 CRUD 操作有多少控制权。
  • @IvanStoev,感谢您的确认。我明白你的意思,这两种方法各有利弊。

标签: c# entity-framework asp.net-core razor entity-framework-core


【解决方案1】:

如果所有子 cmets 都引用该链接,那么您根本不需要 load_comment 部分。 EF 在将父子关系加载到上下文时修复它们,Include(link =&gt; link.Comments) 会加载所有这些关系。

使用ThenInclude加载 cmets 的投票和用户。

【讨论】:

    猜你喜欢
    • 2013-09-17
    • 2019-05-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-13
    • 2011-03-30
    • 1970-01-01
    相关资源
    最近更新 更多