1. 程式人生 > >Spring.NET教程(十六)事務管理(應用篇)

Spring.NET教程(十六)事務管理(應用篇)

目前有很多種資料訪問技術。在.net FCL中,有三類API可以執行事務管理,分別是ADO.NET、企業服務和System.Transactions。其它的資料訪問技術,如物件關係對映(object relational mappers)和結果集對映(result-set mapping)等等的應用也很廣泛,每種技術也都有自己的事務管理API。事務管理的程式碼一般是直接和各種事務API繫結在一起的,所以在開發時必須根據所用的具體技術來決定採用哪種API。但是,這種程式碼與事務API的緊耦合決定了很難通過簡單的重構來解決更換資料訪問技術的問題。而 Spring.NET的事務框架允許在各種資料訪問技術之上使用相同的API。通過配置或者集中的程式設計方式,可以很容易的更換後臺事務API,而不需要對程式碼進行“大修”。

我們可以用業界公認的最佳方式來建立一種資料訪問機制。Martin Fowler的著作《Patterns of EntERPrise Application Architecture》講到了許多在實際應用中非常成功的資料訪問方法。其一便是在應用程式架構中引入一個數據訪問層。資料訪問層不僅要考慮到與不同的資料庫和資料訪問技術的相容性,而且職責要嚴格限制在資料訪問功能上。資料訪問層應該只包含資料訪問物件(DAO)以及“建立/獲取/更新/刪除”(CRUD,Create/Retrieve/Update/Delete)的操作,不應該涉及任何業務邏輯。業務邏輯應該位於單獨的業務服務層,並且需要與一或多個DAO協作來完成高層次的使用者功能。

為了在事務中“要麼全執行要麼全不執行”這些使用者功能,事務環境(transaction context)就應該由業務服務層(或某個“更高”的層次)控制。在實現上,一個很重要的細節是如何讓DAO瞭解在其它層次中開始的“外部”事務。如果讓DAO自己負責連線和事務的管理,就把問題看的過於簡單化了,因為此時每個DAO都會執行自己的事務/資源管理,所以無法在同一事務中執行多個DAO操作。我們需要一種有效的手段,將連線/事務成對的從業務服務層傳遞給DAO。方法有很多種,最不具侵入性的就是將連線/事務作為方法引數顯式的傳遞給DAO。另一種方法是將連線/事務放線上程本地儲存內。不管使用哪種方法,只要在用ADO.NET,就必須得自己建立一個基礎框架來完成這個任務。

 但是,等一下,企業服務不是能解決這個問題嗎還有System.Transactions名稱空間呢?關於這個嘛,答案是對...也不對。企業服務確實能夠讓我們在事務環境中使用“原生”的ADO.NET在同一事務中執行多個DAO操作。但它的缺點是必須通過MS-DTC(Microsoft Distributed Transaction Coordinator)使用分散式(全域性的)事務。如果只為了使用全域性事務就必須依賴MS-DTC,那應用程式在效能上就會大打折扣了。

使用.net2.0新增System.Transactions名稱空間下的TransactonScope類時,也有相同的問題。TransactonScope類的目的實際上是用using語句使一段程式碼成為事務性程式碼。只要訪問事務性的資源,using語句中的普通ADO.NET程式碼就會在一個ADO.NET本地事務中執行。但是,System.Transactions(和資料庫)的“神奇“之處在於,如果需要訪問第二個事務性資源,本地事務就會升級為分散式事務。這個過程叫做PSPE(Promotable Single Phase Enlistment)。此外,還需提醒讀者:在同一個資料庫上使用同一連線字串開啟第二個連線,就會使本地事務升級為分散式事務。所以,如果每個 DAO都執行自己的連線管理,那就完了:本地事務會突然升級為分散式事務!如果應用程式只使用一個數據庫,要想避免這個問題,就必須將唯一的連線物件傳遞給系統中的所有DAO。另外要注意的是某些資料庫不支援PSPE,就算用單個數據庫連線也會使用分散式事務(比如Oracle)。

Spring.NET的宣告式事務管理功能非常強大。在資料庫事務領域,討論宣告式事務管理的話題並不是很多,因為現在開發人員已經可以不再直接用凌亂繁複的事務API來進行事務管理了,而是可以通過在類和方法上應用某些特性來進行。但是在FCL中,只有企業服務提供了這一功能。 Spring.NET則填補了這個空白不管使用哪種事務管理技術:ADO.NET,還是(最常用的)System.Transactions,都可以使用宣告式的事務管理。另外,企業服務也有自己的問題,舉例來說,如果需要為查詢/讀取操作和建立/更新/刪除操作設定不同的隔離等級,就必須將這兩組操作分隔在不同的類中實現,因為在企業服務中,宣告式事務的元資料只對類有效。但總的來說,企業服務,特別是在XP sp2和Server 2003中新出現的“無元件服務”,還有在應用程式程序內駐留的功能都是相當不錯的。不過,雖然有這些優點,企業服務仍尚未在開發社群中引起很大的關注。

Spring.net事務管理的宗旨,就是要減輕FCL或第三方資料訪問技術給開發人員帶來的這些“痛苦”。它支援宣告式事務管理,可以用配置方式獲取事務選項的元資料目前支援兩種宣告方式:在程式碼中用.NET特性宣告;在IoC容器中用XML宣告。

最後,Spring.NET事務管理還允許在同一事務中使用不同的資料訪問技術例如混合使用ADO.NET和NHibernate。

好了,再講估計就有點煩人了,現在我們來看程式碼。

在Spring.NET中,提供了以下實現類:

AdoPlatformTransactionManager- 基於本地ADO.NET的事務。

ServiceDomainPlatformTransactionManager- 由企業服務提供的分散式事務管理器。

TxScopePlatformTransactionManager- 由System.Transactions提供的本地/分散式的事務管理器。

ITransactionDefinition介面封裝了以下資訊:

Isolation:該事務對其它事務操作的隔離級別。例如,用來表示某個事務是否能看到其它事務寫入的、但尚未提交的資訊。

Propagation:一般情況下,TransactionScope內的程式碼都會在為其指定的事務中執行。但是,該屬性可用來設定如果某個事務環境已經存在時,該事務內的方法是否要執行:比如說,是簡單的讓它在現有事務中繼續執行呢(這是一般情況),還是掛起現有事務然後建立一個新事務來執行。

Timeout:在超時(並且被事務基礎框架自動回滾之前)前該事務可以執行多久。

Read-only狀態:只讀的事務不會修改任何資料。在某些情況下(比如使用NHibernate時),只讀事務能顯著提高效能。

讓我們從實現程式碼中學習Spring.NET事務管理的機制。

準備條件:資料中建了2張表,如圖1

 

UserTable為儲存使用者資訊的表,AccountTable為儲存使用者賬號的資訊。

資料庫訪問層:

AccountDao

public interface IAccountDao

{

void Create(string name, string userName);

void Delete(string userName);

}

public class AccountDao : AdoDaoSupport, IAccountDao

{

public void Create(string name, string userName)

{

AdoTemplate.ExecuteNonQuery(CommandType.Text,

String.Format("INSERT INTO AccountTable (UserName,AccountName) VALUES ('{0}', '{1}')", userName, name));

}

public void Delete(string userName)

{

AdoTemplate.ExecuteNonQuery(CommandType.Text,

String.Format("DELETE FROM AccountTable WHERE UserName = '{0}'", userName));

}

}

UserDao

public interface IUserDao

{

void Create(string name, int age);

void Delete(string name);

DataSet Get(string name);

}

public class UserDao : AdoDaoSupport, IUserDao

{

public void Create(string name, int age)

{

AdoTemplate.ExecuteNonQuery(CommandType.Text,

string.Format("INSERT INTO UserTable (UserName,UserAge) VALUES ('{0}', {1})", name, age));

}

public void Delete(string name)

{

AdoTemplate.ExecuteNonQuery(CommandType.Text,

string.Format("DELETE FROM UserTable WHERE UserName = '{0}'", name));

}

public DataSet Get(string name)

{

return AdoTemplate.DataSetCreate(CommandType.Text, 

string.Format("SELECT * FROM UserTable WHERE UserName = '{0}'", name));

}

}

業務處理層:當我們插入使用者資訊後,需要為這個使用者建立一個賬號,當我們刪除使用者時,需要將帳號資訊一起刪除。這裡我們就會用到了事務。

UserService

public interface IUserService

{

void SaveData(string name, int age, string accountName);

void DeleteData(string name);

DataSet Get(string name);

}

public class UserService : IUserService

{

public IUserDao UserDao { get; set; }

public IAccountDao AccountDao { get; set; }

[Transaction]

public void SaveData(string name, int age, string accountName)

{

UserDao.Create(name, age);

AccountDao.Create(accountName, name);

}

[Transaction]

public void DeleteData(string name)

{

UserDao.Delete(name);

AccountDao.Delete(name);

}

[Transaction(ReadOnly = true)]

public DataSet Get(string name)

{

return UserDao.Get(name);

}

}

配置:

Dao.XML

<?XML version="1.0" encoding="utf-8" ?>

<objects xmlns="http://www.springFramework.net" 

 xmlns:db="http://www.springframework.net/database" 

 xmlns:tx="http://www.springframework.net/tx">

<db:provider id="DbProvider"

provider="SqlServer-1.1"

connectionString="Server=(local);Database=SpringLesson16;Uid=sa;Pwd=;Trusted_Connection=False"/>

<object id="userDao" type="Dao.UserDao, Dao">

<property name="AdoTemplate" ref="adoTemplate"/>

</object>

<object id="accountDao" type="Dao.AccountDao, Dao">

<property name="AdoTemplate" ref="adoTemplate"/>

</object>

<object id="userService" type="Service.UserService, Service">

<property name="UserDao" ref="userDao"/>

<property name="AccountDao" ref="accountDao"/>

</object>

<object id="adoTemplate" type="Spring.Data.Core.AdoTemplate, Spring.Data">

<property name="DbProvider" ref="DbProvider"/>

<property name="DataReaderWrapperType" value="Spring.Data.Support.NullMappingDataReader, Spring.Data"/>

</object>

<!--事務管理器-->

<object id="transactionManager"

 type="Spring.Data.Core.AdoPlatformTransactionManager, Spring.Data">

<property name="DbProvider" ref="DbProvider"/>

</object>

<!--事務切面-->

<tx:attribute-driven/>

</objects>

App.config

<?XML version="1.0" encoding="utf-8" ?>

<configuration>

<configSections>

<sectionGroup name="spring">

<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core"/>

<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core"/>

<section name="parsers" type="Spring.Context.Support.NamespaceParsersSectionHandler, Spring.Core"/>

</sectionGroup>

</configSections>

<spring>

<parsers>

<parser type="Spring.Data.Config.DatabaseNamespaceParser, Spring.Data"/>

<parser type="Spring.Transaction.Config.TxNamespaceParser, Spring.Data"/>

</parsers>

<context>

<resource uri="assembly://Dao/Dao/Dao.XML"/>

</context>

</spring>

</configuration>

我們新建一個單元測試:

TransactionTest

[TestFixture]

public class TransactionTest

{

[Test]

public void AdoTransaction()

{

IApplicationContext ctx = ContextReGIStry.GetContext();

IUserService service = (IUserService)ctx.GetObject("userService");

service.SaveData("劉冬", 26, "1233456");

}

}

輸出效果:兩條資料已經插入資料庫(圖2)

 

我們修改一下UserService的DeleteData方法,在呼叫 UserDao.Delete(name)以後丟擲異常,測試資料是否回滾。

DeleteData

[Transaction]

public void DeleteData(string name)

{

UserDao.Delete(name);

new Exception("測試資料是否回滾");

AccountDao.Delete(name);

}

如果資料沒有回滾,則我們得到的資料為UserTable表中沒有資料,AccountTable表中有資料。

TransactionTest

public class TransactionTest

{

[Test]

public void AdoTransaction()

{

IApplicationContext ctx = ContextReGIStry.GetContext();

IUserService service = (IUserService)ctx.GetObject("userService");

service.DeleteData("劉冬");

}

}

輸出效果:

 

圖3

我們發現數據已經回滾。

Spring.net框架幫我們很好的管理了事務。但在我們的編碼過程中經常使用到try...catch語句。要是在DeleteData方法中加入try...catch語句會回滾嗎?我們修改一下UserService的SaveData方法

SaveData

 [Transaction]

public void SaveData(string name, int age, string accountName)

{

try

{

UserDao.Create(name, age);

AccountDao.Create(accountName, name);

}

catch (Exception ex)

{

Console.WriteLine(ex);

}

}

當前的資料庫AccountTable表中已經存在欄位為AccountName的“123456”記錄。因為AccountName欄位是主鍵,我再插入一條資料就會出現異常。

AdoTransaction

[Test]

public void AdoTransaction()

{

IApplicationContext ctx = ContextReGIStry.GetContext();

IUserService service = (IUserService)ctx.GetObject("userService");

service.SaveData("劉鼕鼕", 27, "1233456");

}

輸出效果:

 

 圖4

我們發現在使用try...catch語句以後並沒有回滾。從以上的效果中,我們可以得出結論:使用try...catch的異常叫作“執行期異常”,Spring.NET的事務管理預設是不對執行期異常回滾的。

如果要實現沒有try...catch語句不回滾事務,我們需要在方法上標記[Transaction(NoRollbackFor = new Type[] { typeof(Exception) })]。NoRollbackFor 屬性是Type陣列,意思是運到Exception異常就不回滾,您也可以在增加其它的異常條件,如typeof(ArithmeticException)。這樣,當遇到標有包含NoRollbackFor 屬性的異常時,就不進行回滾。