0.簡介 Abp 框架在其內部實現了倉儲模式,並且支持 EF Core 與 Dapper 來進行資料庫連接與管理,你可以很方便地通過註入通用倉儲來操作你的數據,而不需要你自己來為每一個實體定義單獨的倉儲的實現,通用倉儲包含了常用的 CRUD 介面和一些常用方法。 例如: 1.通用倉儲定義與實現 在 ...
0.簡介
Abp 框架在其內部實現了倉儲模式,並且支持 EF Core 與 Dapper 來進行資料庫連接與管理,你可以很方便地通過註入通用倉儲來操作你的數據,而不需要你自己來為每一個實體定義單獨的倉儲的實現,通用倉儲包含了常用的 CRUD 介面和一些常用方法。
例如:
public class TestAppService : ITransientDependency
{
private readonly IRepository<TestTable> _rep;
// 註入通用倉儲
public TestAppService(IRepository<TestTable> rep)
{
_rep = rep;
}
public void TestMethod()
{
// 插入一條新數據
_rep.Insert(new TestTable{ Name = "TestName" });
}
}
1.通用倉儲定義與實現
在 Abp 內部,倉儲的基本定義存放在 Abp 項目的 Domain/Repositories 內部,包括以下幾個文件:
文件名稱 | 作用描述 |
---|---|
AbpRepositoryBase.cs | 倉儲基類 |
AutoRepositoryTypesAttribute.cs | 自動構建倉儲,用於實體標記 |
IRepository.cs | 倉儲基本介面定義 |
IRepositoryOfTEntity.cs | 倉儲介面定義,預設主鍵為 int 類型 |
IRepositoryOfTEntityAndTPrimaryKey.cs | 倉儲介面定義,主鍵與實體類型由用戶定義 |
ISupportsExplicitLoading.cs | 顯式載入 |
RepositoryExtensions.cs | 倉儲相關的擴展方法 |
1.1 通用倉儲定義
綜上所述,倉儲的基礎定義是由 IRepository
決定的,這個介面沒什麼其他用處,就如同 ITransientDependency
介面與 ISingletonDependency
一樣,只是做一個標識作用。
真正定義了倉儲介面的是在 IRepositoryOfTEntityAndTPrimaryKey<TEntity, TPrimaryKey>
內部,他的介面定義如下:
public interface IRepository<TEntity, TPrimaryKey> : IRepository where TEntity : class, IEntity<TPrimaryKey>
{
// CRUD 方法
}
可以看到,他有兩個泛型參數,第一個是實體類型,第二個是實體的主鍵類型,並且約束了 TEntity
必須實現了 IEntity<TPrimaryKey>
介面,這是因為在倉儲介面內部的一些方法需要得到實體的主鍵才能夠操作,比如修改與查詢方法。
在 Abp 內部還有另外一個倉儲的定義,叫做 IRepository<TEntity>
,這個介面就是預設你的主鍵類型為 int
類型,一般很少使用 IRepository<TEntity, TPrimaryKey>
更多的還是用的 IRepository<TEntity>
。
1.2 通用倉儲的實現
在 Abp 庫裡面,有一個預設的抽象基類實現了倉儲介面,這個基類內部主要註入了 IUnitOfWorkManager
用來控制事務,還有 IIocResolver
用來解析 Ioc 容器內部註冊的組件。
本身在這個抽象倉儲類裡面沒有什麼實質性的東西,它只是之前 IRepository<TEntity>
的簡單實現,在 EfCoreRepositoryBase
類當中則才是具體調用 EF Core API 的實現。
public class EfCoreRepositoryBase<TDbContext, TEntity, TPrimaryKey> :
AbpRepositoryBase<TEntity, TPrimaryKey>,
ISupportsExplicitLoading<TEntity, TPrimaryKey>,
IRepositoryWithDbContext
where TEntity : class, IEntity<TPrimaryKey>
where TDbContext : DbContext
{
/// <summary>
/// 獲得資料庫上下文
/// </summary>
public virtual TDbContext Context => _dbContextProvider.GetDbContext(MultiTenancySide);
/// <summary>
/// 具體的實體表
/// </summary>
public virtual DbSet<TEntity> Table => Context.Set<TEntity>();
// 資料庫事務
public virtual DbTransaction Transaction
{
get
{
return (DbTransaction) TransactionProvider?.GetActiveTransaction(new ActiveTransactionProviderArgs
{
{"ContextType", typeof(TDbContext) },
{"MultiTenancySide", MultiTenancySide }
});
}
}
// 資料庫連接
public virtual DbConnection Connection
{
get
{
var connection = Context.Database.GetDbConnection();
if (connection.State != ConnectionState.Open)
{
connection.Open();
}
return connection;
}
}
// 事務提供器,用於獲取已經激活的事務
public IActiveTransactionProvider TransactionProvider { private get; set; }
private readonly IDbContextProvider<TDbContext> _dbContextProvider;
/// <summary>
/// 構造函數
/// </summary>
/// <param name="dbContextProvider"></param>
public EfCoreRepositoryBase(IDbContextProvider<TDbContext> dbContextProvider)
{
_dbContextProvider = dbContextProvider;
}
}
其實從上方就可以看出來,Abp 對於每一個倉儲都會重新打開一個資料庫鏈接,在 EfCoreRepositoryBase
裡面的 CRUD 方法實際上都是針對 DbContext
來進行的操作。
舉個例子:
// 插入數據
public override TEntity Insert(TEntity entity)
{
return Table.Add(entity).Entity;
}
// 更新數據
public override TEntity Update(TEntity entity)
{
AttachIfNot(entity);
Context.Entry(entity).State = EntityState.Modified;
return entity;
}
// 附加實體狀態
protected virtual void AttachIfNot(TEntity entity)
{
var entry = Context.ChangeTracker.Entries().FirstOrDefault(ent => ent.Entity == entity);
if (entry != null)
{
return;
}
Table.Attach(entity);
}
這裡需要註意的是 Update()
方法,之前遇到過一個問題,假如我傳入了一個實體,它的 ID 是不存在的,那麼我將這個實體傳入 Update()
方法之後執行 SaveChanges()
的時候,會拋出 DbUpdateConcurrencyException
異常。
正確的操作是先使用實體的 ID 去查詢資料庫是否存在該條記錄,存在再執行 Update()
操作。
這裡 AttachIfNot 作用是將實體附加到追蹤上下文當中,如果你之前是通過
Get()
方法獲取實體之後更改了某個實體,那麼在調用Context.ChangeTracker.Entries()
方法的時候會獲取到已經發生變動的身體對象集合。
1.3 通用倉儲的註入
倉儲的註入操作發生在 AbpEntityFrameworkCoreModule
模塊執行 Initialize()
方法的時候,在 Initialize()
方法內部調用了 RegisterGenericRepositoriesAndMatchDbContexes()
方法,其定義如下:
private void RegisterGenericRepositoriesAndMatchDbContexes()
{
// 查找所有資料庫上下文
var dbContextTypes =
_typeFinder.Find(type =>
{
var typeInfo = type.GetTypeInfo();
return typeInfo.IsPublic &&
!typeInfo.IsAbstract &&
typeInfo.IsClass &&
typeof(AbpDbContext).IsAssignableFrom(type);
});
if (dbContextTypes.IsNullOrEmpty())
{
Logger.Warn("No class found derived from AbpDbContext.");
return;
}
using (IScopedIocResolver scope = IocManager.CreateScope())
{
// 遍曆數據庫上下文
foreach (var dbContextType in dbContextTypes)
{
Logger.Debug("Registering DbContext: " + dbContextType.AssemblyQualifiedName);
// 為資料庫上下文每個實體註冊倉儲
scope.Resolve<IEfGenericRepositoryRegistrar>().RegisterForDbContext(dbContextType, IocManager, EfCoreAutoRepositoryTypes.Default);
// 為自定義的 DbContext 註冊倉儲
IocManager.IocContainer.Register(
Component.For<ISecondaryOrmRegistrar>()
.Named(Guid.NewGuid().ToString("N"))
.Instance(new EfCoreBasedSecondaryOrmRegistrar(dbContextType, scope.Resolve<IDbContextEntityFinder>()))
.LifestyleTransient()
);
}
scope.Resolve<IDbContextTypeMatcher>().Populate(dbContextTypes);
}
}
方法很簡單,註釋已經說的很清楚了,就是遍歷實體,通過 EfGenericRepositoryRegistrar
與 EfCoreBasedSecondaryOrmRegistrar
來註冊倉儲。
來看一下具體的註冊操作:
private void RegisterForDbContext(
Type dbContextType,
IIocManager iocManager,
Type repositoryInterface,
Type repositoryInterfaceWithPrimaryKey,
Type repositoryImplementation,
Type repositoryImplementationWithPrimaryKey)
{
foreach (var entityTypeInfo in _dbContextEntityFinder.GetEntityTypeInfos(dbContextType))
{
// 獲取主鍵類型
var primaryKeyType = EntityHelper.GetPrimaryKeyType(entityTypeInfo.EntityType);
if (primaryKeyType == typeof(int))
{
// 建立倉儲的封閉類型
var genericRepositoryType = repositoryInterface.MakeGenericType(entityTypeInfo.EntityType);
if (!iocManager.IsRegistered(genericRepositoryType))
{
// 構建具體的倉儲實現類型
var implType = repositoryImplementation.GetGenericArguments().Length == 1
? repositoryImplementation.MakeGenericType(entityTypeInfo.EntityType)
: repositoryImplementation.MakeGenericType(entityTypeInfo.DeclaringType,
entityTypeInfo.EntityType);
// 註入
iocManager.IocContainer.Register(
Component
.For(genericRepositoryType)
.ImplementedBy(implType)
.Named(Guid.NewGuid().ToString("N"))
.LifestyleTransient()
);
}
}
// 如果主鍵類型為 int 之外的類型
var genericRepositoryTypeWithPrimaryKey = repositoryInterfaceWithPrimaryKey.MakeGenericType(entityTypeInfo.EntityType,primaryKeyType);
if (!iocManager.IsRegistered(genericRepositoryTypeWithPrimaryKey))
{
// 操作跟上面一樣
var implType = repositoryImplementationWithPrimaryKey.GetGenericArguments().Length == 2
? repositoryImplementationWithPrimaryKey.MakeGenericType(entityTypeInfo.EntityType, primaryKeyType)
: repositoryImplementationWithPrimaryKey.MakeGenericType(entityTypeInfo.DeclaringType, entityTypeInfo.EntityType, primaryKeyType);
iocManager.IocContainer.Register(
Component
.For(genericRepositoryTypeWithPrimaryKey)
.ImplementedBy(implType)
.Named(Guid.NewGuid().ToString("N"))
.LifestyleTransient()
);
}
}
}
這裡 RegisterForDbContext()
方法傳入的這些開放類型其實是通過 EfCoreAutoRepositoryTypes.Default
屬性指定,其定義:
public static class EfCoreAutoRepositoryTypes
{
public static AutoRepositoryTypesAttribute Default { get; }
static EfCoreAutoRepositoryTypes()
{
Default = new AutoRepositoryTypesAttribute(
typeof(IRepository<>),
typeof(IRepository<,>),
typeof(EfCoreRepositoryBase<,>),
typeof(EfCoreRepositoryBase<,,>)
);
}
}
2.Entity Framework Core
2.1 工作單元
在之前的文章裡面說過,Abp 本身只實現了一個抽象工作單元基類 UnitOfWorkBase
,而具體的事務處理是存放在具體的持久化模塊裡面進行實現的,在 EF Core 這裡則是通過 EfCoreUnitOfWork
實現的。
首先看一下 EfCoreUnitOfWork
註入了哪些東西:
public class EfCoreUnitOfWork : UnitOfWorkBase, ITransientDependency
{
protected IDictionary<string, DbContext> ActiveDbContexts { get; }
protected IIocResolver IocResolver { get; }
private readonly IDbContextResolver _dbContextResolver;
private readonly IDbContextTypeMatcher _dbContextTypeMatcher;
private readonly IEfCoreTransactionStrategy _transactionStrategy;
/// <summary>
/// 創建一個新的 EF UOW 對象
/// </summary>
public EfCoreUnitOfWork(
IIocResolver iocResolver,
IConnectionStringResolver connectionStringResolver,
IUnitOfWorkFilterExecuter filterExecuter,
IDbContextResolver dbContextResolver,
IUnitOfWorkDefaultOptions defaultOptions,
IDbContextTypeMatcher dbContextTypeMatcher,
IEfCoreTransactionStrategy transactionStrategy)
: base(
connectionStringResolver,
defaultOptions,
filterExecuter)
{
IocResolver = iocResolver;
_dbContextResolver = dbContextResolver;
_dbContextTypeMatcher = dbContextTypeMatcher;
_transactionStrategy = transactionStrategy;
ActiveDbContexts = new Dictionary<string, DbContext>();
}
}
emmm,他註入的基本上都是與 EfCore 有關的東西。
第一個字典是存放處在激活狀態的 DbContext
集合,第二個是 IIocResolver
用於解析組件所需要的解析器,第三個是資料庫上下文的解析器用於創建 DbContext
的,第四個是用於查找 DbContext
的 Matcher,最後一個就是用於 EF Core 事物處理的東東。
根據 UnitOfWork
的調用順序,首先看查看 BeginUow()
方法:
if (Options.IsTransactional == true)
{
_transactionStrategy.InitOptions(Options);
}
沒什麼特殊操作,就拿著 UOW 對象的 Options 去初始化事物策略。
之後按照 UOW 的調用順序(PS:如果看的一頭霧水可以去看一下之前文章針對 UOW 的講解),會調用基類的 CompleteAsync()
方法,在其內部則是會調用 EF Core UOW 實現的 CompleteUowAsync()
方法,其定義如下:
protected override async Task CompleteUowAsync()
{
// 保存所有 DbContext 的更改
await SaveChangesAsync();
// 提交事務
CommitTransaction();
}
public override async Task SaveChangesAsync()
{
foreach (var dbContext in GetAllActiveDbContexts())
{
await SaveChangesInDbContextAsync(dbContext);
}
}
private void CommitTransaction()
{
if (Options.IsTransactional == true)
{
_transactionStrategy.Commit();
}
}
內部很簡單,兩句話,第一句話遍歷所有激活的 DbContext
,然後調用其 SaveChanges()
提交更改到資料庫當中。
之後呢,第二句話就是使用 DbContext
的 dbContext.Database.CommitTransaction();
方法來提交一個事務咯。
public void Commit()
{
foreach (var activeTransaction in ActiveTransactions.Values)
{
activeTransaction.DbContextTransaction.Commit();
foreach (var dbContext in activeTransaction.AttendedDbContexts)
{
if (dbContext.HasRelationalTransactionManager())
{
continue; //Relational databases use the shared transaction
}
dbContext.Database.CommitTransaction();
}
}
}
2.2 資料庫上下文提供器
這個玩意兒的定義如下:
public interface IDbContextProvider<out TDbContext>
where TDbContext : DbContext
{
TDbContext GetDbContext();
TDbContext GetDbContext(MultiTenancySides? multiTenancySide );
}
很簡單的作用,獲取指定類型的資料庫上下文,他的標準實現是 UnitOfWorkDbContextProvider<TDbContext>
,它依賴於 UOW ,使用 UOW 的 GetDbContext<TDbContext>()
方法來取得資料庫上下文。
整個關係如下:
2.3 多資料庫支持
在 Abp 內部針對多資料庫支持是通過覆寫 IConnectionStringResolver
來實現的,這個操作在之前的文章裡面已經講過,這裡僅講解它如何在 Abp 內部實現解析的。
IConnectionStringResolver
是在 EF 的 Uow 才會用到,也就是創建 DbContext
的時候:
public virtual TDbContext GetOrCreateDbContext<TDbContext>(MultiTenancySides? multiTenancySide = null)
where TDbContext : DbContext
{
var concreteDbContextType = _dbContextTypeMatcher.GetConcreteType(typeof(TDbContext));
var connectionStringResolveArgs = new ConnectionStringResolveArgs(multiTenancySide);
connectionStringResolveArgs["DbContextType"] = typeof(TDbContext);
connectionStringResolveArgs["DbContextConcreteType"] = concreteDbContextType;
// 這裡調用了 Resolver
var connectionString = ResolveConnectionString(connectionStringResolveArgs);
// 創建 DbContext
dbContext = _transactionStrategy.CreateDbContext<TDbContext>(connectionString, _dbContextResolver);
return (TDbContext)dbContext;
}
// 傳入了 ConnectionStringResolveArgs 裡面包含了實體類型信息哦
protected virtual string ResolveConnectionString(ConnectionStringResolveArgs args)
{
return ConnectionStringResolver.GetNameOrConnectionString(args);
}
他這裡的預設實現叫做 DefaultConnectionStringResolver
,就是從 IAbpStartupConfiguration
裡面拿去用戶在啟動模塊配置的 DefaultNameOrConnectionString
欄位作為自己的預設資料庫連接字元串。
在之前的 文章 的思路也是通過傳入的 ConnectionStringResolveArgs
參數來判斷傳入的 Type,從而來根據不同的 DbContext
返回不同的連接串。