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

方法很简单,注释已经说的很清楚了,就是遍历实体,通过 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.点此跳转到总目录

[Abp 源码分析]七、仓储与 Entity Framework Core的更多相关文章

  1. ABP源码分析十四:Entity的设计

    IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...

  2. ABP源码分析七:Setting 以及 Mail

    本文主要说明Setting的实现以及Mail这个功能模块如何使用Setting. 首先区分一下ABP中的Setting和Configuration. Setting一般用于需要通过外部配置文件(或数据 ...

  3. ABP源码分析一:整体项目结构及目录

    ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...

  4. ABP源码分析二十七:ABP.Entity Framework

    IRepository:接口定义了Repository常见的方法 AbpRepositoryBase:实现了IRepository接口的常见方法 EfRepositoryBase:实现了AbpRepo ...

  5. [Abp 源码分析]零、文章目录

    0.系列文章目录 一.Abp 框架启动流程分析 二.模块系统 三.依赖注入 四.模块配置 五.系统设置 六.工作单元的实现 七.仓储与 Entity Framework Core 八.缓存管理 九.事 ...

  6. ABP源码分析四十一:ZERO的Audit,Setting,Background Job

    AuditLog: 继承自Entity<long>的实体类.封装AuditLog的信息. AuditingStore: 实现了IAuditingStore接口,实现了将AuditLog的信 ...

  7. [Abp 源码分析]六、工作单元的实现

    0.简介 在 Abp 框架内部实现了工作单元,在这里讲解一下,什么是工作单元? Unit Of Work(工作单元)模式用来维护一个由已经被业务事物修改(增加.删除或更新)的业务对象组成的列表.Uni ...

  8. ABP源码分析十:Unit Of Work

    ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...

  9. ABP源码分析十五:ABP中的实用扩展方法

    类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an   attribu ...

随机推荐

  1. .net core 2.x - ids4 - identity - two factory 登录认证

    本片内容使用到ids4+ids4.Entityframework持久化表单,以及core的identity相关表的一并持久化,然后就是登录认证,认证使用email发送邮件的方式.所以这里涉及到四块内容 ...

  2. tensorflow优化器-【老鱼学tensorflow】

    tensorflow中的优化器主要是各种求解方程的方法,我们知道求解非线性方程有各种方法,比如二分法.牛顿法.割线法等,类似的,tensorflow中的优化器也只是在求解方程时的各种方法. 比较常用的 ...

  3. vs2015创建类时增加默认注释

    我是vs2015修改 C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\ItemTemplatesCache\CSharp ...

  4. day19其他模块

    collections模块 详细内容 http://www.cnblogs.com/Eva-J/articles/7291842.html 1.namedtuple: 生成可以使用名字来访问元素内容的 ...

  5. CentOS 7.0开放指定端口

    >>> CentOS 7.0默认使用的是firewall作为防火墙,使用iptables必须重新设置一下 1.直接关闭防火墙 systemctl stop firewalld.ser ...

  6. 修改 salt-minion 的 ID 后报错解决方法

    当在搭建 Saltstack 集中化管理平台配置完毕时,启动服务时,不知道是否你也越到过如下报错的现象呢? 报错问题如下: [root@saltstack_web1group_1 ~]# vim /e ...

  7. jQuery AJAX相关方法

    接jQuery学习上篇.因为AJAX是相对独立的一块,所以和jQuery的随笔分开记录了.素材同样来自runoob. 先了解下什么是AJAX. AJAX = 异步 JavaScript 和 XML(A ...

  8. 32位二进制IP地址与十进制IP地址互相转换

    代码: import java.util.List; import java.util.ArrayList; import java.util.Scanner; public class Transf ...

  9. CSS矩形、三角形等

    1.圆形 CSS代码如下:宽高一样,border-radius设为宽高的一半 #circle { width: 100px; height: 100px; background: red; -moz- ...

  10. Tips_钉钉免登前端实现

    1.需求:开发钉钉微应用,需要实现钉钉的免登陆功能. #.其实钉钉的文档中心还是很详细的,只是刚开始接触会一头雾水,所以花费了挺多时间....... ?什么是钉钉免登功能. ?企业应用免登开发授权流程 ...