Entity Framework 核心在转换时延迟加载

Entity Framework Core is lazy loading when transforming

我在将实体模型转换为 DTO 时遇到 Entity Framework Core (v2.0.1) 的问题。基本上,用任何其他版本的短语来说,就是在我不想要的时候延迟加载。这是一个简单的 .NET Core 控制台应用程序(带有 Microsoft.EntityFrameworkCore.SqlServer (2.0.1) 包)。

using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;

namespace EfCoreIssue
{
    class Program
    {
        static void Main(string[] args)
        {
            var dbOptions = new DbContextOptionsBuilder<ReportDbContext>()
                .UseSqlServer("Server=.;Database=EfCoreIssue;Trusted_Connection=True;")
                .Options;

            // Create and seed database if it doesn't already exist.
            using (var dbContext = new ReportDbContext(dbOptions))
            {
                if (dbContext.Database.EnsureCreated())
                {
                    string alphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

                    foreach (char alpha in alphas)
                    {
                        var report = new Report { Title = $"Report { alpha }" };

                        for (int tagId = 0; tagId < 10; tagId++)
                            report.Tags.Add(new ReportTag { TagId = tagId });

                        dbContext.Reports.Add(report);
                        dbContext.SaveChanges();
                    }
                }
            }

            using (var dbContext = new ReportDbContext(dbOptions))
            {
                var reports = dbContext.Reports
                    .Select(r => new ReportDto
                    {
                        Id = r.Id,
                        Title = r.Title,
                        Tags = r.Tags.Select(rt => rt.TagId)
                    })
                    .ToList();
            }
        }
    }

    class ReportDbContext : DbContext
    {
        public DbSet<Report> Reports { get; set; }

        public ReportDbContext(DbContextOptions<ReportDbContext> options)
            : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<ReportTag>().HasKey(rt => new { rt.ReportId, rt.TagId });
        }
    }

    [Table("Report")]
    class Report
    {
        [Key]
        public int Id { get; set; }
        public string Title { get; set; }
        public virtual ICollection<ReportTag> Tags { get; set; }

        public Report()
        {
            Tags = new HashSet<ReportTag>();
        }
    }

    [Table("ReportTag")]
    class ReportTag
    {
        public int ReportId { get; set; }
        public int TagId { get; set; }
    }

    class ReportDto
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public IEnumerable<int> Tags { get; set; }
    }
}

现在执行ToList()方法获取数据时,会执行下面的SQL

SELECT [r].[Id], [r].[Title]
FROM [Report] AS [r]

如您所见,它没有努力加入 [ReportTag] table,如果您实际尝试读取 Tags 属性 的值] 在 ReportDto 上然后触发另一个 SQL 查询

SELECT [rt].[TagId]
FROM [ReportTag] AS [rt]
WHERE @_outer_Id = [rt].[ReportId]

现在我知道 EF Core 不支持延迟加载,但这对我来说看起来很像延迟加载。在这种情况下,我不希望它延迟加载。我试过将 var reports = dbContext.Reports 更改为 var reports = dbContext.Reports.Include(r => r.Tags) 但没有效果。

我什至尝试将 Tags = r.Tags.Select(rt => rt.TagId) 更改为 Tags = r.Tags.Select(rt => rt.TagId).ToList(),但这只会触发上述辅助 SQL 查询 26 次。

最后在绝望中我尝试将 var reports = dbContext.Reports 更改为 var reports = dbContext.Reports.Include(r => r.Tags).ThenInclude((ReportTag rt) => rt.TagId) 但可以理解的是抛出一个异常 ReportTag.TagId 不是导航 属性.

有没有人知道我可以做什么,以便它急切地加载到 ReportDto.Tags 属性?

正如您所注意到的,目前包含集合投影的 EF Core 投影查询存在两个问题 - (1) 它们导致每个集合执行 N 个查询,以及 (2) 它们延迟执行。

问题 (2) 很奇怪,因为具有讽刺意味的是 EF Core 不支持 延迟加载 相关实体数据,而这种行为有效地实现了它的投影。正如您已经发现的那样,至少您可以使用 ToList() 或类似的方法强制立即执行。

问题(1)暂时无法解决。它由 Query: optimize queries projecting correlated collections, so that they don't result in N+1 database queries #9282 and according to the Roadmap 跟踪(减少 n + 1 个查询 项)最终将在下一个 EF Core 2.1 版本中修复(改进)。

我能想到的唯一解决方法是(以更高的数据传输和内存使用为代价)使用 eager loading 并在之后进行投影(在 LINQ 的上下文中)到实体):

var reports = dbContext.Reports
    .Include(r => r.Tags) // <-- eager load
    .AsEnumerable() // <-- force the execution of the LINQ to Entities query
    .Select(r => new ReportDto
    {
        Id = r.Id,
        Title = r.Title,
        Tags = r.Tags.Select(rt => rt.TagId)
    })
    .ToList();