Asp.Net Core仓储模式+工作单元
仓储模式+工作单元
仓储模式
仓储(Repository)模式自2004年首次作为领域驱动模型DDD设计的一部分引入,仓储本质上是提供提供数据的抽象,以便应用程序可以使用具有接口的相似的简单抽象集合。从此集合中CURD是通过一些列直接的方法完成,无需处理连接、命令等问题,使用此种模式可帮助实现松耦合,并保持领域对象的持久性无知。
- 仓储模式是为了在程序的数据访问层和业务逻辑层之间创建的一个抽象层
- 仓储模式是一种数据访问模式,提供一种更松散耦合的数据访问方法
- 将创建数据访问的逻辑写在单独的类中即仓储
- 仓储负责和业务层进行持久化通信
仓储(Repository)是存在于工作单元和数据库之间单独分离出来的一层,是对数据访问的封装。其优点是
- 业务层无需知道具体实现达到分离关注点
- 提高对数据库访问的维护,对于仓储的改变并不改变业务的逻辑。
如何处理多个Repository库?
下面想象下如下场景,我们数据库中有多个表,那样我们需要为每个表创建一个Reporsitory类。(好多重复工作的说,其实这不是问题)
为什么每个Repository要拥有一个数据上下文的实例呢?为什么不在一些地方创建一个它的实例,然后在repository被实例化的时候作为参数传递进去呢。现在这个新的类被命名为 UnitOfWork ,此类将负责创建数据上下文实例并移交到控制器的所有repository实例。
对于仓储Repository需要说明两个方面的内容。一个是解决持久化的问题,一个是对数据层做屏蔽,避免应用或展现层直接跳过领域层对数据库进行操作而使领域模型最终无用。在有了Repository后,我们不再关心对象的存储和访问操作,而将重心真正转移到领域模型本身。或者叫使应用程序和领域设计与持久化技术解耦。
对于工厂和仓储的关系,工厂负责对象生命周期的开始,而仓储负责对象生命周期的中间或结束。当对象驻留在内存或对象数据库的时候很好理解。但是至少有一部分数据会持久化存在到类似关系型数据库或文件中,这样检索出来的数据就必须重建为对象形式。
对于工厂和仓储的协同,有些理解和书上有些不一致。个人理解工厂不仅仅应该关注复杂对象的创建,同时也应该关注复杂对象的保存。工厂不负责对象的持久化,工厂将持久化职责委托到仓储来完成。仓储不应该直接和应用层打交道,对于整个领域层来说。和应用层打交道的是Service接口,而和持久化层打交道的是Repository接口而已。和书里面理解最大的差异就是Repository没有保留给Client,也不是Repository委托Factory来重建对象。
举一个场景来说,根据订单号获取一个聚合复杂对象采购订单,对采购订单进行修改后再进行保存。这个时候和持久化层存在两次交互,第一次是数据的读取,第二次是修改后数据的存入。
对于数据读取,到领域层则是Factory需要实例化一个聚合对象并返回应用层。而Factory将该工作分解到聚合里美的每一个子实体,子实体通过Repository接口获取到ResultSet并进行OR转换后返回,Factory将拿到的所有实例化对象进行聚合返回一个完整的聚合对象实例。对于数据存储,仍然应该是Factory接管该操作,然后对数据进行分解后分别调用聚合中的每一个实体的仓储接口本身的保存方法,对数据进行持久化,在Factory层进行完整的事务控制并返回结果。
工作单元
UnitOfoWork是这个老兄(马丁)提出的,上面是他说的话,这里要注意的就是分两个时间点
a)业务操作过程中,对操作的CUD操作的对象的状态进行跟踪操作;
b) CUD操作完必经的一步骤当然是提交到数据库,UnitOfoWor保证业务提交使用同意上下文对象,从而保证了事务的一致性,假设提交失败直接回滚。
简单说:UnitOfoWor可以说是一个开关,开:用于上下文的获取,供所有的仓储对象使用;关:提交操作,提交的过程包含事务控制(是否会滚)。
所以他这一句话我们可以明确这几个东西:
①:一个对象用于存放 业务操作的对象(我们结合仓储使用就是仓储了) repository的临时存储对象;
②:同一业务操作的上下文必须保持一致(同一上下文对象)
3 :维护当前业务的变更操作
④:事务控制
依据这几点,所以我们可以和容易写出我们想要的代码,(临时先刨除 第三点 ,后面会有补充),先声明:这里未考虑多上下文的情况,因为我这边用不到,但是实现也比较简单,可以将涉及到的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 { #region fields /// <summary> /// 服务提供器,主要用于查找 框架配置对象,以及DbContextOptionBuilder对象 /// </summary> private readonly IServiceProvider _provider; /// <summary> /// 当前请求涉及的scope生命的仓储对象 /// </summary> private Hashtable repositorys; private IDbContextTransaction _dbTransaction { get; set; } /// <summary> /// 上下文对象,UOW内部初始化上下文对象,供当前scope内的操作使用,保证同一上下文 /// </summary> public DbContext DbContext => GetDbContext(); #endregion #region ctor public UnitOfWork(IServiceProvider provider) { _provider = provider; } #endregion #region public 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); } #endregion #region private 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);//TODO null可以换成缓存中获取connection对象,以便性能的提升 if (!(ActivatorUtilities.CreateInstance(_provider, options.ESoftorDbOption.DbContextType, optionsBuilder.Options) is DbContext 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; } } #endregion #region override public void Dispose() { _dbTransaction.Dispose(); DbContext.Dispose(); GC.SuppressFinalize(this); } #endregion }
就目前而言,博客园中可见到的大部分的 实现都是将UnitOfoWork注入到 repository,通过 UnitOfoWork获取上下文对象,然后在 service中只是直接注入所需的 repository对象,是的,这的确满足我们的开发需求了,也能正常运行。
public class TestService { private readonly ITestRepository _testRepository; public TestService(ITestRepository testRepository){ _testRepository = testRepository; } //......其他方法实现 }
如果有多个仓储对象,依次如上面的方式注入即可。但是,这样做的话,当以后我们有表删除或者新增的时候,我们不得不维护这样的列表。这完全不符合OO设计原则;如果我们有新表的创建或者删除,改动就比较多了。
如果你有细细观察的话,我们这里的 UnitOfoWork实现稍有不同,也就涉及到当前请求的 仓储对象(repository),我们在这零时存储到了一个 hashable对象中,那么这时候我们在 service中使用uow和仓储的时候,就不用像上面那样,有多少个需要注册多少次,而只需要注入我们的一个UnitOfoWork对象即可。然后通过uow获取 仓储(repository)对象,因为我们零时将涉及到当前请求(事务)的 仓储已经存储到私有变量的 hashtable中,
public class TestService { private readonly IUnitOfWork _uow; public TestService(IUnitOfWork uow){ _uow = uow; } //......其他方法实现 }
然后我们在使用仓储(repository)的时候,只需要如下方式使用即可:
var userRepository = _uow.Repository<User,Guid>(); var roleRepository = _uow.Repository<Role,Guid>(); ... 而在我们用到事务的地方,直接使用uow中的commit提交即可: _uow.BeginTransaction(); var userRepository = _uow.Repository<User,Guid>(); var roleRepository = _uow.Repository<Role,Guid>(); ...//一些列其他操作,(CRUD) _uow.Commit();
就像上面说到的,这样保证了当前业务操作涉及的 仓储对象(repository),会保证在 hashtable对象中,同时使用同一个上线问对象(DbContext),Commit提交的时候保证了事务(上下文)的一致性。而且如上面提到的,我们只需要在service层中注入一个UnitOfoWork即可,不论表如何变动,删除或者新增表,我们这里不会收到任何影响。比较理想的一种方式。
UnitOfoWork模式注意点,也就是UnitOfoWork的说明 即:
1.由UnitOfoWork初始化上下文对象,也就是我们代码中的DbContext,;
2.由UnitOfoWork提供事务的控制方法,以及控制事务回滚,保证最终一致性
3.这里我们还使用了UnitOfoWork进行仓储对象的 获取。
4.其他
UnitOfoWork还需要对操作状态的控制 简单说就是,一系列的 增、删、改的 命令操作 的状态控制 基本原理就是 类似我们定义的 hashtable对象,定义三个 Dictionary 变量,用于存储当前 业务操作涉及的 增、删、改、三种操作的 存储变量
https://www.it1352.com/2064843.html 使用ReadCommitted隔离级别的事务