【问题标题】:EF Code First Delete Batch From IQueryable<T>?EF 代码首先从 IQueryable<T> 中删除批次?
【发布时间】:2012-01-22 06:13:13
【问题描述】:

我知道这在 LINQ-to-SQL 中是可能的,而且我看到的点点滴滴让我相信它在 EF 中是可能的。是否有一个扩展可以做这样的事情:

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

peopleQuery.DeleteBatch();

DeleteBatch 只是挑选出 peopleQuery 并创建一条 SQL 语句来删除所有适当的记录,然后直接执行查询,而不是将所有这些实体标记为删除并让它一个一个地执行它们。我以为我在下面的代码中找到了类似的东西,但它立即失败,因为实例无法转换为 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;
}

【问题讨论】:

    标签: .net sql entity-framework ef-code-first extension-methods


    【解决方案1】:

    如果其他人正在寻找此功能,我使用了一些 Ladislav 的 cmets 来改进他的示例。就像他说的那样,使用原始解决方案,当您调用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));
    }
    

    【讨论】:

      【解决方案2】:

      实体框架不支持批处理操作。我喜欢代码解决问题的方式,但即使它完全符合您的要求(但对于 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"));
      

      【讨论】:

      • 我 100% 同意您的所有观点,但考虑到以官方方式执行此操作会严重影响性能,这在我们的项目中是可以接受的风险。感谢您的回复!
      【解决方案3】:

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

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

      【讨论】:

      • 我知道它们目前不受支持,但我希望我可以像过去那样直接从表达式构建删除命令。
      猜你喜欢
      • 2012-05-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-05-22
      • 1970-01-01
      • 1970-01-01
      • 2013-05-16
      • 2023-04-05
      相关资源
      最近更新 更多