1.工作單元(UnitOfWork)是什么?
Maintains a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems.
UnitOfWork是Martin Fowler提出的,上面是他說的話,這里要注意的就是分兩個時間點,a)業務操作過程中,對操作的CUD操作的對象狀態進行跟蹤操作; b) CUD操作完畢提交到數據庫,UnitOfWork保證業務提交使用同意上下文對象,從而保證了事務的一致性,假設提交失敗直接回滾。
簡單說:UnitOfWork可以說是一個開關,開:用於上下文的獲取,供所有的倉儲對象使用;關:提交操作,提交的過程包含事務控制(是否回滾)。
所以他這一句話我們可以明確這幾個東西:
1. 一個對象用於存放 業務操作的對象(我們結合倉儲使用就是倉儲了) repository的臨時存儲對象;
2. 同一業務操作的上下文必須保持一致(同一上下文對象);
3. 維護當前業務的變更操作;
4. 事務控制;
依據這幾點,我們就可以寫出想要的代碼,(臨時先刨除 第三點 ,后面會有補充),先聲明:這里未考慮多上下文的情況,因為我這邊用不到,但是實現也比較簡單,可以將涉及到的hashtable對象換成dictionary等,鍵為上下位對象類型或者名稱。
接口定義:
/// <summary>
/// 工作單元接口
/// </summary>
public interface IUnitOfWork : IDisposable
{
IRepository<TEntity, TKey> Repository<TEntity, TKey>() where TEntity : class, IEntity<TKey>;
void BeginTransaction();
int Commit();
Task<int> CommitAsync();
}
public class UnitOfWork : IUnitOfWork
{
/// <summary>
/// 服務提供器,主要用於查找 框架配置對象,以及DbContextOptionBuilder對象
/// </summary>
private readonly IServiceProvider _provider;
/// <summary>
/// 當前請求涉及的scope生命的倉儲對象
/// </summary>
private Hashtable repositorys;
private IDbContextTransaction _dbTransaction { get; set; }
/// <summary>
/// 上下文對象,UnitOfWork內部初始化上下文對象,供當前scope內的操作使用,保證同一上下文
/// </summary>
public DbContext DbContext => GetDbContext();
public UnitOfWork(IServiceProvider provider)
{
_provider = provider;
}
public IRepository<TEntity, TKey> Repository<TEntity, TKey>() where TEntity : class, IEntity<TKey>
{
if (repositorys == null)
repositorys = new Hashtable();
var entityType = typeof(TEntity);
if (!repositorys.ContainsKey(entityType.Name))
{
var baseType = typeof(Repository<,>);
var repositoryInstance = Activator.CreateInstance(baseType.MakeGenericType(entityType), DbContext);
repositorys.Add(entityType.Name, repositoryInstance);
}
return (IRepository<TEntity, TKey>)repositorys[entityType.Name];
}
public void BeginTransaction()
{
//DbContext.Database.UseTransaction(_dbTransaction);//如果多上下文,我們可是在其他上下文直接使用 UserTransaction使用已存在的事務
_dbTransaction = DbContext.Database.BeginTransaction();
}
public int Commit()
{
int result = 0;
try
{
result = DbContext.SaveChanges();
if (_dbTransaction != null)
_dbTransaction.Commit();
}
catch (Exception ex)
{
result = -1;
CleanChanges(DbContext);
_dbTransaction.Rollback();
throw new Exception($"Commit 異常:{ex.InnerException}/r{ ex.Message}");
}
return result;
}
public async Task<int> CommitAsync()
{
int result = 0;
try
{
result = await DbContext.SaveChangesAsync();
if (_dbTransaction != null)
_dbTransaction.Commit();
}
catch (Exception ex)
{
result = -1;
CleanChanges(DbContext);
_dbTransaction.Rollback();
throw new Exception($"Commit 異常:{ex.InnerException}/r{ ex.Message}");
}
return await Task.FromResult(result);
}
private DbContext GetDbContext()
{
var options = _provider.ESoftorOption();
IDbContextOptionsBuilderCreator builderCreator = _provider.GetServices<IDbContextOptionsBuilderCreator>()
.FirstOrDefault(d => d.DatabaseType == options.ESoftorDbOption.DatabaseType);
if (builderCreator == null)
throw new Exception($"無法解析數據庫類型為:{options.ESoftorDbOption.DatabaseType}的{typeof(IDbContextOptionsBuilderCreator).Name}實例");
//DbContextOptionsBuilder
var optionsBuilder = builderCreator.Create(options.ESoftorDbOption.ConnectString, null);//null可以換成緩存中獲取connection對象,以便性能的提升
if (!(ActivatorUtilities.CreateInstance(_provider, options.ESoftorDbOption.DbContextType, optionsBuilder.Options) is DbContext))
throw new Exception($"上下文對象'{options.ESoftorDbOption.DbContextType.AssemblyQualifiedName}'實例化失敗,請確認配置文件已正確配置");
return dbContext;
}
/// <summary>
/// 操作失敗,還原跟蹤狀態
/// </summary>
/// <param name="context"></param>
private static void CleanChanges(DbContext context)
{
var entries = context.ChangeTracker.Entries().ToArray();
for (int i = 0; i < entries.Length; i++)
{
entries[i].State = EntityState.Detached;
}
}
public void Dispose()
{
_dbTransaction.Dispose();
DbContext.Dispose();
GC.SuppressFinalize(this);
}
}
2.怎么用?
就目前而言,博客園中可見到的大部分的 實現都是將 UnitOfWork 注入到 repository,通過 UnitOfWork 獲取上下文對象,然后在 service 中只是直接注入所需的 repository 對象,是的,這的確滿足我們的開發需求了,也能正常運行。
public class TestService
{
private readonly ITestRepository _testRepository;
public TestService(ITestRepository testRepository){
_testRepository = testRepository;
}
//......其他方法實現
}
如果有多個倉儲對象,依次如上面的方式注入即可。但是,這樣做的話,當以后我們有表刪除或者新增的時候,我們不得不維護這樣的列表。這完全不符合OO設計原則;如果我們有新表的創建或者刪除,改動就比較多了。
如果你有細細觀察的話,我們這里的 UnitOfWork實現稍有不同,也就涉及到當前請求的 倉儲對象(repository),我們在這臨時存儲到了一個 hashable對象中,那么這時候我們在 service中使用UnitOfWork和倉儲的時候,就不用像上面那樣,有多少個需要注冊多少次,而只需要注入一個UnitOfWork對象即可。然后通過UnitOfWork獲取 倉儲對象(repository),因為我們臨時將涉及到當前請求(事務)的 倉儲已經存儲到私有變量的 hashtable中。
public class TestService
{
private readonly IUnitOfWork _UnitOfWork;
public TestService(IUnitOfWork UnitOfWork){
_UnitOfWork = UnitOfWork;
}
//......其他方法實現
}
然后我們在使用倉儲(repository)的時候,只需要如下方式使用即可:
var userRepository = _UnitOfWork.Repository<User,Guid>();
var roleRepository = _UnitOfWork.Repository<Role,Guid>();
...
而在我們用到事務的地方,直接使用UnitOfWork中的commit提交即可:
_UnitOfWork.BeginTransaction();
var userRepository = _UnitOfWork.Repository<User,Guid>();
var roleRepository = _UnitOfWork.Repository<Role,Guid>();
...//一些列其他操作,(CRUD)
_UnitOfWork.Commit();
就像上面說到的,這樣保證了當前業務操作涉及的 倉儲對象(repository),會保證在 hashtable對象中,同時使用同一個上下文對象(DbContext),Commit提交的時候保證了事務(上下文)的一致性。而且如上面提到的,我們只需要在service層中注入一個UnitOfWork即可,不論表如何變動,刪除或者新增表,我們這里不會受到任何影響。比較理想的一種方式。
3.注意點?
UnitOfWork模式注意點:
1.由UnitOfWork初始化上下文對象,也就是我們代碼中的DbContext
2.由UnitOfWork提供事務的控制方法,以及控制事務回滾,保證最終一致性
3.這里我們還使用了UnitOfWork進行倉儲對象的獲取。
4.其他
4.補充:對象操作狀態的控制
上面有說到,UnitOfWork還需要對操作狀態的控制,簡單說就是,一系列的 增、刪、改 命令操作的狀態控制,可參考:https://www.cnblogs.com/zhaoshujie/p/12260188.html
基本原理就是類似我們定義的 hashtable對象,定義三個 Dictionary 變量,用於存儲當前業務操作涉及的 增、刪、改 三種操作的存儲變量。