1. 程式人生 > >[Abp 源碼分析]七、倉儲與 Entity Framework Core

[Abp 源碼分析]七、倉儲與 Entity Framework Core

val imp logger arguments 方便 解析 depend 有一個 eof

0.簡介

Abp 框架在其內部實現了倉儲模式,並且支持 EF Core 與 Dapper 來進行數據庫連接與管理,你可以很方便地通過註入倉儲來操作你的數據。

例如:

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);
    }
}

方法很簡單,註釋已經說的很清楚了,就是遍歷實體,通過 EfGenericRepositoryRegistrarEfCoreBasedSecondaryOrmRegistrar 來註冊倉儲。

來看一下具體的註冊操作:

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() 提交更改到數據庫當中。

之後呢,第二句話就是使用 DbContextdbContext.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 返回不同的連接串。

[Abp 源碼分析]七、倉儲與 Entity Framework Core