從壹開始學(xué)習(xí)NetCore 45 ║ 終于解決了事務(wù)問題

一、項(xiàng)目說明

哈嘍,又來寫文章了,原來放假可以這么爽,可以學(xué)習(xí)和分享,??噓,大家要好好的工作喲。昨天發(fā)表的問題,嗯,給我留下了一點(diǎn)點(diǎn)沖擊,夜里輾轉(zhuǎn)反側(cè),想了很多,從好到壞再到好再到壞,從希望到失望再到希望再到失望,想起來當(dāng)年高四了,不想解釋什么了,四年后再見?,不說廢話,直接說說今天的內(nèi)容吧。

今天這個(gè)內(nèi)容,還是來源于兩個(gè)多月前,我的項(xiàng)目的一個(gè) issue ,當(dāng)時(shí)說到了如何使用事務(wù),(為啥要使用事務(wù),我就不多說了,相信肯定都知道,還有那個(gè)每次面試都問的題,事務(wù)四大特性。不知道還有沒有小伙伴記得,不,是都記得?。?我一直也是各種嘗試,直到前幾天也嘗試了幾個(gè)辦法,還是無果,然后又和 sqlsugar 的作者凱旋討論這個(gè)問題。他說只要能保證每次http 的scope 會話中的 sugar client 是同一個(gè)就行了,而且又不能把 client 設(shè)置為單例,天天看著這個(gè) issue,心里難免波瀾,終于喲,昨天群管 @大黃瓜 小伙伴研究出來了,我很開心,表揚(yáng)下他,下邊就正式說說在我的項(xiàng)目中,如果使用事務(wù)的:

image

項(xiàng)目介紹: netcore 2.2 + Sqlsugar 5.0 + UnitOfWork + async Repository + Service 。

投稿作者:QQ群:大黃瓜(博客園地址不詳)

項(xiàng)目已經(jīng)修改,不僅僅實(shí)現(xiàn)了單一倉儲服務(wù)的事務(wù)提交,而且也可以跨類跨倉儲服務(wù)來實(shí)現(xiàn)事務(wù),歡迎大家下載與公測,沒問題,我會merge 到 master。

為了防止大家不必要的更新錯(cuò)誤,我新建了一個(gè)分支,大家自己去看分支即可——https://github.com/anjoy8/Blog.Core/tree/Trans1.0 。

Tips:

我認(rèn)為 sqlsugar 還是很不錯(cuò),很好用,當(dāng)然,不能用萬能來形容客觀事物,這本身就不是一個(gè)成年人該有的思維,在我推廣 sqlsugar 這一年來,我也一直給凱旋提一些需求和Bug,他都特別及時(shí)的解決了,而且使用上也很順手,目前已經(jīng)實(shí)現(xiàn)了跨服務(wù)事務(wù)操作了,下一步就是在blog.core 中,使用主從數(shù)據(jù)庫,分離了,加油。

二、重新設(shè)計(jì)SqlSugarClient

1、創(chuàng)建工作單元接口

首先我們需要在 Blog.Core.IRepository 層,創(chuàng)建一個(gè)文件夾 UnitOfWork ,然后創(chuàng)建接口 IUnitOfWork.cs ,用來對工作單元進(jìn)行定義相應(yīng)的行為操作:

 public interface IUnitOfWork
 { // 創(chuàng)建 sqlsugar client 實(shí)例
 ISqlSugarClient GetDbClient(); // 開始事務(wù)
     void BeginTran(); // 提交事務(wù)
     void CommitTran(); // 回滾事務(wù)
     void RollbackTran();
 }

2、對 UnitOfWork 接口進(jìn)行實(shí)現(xiàn)

在 Blog.Core.Repository 層,創(chuàng)建一個(gè)文件夾 UnitOfWork,然后創(chuàng)建事務(wù)接口實(shí)現(xiàn)類 UnitOfWork.cs ,來對事務(wù)行為做實(shí)現(xiàn)。

   public class UnitOfWork : IUnitOfWork
    { private readonly ISqlSugarClient _sqlSugarClient; // 注入 sugar client 實(shí)例
        public UnitOfWork(ISqlSugarClient sqlSugarClient)
        {
            _sqlSugarClient = sqlSugarClient;
        } // 保證每次 scope 訪問,多個(gè)倉儲類,都用一個(gè) client 實(shí)例 // 注意,不是單例模型?。?!
        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();
        }

    }

具體的內(nèi)容,很簡單,這里不過多解釋。

3、用 UnitOfWork 接管 SqlguarClient

在基類泛型倉儲類 BaseRepository<TEntity> 中,我們修改構(gòu)造函數(shù),注入工作單元接口,用來將 sqlsugar 實(shí)例統(tǒng)一起來,不是每次都 new,而且通過工作單元來控制:


private ISqlSugarClient _db; private readonly IUnitOfWork _unitOfWork; // 構(gòu)造函數(shù),通過 unitofwork,來控制sqlsugar 實(shí)例
public BaseRepository(IUnitOfWork unitOfWork)
{
    _unitOfWork = unitOfWork;
    _db = unitOfWork.GetDbClient(); // 好像這個(gè)可以去掉,先保留
 DbContext.Init(BaseDBConfig.ConnectionString, (DbType)BaseDBConfig.DbType);
}

你可以對比下以前的代碼,就知道了,這么做的目的,就是把 sugar client 統(tǒng)一起來,這樣就能保證每次一個(gè)scope ,都能是同一個(gè)實(shí)例。

4、修改每一個(gè)倉儲的構(gòu)造函數(shù)

上邊我們?yōu)榱藢?shí)現(xiàn)對 sugar client的控制,在基類倉儲的構(gòu)造函數(shù)中,注入了IUnitOfWork,但是這樣會導(dǎo)致子類的倉儲報(bào)錯(cuò),畢竟父類構(gòu)造函數(shù)修改了嘛,所以目前有兩個(gè)方案:

1、去掉子倉儲,只使用泛型基類倉儲,在service層中,使用 private readonly IRepository<實(shí)體類> _repository; 這種方法。

2、去一一的修改子倉儲,增加構(gòu)造函數(shù),將 IUnitOfWork 傳給父類,具體的看我的代碼即可:

image

5、依賴注入 ISqlSugarClient

這個(gè)是肯定的,大家還記得上邊說的呢,我們要在 BaseRepository 中,注入 ISqlSugarClient ,所以就必須依賴注入:


 // 這里我不是引用了命名空間,因?yàn)槿绻妹臻g的話,會和Microsoft的一個(gè)GetTypeInfo存在二義性,所以就直接這么使用了。
 services.AddScoped<SqlSugar.ISqlSugarClient>(o => { return new SqlSugar.SqlSugarClient(new SqlSugar.ConnectionConfig()
     {
         ConnectionString = BaseDBConfig.ConnectionString,//必填, 數(shù)據(jù)庫連接字符串
         DbType = (SqlSugar.DbType)BaseDBConfig.DbType,//必填, 數(shù)據(jù)庫類型
         IsAutoCloseConnection = true,//默認(rèn)false, 時(shí)候知道關(guān)閉數(shù)據(jù)庫連接, 設(shè)置為true無需使用using或者Close操作
         IsShardSameThread=true,//共享線程
         InitKeyType = SqlSugar.InitKeyType.SystemTable//默認(rèn)SystemTable, 字段信息讀取, 如:該屬性是不是主鍵,標(biāo)識列等等信息
 });
 });

這里有一個(gè)小知識點(diǎn),就是我們的 IUnitOfWork 已經(jīng)隨著 倉儲層 依賴注入了,就不許單獨(dú)注入了,是不是這個(gè)時(shí)候感覺使用 Autofac 很方便?

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

三、正式使用事務(wù)

1、直接操作跨 Service 事務(wù)

現(xiàn)在我們就可以使用如何使用事務(wù)了,第一個(gè)簡單粗暴的,就是全部寫到 controller 里,我已經(jīng)寫好了一個(gè)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($""); //開始事務(wù)
                Console.WriteLine($"Begin Transaction");
                _unitOfWork.BeginTran();
                Console.WriteLine($""); var passwords = await _passwordLibServices.Query(); // 第一次密碼表的數(shù)據(jù)條數(shù)
                Console.WriteLine($"first time : the count of passwords is :{passwords.Count}"); // 向密碼表添加一條數(shù)據(jù)
                Console.WriteLine($"insert a data into the table PasswordLib now."); var insertPassword = await _passwordLibServices.Add(new PasswordLib()
                {
                    IsDeleted = false,
                    plAccountName = "aaa",
                    plCreateTime = DateTime.Now
                }); // 第二次查看密碼表有多少條數(shù)據(jù),判斷是否添加成功
                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; // 出現(xiàn)了一個(gè)異常!
                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}"); //事務(wù)提交
 _unitOfWork.CommitTran();
            } catch (Exception)
            { // 事務(wù)回滾
 _unitOfWork.RollbackTran(); var passwords = await _passwordLibServices.Query(); // 第三次查看密碼表有幾條數(shù)據(jù),判斷是否回滾成功
                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" };
}

項(xiàng)目的過程,在上邊注釋已經(jīng)說明了,大家可以看一下,很簡單,就是查詢,添加,再查詢,判斷是否操作成功,那現(xiàn)在我們就測試一下,數(shù)據(jù)庫表是空的:

image

然后我們執(zhí)行方法,動圖如下:
image

可以看到,我們是密碼表已經(jīng)添加了一條數(shù)據(jù)的前提下,后來回滾后,數(shù)據(jù)都被刪掉了,數(shù)據(jù)庫也沒有對應(yīng)的值,達(dá)到的目的。

但是這里有兩個(gè)小問題:

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

2、我們每次都這么寫,會不會很麻煩呢,能不能用統(tǒng)一AOP呢?

答案都是肯定的!

2、建立事務(wù)AOP,解決多倉儲內(nèi)的事務(wù)操作

在 Blog.Core api 層的 AOP 文件夾下,創(chuàng)建 BlogTranAOP.cs 文件,用來實(shí)現(xiàn)事務(wù)AOP操作:


   public class BlogTranAOP : IInterceptor
    { // 依賴注入工作單元接口
        private readonly IUnitOfWork _unitOfWork; public BlogTranAOP(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        } /// <summary>
        /// 實(shí)例化IInterceptor唯一方法 /// </summary>
        /// <param name="invocation">包含被攔截方法的信息</param>
        public void Intercept(IInvocation invocation)
        { var method = invocation.MethodInvocationTarget ?? invocation.Method; //對當(dāng)前方法的特性驗(yàn)證 //如果需要驗(yàn)證
            if (method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(UseTranAttribute)) is UseTranAttribute) { try {
                    Console.WriteLine($"Begin Transaction");
                    _unitOfWork.BeginTran();

                    invocation.Proceed(); // 異步獲取異常,普通的 try catch 外層不能達(dá)到目的,畢竟是異步的
                    if (IsAsyncMethod(invocation.Method))
                    { if (invocation.Method.ReturnType == typeof(Task))
                        {
                            invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
                                (Task)invocation.ReturnValue, async () => await TestActionAsync(invocation),
                                ex => {
                                    _unitOfWork.RollbackTran();//事務(wù)回滾
                                });
                        } else //Task<TResult>
 {
                            invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
                             invocation.Method.ReturnType.GenericTypeArguments[0],
                             invocation.ReturnValue, async () => await TestActionAsync(invocation),
                             ex => {
                                 _unitOfWork.RollbackTran();//事務(wù)回滾
                             });
                        }
                    }
                    _unitOfWork.CommitTran();

                } catch (Exception)
                {
                    Console.WriteLine($"Rollback Transaction");
                    _unitOfWork.RollbackTran();
                }
            } else {
                invocation.Proceed();//直接執(zhí)行被攔截方法
 }

        } 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以后,肯定就能看懂這個(gè)事務(wù)AOP的內(nèi)容,這里只是有一點(diǎn),需要增加一個(gè)特性,public class UseTranAttribute : Attribute,這個(gè)和當(dāng)時(shí)的緩存AOP是一樣的,只有配置了才會實(shí)現(xiàn)事務(wù)提交,具體的請查看 UseTranAttribute.cs 類。

然后我們測試一個(gè)子倉儲項(xiàng)目,具體的代碼如下:

在 Blog.Core.Services 層下的 GuestbookServices.cs 內(nèi),增加一個(gè) Task<bool> TestTranInRepositoryAOP() 方法,內(nèi)容和上邊 controller 中的控制 service 類似,只不過是用 Repository 操作類:

image

增加事務(wù)特性 [UseTran] ,然后在控制器正常的調(diào)用,具體的操作和結(jié)果就不展示了,已經(jīng)測試過了,沒問題。

到這里,就終于解決了事務(wù)的相關(guān)操作,當(dāng)然這里還是有很多的問題需要考究,我也在考慮有沒有更好的點(diǎn)子和方案,期待后續(xù)報(bào)道。

四、Github && Gitee

注意情況分支:Trans1.0

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容