從壹開始學習NetCore 45 ║ 終於解決了事務問題
- 2019 年 10 月 3 日
- 筆記
一、項目說明
哈嘍,又來寫文章了,原來放假可以這麼爽,可以學習和分享,?噓,大家要好好的工作喲。昨天發表的問題,嗯,給我留下了一點點衝擊,夜裡輾轉反側,想了很多,從好到壞再到好再到壞,從希望到失望再到希望再到失望,想起來當年高四了,不想解釋什麼了,四年後再見❤,不說廢話,直接說說今天的內容吧。
今天這個內容,還是來源於兩個多月前,我的項目的一個 issue ,當時說到了如何使用事務,(為啥要使用事務,我就不多說了,相信肯定都知道,還有那個每次面試都問的題,事務四大特性。不知道還有沒有小夥伴記得,不,是都記得!)我一直也是各種嘗試,直到前幾天也嘗試了幾個辦法,還是無果,然後又和 sqlsugar 的作者凱旋討論這個問題。他說只要能保證每次http 的scope 會話中的 sugar client 是同一個就行了,而且又不能把 client 設置為單例,天天看著這個 issue,心裡難免波瀾,終於喲,昨天群管 @大黃瓜 小夥伴研究出來了,我很開心,表揚下他,下邊就正式說說在我的項目中,如果使用事務的:
項目介紹: netcore 2.2 + Sqlsugar 5.0 + UnitOfWork + async Repository + Service 。
投稿作者:QQ群:大黃瓜(部落格園地址不詳)
項目已經修改,不僅僅實現了單一倉儲服務的事務提交,而且也可以跨類跨倉儲服務來實現事務,歡迎大家下載與公測,沒問題,我會merge 到 master。
為了防止大家不必要的更新錯誤,我新建了一個分支,大家自己去看分支即可——https://github.com/anjoy8/Blog.Core/tree/Trans1.0 。
Tips:
我認為 sqlsugar 還是很不錯,很好用,當然,不能用萬能來形容客觀事物,這本身就不是一個成年人該有的思維,在我推廣 sqlsugar 這一年來,我也一直給凱旋提一些需求和Bug,他都特別及時的解決了,而且使用上也很順手,目前已經實現了跨服務事務操作了,下一步就是在blog.core 中,使用主從資料庫,分離了,加油。
二、重新設計SqlSugarClient
1、創建工作單元介面
首先我們需要在 Blog.Core.IRepository 層,創建一個文件夾 UnitOfWork ,然後創建介面 IUnitOfWork.cs ,用來對工作單元進行定義相應的行為操作:
public interface IUnitOfWork { // 創建 sqlsugar client 實例 ISqlSugarClient GetDbClient(); // 開始事務 void BeginTran(); // 提交事務 void CommitTran(); // 回滾事務 void RollbackTran(); }
2、對 UnitOfWork 介面進行實現
在 Blog.Core.Repository 層,創建一個文件夾 UnitOfWork,然後創建事務介面實現類 UnitOfWork.cs ,來對事務行為做實現。
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(); } }
具體的內容,很簡單,這裡不過多解釋。
3、用 UnitOfWork 接管 SqlguarClient
在基類泛型倉儲類 BaseRepository<TEntity> 中,我們修改構造函數,注入工作單元介面,用來將 sqlsugar 實例統一起來,不是每次都 new,而且通過工作單元來控制:
private ISqlSugarClient _db; private readonly IUnitOfWork _unitOfWork; // 構造函數,通過 unitofwork,來控制sqlsugar 實例 public BaseRepository(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; _db = unitOfWork.GetDbClient(); // 好像這個可以去掉,先保留 DbContext.Init(BaseDBConfig.ConnectionString, (DbType)BaseDBConfig.DbType); }
你可以對比下以前的程式碼,就知道了,這麼做的目的,就是把 sugar client 統一起來,這樣就能保證每次一個scope ,都能是同一個實例。
4、修改每一個倉儲的構造函數
上邊我們為了實現對 sugar client的控制,在基類倉儲的構造函數中,注入了IUnitOfWork,但是這樣會導致子類的倉儲報錯,畢竟父類構造函數修改了嘛,所以目前有兩個方案:
1、去掉子倉儲,只使用泛型基類倉儲,在service層中,使用 private readonly IRepository<實體類> _repository; 這種方法。
2、去一一的修改子倉儲,增加構造函數,將 IUnitOfWork 傳給父類,具體的看我的程式碼即可:
5、依賴注入 ISqlSugarClient
這個是肯定的,大家還記得上邊說的呢,我們要在 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操作 IsShardSameThread=true,//共享執行緒 InitKeyType = SqlSugar.InitKeyType.SystemTable//默認SystemTable, 欄位資訊讀取, 如:該屬性是不是主鍵,標識列等等資訊 }); });
這裡有一個小知識點,就是我們的 IUnitOfWork 已經隨著 倉儲層 依賴注入了,就不許單獨注入了,是不是這個時候感覺使用 Autofac 很方便?
到了這裡,修改就完成了,下邊就是如何使用了。
三、正式使用事務
1、直接操作跨 Service 事務
現在我們就可以使用如何使用事務了,第一個簡單粗暴的,就是全部寫到 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" }; }
項目的過程,在上邊注釋已經說明了,大家可以看一下,很簡單,就是查詢,添加,再查詢,判斷是否操作成功,那現在我們就測試一下,資料庫表是空的:
然後我們執行方法,動圖如下:
可以看到,我們是密碼錶已經添加了一條數據的前提下,後來回滾後,數據都被刪掉了,資料庫也沒有對應的值,達到的目的。
但是這裡有兩個小問題:
1、我們控制的是 Service 類,那我們能不能控制倉儲 Repository 類呢?
2、我們每次都這麼寫,會不會很麻煩呢,能不能用統一AOP呢?
答案都是肯定的!
2、建立事務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] ,然後在控制器正常的調用,具體的操作和結果就不展示了,已經測試過了,沒問題。
到這裡,就終於解決了事務的相關操作,當然這裡還是有很多的問題需要考究,我也在考慮有沒有更好的點子和方案,期待後續報道。
四、Github && Gitee
注意情況分支:Trans1.0
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core