一、項(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ù)的:

項(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ù)庫表是空的:


可以看到,我們是密碼表已經(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 操作類:

增加事務(wù)特性 [UseTran] ,然后在控制器正常的調(diào)用,具體的操作和結(jié)果就不展示了,已經(jīng)測試過了,沒問題。
到這里,就終于解決了事務(wù)的相關(guān)操作,當(dāng)然這里還是有很多的問題需要考究,我也在考慮有沒有更好的點(diǎn)子和方案,期待后續(xù)報(bào)道。
四、Github && Gitee
注意情況分支:Trans1.0
