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

0.简介

Abp 框架在其内部实现了仓储模式,而且支持 EF Core 与 Dapper 来进行数据库链接与管理,你能够很方便地经过注入通用仓储来操做你的数据,而不须要你本身来为每个实体定义单独的仓储的实现,通用仓储包含了经常使用的 CRUD 接口和一些经常使用方法。html

例如:数据库

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 内部,包括如下几个文件:app

文件名称 做用描述
AbpRepositoryBase.cs 仓储基类
AutoRepositoryTypesAttribute.cs 自动构建仓储,用于实体标记
IRepository.cs 仓储基本接口定义
IRepositoryOfTEntity.cs 仓储接口定义,默认主键为 int 类型
IRepositoryOfTEntityAndTPrimaryKey.cs 仓储接口定义,主键与实体类型由用户定义
ISupportsExplicitLoading.cs 显式加载
RepositoryExtensions.cs 仓储相关的扩展方法

1.1 通用仓储定义

综上所述,仓储的基础定义是由 IRepository 决定的,这个接口没什么其余用处,就如同 ITransientDependency 接口与 ISingletonDependency 同样,只是作一个标识做用。框架

真正定义了仓储接口的是在 IRepositoryOfTEntityAndTPrimaryKey<TEntity, TPrimaryKey> 内部,他的接口定义以下:async

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

能够看到,他有两个泛型参数,第一个是实体类型,第二个是实体的主键类型,而且约束了 TEntity 必须实现了 IEntity<TPrimaryKey> 接口,这是由于在仓储接口内部的一些方法须要获得实体的主键才可以操做,好比修改与查询方法。ide

在 Abp 内部还有另一个仓储的定义,叫作 IRepository<TEntity> ,这个接口就是默认你的主键类型为 int类型,通常不多使用 IRepository<TEntity, TPrimaryKey> 更多的仍是用的 IRepository<TEntity>函数

1.2 通用仓储的实现

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

自己在这个抽象仓储类里面没有什么实质性的东西,它只是以前 IRepository<TEntity> 的简单实现,在 EfCoreRepositoryBase 类当中则才是具体调用 EF Core API 的实现。3d

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 来进行的操做。code

举个例子:

// 插入数据
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 返回不一样的链接串。

3.点此跳转到总目录

相关文章
相关标签/搜索