仓储模式到底是不是反模式?
- 2021 年 1 月 10 日
- 筆記
- Asp.Net Core, EntityFramework Core 3.x, EntityFramework Core 5.0
前言
仓储模式我们已耳熟能详,但当我们将其进行应用时,真的是那么得心应手吗?确定是解放了生产力吗?这到底是怎样的一个存在,确定不是反模式?,一篇详文我们探讨仓储模式,这里仅我个人的思考,若有更深刻的理解,请在评论中给出
仓储反模式
5年前我在Web APi中使用EntityFramework中写了一个仓储模式,并将其放在我个人github上,此种模式也完全是参考所流行的网传模式,现如今在我看来那是极其错误的仓储模式形式,当时在EntityFramework中有IDbSet接口,然后我们又定义一个IDbContext接口等等,大同小异,接下来我们看看在.NET Core中大多是如何使用的呢?
定义通用IRepository接口
public interface IRepository<TEntity> where TEntity : class { /// <summary> /// 通过id获得实体 /// </summary> /// <param name="id"></param> /// <returns></returns> TEntity GetById(object id); //其他诸如修改、删除、查询接口 }
当然还有泛型类可能需要基础子基础类等等,这里我们一并忽略
定义EntityRepository实现IRepository接口
public abstract class EntityRepository<TEntity> : IRepository<TEntity> where TEntity : class { private readonly DbContext _context; public EntityRepository(DbContext context) { _context = context; } /// <summary> /// 通过id获取实体 /// </summary> /// <param name="id"></param> /// <returns></returns> public TEntity GetById(object id) { return _context.Set<TEntity>().Find(id); } }
定义业务仓储接口IUserRepository接口
public interface IUserRepository : IRepository<User> { /// <summary> /// 其他非通用接口 /// </summary> /// <returns></returns> List<User> Other(); }
定义业务仓储接口具体实现UserRepository
public class UserRepository : EntityRepository<User>, IUserRepository { public List<User> Other() { throw new NotImplementedException(); } }
我们定义基础通用接口和实现,然后每一个业务都定义一个仓储接口和实现,最后将其进行注入,如下:
services.AddDbContext<EFCoreDbContext>(options => { options.UseSqlServer(@"Server=.;Database=EFCore;Trusted_Connection=True;"); }); services.AddScoped(typeof(IRepository<>), typeof(EntityRepository<>)); services.AddScoped<IUserRepository, UserRepository>();
有一部分童鞋在项目中可能就是使用如上方式,每一个具体仓储实现我们将其看成传统的数据访问层,紧接着我们还定义一套业务层即服务层,如此第一眼看来和传统三层架构无任何区别,只是分层名称有所不同而已。每一个具体仓储接口都继承基础仓储接口,然后每个具体仓储实现继承基础仓储实现,对于服务层同理,反观上述一系列操作本质,其实我们回到了原点,那还不如直接通过上下文操作一步到位来的爽快。上述仓储模式并没有带来任何益处,分层明确性从而加大了复杂性和重复性,根本没有解放生产率,我们将专注力全部放在了定义多层接口和实现上而不是业务逻辑,如此使用,这就是仓储模式的反模式实现仓储模式思考
仓储模式思考
所有脱离实际项目和业务的思考都是耍流氓,若只是小型项目,直接通过上下文操作未尝不可,既然用到了仓储模式说明是想从一定程度上解决项目中所遇到的痛点所在,要不然只是随波逐流,终将是自我打脸
根据如下官方在微服务所使用仓储链接,官方推崇仓储模式,但在其链接中是直接在具体仓储实现中所使用上下文进行操作,毫无以为这没半点毛病
EntityFramework Core基础设施持久化层
//docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core
但我们想在上下文的基础上进一步将基本增、删、改、查询进行封装,那么我们如何封装基础仓储而避免出现反模式呢?
我思仓储模式
在进行改造之前,我们思考两个潜在需要解决的重点问题
其一,每一个具体业务仓储实现,定义仓储接口是一定必要的吗?我认为完全没必要,有的童鞋就疑惑了,若我们有非封装基础通用接口,需额外定义,那怎么搞,我们可以基于基础仓储接口定义扩展方法
其二,若与其他仓储进行互操作,此时基础仓储不满足需求,那怎么搞,我们可以在基础仓储接口中定义暴露获取上下文Set属性
其三,若非常复杂的查询,可通过底层连接实现或引入Dapper
首先,我们保持上述封装基础仓储接口前提下添加暴露上下文Set属性,如下:
/// <summary> /// 基础通用接口 /// </summary> /// <typeparam name="TEntity"></typeparam> public interface IRepository<T> where T : class { IQueryable<T> Queryable { get; } T GetById(object id); }
上述我们将基础仓储接口具体实现类,将其定义为抽象,既然我们封装了针对基础仓储接口的实现,外部只需调用即可,那么该类理论上就不应该被继承,所以接下来我们将其修饰为密封类,如下:
public sealed class EntityRepository<T> : IRepository<T> where T : class { private readonly DbContext _context; public EntityRepository(DbContext context) { _context = context; } public T GetById(object id) { return _context.Set<T>().Find(id); } }
我们从容器中获取上下文并进一步暴露上下文Set属性
public sealed class EntityRepository<T> : IRepository<T> where T : class { private readonly IServiceProvider _serviceProvider; private EFCoreDbContext _context => (EFCoreDbContext) _serviceProvider.GetService(typeof(EFCoreDbContext)); private DbSet<T> Set => _context.Set<T>(); public IQueryable<T> Queryable => Set; public EntityRepository(IServiceProvider serviceProvider) { _serviceProvider = serviceProvider; } public T GetById(object id) { return Set.Find(id); } }
若为基础仓储接口不满足实现,则使用具体仓储的扩展方法
public static class UserRepository { public static List<User> Other(this IRepository<User> repository) { // 自定义其他实现 } }
最后到了服务层,则是我们的业务层,我们只需要使用上述基础仓储接口或扩展方法即可
public class UserService { private readonly IRepository<User> _repository; public UserService(IRepository<User> repository) { _repository = repository; } }
最后在注入时,我们将省去注册每一个具体仓储实现,如下:
services.AddDbContext<EFCoreDbContext>(options => { options.UseSqlServer(@"Server=.;Database=EFCore;Trusted_Connection=True;"); }); services.AddScoped(typeof(IRepository<>), typeof(EntityRepository<>)); services.AddScoped<UserService>();
以上只是针对第一种反模式的基本改造,对于UnitOfWork同理,其本质不过是管理操作事务,并需我们手动管理上下文释放时机就好,这里就不再多讲
我们还可以根据项目情况可进一步实现其对应规则,比如在是否需要在进行指定操作之前实现自定义扩展,比如再抽取一个上下文接口等等,ABP vNext中则是如此,ABP vNext对EF Core扩展是我看过最完美的实现方案,接下来我们来看看
ABP vNext仓储模式
其核心在Volo.Abp.EntityFrameworkCore包中,将其单独剥离出来除了抽象通用封装外,还有一个则是调用了EF Core底层APi,一旦EF Core版本变动,此包也需同步更新
ABP vNext针对EF Core做了扩展,通过查看整体实现,主要通过扩展中特性实现指定属性更新,EF Core中当模型被跟踪时,直接提交则更新变化属性,若未跟踪,我们直接Update但想要更新指定属性,这种方式不可行,在ABP vNext则得到了良好的解决
在其EF Core包中的AbpDbContext上下文中,针对属性跟踪更改做了良好的实现,如下:
protected virtual void ChangeTracker_Tracked(object sender, EntityTrackedEventArgs e) { FillExtraPropertiesForTrackedEntities(e); } protected virtual void FillExtraPropertiesForTrackedEntities(EntityTrackedEventArgs e) { var entityType = e.Entry.Metadata.ClrType; if (entityType == null) { return; } if (!(e.Entry.Entity is IHasExtraProperties entity)) { return; } ..... }
除此之外的第二大亮点则是对UnitOfWork(工作单元)的完美方案,将其封装在Volo.Abp.Uow包中,通过UnitOfWorkManager管理UnitOfWork,其事务提交不简单是像如下形式
private IDbContextTransaction _transaction; public void BeginTransaction() { _transaction = Database.BeginTransaction(); } public void Commit() { try { SaveChanges(); _transaction.Commit(); } finally { _transaction.Dispose(); } } public void Rollback() { _transaction.Rollback(); _transaction.Dispose(); }
额外的还实现了基于环境流动的事务(AmbientUnitOfWork),反正ABP vNext在EF Core这块扩展实现令人叹服,我也在持续学习中,其他就不多讲了,博客园中讲解原理的文章比比皆是
好了,本文到此结束,倒没什么可总结的,在文中已有概括,我们下次再会