【问题标题】:Entity Framework Core Nested List Performance实体框架核心嵌套列表性能
【发布时间】:2019-11-19 12:58:45
【问题描述】:

当我尝试向嵌套列表中添加其他项目时,我在实体框架的核心性能方面遇到了困难。

我们举个例子: 我有多个项目,项目包含多个房屋,房屋有多个立面,立面有多个窗户。 如果我现在想为特定项目、房屋、立面添加一个额外的窗口,我会这样做:

    public async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, WindowEntity windowEntity)
    {
        using (ProjectsDbContext context = new ProjectsDbContext())
        {
            var windowList = context.ProjectSet
                .Include(p => p.Houses)
                .ThenInclude(h => h.Facades)
                .ThenInclude(f => f.Windows)
                .First(p => p.Id == projectId).Houses
                .First(h => h.Id == houseId).Facades
                .First(f => f.Id == facadeId).Windows;

            windowList.Add(windowEntity);

            await context.SaveChangesAsync();
        }
    }

这在功能方面效果很好。但是,当数据库增加时,性能会越来越慢。有没有更高效的方式将项目添加到嵌套列表?

更新 1

我用这个包含 50 个项目的未来派对象创建了一个简单的测试数据库,每个项目有 10 个房子,每个房子有 10 个立面,每个立面有 10 个窗户。这导致数据库大小约为 10Mb。

在测试中,我一个接一个地添加了 1000 个 Windows(没有批量):

上面提到的解决方案总共需要145s。

@David Browne 提到的解决方案——微软大约需要 54 秒

var facadeEntity = context.Set<FacadeEntity>()
    .Include(f => f.Windows)
    .Single(f => f.Id == facadeId);

facadeEntity.Windows.Add(windowEntity);

await context.SaveChangesAsync();

更新 2

按照@David Browne 的建议,我在窗口中添加了一个 ForeignKey:

modelBuilder.Entity<FacadeEntity>()
.HasMany(f => f.Windows).WithOne()
.HasForeignKey(f => f.FacadeId)
.OnDelete(DeleteBehavior.Cascade);

保存是这样执行的:

context.Entry(windowEntity).Property(nameof(WindowEntity.FacadeId)).CurrentValue = facadeId;
context.Set<WindowEntity>().Add(windowEntity);
await context.SaveChangesAsync();

这个问题是一样的,我拥有的窗口越多,添加所需的时间就越长。 1000 个窗口的持续时间约为 53 秒。

【问题讨论】:

  • 你能检查一下查询是如何形成的吗?除了Include,您可以编写基于Join 的查询来获取外观并将窗口添加到子对象中。
  • 第一个First 将撤回第一个项目以及它的所有房屋、外墙和窗户。而是使用WhereSelectMany 的组合进行过滤,然后取第一个。
  • @juharr 你的意思是: var windowList = context.ProjectSet.Where(p => p.Id == projectId).SelectMany(p => p.Houses).Where(h => h .Id == facadeId).SelectMany(f => f.Facades).Where(f => f.Id == facadeId).SelectMany(w => w.Windows).ToList(); .Where(f => f.Id == facadeId).SelectMany(w => w.Windows).ToList();

标签: c# .net database entity-framework


【解决方案1】:

我目前只有一个“DbSet ProjectSet”,你会在上下文中添加一个额外的 DbSet 吗?

如果您没有为实体声明 DbSet&lt;T&gt;,请通过 DbContext.Set&lt;T&gt;() 访问它,例如:

public static async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, Window windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        var facade = context.Set<Facade>()
                            .Where(f => f.FacadeId == facadeId)
                            .Single();

        facade.Windows.Add(windowEntity);

        await context.SaveChangesAsync();
    }
}

这转化为:

SELECT TOP(2) [f].[FacadeId], [f].[HouseId]
FROM [Facade] AS [f]
WHERE [f].[FacadeId] = @__facadeId_0

然后:

INSERT INTO [Window] ([WindowId], [FacadeId])
VALUES (@p0, @p1)

假设Facade 有一个单列主键。如果它具有 (ProjectId,HouseId,FacadeId) 的复合键,则将它们添加到 Where

不过,最好的方法是设置 Window.FacadeId 的外键属性而不加载外观。在 EF Core 中,如果您没有外键属性,则可以使用影子属性执行此操作。例如:

public static async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, Window windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        context.Entry(windowEntity).Property("FacadeId").CurrentValue = facadeId;
        context.Set<Window>().Add(windowEntity);

        await context.SaveChangesAsync();
    }
}

【讨论】:

  • 这要快得多,但是当外观列表中有更多窗口时,它仍然需要更长的时间。是否可以避免加载整个外观列表?
  • 该代码将加载单个外观,然后插入单个窗口(请参阅更新答案)。为什么说“门面列表中的窗口越多,需要的时间越长”?
  • 我有一个单元测试,它将 10'000 个窗口添加到外观,并且每次都记录 addWindow 调用的持续时间。总持续时间约为 3000 秒,第一次 addWindow 调用大约需要 0.7 秒,一旦它被抖动大约需要 0.2 秒,然后持续时间会随着时间的推移而增加。 10'000 个窗口添加大约需要 1 秒。使用此代码,外观会加载列表中包含的所有窗口。
  • 测试是否使用单个 DbContext?如果是这样,更改跟踪器必须跟踪所有这些对象。
  • 我认为 shadow 属性是要走的路。我不知道 ef core 支持没有“Microsoft.EntityFrameworkCore.Proxies”的延迟加载,我不使用这个包。
【解决方案2】:

你应该首先从你的 Windows DbSet 中选择然后加入 Facades 然后加入 house 你的查询应该是这样的

 var windowList = context.DbSet<Windows>().Where(w => w.Facade.Id== facadeId && w.Facade.House.Id == houseId && w.Facade.House.Project.Id == projectId)

【讨论】:

  • 我目前只有一个“DbSet ProjectSet”,你会在上下文中添加一个额外的 DbSet 吗?
  • 确实,它不应该改变你的数据库结构,但是在你的代码中你可以添加这个实体,即使没有像 Mel 写的那样选择。
  • 这意味着我需要在每个对象上都有一个“父”对象的 ID,但到目前为止我还没有。
  • 你可以添加它,比你的性能应该提高。它也不会改变数据库架构。
  • 那我就得处理所有已经存在的数据库了。
【解决方案3】:

如果您尝试添加一个窗口到特定项目/房屋/立面,您可以直接在 WindowEntity 上设置 FacadeId 并保存它吗?据推测,Window 有一个 FacadeId 属性,就像 Facade 有一个 HouseId,而 House 有一个 ProjectId。如果 Window 有其所有父项的 Id(不必要),则只需设置 House 和 Project Id。

public async Task SaveWindowAsync(Guid projectId, Guid houseId, Guid facadeId, WindowEntity windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {            
        windowEntity.FacadeId = facadeId;
        context.WindowSet.Add(windowEntity);
        await context.SaveChangesAsync();
    }
}

更新: 如果您不想直接在实体上设置 facadeId,那么您可以加载 just 外观,并设置其 Facade 属性而不是 Id。如果您要继续使用该集合的实例,您也可以选择将 windowEntity 添加到 Facade 的 Windows 集合中。

public async Task SaveWindowAsync(Guid facadeId, WindowEntity windowEntity)
{
    using (ProjectsDbContext context = new ProjectsDbContext())
    {
        var facade = Facades.Single(x => x.Id == facadeId);
        windowEntity.Facade = facade;
        facade.Windows.Add(windowEntity);
        await context.SaveChangesAsync();
    }
}

【讨论】:

  • 我不喜欢孩子直接拥有“父母”的ID,因此到目前为止设置父母ID不是一个选项。
  • 你仍然可以跳过所有导航,直接加载外观,然后直接添加窗口。您显然知道 FacadeId,因为您在示例查询中使用了它。您仍然可以跳过浏览项目和房屋来到达那里。请参阅上面的更新答案:
  • 是的,我知道外观 ID,但当前窗口不包含外观 ID 或外观。只有门面知道对应的窗口。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-01-20
  • 1970-01-01
  • 2022-01-08
  • 2021-07-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多