倉儲(chǔ)模式到底是不是反模式?
本文轉(zhuǎn)載自微信公眾號(hào)「JeffckyShare」,作者Jeffcky。轉(zhuǎn)載本文請(qǐng)聯(lián)系JeffckyShare公眾號(hào)。
倉儲(chǔ)反模式
5年前我在Web APi中使用EntityFramework中寫了一個(gè)倉儲(chǔ)模式,并將其放在我個(gè)人github上,此種模式也完全是參考所流行的網(wǎng)傳模式,現(xiàn)如今在我看來那是極其錯(cuò)誤的倉儲(chǔ)模式形式,當(dāng)時(shí)在EntityFramework中有IDbSet接口,然后我們又定義一個(gè)IDbContext接口等等,大同小異,接下來我們看看在.NET Core中大多是如何使用的呢?
定義通用IRepository接口
- public interface IRepository<TEntity> where TEntity : class
- {
- /// <summary>
- /// 通過id獲得實(shí)體
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- TEntity GetById(object id);
- //其他諸如修改、刪除、查詢接口
- }
當(dāng)然還有泛型類可能需要基礎(chǔ)子基礎(chǔ)類等等,這里我們一并忽略
定義EntityRepository實(shí)現(xiàn)IRepository接口
- public abstract class EntityRepository<TEntity> : IRepository<TEntity> where TEntity : class
- {
- private readonly DbContext _context;
- public EntityRepository(DbContext context)
- {
- _context = context;
- }
- /// <summary>
- /// 通過id獲取實(shí)體
- /// </summary>
- /// <param name="id"></param>
- /// <returns></returns>
- public TEntity GetById(object id)
- {
- return _context.Set<TEntity>().Find(id);
- }
- }
定義業(yè)務(wù)倉儲(chǔ)接口IUserRepository接口
- public interface IUserRepository : IRepository<User>
- {
- /// <summary>
- /// 其他非通用接口
- /// </summary>
- /// <returns></returns>
- List<User> Other();
- }
定義業(yè)務(wù)倉儲(chǔ)接口具體實(shí)現(xiàn)UserRepository
- public class UserRepository : EntityRepository<User>, IUserRepository
- {
- public List<User> Other()
- {
- throw new NotImplementedException();
- }
- }
我們定義基礎(chǔ)通用接口和實(shí)現(xiàn),然后每一個(gè)業(yè)務(wù)都定義一個(gè)倉儲(chǔ)接口和實(shí)現(xiàn),最后將其進(jìn)行注入,如下:
- 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>());
有一部分童鞋在項(xiàng)目中可能就是使用如上方式,每一個(gè)具體倉儲(chǔ)實(shí)現(xiàn)我們將其看成傳統(tǒng)的數(shù)據(jù)訪問層,緊接著我們還定義一套業(yè)務(wù)層即服務(wù)層,如此第一眼看來和傳統(tǒng)三層架構(gòu)無任何區(qū)別,只是分層名稱有所不同而已
每一個(gè)具體倉儲(chǔ)接口都繼承基礎(chǔ)倉儲(chǔ)接口,然后每個(gè)具體倉儲(chǔ)實(shí)現(xiàn)繼承基礎(chǔ)倉儲(chǔ)實(shí)現(xiàn),對(duì)于服務(wù)層同理,反觀上述一系列操作本質(zhì),其實(shí)我們回到了原點(diǎn),那還不如直接通過上下文操作一步到位來的爽快
上述倉儲(chǔ)模式并沒有帶來任何益處,分層明確性從而加大了復(fù)雜性和重復(fù)性,根本沒有解放生產(chǎn)率,我們將專注力全部放在了定義多層接口和實(shí)現(xiàn)上而不是業(yè)務(wù)邏輯,如此使用,這就是倉儲(chǔ)模式的反模式實(shí)現(xiàn)
倉儲(chǔ)模式思考
所有脫離實(shí)際項(xiàng)目和業(yè)務(wù)的思考都是耍流氓,若只是小型項(xiàng)目,直接通過上下文操作未嘗不可,既然用到了倉儲(chǔ)模式說明是想從一定程度上解決項(xiàng)目中所遇到的痛點(diǎn)所在,要不然只是隨波逐流,終將是自我打臉
根據(jù)如下官方在微服務(wù)所使用倉儲(chǔ)鏈接,官方推崇倉儲(chǔ)模式,但在其鏈接中是直接在具體倉儲(chǔ)實(shí)現(xiàn)中所使用上下文進(jìn)行操作,毫無以為這沒半點(diǎn)毛病
EntityFramework Core基礎(chǔ)設(shè)施持久化層
https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/infrastructure-persistence-layer-implementation-entity-framework-core
但我們想在上下文的基礎(chǔ)上進(jìn)一步將基本增、刪、改、查詢進(jìn)行封裝,那么我們?nèi)绾畏庋b基礎(chǔ)倉儲(chǔ)而避免出現(xiàn)反模式呢?
我思倉儲(chǔ)模式
在進(jìn)行改造之前,我們思考兩個(gè)潛在需要解決的重點(diǎn)問題
其一,每一個(gè)具體業(yè)務(wù)倉儲(chǔ)實(shí)現(xiàn),定義倉儲(chǔ)接口是一定必要的嗎?我認(rèn)為完全沒必要,有的童鞋就疑惑了,若我們有非封裝基礎(chǔ)通用接口,需額外定義,那怎么搞,我們可以基于基礎(chǔ)倉儲(chǔ)接口定義擴(kuò)展方法
其二,若與其他倉儲(chǔ)進(jìn)行互操作,此時(shí)基礎(chǔ)倉儲(chǔ)不滿足需求,那怎么搞,我們可以在基礎(chǔ)倉儲(chǔ)接口中定義暴露獲取上下文Set屬性
其三,若非常復(fù)雜的查詢,可通過底層連接實(shí)現(xiàn)或引入Dapper
首先,我們保持上述封裝基礎(chǔ)倉儲(chǔ)接口前提下添加暴露上下文Set屬性,如下:
- /// <summary>
- /// 基礎(chǔ)通用接口
- /// </summary>
- /// <typeparam name="TEntity"></typeparam>
- public interface IRepository<T> where T : class
- {
- IQueryable<T> Queryable { get; }
- T GetById(object id);
- }
上述我們將基礎(chǔ)倉儲(chǔ)接口具體實(shí)現(xiàn)類,將其定義為抽象,既然我們封裝了針對(duì)基礎(chǔ)倉儲(chǔ)接口的實(shí)現(xiàn),外部只需調(diào)用即可,那么該類理論上就不應(yīng)該被繼承,所以接下來我們將其修飾為密封類,如下:
- 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);
- }
- }
我們從容器中獲取上下文并進(jìn)一步暴露上下文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);
- }
- }
若為基礎(chǔ)倉儲(chǔ)接口不滿足實(shí)現(xiàn),則使用具體倉儲(chǔ)的擴(kuò)展方法
- public static class UserRepository
- {
- public static List<User> Other(this IRepository<User> repository)
- {
- // 自定義其他實(shí)現(xiàn)
- }
- }
最后到了服務(wù)層,則是我們的業(yè)務(wù)層,我們只需要使用上述基礎(chǔ)倉儲(chǔ)接口或擴(kuò)展方法即可
- public class UserService
- {
- private readonly IRepository<User> _repository;
- public UserService(IRepository<User> repository)
- {
- _repository = repository;
- }
- }
最后在注入時(shí),我們將省去注冊(cè)每一個(gè)具體倉儲(chǔ)實(shí)現(xiàn),如下:
- services.AddDbContext<EFCoreDbContext>(options =>
- {
- options.UseSqlServer(@"Server=.;Database=EFCore;Trusted_Connection=True;");
- });
- services.AddScoped(typeof(IRepository<>), typeof(EntityRepository<>));
- services.AddScoped<UserService>();
以上只是針對(duì)第一種反模式的基本改造,對(duì)于UnitOfWork同理,其本質(zhì)不過是管理操作事務(wù),并需我們手動(dòng)管理上下文釋放時(shí)機(jī)就好,這里就不再多講
我們還可以根據(jù)項(xiàng)目情況可進(jìn)一步實(shí)現(xiàn)其對(duì)應(yīng)規(guī)則,比如在是否需要在進(jìn)行指定操作之前實(shí)現(xiàn)自定義擴(kuò)展,比如再抽取一個(gè)上下文接口等等,ABP vNext中則是如此,ABP vNext對(duì)EF Core擴(kuò)展是我看過最完美的實(shí)現(xiàn)方案,接下來我們來看看
ABP vNext倉儲(chǔ)模式
其核心在Volo.Abp.EntityFrameworkCore包中,將其單獨(dú)剝離出來除了抽象通用封裝外,還有一個(gè)則是調(diào)用了EF Core底層APi,一旦EF Core版本變動(dòng),此包也需同步更新
ABP vNext針對(duì)EF Core做了擴(kuò)展,通過查看整體實(shí)現(xiàn),主要通過擴(kuò)展中特性實(shí)現(xiàn)指定屬性更新,EF Core中當(dāng)模型被跟蹤時(shí),直接提交則更新變化屬性,若未跟蹤,我們直接Update但想要更新指定屬性,這種方式不可行,在ABP vNext則得到了良好的解決
在其EF Core包中的AbpDbContext上下文中,針對(duì)屬性跟蹤更改做了良好的實(shí)現(xiàn),如下:
- 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;
- }
- .....
- }
除此之外的第二大亮點(diǎn)則是對(duì)UnitOfWork(工作單元)的完美方案,將其封裝在Volo.Abp.Uow包中,通過UnitOfWorkManager管理UnitOfWork,其事務(wù)提交不簡(jiǎn)單是像如下形式
- 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();
- }
額外的還實(shí)現(xiàn)了基于環(huán)境流動(dòng)的事務(wù)(AmbientUnitOfWork),反正ABP vNext在EF Core這塊擴(kuò)展實(shí)現(xiàn)令人嘆服,我也在持續(xù)學(xué)習(xí)中,其他就不多講了,博客園中講解原理的文章比比皆是