EF Code First从IQueryable <t>?</t>删除批次

时间:2011-12-16 19:02:04

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

我知道这在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;
}

3 个答案:

答案 0 :(得分:5)

实体框架不支持批量操作。我喜欢代码解决问题的方式,但即使它完全符合您的要求(但对于ObjectContext API),这是一个错误的解决方案。

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

仅在某些情况下有效。它肯定不适用于任何高级映射解决方案,其中实体映射到多个表(实体拆分,TPT继承)。我几乎可以肯定,由于查询的复杂性,你可以找到另一种无法工作的情况。

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

最重要的问题是修改EF生成的SQL查询以及您对该查询所做的假设。您期望EF将查询中使用的第一个表命名为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; } } 。是的,它现在真的使用该名称,但它是内部EF实现。它可以在EF的任何微小更新中改变。围绕任何API的内部构建自定义逻辑被认为是一种不好的做法。

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

如果您认为这些风险并不重要,您可以对代码进行细微更改,使其在DbContext API中运行:

context.DeleteBatch(context.People.Where(p => p.Name == "Jim"));

现在您将以这种方式调用批量删除:

{{1}}

答案 1 :(得分:1)

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

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

答案 2 :(得分: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));
}
相关问题