一切从一段代码说起。。。

下面一段代码是最近我在对一EF项目进行重构时发现的。

protected override void DoRemove(T entity)
{
this.dbContext.Entry(entity).State = EntityState.Deleted;
Committed = false;
} protected override int DoRemove(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
{
var set = dbContext.Set<T>().AsQueryable();
set = (predicate == null) ? set : set.Where(predicate); int i = ;
foreach (var item in set)
{
DoRemove(item);
i++;
}
return i;
}

没错,这是使用Expression表达式实现批量删除数据的代码。当中的foreach代码块循环调用DoRemove(T entity)方法来修改符合条件的实体的状态码,最后执行dbContext的SaveChanges方法来提交到数据库执行删除命令。正是这个foreach引起了我的思考,我们知道EF会自动生成SQL语句提交到数据库执行,删除一条记录的话,会生成一条delete语句。如果要删除多条记录,手写SQL的话,只需一条带where条件的delete语句即可(如:delete from [tableName] where [condition]),而通过执行上面代码的批量删除方法,EF会不会生成一条类似这样的SQL呢?通过SQL Server Profiler跟踪发现这样的结果:

一向NB的EF这下反而SB了,生成的是多条delete语句,一条记录生成一条SQL!这样搞的话,如果要删除的记录有上百条、上千条,甚至上万条的话,那不是把EF给累死,数据库DB也会跟着被连累:”老大,一句话可以说完的事,你给我拆开一个字一个字来说,欠揍啊!”。我们知道任何ORM框架在生成SQL时都会存在性能的损耗,某些SQL会被优化后生成,但在按指定条件批量删除记录这方面很多ORM框架都存在上面出现的尴尬场面。正如老赵在《扩展LINQ to SQL:使用Lambda Expression批量删除数据》一文所说这并不是ORM的问题,只是使用起来不方便。老赵一文所提的方法不失为一种好方案,主要思路是通过解析Expression表达式构造SQL的Where条件子句。看到这里大家估计也清楚了问题的焦点其实就是一条SQL。如果手写SQL的话上面的批量删除SQL语句会类似这样:delete from [DomainObjectA] where [Prop1] in (N'prop2',N'prop3',N'prop5'),而EF对于Expression参数的查询是十分给力的,自动生成的查询SQL是这样的:

SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Prop1] AS [Prop1],
[Extent1].[Prop2] AS [Prop2]
FROM [dbo].[DomainObjectA] AS [Extent1]
WHERE [Extent1].[Prop1] IN (N'prop2',N'prop3',N'prop5')

大家发现了吧,EF生成的查询SQL跟手写的删除SQL语句结构上是如此的神似!如果我们把生成的SQL从SELECT到FROM那段替换成DELETE FROM的话,其实就是我们期望得到的一段SQL。那就按照这个思路动手吧,首先要获取EF生成的SQL语句,对于返回数据集是IQueryable<T>类型的,我们直接对结果ToString()即可获得,够简单吧。

var set = dbContext.Set<T>().AsQueryable();

string sql = set.Where(predicate).ToString().Replace("\r", "").Replace("\n", "").Trim();

对于获取到的SQL字符串我们要进行处理,去除换行符、回车符、多余的空白字符,最关键是要把select到from这一段替换掉。如何替换呢?其实还有个细节遗漏了,先看看替换后的SQL,等会再讲明。

DELETE
FROM [dbo].[DomainObjectA] AS [Extent1]
WHERE [Extent1].[Prop1] IN (N'prop2',N'prop3',N'prop5')

一眼看上去,似乎没问题,挺标准的一段SQL呀。问题恰恰是在太过标准了!在SQL查询器上运行会报这样的出错提示:“Incorrect syntax near the keyword 'AS'.” 原来delete from。。。这样的SQL不接受“AS”关键字啊!其实也不能说完全不接受“AS",如果改为下面这样是可以通过的。

DELETE
[dbo].[DomainObjectA]
FROM (
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Prop1] AS [Prop1],
[Extent1].[Prop2] AS [Prop2]
FROM [dbo].[DomainObjectA] AS [Extent1]
WHERE [Extent1].[Prop1] IN (N'prop2',N'prop3',N'prop5')
) AS [T1]

细心的你可能已经发现了,括号里面的SQL其实就是EF生成的那段。如果这样做的话,我们完全可以把生成的SQL整段拿来用,只需在delete后再指定表名即可。
现在问题变为如何从生成的SQL中提取出表名了。从前面EF生成的SQL中可以看出,它的结构是比较固定的(SELECT...FROM... AS...[WHERE...],当返回所有结果的话WHERE不会被生成)。如果熟悉正则表达式的话你可能第一时间已经想到了,没错!其实我们一开始就可以用正则表达式来解决。只怪本人的正则表达式功力欠佳,走了很多弯路。这次只能求助“度娘”了。。。求助中。。。有答案了,原来很早很早前就已经有位大牛在一篇名为《Linq to Sql: 批量删除之投机取巧版》解决了我所面临的问题,看来园子里不差牛人,闲来多逛逛,还是有意外收获的。借用他文中的正则表达式得了,省时省力。写到这,插个题外话:作为开发人员,信息搜索能力也是必备的(甚至可以毫不夸张地说是开发人员的第一门学问),对遇到的问题难题能迅速搜索到解决方法,从中借鉴别人的经验(不代表抄袭别人的作品),可以少走弯路,节省的时间、精力可以更多花在业务知识方面。言归正传,上代码!

Regex reg = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);

Match match = reg.Match(sql);

if (!match.Success)
throw new ArgumentException("Cannot delete this type of collection"); string table = match.Groups["Table"].Value.Trim();
string tableAlias = match.Groups["TableAlias"].Value.Trim();
string condition = match.Groups["Condition"].Value.Trim().Replace(tableAlias, table); string sql1 = string.Format("DELETE FROM {0} WHERE {1}", table, condition);

现在已经得到我们期望的SQL了,接下来是想办法让数据库去执行它即可,还好EF还是比较人性化的,它支持开放底层ADO.net框架,有三个API可以支持直接访问数据库。

、DbContext.Database.ExecuteSqlCommand

、DbContext.Database.SqlQuery

、DbSet.SqlQuery

从名字我们知道后两个API主要用来查询数据,我们选用第一个API:DbContext.Database.ExecuteSqlCommand,而且返回是受影响的结果数,我们还能知道被删除的数据有多少条,简直是为此量身定制嘛。

protected override int DoRemove(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
{
var set = dbContext.Set<T>().AsQueryable();
set = (predicate == null) ? set : set.Where(predicate); string sql = set.ToString().Replace("\r", "").Replace("\n", "").Trim();
if (predicate == null && !string.IsNullOrEmpty(sql) && !string.IsNullOrWhiteSpace(sql))
sql += " WHERE 1=1"; Regex reg = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);
Match match = reg.Match(sql); if (!match.Success)
throw new ArgumentException("Cannot delete this type of collection"); string table = match.Groups["Table"].Value.Trim();
string tableAlias = match.Groups["TableAlias"].Value.Trim();
string condition = match.Groups["Condition"].Value.Trim().Replace(tableAlias, table); string sql1 = string.Format("DELETE FROM {0} WHERE {1}", table, condition);
int i = ;
i = dbContext.Database.ExecuteSqlCommand(sql1);return i;
}

执行上面方法,通过SQL Server Profiler跟踪,发现确实是我们想要的结果,只有一条DELETE语句,而且满足条件的数据也确实被删除了。

搞定,收工!等等。。。还有什么问题呢?貌似还漏了点东西。嗯。。。还有”事务“!差点被人说成”标题党“啦。我们知道EF对于实体对象的新增(Add)、修改(Update)、删除(Remove)等操作都要等到最后DbContext.SaveChanges()方法执行后才最终提交到数据库执行。而DbContext.Database.ExecuteSqlCommand是绕过EF直接交给数据库去执行了,这样就出现很尴尬的情况:调用Remove(T entity)方法删除一条数据时要执行SaveChanges(),而通过批量删除的方法删除一条或更多的数据就不用经过SaveChanges()直接可以删除了。如何将SaveChanges和ExecuteSqlCommand放到同一个事务来提交呢?这正是下面要继续探讨的。通过查看DbContext.Database命名空间下面的方法,发现了这样一个方法:

DbContext.Database.UseTransaction(DbTransaction transaction)

这个方法的摘要说明如下:

// 摘要:
// Enables the user to pass in a database transaction created outside of the
// System.Data.Entity.Database object if you want the Entity Framework to execute
// commands within that external transaction. Alternatively, pass in null to
// clear the framework's knowledge of that transaction.

也就是说,我们可以给EF指定一个外部的事务(参数:transaction)来控制其提交Commands到数据库,这样一来所有由EF产生的Commands的提交都要通过这个外部事务(参数:transaction)来控制了。

protected override int DoRemove(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
{
var set = dbContext.Set<T>().AsQueryable();
set = (predicate == null) ? set : set.Where(predicate); string sql = set.ToString().Replace("\r", "").Replace("\n", "").Trim();
if (predicate == null && !string.IsNullOrEmpty(sql) && !string.IsNullOrWhiteSpace(sql))
sql += " WHERE 1=1"; Regex reg = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);
Match match = reg.Match(sql); if (!match.Success)
throw new ArgumentException("Cannot delete this type of collection"); string table = match.Groups["Table"].Value.Trim();
string tableAlias = match.Groups["TableAlias"].Value.Trim();
string condition = match.Groups["Condition"].Value.Trim().Replace(tableAlias, table); string sql1 = string.Format("DELETE FROM {0} WHERE {1}", table, condition);
int i = ;
dbContext.UseTransaction(efContext.Transaction);
i = dbContext.Database.ExecuteSqlCommand(sql1);
efContext.Committed = false;
return i;
}

接下来我们要把DbContext的SaveChanges()和事务Transaction的Commit()封装到同一个方法Commit中去,为此采用Repository模式来实现。具体的实现过程就不细说了,直接给出重构后的代码吧。

 public interface IEntityFrameworkRepositoryContext
{
DbContext Context { get; } DbTransaction Transaction { get; set; } bool Committed { get; set; } void Commit(); void Rollback(); }

IEntityFrameworkRepositoryContext

 public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext
{
private readonly DbContext efContext;
private readonly object sync = new object(); public EntityFrameworkRepositoryContext(DbContext efContext)
{
this.efContext = efContext;
} public DbContext Context
{
get { return this.efContext; }
} private DbTransaction _transaction = null;
public DbTransaction Transaction
{
get
{
if (_transaction == null)
{
var connection = this.efContext.Database.Connection;
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
_transaction = connection.BeginTransaction();
}
return _transaction;
}
set { _transaction = value; }
} private bool _committed;
public bool Committed
{
get
{
return _committed;
}
set
{
_committed = value;
}
} public void Commit()
{
if (!Committed)
{
lock (sync)
{
efContext.SaveChanges(); if (_transaction != null)
_transaction.Commit();
}
Committed = true;
}
} public override void Rollback()
{
Committed = false;
if (_transaction != null)
_transaction.Rollback();
} //其他方法略。。。 }

EntityFrameworkRepositoryContext

 public class EntityFrameworkRepository<T> : IRepository<T>
where T : class,IEntity
{
private readonly IEntityFrameworkRepositoryContext efContext; public EntityFrameworkRepository(IEntityFrameworkRepositoryContext context)
{
this.efContext = context
} //Add, Update, Find等等其他方法略。。。 public int Remove(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
{
var set = efContext.Context.Set<T>().AsQueryable();
set = (predicate == null) ? set : set.Where(predicate); string sql = set.ToString().Replace("\r", "").Replace("\n", "").Trim();
if (predicate == null && !string.IsNullOrEmpty(sql) && !string.IsNullOrWhiteSpace(sql))
sql += " WHERE 1=1"; Regex reg = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);
Match match = reg.Match(sql); if (!match.Success)
throw new ArgumentException("Cannot delete this type of collection");
string table = match.Groups["Table"].Value.Trim();
string tableAlias = match.Groups["TableAlias"].Value.Trim();
string condition = match.Groups["Condition"].Value.Trim().Replace(tableAlias, table); string sql1 = string.Format("DELETE FROM {0} WHERE {1}", table, condition);
int i = ;
efContext.Context.Database.UseTransaction(efContext.Transaction);
i = efContext.Context.Database.ExecuteSqlCommand(sql1);
efContext.Committed = false;
return i;
} }

EntityFrameworkRepository

 public interface IRepository<T>
{
void Add(T entity);
void Update(T entity);
void Remove(T entity);
int Remove(Expression<Func<T, bool>> predicate = null);
IQueryable<T> Find(Expression<Func<T, bool>> predicate = null);
......
}

IRepository

EntityFramework:支持同一事务提交的批量删除数据实现思路的更多相关文章

  1. ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除)

    原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除) ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇)   ...

  2. php 批量删除数据

    php 批量删除数据 :比如我们在看邮箱文件的时候,积攒了一段时间以后,看到有些文件没有用了 这时候我们就会想到把这些 没用的文件删除,这时候就用到了批量删除数据的功能,这里我是用了数据库原有的一个表 ...

  3. crm使用soap批量删除数据

    //批量删除数据 function demo() {     //实体名称     var entityname = "fw_student";     var data = [] ...

  4. ssm框架下怎么批量删除数据?

    ssm框架下批量删除怎么删除? 1.单击删除按钮选中选项后,跳转到js函数,由函数处理 2. 主要就是前端的操作 js 操作(如何全选?如何把选中的数据传到Controller中) 3.fun()函数 ...

  5. sqlalchemy批量删除数据、全量删除

    问题:sqlalchemy如何批量删除多条数据解决:使用参数synchronize_session=False,或for循环方法:        users = self.db.query(User) ...

  6. sql 2008批量删除数据表格

    DECLARE @Table NVARCHAR(300) DECLARE @Count Int = 0 DECLARE tmpCur CURSOR FOR SELECT name FROM sys.o ...

  7. 使用事务和SqlBulkCopy批量插入数据

    SqlBulkCopy是.NET Framework 2.0新增的类,位于命名空间System.Data.SqlClient下,主要提供把其他数据源的数据有效批量的加载到SQL Server表中的功能 ...

  8. PHP后台批量删除数据

    html <form action="" method="post"> <div><input type="submit ...

  9. mysql 批量删除数据

    批量删除2000w数据 使用delete from table太慢 //DELIMITER DROP PROCEDURE if EXISTS deleteManyTable; create PROCE ...

随机推荐

  1. CSS预处理器实践之Sass、Less大比拼[转]

    什么是CSS预处理器? CSS可以让你做很多事情,但它毕竟是给浏览器认的东西,对开发者来说,Css缺乏很多特性,例如变量.常量以及一些编程语法,代码难易组织和维护.这时Css预处理器就应运而生了.Cs ...

  2. HDOJ 1429 胜利大逃亡(续)

    胜利大逃亡(续) Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total Su ...

  3. Javacript中(function(){})() 与 (function(){}()) 区别 {转}

    这个问题可以从不同的角度来看,但从结果上来说 :他们是一样的.首先,如果从AST(抽象语法树)的角度来看,两者的AST是一模一样的,最终结果都是一次函数调用.因此,就解析器产生的结果论而言,两者是没有 ...

  4. Openstack os-networks API create network 方法

    官方文档在请求方法和地址上有错误: http://api.openstack.org/api-ref.html#ext-os-networks 正确的地址为: /v2/{tenant_id}/os-n ...

  5. POJ 2021 Relative Relatives(map+树的遍历)

    题意: 今天是Ted的100岁生日.凑巧的是,他家族里面每个人都跟他同一天生日,但是年份不同. 现在只给出一些 父亲的名字,孩子的名字,以及孩子出生时父亲的年龄, 要求将Ted以外的家族成员按年龄降序 ...

  6. 浅谈mysql中varchar(m)与char(n)的区别与联系

    mysql建表长度的限制 在mysql建表时,出现以下报错信息: 错误一:行大小过大,所使用的表这种类型的最大的行大小,不算BLOB类型,是65535.(这是我翻译的)    原因是MySQL在建表的 ...

  7. YARN学习笔记 ResourceManager部分

    CompositeService 多个service封装,service定义了状态机状态改变的合法情况. 重要的方法是(子类需要实现的):serviceStart,serviceInit,servic ...

  8. Linux软链接和硬链接

    Linux中的链接有两种方式,软链接和硬链接.本文试图清晰彻底的解释Linux中软链接和硬链接文件的区别. 1.Linux链接文件 1)软链接文件  软链接又叫符号链接,这个文件包含了另一个文件的路径 ...

  9. IOS 视频分解图片、图片合成视频

    在IOS视频处理中,视频分解图片和图片合成视频是IOS视频处理中经常遇到的问题,这篇博客就这两个部分对IOS视频图像的相互转换做一下分析. (1)视频分解图片 这里视频分解图片使用的是AVAssetI ...

  10. iOS开发--Bison详解连连支付集成简书

    "最近由于公司项目需要集成连连支付,文档写的不是很清楚,遇到了一些坑,因此记录一下,希望能帮到有需要的人." 前面简单的集成没有遇到什么坑,在此整理一下官方的集成文档,具体步骤如下 ...