哈嘍,又來寫文章了,原來放假能夠這麼爽,能夠學習和分享,🤫噓,你們要好好的工做喲。昨天發表的問題,嗯,給我留下了一點點衝擊,夜裏展轉反側,想了不少,從好到壞再到好再到壞,從但願到失望再到但願再到失望,想起來當年高四了,不想解釋什麼了,四年後再見❤,不說廢話,直接說說今天的內容吧。git
今天這個內容,仍是來源於兩個多月前,個人項目的一個 issue ,當時說到了如何使用事務,(爲啥要使用事務,我就很少說了,相信確定都知道,還有那個每次面試都問的題,事務四大特性。不知道還有沒有小夥伴記得,不,是都記得!)我一直也是各類嘗試,直到前幾天也嘗試了幾個辦法,仍是無果,而後又和 sqlsugar 的做者凱旋討論這個問題。他說只要能保證每次http 的scope 會話中的 sugar client 是同一個就好了,並且又不能把 client 設置爲單例,每天看着這個 issue,內心不免波瀾,終於喲,昨天羣管 @大黃瓜 小夥伴研究出來了,我很開心,表揚下他,下邊就正式說說在個人項目中,若是使用事務的:github
項目介紹: netcore 2.2 + Sqlsugar 5.0 + UnitOfWork + async Repository + Service 。面試
投稿做者:QQ羣:大黃瓜(博客園地址不詳) sql
項目已經修改,不只僅實現了單一倉儲服務的事務提交,並且也能夠跨類跨倉儲服務來實現事務,歡迎你們下載與公測,沒問題,我會merge 到 master。數據庫
爲了防止你們沒必要要的更新錯誤,我新建了一個分支,你們本身去看分支便可——https://github.com/anjoy8/Blog.Core/tree/Trans1.0 。目前已經合併到master分支,Trans1.0分支已刪除。api
Tips:緩存
我認爲 sqlsugar 仍是很不錯,很好用,固然,不能用萬能來形容客觀事物,這自己就不是一個成年人該有的思惟,在我推廣 sqlsugar 這一年來,我也一直給凱旋提一些需求和Bug,他都特別及時的解決了,並且使用上也很順手,目前已經實現了跨服務事務操做了,下一步就是在blog.core 中,使用主從數據庫,分離了,加油。ssh
首先咱們須要在 Blog.Core.IRepository 層,建立一個文件夾 UnitOfWork ,而後建立接口 IUnitOfWork.cs ,用來對工做單元進行定義相應的行爲操做:異步
public interface IUnitOfWork { // 建立 sqlsugar client 實例 ISqlSugarClient GetDbClient(); // 開始事務 void BeginTran(); // 提交事務 void CommitTran(); // 回滾事務 void RollbackTran(); }
在 Blog.Core.Repository 層,建立一個文件夾 UnitOfWork,而後建立事務接口實現類 UnitOfWork.cs ,來對事務行爲作實現。async
public class UnitOfWork : IUnitOfWork { private readonly ISqlSugarClient _sqlSugarClient; // 注入 sugar client 實例 public UnitOfWork(ISqlSugarClient sqlSugarClient) { _sqlSugarClient = sqlSugarClient; } // 保證每次 scope 訪問,多個倉儲類,都用一個 client 實例 // 注意,不是單例模型!!! public ISqlSugarClient GetDbClient() { return _sqlSugarClient; } public void BeginTran() { GetDbClient().Ado.BeginTran(); } public void CommitTran() { try { GetDbClient().Ado.CommitTran(); // } catch (Exception ex) { GetDbClient().Ado.RollbackTran(); } } public void RollbackTran() { GetDbClient().Ado.RollbackTran(); } }
具體的內容,很簡單,這裏不過多解釋。
在基類泛型倉儲類 BaseRepository<TEntity> 中,咱們修改構造函數,注入工做單元接口,用來將 sqlsugar 實例統一塊兒來,不是每次都 new,並且經過工做單元來控制:
private ISqlSugarClient _db; private readonly IUnitOfWork _unitOfWork; // 構造函數,經過 unitofwork,來控制sqlsugar 實例 public BaseRepository(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; _db = unitOfWork.GetDbClient(); }
你能夠對比下之前的代碼,就知道了,這麼作的目的,就是把 sugar client 統一塊兒來,這樣就能保證每次一個scope ,都能是同一個實例。
上邊咱們爲了實現對 sugar client的控制,在基類倉儲的構造函數中,注入了IUnitOfWork,可是這樣會致使子類的倉儲報錯,畢竟父類構造函數修改了嘛,因此目前有兩個方案:
一、去掉子倉儲,只使用泛型基類倉儲,在service層中,使用 private readonly IRepository<實體類> _repository; 這種方法。
二、去一一的修改子倉儲,增長構造函數,將 IUnitOfWork 傳給父類,具體的看個人代碼便可:
這個是確定的,你們還記得上邊說的呢,咱們要在 BaseRepository 中,注入 ISqlSugarClient ,因此就必須依賴注入:
// 這裏我不是引用了命名空間,由於若是引用命名空間的話,會和Microsoft的一個GetTypeInfo存在二義性,因此就直接這麼使用了。 services.AddScoped<SqlSugar.ISqlSugarClient>(o => { return new SqlSugar.SqlSugarClient(new SqlSugar.ConnectionConfig() { ConnectionString = BaseDBConfig.ConnectionString,//必填, 數據庫鏈接字符串 DbType = (SqlSugar.DbType)BaseDBConfig.DbType,//必填, 數據庫類型 IsAutoCloseConnection = true,//默認false, 時候知道關閉數據庫鏈接, 設置爲true無需使用using或者Close操做 InitKeyType = SqlSugar.InitKeyType.SystemTable//默認SystemTable, 字段信息讀取, 如:該屬性是否是主鍵,標識列等等信息 }); });
這裏有一個小知識點,就是咱們的 IUnitOfWork 已經隨着 倉儲層 依賴注入了,就不準單獨注入了,是否是這個時候感受使用 Autofac 很方便?
到了這裏,修改就完成了,下邊就是如何使用了。
如今咱們就可使用如何使用事務了,第一個簡單粗暴的,就是所有寫到 controller 裏,我已經寫好了一個demo,你們來看看:
// 依賴注入 public TransactionController(IUnitOfWork unitOfWork, IPasswordLibServices passwordLibServices, IGuestbookServices guestbookServices) { _unitOfWork = unitOfWork; _passwordLibServices = passwordLibServices; _guestbookServices = guestbookServices; }
[HttpGet] public async Task<IEnumerable<string>> Get() { try { Console.WriteLine($""); //開始事務 Console.WriteLine($"Begin Transaction"); _unitOfWork.BeginTran(); Console.WriteLine($""); var passwords = await _passwordLibServices.Query(); // 第一次密碼錶的數據條數 Console.WriteLine($"first time : the count of passwords is :{passwords.Count}"); // 向密碼錶添加一條數據 Console.WriteLine($"insert a data into the table PasswordLib now."); var insertPassword = await _passwordLibServices.Add(new PasswordLib() { IsDeleted = false, plAccountName = "aaa", plCreateTime = DateTime.Now }); // 第二次查看密碼錶有多少條數據,判斷是否添加成功 passwords = await _passwordLibServices.Query(d => d.IsDeleted == false); Console.WriteLine($"second time : the count of passwords is :{passwords.Count}"); //...... Console.WriteLine($""); var guestbooks = await _guestbookServices.Query(); Console.WriteLine($"first time : the count of guestbooks is :{guestbooks.Count}"); int ex = 0; // 出現了一個異常! Console.WriteLine($"\nThere's an exception!!"); int throwEx = 1 / ex; Console.WriteLine($"insert a data into the table Guestbook now."); var insertGuestbook = await _guestbookServices.Add(new Guestbook() { username = "bbb", blogId = 1, createdate = DateTime.Now, isshow = true }); guestbooks = await _guestbookServices.Query(); Console.WriteLine($"second time : the count of guestbooks is :{guestbooks.Count}"); //事務提交 _unitOfWork.CommitTran(); } catch (Exception) { // 事務回滾 _unitOfWork.RollbackTran(); var passwords = await _passwordLibServices.Query(); // 第三次查看密碼錶有幾條數據,判斷是否回滾成功 Console.WriteLine($"third time : the count of passwords is :{passwords.Count}"); var guestbooks = await _guestbookServices.Query(); Console.WriteLine($"third time : the count of guestbooks is :{guestbooks.Count}"); } return new string[] { "value1", "value2" }; }
項目的過程,在上邊註釋已經說明了,你們能夠看一下,很簡單,就是查詢,添加,再查詢,判斷是否操做成功,那如今咱們就測試一下,數據庫表是空的:
而後咱們執行方法,動圖以下:
能夠看到,咱們是密碼錶已經添加了一條數據的前提下,後來回滾後,數據都被刪掉了,數據庫也沒有對應的值,達到的目的。
可是這裏有兩個小問題:
一、咱們控制的是 Service 類,那咱們能不能控制倉儲 Repository 類呢?
二、咱們每次都這麼寫,會不會很麻煩呢,能不能用統一AOP呢?
答案都是確定的!
在 Blog.Core api 層的 AOP 文件夾下,建立 BlogTranAOP.cs 文件,用來實現事務AOP操做:
public class BlogTranAOP : IInterceptor { // 依賴注入工做單元接口 private readonly IUnitOfWork _unitOfWork; public BlogTranAOP(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } /// <summary> /// 實例化IInterceptor惟一方法 /// </summary> /// <param name="invocation">包含被攔截方法的信息</param> public void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當前方法的特性驗證 //若是須要驗證 if (method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(UseTranAttribute)) is UseTranAttribute) { try { Console.WriteLine($"Begin Transaction"); _unitOfWork.BeginTran(); invocation.Proceed(); // 異步獲取異常,普通的 try catch 外層不能達到目的,畢竟是異步的 if (IsAsyncMethod(invocation.Method)) { if (invocation.Method.ReturnType == typeof(Task)) { invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally( (Task)invocation.ReturnValue, async () => await TestActionAsync(invocation), ex => { _unitOfWork.RollbackTran();//事務回滾 }); } else //Task<TResult> { invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult( invocation.Method.ReturnType.GenericTypeArguments[0], invocation.ReturnValue, async () => await TestActionAsync(invocation), ex => { _unitOfWork.RollbackTran();//事務回滾 }); } } _unitOfWork.CommitTran(); } catch (Exception) { Console.WriteLine($"Rollback Transaction"); _unitOfWork.RollbackTran(); } } else { invocation.Proceed();//直接執行被攔截方法 } } public static bool IsAsyncMethod(MethodInfo method) { return ( method.ReturnType == typeof(Task) || (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>)) ); } private async Task TestActionAsync(IInvocation invocation) { } }
上邊具體的操做很簡單,若是你看過個人緩存AOP和日誌AOP之後,確定就能看懂這個事務AOP的內容,這裏只是有一點,須要增長一個特性,public class UseTranAttribute : Attribute,這個和當時的緩存AOP是同樣的,只有配置了纔會實現事務提交,具體的請查看 UseTranAttribute.cs 類。
而後咱們測試一個子倉儲項目,具體的代碼以下:
在 Blog.Core.Services 層下的 GuestbookServices.cs 內,增長一個 Task<bool> TestTranInRepositoryAOP() 方法,內容和上邊 controller 中的控制 service 相似,只不過是用 Repository 操做類:
增長事務特性 [UseTran] ,而後在控制器正常的調用,具體的操做和結果就不展現了,已經測試過了,沒問題。
到這裏,就終於解決了事務的相關操做,固然這裏仍是有不少的問題須要考究,我也在考慮有沒有更好的點子和方案,期待後續報道。
注意狀況分支:Trans1.0