Linq to SQL支持三種事務(wù)處理模型:顯式本地事務(wù)、顯式可分發(fā)事務(wù)、隱式事務(wù)。(from MSDN: 事務(wù) (LINQ to SQL))。MSDN中描述得相對(duì)比較粗狂,下面就結(jié)合實(shí)例來對(duì)此進(jìn)行闡述。
0. 測(cè)試環(huán)境
| OS | Windows Server 2008 Enterprise + sp1 |
| IDE | Visual Studio 2008, .net framework 3.5 + SP1 |
| DB | SQL Server 2000 + sp4 SQL Server 2008 Enterprise Edition |
1. 隱式事務(wù)
當(dāng)調(diào)用SubmitChanges 時(shí),L2S會(huì)檢查當(dāng)前環(huán)境是否開啟了事務(wù)(下面兩節(jié)中創(chuàng)建的事務(wù)),如果沒有開始事務(wù),則 L2S啟動(dòng)本地事務(wù)(IDbTransaction),并使用此事務(wù)執(zhí)行所生成的 SQL 命令。
使用Reflector查看DataContext.SubmitChanges的源代碼(見本文最后的附錄),可以看到,如果開啟了一個(gè)事務(wù),并將其傳給DataContext.Transaction,則在執(zhí)行SubmitChanges時(shí)就不會(huì)再重新開啟一個(gè)新的事務(wù)了。
例如,下面的代碼會(huì)創(chuàng)建應(yīng)隱式事務(wù):
1: public static void TestTranIn2000()
2: {3: using (SQL2000.Sql2000DataContext context1 = new SQL2000.Sql2000DataContext())
4: { 5: List<SQL2000.TLINQ> linq = context1.TLINQ.ToList(); 6: linq.ForEach(e => e.Value += e.Value); 7: context1.SubmitChanges(); 8: } 9: }可以打開SQL Server Profile來查看生成的T-SQL,生成的Update語句被包含在Begin Transaction和Commit Transaction之間,如下圖所示:
上圖是我使用Linq to SQL + SQL Server 2000進(jìn)行測(cè)試的結(jié)果。下圖是我用Linq to SQL + SQL Server 2008測(cè)試的結(jié)果:
很奇怪的是,居然沒有Begin Transaction和Commit Transaction了。Begin/commit trand的事件類型是“SQL:BatchStarting”/“SQL:BatchCompleted”,從這個(gè)圖中可以看到,我有跟蹤這個(gè)事件(譬如第一個(gè)框中的Select命令),汗……是MSDN上說錯(cuò)了?
抱著這個(gè)疑問,我又針對(duì)Linq to Sql + SQL Server 2008做了進(jìn)一步測(cè)試(這個(gè)例子也可以用來測(cè)試后面兩節(jié)中的事務(wù)處理,確保所有操作被分封裝在同一個(gè)事務(wù)中。):
這里里面做了兩件事:創(chuàng)建一個(gè)新對(duì)象,同時(shí)還修改原有記錄中的值。我故意Insert可以執(zhí)行成功,而讓Update語句執(zhí)行出錯(cuò);如果有啟用事務(wù),則出錯(cuò)后事務(wù)會(huì)回滾,最終不會(huì)創(chuàng)建新記錄;如果沒有啟用事務(wù),則可以正常插入,但不會(huì)執(zhí)行第二步中的更新。
注意:從理論上講,執(zhí)行SubmitChanges時(shí),并不一定是按照上面代碼的順序,先執(zhí)行插入再執(zhí)行更新;下面是MSDN上的說法:
|
當(dāng)您進(jìn)行此調(diào)用時(shí),DataContext 會(huì)設(shè)法將您所做的更改轉(zhuǎn)換為等效的 SQL 命令。您可以使用自己的自定義邏輯來重寫這些操作,但提交順序是由 DataContext 的一項(xiàng)稱作“更改處理器”的服務(wù)來協(xié)調(diào)的。事件的順序如下:
此時(shí),如果數(shù)據(jù)庫檢測(cè)到任何錯(cuò)誤,都會(huì)造成提交進(jìn)程停止并引發(fā)異常。將回滾對(duì)數(shù)據(jù)庫的所有更改,就像未進(jìn)行過提交一樣。DataContext 仍具有所有更改的完整記錄。 |
因此,這里還是打開SQL Server Profile來確認(rèn):
OK,現(xiàn)在可以放心了,這里的確是先執(zhí)行Insert,再執(zhí)行Update;執(zhí)行Update時(shí)出現(xiàn)了SqlException異常。這時(shí)查看測(cè)試表TLINQ中的數(shù)據(jù),發(fā)現(xiàn)沒有插入新的記錄進(jìn)去。也就是說,這里使用了事務(wù),但是SQL Server Profile沒有跟蹤到。
至于為啥會(huì)這樣,我暫時(shí)也沒有搞清楚;還有就是下面一節(jié)中即使執(zhí)行DbConnection.BeginTransaction(),也不會(huì)跟蹤到begin tran和commit tran。不知道是不是SQL Server 2008里面升級(jí)了啥。哪位如果知道原因,麻煩告知我一聲,謝謝。
最后總結(jié)一下隱式事務(wù)的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):使用簡單,L2S都幫我們搞定了,我們不需要寫任何代碼。
缺點(diǎn):只能處理單個(gè)DataContext中的單個(gè)SubmitChanges()函數(shù)的調(diào)用。如果需要將SubmitChanges()與其他自定義更新操作(譬如ExcuteCommand)共用一個(gè)Transaction、或者與其他DataContext共用一個(gè)DBTransation,就沒轍了.......
2. 顯式本地事務(wù)
SubmitChanges只能處理最基本的CUD(Create/Update/Delete)操作;而在日常的應(yīng)用中,邏輯往往沒有這么簡單,或者考慮性能等因素,我們需要配合ADO.Net執(zhí)行原生的TSQL;這時(shí)我們可能要讓ADO.Net + Linq to SQL來進(jìn)行配合處理。
也就是說,我們可以手工創(chuàng)建一個(gè)DbConnection和DbTransaction,然后傳給DataContext,代碼示例如下:
1: public static void TestTranInSQL2008()
2: {3: using(SqlConnection conn = new SqlConnection(Settings.Default.AdventureWorksConnectionString))
4: {5: conn.Open(); //1. 創(chuàng)建并打開DbConnection連接
6: using (SqlTransaction tran = conn.BeginTransaction()) //2. 開啟DbTransaction
7: {8: //3. 使用ADO.Net執(zhí)行TSQL
9: SqlCommand cmd = new SqlCommand("Update TLinq SET Value=10", conn, tran);
10: cmd.ExecuteNonQuery(); 11: 12: //4. 配合Ado.Net和Linq to Sql: 將ADO.Net的DbConnection和DbTransaction傳給Linq to Sql
13: using (AdventureWorksDataContext context1 = new AdventureWorksDataContext(conn))
14: { 15: context1.Transaction = tran; 16: List<TLINQ> linq = context1.TLINQ.ToList();17: context1.TLINQ.InsertOnSubmit(new TLINQ() { Value = "1" });
18: linq.ForEach(e => e.Value = (Convert.ToInt32(e.Value) + 1).ToString()); 19: context1.SubmitChanges(); 20: } 21: 22: tran.Commit(); //5. 需要提交手工創(chuàng)建的事務(wù)
23: } 24: } 25: }最后總結(jié)一下使用顯式本地事務(wù)的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):可以配合Ado.Net一起使用,或者跨DataContext使用,實(shí)現(xiàn)負(fù)責(zé)的業(yè)務(wù)邏輯。
缺點(diǎn):處理步驟比較繁瑣。L2S中的DataContext已經(jīng)提供了ExcuteCommand方法來執(zhí)行原生的TSQL,這里還這樣使用Ado.net就太折騰自己了.......
3. 顯式可分發(fā)事務(wù)
使用TransactionScope來進(jìn)行顯示可分發(fā)事務(wù)控制。TransactionScope就像事務(wù)處理里面的一面魔鏡,如果需要事務(wù),就對(duì)著它喊:“主啊,請(qǐng)賜我事務(wù)!”,于是這個(gè)操作就有了事務(wù)功能。關(guān)于TransactionScope的詳細(xì)介紹,可以參考MSDN:使用事務(wù)范圍實(shí)現(xiàn)隱式事務(wù),及SQL Server的聯(lián)機(jī)叢書:CLR 集成和事務(wù)
使用顯式可分發(fā)事務(wù)進(jìn)行處理的示例代碼如下:
1: public static void TestTransactionScopeInSQL2008()
2: {3: Action action = () => //1.把要執(zhí)行的操作封裝在一個(gè)或多個(gè)Action中
4: {5: using (AdventureWorksDataContext context1 = new AdventureWorksDataContext())
6: {7: context1.ExecuteCommand("Update TLinq SET Value={0}", 10);
8: List<TLINQ> linq = context1.TLINQ.ToList();9: context1.TLINQ.InsertOnSubmit(new TLINQ() { Value = "1" });
10: linq.ForEach(e => e.Value = (Convert.ToInt32(e.Value) + 1).ToString()); 11: context1.SubmitChanges(); 12: } 13: }; 14: TransactioExtension.Excute(action); 15: }或者這樣:
1: /// <summary>
2: /// 此方法里面完全不必考慮事務(wù)
3: /// </summary>
4: public static void TestTransactionScopeInSQL2008()
5: {6: using (AdventureWorksDataContext context1 = new AdventureWorksDataContext())
7: {8: context1.ExecuteCommand("Update TLinq SET Value={0}", 10);
9: List<TLINQ> linq = context1.TLINQ.ToList();10: context1.TLINQ.InsertOnSubmit(new TLINQ() { Value = "1" });
11: linq.ForEach(e => e.Value = (Convert.ToInt32(e.Value) + 1).ToString()); 12: context1.SubmitChanges(); 13: } 14: } 15: 16: //而在外面直接這樣使用
17: TransactioExtension.Excute(() => TestTransactionScopeInSQL2008());灰常灰常地簡潔,把要執(zhí)行的操作封裝在一個(gè)或多個(gè)Action中,然后傳遞給TransactioExtension.Excute即可。對(duì)于封裝在TransactionScope里面執(zhí)行的所有操作(譬如SubmitChanges,ExcuteCommand、ExecuteQuery),最終都會(huì)解析為對(duì)ADO.NET的調(diào)用;而ADO.Net會(huì)在調(diào)用 Connection.Open 方法時(shí)自動(dòng)檢查Transaction.Current,并在該事務(wù)中以透明方式登記連接(除非在連接字符串中將 Enlist 關(guān)鍵字設(shè)置為 false)。
SqlConnection 對(duì)象的 ConnectionString 屬性支持 Enlist 關(guān)鍵字,該關(guān)鍵字指示 System.Data.SqlClient 是否檢測(cè)事務(wù)上下文并在分布式事務(wù)中自動(dòng)登記連接。如果此關(guān)鍵字設(shè)置為 True(默認(rèn)設(shè)置),則會(huì)在打開的線程的當(dāng)前事務(wù)上下文中自動(dòng)登記連接。如果此關(guān)鍵字設(shè)置為 False,則 SqlClient 連接不會(huì)與分布式事務(wù)交互。如果未在連接字符串中指定 Enlist,并且如果在打開相應(yīng)連接時(shí)檢測(cè)到一個(gè)分布式事務(wù),則會(huì)在此分布式事務(wù)中自動(dòng)登記此連接。(FROM Sql Server 2008 聯(lián)機(jī)叢書)
關(guān)于TransactioExtension的封裝,代碼如下所示:(由于TransactionScope默認(rèn)的事務(wù)隔離級(jí)別是IsolationLevel.Serializable,這里調(diào)整為ReadCommitted隔離級(jí)別,以保持與Sql Server的默認(rèn)隔離級(jí)別一致):
1: public static class TransactioExtension
2: {3: public static void Excute(params Action[] actions)
4: {5: //使用ReadCommitted隔離級(jí)別,保持與Sql Server的默認(rèn)隔離級(jí)別一致
6: Excute(IsolationLevel.ReadCommitted, null, actions);
7: } 8: 9: public static void Excute(IsolationLevel level, params Action[] actions)
10: {11: Excute(level, null, actions);
12: } 13: 14: public static void Excute(int timeOut, params Action[] actions)
15: { 16: Excute(IsolationLevel.ReadCommitted, timeOut, actions); 17: } 18: 19: public static void Excute(IsolationLevel level, int? timeOut, params Action[] actions)
20: {21: if (actions == null || actions.Length == 0)
22: return;
23: 24: TransactionOptions options = new TransactionOptions();
25: options.IsolationLevel = level; //默認(rèn)為Serializable,這里根據(jù)參數(shù)來進(jìn)行調(diào)整
26: if(timeOut.HasValue)
27: options.Timeout = new TimeSpan(0, 0, timeOut.Value); //默認(rèn)60秒
28: using (TransactionScope tran = new TransactionScope(TransactionScopeOption.Required, options))
29: { 30: Array.ForEach<Action>(actions, action => action());31: tran.Complete(); //通知事務(wù)管理器它可以提交事務(wù)
32: } 33: } 34: }不過在使用TransactionScope時(shí),需要注意,如果使用的數(shù)據(jù)庫是SQL Server 2000,或者需要實(shí)現(xiàn)跨多個(gè)數(shù)據(jù)庫進(jìn)行事務(wù)控制,則需要開啟DTC服務(wù)(位于:開始->管理工具->服務(wù)->Distributed Transaction Coordinator),下面是我的測(cè)試結(jié)果(我沒有裝SQL Server 2005,所以沒測(cè)2005的情況):
| 測(cè)試環(huán)境 | 是否需要開啟DTC | 創(chuàng)建出來的事務(wù)類型 |
| Linq to Sql + Sql Server 2000(單一數(shù)據(jù)庫) | 需要 | 分布式事務(wù) |
| Linq to Sql + Sql Server 2008(單一數(shù)據(jù)庫) | 不需要 | 輕型本地事務(wù) |
| Linq to Sql + Sql Server 2008(跨多個(gè)數(shù)據(jù)庫) | 需要 | 訪問第一個(gè)數(shù)據(jù)庫時(shí),會(huì)創(chuàng)建輕型本地事務(wù);當(dāng)繼續(xù)訪問更多的數(shù)據(jù)庫時(shí),會(huì)將事務(wù)升級(jí)為完全可分發(fā)的分布式事務(wù) |
最后總結(jié)一下使用顯式可分發(fā)事務(wù)的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):使用簡單,可以配合ADO.Net或者DataContext.ExcuteCommand使用,可以跨DataContext使用,可以跨數(shù)據(jù)庫使用,可以跨服務(wù)器使用。
缺點(diǎn):分布式事務(wù)通常會(huì)使用大量的系統(tǒng)資源。Microsoft 分布式事務(wù)處理協(xié)調(diào)器 (MS DTC) 會(huì)管理此類事務(wù),并集成在這些事務(wù)中訪問的所有資源管理器。慶幸的是:在打開一個(gè)具有活動(dòng)TransactionScope事務(wù)的連接而未打開任何其他連接的情況下,該事務(wù)會(huì)以輕型事務(wù)的形式提交,而不是產(chǎn)生完全分布式事務(wù)的額外開銷。
最后來個(gè)匯總:
| 事務(wù)類型 | 優(yōu)點(diǎn) | 缺點(diǎn) |
| 隱式事務(wù) | 使用簡單,由L2S自動(dòng)處理。 | 僅限于單個(gè)DataContext中的SubmitChanges方法內(nèi)使用。 |
| 顯式本地事務(wù) | 可以配合Ado.Net一起使用,可以跨多個(gè)DataContext來使用 | 使用相對(duì)繁瑣一點(diǎn)兒;且不支持與DataContext.ExecuteCommand配合使用 |
| 顯式可分發(fā)事務(wù) | 功能強(qiáng)大(可以配合ADO.Net或者DataContext.ExcuteCommand使用,可以跨DataContext使用,可以跨數(shù)據(jù)庫使用,可以跨服務(wù)器使用),使用簡單 | 可能會(huì)對(duì)性能有一些影響(我暫時(shí)也沒有測(cè)試過-,-) |
附:用Reflector查看DataContext.SubmitChanges的源代碼如下:
1: public virtual void SubmitChanges(ConflictMode failureMode)
2: {3: this.CheckDispose();
4: this.CheckNotInSubmitChanges();
5: this.VerifyTrackingEnabled();
6: this.conflicts.Clear();
7: try
8: {9: this.isInSubmitChanges = true;
10: if ((Transaction.Current == null) && (this.provider.Transaction == null)) //如果不在事務(wù)環(huán)境內(nèi)
11: {12: bool flag = false;
13: DbTransaction transaction = null;
14: try
15: {16: if (this.provider.Connection.State == ConnectionState.Open)
17: {18: this.provider.ClearConnection();
19: }20: if (this.provider.Connection.State == ConnectionState.Closed)
21: {22: this.provider.Connection.Open();
23: flag = true;
24: }25: transaction = this.provider.Connection.BeginTransaction(IsolationLevel.ReadCommitted); //開啟事務(wù)
26: this.provider.Transaction = transaction;
27: new ChangeProcessor(this.services, this).SubmitChanges(failureMode);
28: this.AcceptChanges();
29: this.provider.ClearConnection();
30: transaction.Commit();31: return;
32: }33: catch
34: {35: if (transaction != null)
36: {37: try
38: { 39: transaction.Rollback(); 40: }41: catch
42: { 43: } 44: }45: throw;
46: }47: finally
48: {49: this.provider.Transaction = null;
50: if (flag)
51: {52: this.provider.Connection.Close();
53: } 54: } 55: }56: new ChangeProcessor(this.services, this).SubmitChanges(failureMode);
57: this.AcceptChanges();
58: }59: finally
60: {61: this.isInSubmitChanges = false;
62: } 63: }
浙公網(wǎng)安備 33010602011771號(hào)