倉儲模式到底是不是反模式?
本文轉載自微信公眾號「JeffckyShare」,作者Jeffcky。轉載本文請聯系JeffckyShare公眾號。
倉儲反模式
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>();
- services.AddScoped<IUserService, UserService>());
有一部分童鞋在項目中可能就是使用如上方式,每一個具體倉儲實現我們將其看成傳統的數據訪問層,緊接著我們還定義一套業務層即服務層,如此第一眼看來和傳統三層架構無任何區別,只是分層名稱有所不同而已
每一個具體倉儲接口都繼承基礎倉儲接口,然后每個具體倉儲實現繼承基礎倉儲實現,對于服務層同理,反觀上述一系列操作本質,其實我們回到了原點,那還不如直接通過上下文操作一步到位來的爽快
上述倉儲模式并沒有帶來任何益處,分層明確性從而加大了復雜性和重復性,根本沒有解放生產率,我們將專注力全部放在了定義多層接口和實現上而不是業務邏輯,如此使用,這就是倉儲模式的反模式實現
倉儲模式思考
所有脫離實際項目和業務的思考都是耍流氓,若只是小型項目,直接通過上下文操作未嘗不可,既然用到了倉儲模式說明是想從一定程度上解決項目中所遇到的痛點所在,要不然只是隨波逐流,終將是自我打臉
根據如下官方在微服務所使用倉儲鏈接,官方推崇倉儲模式,但在其鏈接中是直接在具體倉儲實現中所使用上下文進行操作,毫無以為這沒半點毛病
EntityFramework Core基礎設施持久化層
https://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這塊擴展實現令人嘆服,我也在持續學習中,其他就不多講了,博客園中講解原理的文章比比皆是