[Abp 源码分析]七、仓储与 Entity Framework Core


0.简介

Abp 框架在其内部实现了仓储模式,并且支持 EF Core 与 Dapper 来进行数据库连接与管理,你可以很方便地通过注入通用仓储来操作你的数据,而不需要你自己来为每一个实体定义单独的仓储的实现,通用仓储包含了常用的 CRUD 接口和一些常用方法。

例如:

public class TestAppService : ITransientDependency
{
    private readonly IRepository _rep;
    
    // 注入通用仓储
    public TestAppService(IRepository 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 内部,他的接口定义如下:

public interface IRepository : IRepository where TEntity : class, IEntity
{
	// CRUD 方法
}

可以看到,他有两个泛型参数,第一个是实体类型,第二个是实体的主键类型,并且约束了 TEntity 必须实现了 IEntity 接口,这是因为在仓储接口内部的一些方法需要得到实体的主键才能够操作,比如修改与查询方法。

在 Abp 内部还有另外一个仓储的定义,叫做 IRepository ,这个接口就是默认你的主键类型为 int类型,一般很少使用 IRepository 更多的还是用的 IRepository

1.2 通用仓储的实现

在 Abp 库里面,有一个默认的抽象基类实现了仓储接口,这个基类内部主要注入了 IUnitOfWorkManager 用来控制事务,还有 IIocResolver 用来解析 Ioc 容器内部注册的组件。

本身在这个抽象仓储类里面没有什么实质性的东西,它只是之前 IRepository 的简单实现,在 EfCoreRepositoryBase 类当中则才是具体调用 EF Core API 的实现。

public class EfCoreRepositoryBase : 
    AbpRepositoryBase,
    ISupportsExplicitLoading,
    IRepositoryWithDbContext
    
    where TEntity : class, IEntity
    where TDbContext : DbContext
{
    /// 
    /// 获得数据库上下文
    /// 
    public virtual TDbContext Context => _dbContextProvider.GetDbContext(MultiTenancySide);

    /// 
    /// 具体的实体表
    /// 
    public virtual DbSet Table => Context.Set();

	// 数据库事务
    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 _dbContextProvider;

    /// 
    /// 构造函数
    /// 
    /// 
    public EfCoreRepositoryBase(IDbContextProvider 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().RegisterForDbContext(dbContextType, IocManager, EfCoreAutoRepositoryTypes.Default);

            // 为自定义的 DbContext 注册仓储
            IocManager.IocContainer.Register(
                Component.For()
                    .Named(Guid.NewGuid().ToString("N"))
                    .Instance(new EfCoreBasedSecondaryOrmRegistrar(dbContextType, scope.Resolve()))
                    .LifestyleTransient()
            );
        }

        scope.Resolve().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 ActiveDbContexts { get; }
    protected IIocResolver IocResolver { get; }

    private readonly IDbContextResolver _dbContextResolver;
    private readonly IDbContextTypeMatcher _dbContextTypeMatcher;
    private readonly IEfCoreTransactionStrategy _transactionStrategy;

    /// 
    /// 创建一个新的 EF UOW 对象
    /// 
    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();
    }
}

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
    where TDbContext : DbContext
{
    TDbContext GetDbContext();

    TDbContext GetDbContext(MultiTenancySides? multiTenancySide );
}

很简单的作用,获取指定类型的数据库上下文,他的标准实现是 UnitOfWorkDbContextProvider,它依赖于 UOW ,使用 UOW 的 GetDbContext() 方法来取得数据库上下文。

整个关系如下:

2.3 多数据库支持

在 Abp 内部针对多数据库支持是通过覆写 IConnectionStringResolver 来实现的,这个操作在之前的文章里面已经讲过,这里仅讲解它如何在 Abp 内部实现解析的。

IConnectionStringResolver 是在 EF 的 Uow 才会用到,也就是创建 DbContext 的时候:

public virtual TDbContext GetOrCreateDbContext(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(connectionString, _dbContextResolver);

    return (TDbContext)dbContext;
}

// 传入了 ConnectionStringResolveArgs 里面包含了实体类型信息哦
protected virtual string ResolveConnectionString(ConnectionStringResolveArgs args)
{
    return ConnectionStringResolver.GetNameOrConnectionString(args);
}

他这里的默认实现叫做 DefaultConnectionStringResolver ,就是从 IAbpStartupConfiguration 里面拿去用户在启动模块配置的 DefaultNameOrConnectionString 字段作为自己的默认数据库连接字符串。

在之前的 文章 的思路也是通过传入的 ConnectionStringResolveArgs 参数来判断传入的 Type,从而来根据不同的 DbContext 返回不同的连接串。

3.