從壹開始學習NetCore 45 ║ 終於解決了事務問題

1、項目說明

哈嘍,又來寫文章了,原來放假能夠這麼爽,能夠學習和分享,🤫噓,你們要好好的工做喲。昨天發表的問題,嗯,給我留下了一點點衝擊,夜裏展轉反側,想了不少,從好到壞再到好再到壞,從但願到失望再到但願再到失望,想起來當年高四了,不想解釋什麼了,四年後再見❤,不說廢話,直接說說今天的內容吧。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

 

2、從新設計SqlSugarClient

一、建立工做單元接口

首先咱們須要在 Blog.Core.IRepository 層,建立一個文件夾 UnitOfWork ,而後建立接口 IUnitOfWork.cs ,用來對工做單元進行定義相應的行爲操做異步

 public interface IUnitOfWork
 {
     // 建立 sqlsugar client 實例
     ISqlSugarClient GetDbClient();
     // 開始事務
     void BeginTran();
     // 提交事務
     void CommitTran();
     // 回滾事務
     void RollbackTran();
 }

 

二、對 UnitOfWork 接口進行實現

在 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();
        }

    }

具體的內容,很簡單,這裏不過多解釋。

 

三、用 UnitOfWork 接管 SqlguarClient

在基類泛型倉儲類  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 傳給父類,具體的看個人代碼便可:

 

五、依賴注入 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操做
         InitKeyType = SqlSugar.InitKeyType.SystemTable//默認SystemTable, 字段信息讀取, 如:該屬性是否是主鍵,標識列等等信息
     });
 });

 

這裏有一個小知識點,就是咱們的 IUnitOfWork 已經隨着 倉儲層 依賴注入了,就不準單獨注入了,是否是這個時候感受使用 Autofac 很方便?

到了這裏,修改就完成了,下邊就是如何使用了。

 

 

3、正式使用事務

一、直接操做跨 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" };
}

 

項目的過程,在上邊註釋已經說明了,你們能夠看一下,很簡單,就是查詢,添加,再查詢,判斷是否操做成功,那如今咱們就測試一下,數據庫表是空的:

 

 

 

 而後咱們執行方法,動圖以下:

 

 

 

能夠看到,咱們是密碼錶已經添加了一條數據的前提下,後來回滾後,數據都被刪掉了,數據庫也沒有對應的值,達到的目的。

可是這裏有兩個小問題:

一、咱們控制的是 Service 類,那咱們能不能控制倉儲 Repository 類呢?

二、咱們每次都這麼寫,會不會很麻煩呢,能不能用統一AOP呢?

答案都是確定的!

 

二、創建事務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] ,而後在控制器正常的調用,具體的操做和結果就不展現了,已經測試過了,沒問題。

到這裏,就終於解決了事務的相關操做,固然這裏仍是有不少的問題須要考究,我也在考慮有沒有更好的點子和方案,期待後續報道。 

 

 

4、Github && Gitee

 

注意狀況分支:Trans1.0

https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

相關文章
相關標籤/搜索