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();
我在将实体模型转换为 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();