[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>()
方法来取得数据库上下文。
整个关系如下:
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的更多相关文章
- ABP源码分析十四:Entity的设计
IEntity<TPrimaryKey>: 封装了PrimaryKey:Id,这是一个泛型类型 IEntity: 封装了PrimaryKey:Id,这是一个int类型 Entity< ...
- ABP源码分析七:Setting 以及 Mail
本文主要说明Setting的实现以及Mail这个功能模块如何使用Setting. 首先区分一下ABP中的Setting和Configuration. Setting一般用于需要通过外部配置文件(或数据 ...
- ABP源码分析一:整体项目结构及目录
ABP是一套非常优秀的web应用程序架构,适合用来搭建集中式架构的web应用程序. 整个Abp的Infrastructure是以Abp这个package为核心模块(core)+15个模块(module ...
- ABP源码分析二十七:ABP.Entity Framework
IRepository:接口定义了Repository常见的方法 AbpRepositoryBase:实现了IRepository接口的常见方法 EfRepositoryBase:实现了AbpRepo ...
- [Abp 源码分析]零、文章目录
0.系列文章目录 一.Abp 框架启动流程分析 二.模块系统 三.依赖注入 四.模块配置 五.系统设置 六.工作单元的实现 七.仓储与 Entity Framework Core 八.缓存管理 九.事 ...
- ABP源码分析四十一:ZERO的Audit,Setting,Background Job
AuditLog: 继承自Entity<long>的实体类.封装AuditLog的信息. AuditingStore: 实现了IAuditingStore接口,实现了将AuditLog的信 ...
- [Abp 源码分析]六、工作单元的实现
0.简介 在 Abp 框架内部实现了工作单元,在这里讲解一下,什么是工作单元? Unit Of Work(工作单元)模式用来维护一个由已经被业务事物修改(增加.删除或更新)的业务对象组成的列表.Uni ...
- ABP源码分析十:Unit Of Work
ABP以AOP的方式实现UnitOfWork功能.通过UnitOfWorkRegistrar将UnitOfWorkInterceptor在某个类被注册到IOCContainner的时候,一并添加到该类 ...
- ABP源码分析十五:ABP中的实用扩展方法
类名 扩展的类型 方法名 参数 作用 XmlNodeExtensions XmlNode GetAttributeValueOrNull attributeName Gets an attribu ...
随机推荐
- 1到n的最小步数
1到n的最小步数 Time Limit: 1 Sec Memory Limit: 128 MB 给你一个数n,让你求从1到n的最小步数是多少. 对于当前的数x有三种操作: 1: x+1 2: x ...
- mac抓包工具anyproxy
本文以 mac为代理,ios手机为客户端举例. 文档地址:http://anyproxy.io/ 1.环境配置: 安装 node :参考 https://www.jianshu.com/p/3 ...
- java写word转pdf
https://blog.csdn.net/csdnFlyFun/article/details/79523262
- 启动nginx报错问题
为了解决生产环境的bug,模拟生产环境,我使用了nginx,在安装启动的过程中,出现了很多问题. 1.nginx下载地址 http://nginx.org/en/download.html 这是ngi ...
- 在deepin上安装YouCompleteMe
详细安装步骤在github上有,https://github.com/Valloric/YouCompleteMe,我这里是自己总结的简化版安装步骤. 步骤1.安装Vundle 首先,clone到本地 ...
- Java Web 获取客户端真实IP
Java Web 获取客户端真实IP 发生的场景:服务器端接收客户端请求的时候,一般需要进行签名验证,客户端IP限定等情况,在进行客户端IP限定的时候,需要首先获取该真实的IP.一般分为两种情况: 方 ...
- NPOI操作创建Excel
一.下载NPOI类库 使用Nuget在线搜索NPOI,下载安装 二.代码开撸 var workBook = new HSSFWorkbook(); #region 设置样式 IFont font = ...
- 蓝鲸单机安装mysql问题记录
1.注意默认启动用户为mysql 2.由于执行指令用的root,有些文件会生成为root用户 3.注意将datadir basedir socket dir 修改称mysql chown mys ...
- Java变成遇到的简单乱码问题
1.乱码 --- 编码集 编码集的本质是让数字与字符产生一个映射关系,不同的编码集映射实现也不同 比如UTF-8: "中"----> -28 -72 -83 对应 ...
- java中class文件与jar文件
1. JAR 文件包 JAR 文件就是 Java Archive File,顾名思意,它的应用是与 Java 息息相关的,是 Java 的一种文档格式.JAR 文件非常类似 ZIP 文件——准确的说, ...