前言
倉儲模式我們已耳熟能詳,但當我們將其進行應用時,真的是那麼得心應手嗎?確定是解放了生產力嗎?這到底是怎樣的一個存在,確定不是反模式?,一篇詳文我們探討倉儲模式,這裡僅我個人的思考,若有更深刻的理解,請在評論中給出
倉儲反模式
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基礎設施持久化層
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這塊擴充套件實現令人歎服,我也在持續學習中,其他就不多講了,部落格園中講解原理的文章比比皆是
好了,本文到此結束,倒沒什麼可總結的,在文中已有概括,我們下次再會