EntityFramework:支持同一事務提交的批量刪除數據實現思路


一切從一段代碼說起。。。

下面一段代碼是最近我在對一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 = 0;
    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可以支持直接訪問數據庫。

1、DbContext.Database.ExecuteSqlCommand

2、DbContext.Database.SqlQuery

3、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 = 0;
    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 = 0;
    dbContext.UseTransaction(efContext.Transaction);     i = dbContext.Database.ExecuteSqlCommand(sql1);
    efContext.Committed = false;
    return i;
}    

接下來我們要把DbContext的SaveChanges()和事務Transaction的Commit()封裝到同一個方法Commit中去,為此采用Repository模式來實現。具體的實現過程就不細說了,直接給出重構后的代碼吧。

 1 public interface IEntityFrameworkRepositoryContext
 2 {
 3     DbContext Context { get; }
 4 
 5     DbTransaction Transaction { get; set; }
 6 
 7     bool Committed { get; set; }
 8 
 9     void Commit();
10 
11     void Rollback();
12 
13 }
IEntityFrameworkRepositoryContext

 

 1 public class EntityFrameworkRepositoryContext : IEntityFrameworkRepositoryContext
 2 {
 3     private readonly DbContext efContext;
 4     private readonly object sync = new object();
 5 
 6     public EntityFrameworkRepositoryContext(DbContext efContext)
 7     {
 8         this.efContext = efContext;
 9     }
10 
11     public DbContext Context
12     {
13         get { return this.efContext; }
14     }
15 
16     private DbTransaction _transaction = null;
17     public DbTransaction Transaction
18     {
19         get
20         {
21             if (_transaction == null)
22             {
23                 var connection = this.efContext.Database.Connection;
24                 if (connection.State != ConnectionState.Open)
25                 {
26                     connection.Open();
27                 }
28                 _transaction = connection.BeginTransaction();
29             }
30             return _transaction;
31         }
32         set { _transaction = value; }
33     }
34 
35     private bool _committed;
36     public bool Committed
37     {
38         get
39         {
40             return _committed;
41         }    
42         set
43         {
44             _committed = value;
45         }
46     }
47 
48     public void Commit()
49     {
50         if (!Committed)
51         {
52             lock (sync)
53             {
54                 efContext.SaveChanges();
55 
56                 if (_transaction != null)
57                         _transaction.Commit();
58             }
59             Committed = true;
60         }
61     }
62 
63     public override void Rollback()
64     {
65         Committed = false;
66         if (_transaction != null)
67         _transaction.Rollback();
68     }
69 
70     //其他方法略。。。
71 
72 }            
EntityFrameworkRepositoryContext

 

 1 public class EntityFrameworkRepository<T> : IRepository<T>         
 2     where T : class,IEntity
 3 {
 4     private readonly IEntityFrameworkRepositoryContext efContext;
 5 
 6     public EntityFrameworkRepository(IEntityFrameworkRepositoryContext context)
 7     {
 8         this.efContext = context
 9     }
10 
11     //Add, Update, Find等等其他方法略。。。
12 
13     public int Remove(System.Linq.Expressions.Expression<Func<T, bool>> predicate = null)
14     {        
15         var set = efContext.Context.Set<T>().AsQueryable();
16         set = (predicate == null) ? set : set.Where(predicate);
17 
18         string sql = set.ToString().Replace("\r", "").Replace("\n", "").Trim();
19         if (predicate == null && !string.IsNullOrEmpty(sql) && !string.IsNullOrWhiteSpace(sql))
20             sql += " WHERE 1=1";
21 
22         Regex reg = new Regex("^SELECT[\\s]*(?<Fields>.*)[\\s]*FROM[\\s]*(?<Table>.*)[\\s]*AS[\\s]*(?<TableAlias>.*)[\\s]*WHERE[\\s]*(?<Condition>.*)", RegexOptions.IgnoreCase);
23         Match match = reg.Match(sql);
24 
25         if (!match.Success)
26             throw new ArgumentException("Cannot delete this type of collection");
27         string table = match.Groups["Table"].Value.Trim();
28         string tableAlias = match.Groups["TableAlias"].Value.Trim();
29         string condition = match.Groups["Condition"].Value.Trim().Replace(tableAlias, table);
30 
31         string sql1 = string.Format("DELETE FROM {0} WHERE {1}", table, condition);
32         int i = 0;
33         efContext.Context.Database.UseTransaction(efContext.Transaction);
34         i = efContext.Context.Database.ExecuteSqlCommand(sql1);
35         efContext.Committed = false;
36         return i;
37     }
38 
39 }        
EntityFrameworkRepository

 

1 public interface IRepository<T>
2 {
3     void Add(T entity);
4     void Update(T entity);
5     void Remove(T entity);
6     int Remove(Expression<Func<T, bool>> predicate = null);
7     IQueryable<T> Find(Expression<Func<T, bool>> predicate = null);
8     ......
9 }
IRepository

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM