如何在EF Core 2.1中避免n+1查询?

5

我正在使用预览版的EF Core 2.1,它应该可以减少N+1查询问题。我试图制作一个查询,选取帖子作者的论坛主题:

dbContext.ForumThreads
   .Include(t => t.Posts)
   .Take(n)
   .Select(t => new
   {
      t.Id,
      t.Title,
      PostAuhtors = t.Posts.Select(p => p.Author).Take(5)
   }).ToArray();

这会产生n+1个查询:对于每个论坛主题,它选择帖子作者。
这个模式很简单:
public class ForumThread 
{
   public Guid Id {get;set;}
   public string Title {get;set;}
   public ICollection<ForumPost> Posts {get;set;}
}

public class ForumPost 
{
  public Guid Id {get;set;}
  public string Author {get;set;}
  public string Content {get;set;}
}
2个回答

1
我认为您可以通过更少的查询(仅2个)来实现这一目标,并将其中一些行为放入内存中。这段代码是否符合您的要求?
class Program
    {
        static void Main(string[] args)
        {
            using (var db = new SampleContext())
            {
                Console.ReadLine();
                var result = db.Threads
                    .Include(t => t.Posts)
                    .Take(10)
                    .Select(t => new
                    {
                        t.Id,
                        t.Title,
                        t.Posts
                        // Do this in memory  
                        //PostAuhtors = t.Posts.Select(p => p.Author).Take(5)
                    }).ToArray();

                Console.WriteLine($"» {result.Count()} Threads.");
                foreach (var thread in result)
                {
                    // HERE !!
                    var PostAuhtors = thread.Posts.Select(p => p.Author).Take(5);
                    Console.WriteLine($"» {thread.Title}:  {string.Join("; ", PostAuhtors)} authors");
                }
                Console.ReadLine();
            }
        }
    }

    public class SampleContext : DbContext
    {
        public static readonly LoggerFactory MyLoggerFactory = new LoggerFactory(new[] {
                new ConsoleLoggerProvider((category, level)
                    => category == DbLoggerCategory.Database.Command.Name
                       && level == LogLevel.Debug, true)
            });

        public DbSet<ForumThread> Threads { get; set; }
        public DbSet<ForumPost> Posts { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder  
                .EnableSensitiveDataLogging()
                .UseLoggerFactory(MyLoggerFactory)
                .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFStart;Trusted_Connection=True;");
        }
    }

    public class ForumThread
    {
        public Guid Id { get; set; }
        public string Title { get; set; }
        public ICollection<ForumPost> Posts { get; set; }
    }

    public class ForumPost
    {
        public Guid Id { get; set; }
        public string Author { get; set; }
        public string Content { get; set; }
    }

这是输出结果: enter image description here


我不想加载所有的文章,因为它可能是几兆字节的数据。 - Liero
我认为目前EFCore不允许其他行为,但这个问题正在得到解决:https://github.com/aspnet/EntityFrameworkCore/issues/4007 - Luís Antunes
那就是问题所在。我正在使用2.1预览版,但仍然得到N+1查询。 - Liero
据我所见,我认为问题在于Take(5)会产生n+1个查询,如果不使用Take只会产生2个查询。(#9282) 也许一个替代方案是使用Where子句来限制从主题中发布的帖子?(我知道这不是同一件事...) - Luís Antunes

0

你的方法有优点和缺点。让我解释一下,

你的方法

缺点 => 查询了 n+1 次。

优点 => 你只请求了你的帖子中的 5 个作者。(n 次)。

SELECT TOP(@__p_0) [t].[Id], [t].[Title] FROM [ForumThreads] AS [t] => 1 time

SELECT TOP(5) [p].[Author] FROM [ForumPosts] AS [p] => n time (n => number of ForumThread)

根据数据库中数据的大小,您可以选择其他方法。

        var source = dbContext.ForumThreads.Include(t => t.Posts).Take(5).ToList();
        var result = source.Select(t => new { t.Id, t.Title, PostAuhtors = t.Posts.Select(p => p.Author).Take(5).ToList() }).ToArray();

优点 => 一次查询。

缺点 => 需要从数据库中获取您所有文章的作者,然后进行筛选。(根据您的数据规模,这可能会成本过高)。

    SELECT [t.Posts].[Id], [t.Posts].[Author], [t.Posts].[Content], [t.Posts].[ForumThreadId] FROM [ForumPosts] AS [t.Posts]
INNER JOIN (SELECT TOP(@__p_0) [t0].[Id]
    FROM [ForumThreads] AS [t0] ORDER BY [t0].[Id]
) AS [t1] ON [t.Posts].[ForumThreadId] = [t1].[Id] ORDER BY [t1].[Id]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接