天天看點

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

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>()

方法來取得資料庫上下文。

整個關系如下:

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

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

傳回不同的連接配接串。

3.點此跳轉到總目錄