如何在实体框架的聚合根内部的子实体中执行过滤器

时间:2018-10-11 19:50:22

标签: c# entity-framework entity-framework-6

我正在尝试访问聚合根目录中列表中的项目,但是由于它具有大量条目(40K +),因此Entity Framework在我的开发机上需要花费150.180 ms的时间来执行它。

这是一个简化的示例,显示了此行为:

public class Parent
{
    public int Id { get; private set; }
    public virtual ICollection<Child> Children { get; private set; }

    public void Remove(string someProperty)
    {
        var itensToRemove = Children
            .Where(x => x.SomeProperty == someProperty)
            .ToList(); // -> this is where it takes a long time to run

        // remove...
    }
}

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

播种:

INSERT [dbo].[Parent] ([Id]) VALUES (1)
INSERT [dbo].[Child] ([Id], [Parent_Id]) VALUES (1, 1)
...
INSERT [dbo].[Child] ([Id], [Parent_Id]) VALUES (40000, 1)

我还尝试过强制转换为List并使用.RemoveAll(),但是结果是相同的。

(Children as List<Child>).RemoveAll(x => x.SomeProperty == someProperty);

由于我使用的是延迟加载,所以我一直以为EF会考虑.Where(...)并创建一个过滤的SQL查询,但是SQL Profiler告诉我它不会:

exec sp_executesql N'SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Parent_Id] AS [Parent_Id]
    FROM [dbo].[Child] AS [Extent1]
    WHERE 
        ([Extent1].[Parent_Id] IS NOT NULL) AND 
        ([Extent1].[Parent_Id] = @EntityKeyValue1)
',N'@EntityKeyValue1 int',@EntityKeyValue1=1

有趣的是,当我在SSMS中运行以上查询时,它会立即返回所有行。

在设计方面,我正在考虑直接基于this answer访问它,但是我认为这会破坏DDD设计,因为它涉及到属于父级的业务逻辑。

1 个答案:

答案 0 :(得分:0)

我不会在实体内部尝试这种逻辑。在读取父项时,“子项”会急于加载为列表,或者在被引用时会在上下文范围内延迟加载。

尝试时:

var itensToRemove = Children
            .Where(x => x.SomeProperty == someProperty)
            .ToList(); 

......在条件条件执行之前,此延迟为父级加载了 all 个子级。如果这在许多父母或大批孩子中遇到,那将是非常低效的。

实体不是业务逻辑,它们是数据。如果您有要求从父实体或从几个/所有父实体中删除所有匹配子级的操作之类的要求,则应在服务或存储库级别对此进行处理,以封装业务逻辑并使用这些实体来表示数据。

例如,给定一个父ID,以删除所有“ childType”为“ Answer”的孩子。如果父级有几个子级,并且子级是相对较小的实体,则可以加载其父级已加载的子级,并删除适用的实体,然后保存父级:

var parent = context.Parents.Where(p => p.ParentId == parentId)
  .Include(p => p.Children)
  .Single();
var childrenToRemove = parent.Children.Where(c => c.ChildType == "Answer").ToList();
foreach (child in childrenToRemove)
   parent.Children.Remove(child);

context.SaveChanges();

如果您要从许多/所有父母中批量删除孩子,或者孩子实体的体积很大,那么我会考虑将孩子保留为顶级实体(在上下文中带有DbSet)并直接删除它们。假定父子之间存在一对多关系(子包含父id),子本身没有子:

List<Child> childrenToDelete = new List<Child>();
do
{
  childrenToDelete = context.Children
    .Where(c => c.ChildType == "Answer")
    .Select(c => c.ChildId)
    .Take(1000)
    .ToList() // Execute the query to get our 1000 IDs. We need Linq2Obj references to continue.
    .Select(id => new Child { ChildId = id})
    .ToList();

   foreach(var child in childrenToDelete)
     context.Children.Attach(child);

   context.Children.RemoveRange(childrenToDelete);
   context.SaveChanges();
} while (childrenToDelete.Any());

以上内容加载了适用的子代ID,并使用这些ID撰写/附加了新的子实体引用。这样可以节省加载所有子数据来执行删除的过程。我们会批量加载和删除1000张,以保持合理的交易规模。这还需要在尚未加载父母/孩子的“干净”上下文中完成,因为这会导致附加问题。这也需要考虑错误处理,并考虑处理错误,因为每批将提交1000个错误。

如果您遇到的子项有其自身的子项引用,则可以查看专门为此操作定义的上下文和实体模型集,其中唯一的数据是必需的引用。 (PK和FK),您可以在其中从有界上下文中加载子项及其相关实体以进行操作并发出删除操作。同样,如果这将影响大量行,请分批提取数据。

我建议的最后一种选择是对操作使用存储过程,然后确保上下文中所有已加载的父实体都已重新加载。

希望能提供一些有关如何处理场景的想法。