EF Code First如何从IQueryable<T>中批量删除数据?

5

我知道在LINQ-to-SQL中这是可行的,我还看到了一些线索,让我相信它在EF中也是可行的。是否有扩展可以像这样做:

var peopleQuery = Context.People.Where(p => p.Name == "Jim");

peopleQuery.DeleteBatch();

DeleteBatch 就是从 peopleQuery 中拆分出所有相应记录并创建一个单一的 SQL 语句,然后直接执行查询而不是标记所有这些实体进行删除并逐个进行。我认为在下面的代码中找到了类似的东西,但它立即失败,因为 instance 无法转换为 ObjectSet。 有人知道如何修复它以与 EF Code First 兼容吗?或者知道有任何这样做的示例吗?

public static IQueryable<T> DeleteBatch<T>(this IQueryable<T> instance) where T : class
{
    ObjectSet<T> query = instance as ObjectSet<T>;
    ObjectContext context = query.Context;

    string sqlClause = GetClause<T>(instance);
    context.ExecuteStoreCommand("DELETE {0}", sqlClause);

    return instance;
}

public static string GetClause<T>(this IQueryable<T> clause) where T : class
{
    string snippet = "FROM [dbo].[";

    string sql = ((ObjectQuery<T>)clause).ToTraceString();
    string sqlFirstPart = sql.Substring(sql.IndexOf(snippet));

    sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", "");
    sqlFirstPart = sqlFirstPart.Replace("[Extent1].", "");

    return sqlFirstPart;
}
3个回答

5

Entity Framework不支持批量操作。我喜欢代码解决问题的方式,但即使它完全符合您想要的(但针对ObjectContext API),也是一个错误的解决方案。

为什么这是错的解决方案?

它只适用于某些情况。在任何高级映射解决方案中,其中实体映射到多个表格(实体拆分,TPT继承),它肯定不起作用。我几乎可以确定,您可以找到其他情况,由于查询的复杂性,它将无法工作。

它使上下文和数据库不一致。这是针对DB执行的任何SQL的问题,但在这种情况下,SQL被隐藏了,使用您的代码的另一个程序员可能会错过它。如果您删除任何记录,该记录与同时加载到上下文实例中,则该实体不会被标记为已删除并从上下文中删除(除非您将该代码添加到DeleteBatch方法中-如果删除的记录实际上映射到多个实体(表拆分),则这将特别复杂)。

最重要的问题是修改EF生成的SQL查询和对该查询进行的假设。您期望EF将在查询中使用的第一个表命名为Extent1。是的,它现在确实使用了那个名字,但这是EF的内部实现。它可能会在任何EF的次要更新中更改。围绕任何API的内部构建自定义逻辑被认为是一种不良实践。

因此,您已经必须在SQL级别上处理查询,以便像@mreyeros所显示的那样直接调用SQL查询并避免此解决方案中的风险。您将不得不处理表格和列的实际名称,但这是您可以控制的(您的映射可以定义它们)。

如果您不认为这些风险很重要,您可以对代码进行小改动,使其在DbContext API中工作:

public static class DbContextExtensions
{
    public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : class
    {
        string sqlClause = GetClause<T>(query);
        context.Database.ExecuteSqlCommand(String.Format("DELETE {0}", sqlClause));
    }

    private static string GetClause<T>(IQueryable<T> clause) where T : class
    {
        string snippet = "FROM [dbo].[";

        string sql = clause.ToString();
        string sqlFirstPart = sql.Substring(sql.IndexOf(snippet));

        sqlFirstPart = sqlFirstPart.Replace("AS [Extent1]", "");
        sqlFirstPart = sqlFirstPart.Replace("[Extent1].", "");

        return sqlFirstPart;
    }
}

现在您可以这样调用批量删除功能:
context.DeleteBatch(context.People.Where(p => p.Name == "Jim"));

我完全同意您的所有观点,但考虑到按照官方方式执行会导致严重的性能损失,在我们的项目中采取这种方式是可以接受的风险。感谢您的回复! - Ocelot20

1

我不相信EF目前支持批量操作,比如删除。你可以执行原始查询:

 context.Database.ExecuteSqlCommand("delete from dbo.tbl_Users where isActive = 0"); 

我知道它们目前不受支持,但我希望我能像过去一样直接从表达式中构建删除命令。 - Ocelot20

1

如果有其他人正在寻找此功能,我已经使用了Ladislav的一些评论来改进他的示例。就像他所说的那样,使用原始解决方案时,当您调用SaveChanges()时,如果上下文已经在跟踪您删除的实体之一,则会调用它自己的删除。这不会修改任何记录,EF将其视为并发问题并引发异常。下面的方法比原始方法慢,因为它必须首先查询要删除的项目,但它不会为每个删除的实体编写单个删除查询,这是真正的性能优助。 它分离了所有被查询的实体,因此如果其中任何一个已经被跟踪,则它将知道不再删除它们。

public static void DeleteBatch<T>(this DbContext context, IQueryable<T> query) where T : LcmpTableBase
{
    IEnumerable<T> toDelete = query.ToList();

    context.Database.ExecuteSqlCommand(GetDeleteCommand(query));

    var toRemove = context.ChangeTracker.Entries<T>().Where(t => t.State == EntityState.Deleted).ToList();

    foreach (var t in toRemove)
        t.State = EntityState.Detached;
}

我还改了这一部分的代码,使用了正则表达式,因为我发现在FROM部分附近有不确定数量的空白。我也保留了 "[Extent1]",因为原来的DELETE查询方式无法处理带有INNER JOINS的查询:

public static string GetDeleteCommand<T>(this IQueryable<T> clause) where T : class
{
    string sql = clause.ToString();

    Match match = Regex.Match(sql, @"FROM\s*\[dbo\].", RegexOptions.IgnoreCase);

    return string.Format("DELETE [Extent1] {0}", sql.Substring(match.Index));
}

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