EF Core使用步骤
普通项目
Nuget安装包
Microsoft.EntityFrameworkCore.SqlServer、
Microsoft.EntityFrameworkCore
1)建实体类
2)建配置类
创建实现IEntityTypeConfiguration
接口的实体配置类,配置实体类和数据库表的对应关系
注意:尽量用标准的、Provider无关的这些FluentAPI去配置,不要和数据库耦合。
如果真的需要在IEntityTypeConfiguration
中判断数据库类型,那么就定义一个接口提供DbContext
属性,仿照ApplyConfigurationsFromAssembly
写一个给IEntityTypeConfiguration
。实现类注入DbContext
,然后Dbcontext.Database.IsSqlServer();
scss
class BookConfig : IEntityTypeConfiguration {
public void Configure(EntityTypeBuilder builder) {
builder.ToTable("T_Books");//表名
//Title列最大长度为50,不可为空
builder.Property(e=>e.Title).HasMaxLength(50).IsRequired();
//不创建Age2列
builder.Ignore(b => b.Age2);
}
}
3)建DbContext的派生类
作用:相当于逻辑上的数据库。规定数据库中建哪些表,连接什么数据库,从哪里读取表的配置
csharp
//数据库中建哪些表
//不要忘了写set,否则拿到的DbContext的实体类为null。写成private是参考了杨中科大项目
public DbSet Rabbits { get; private set; }
public DbSet Books { get; private set; }
//连接什么数据库
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
base.OnConfiguring(optionsBuilder);
//连接串
optionsBuilder.UseSqlServer(connStr);
}
//从哪里读取配置
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
//从DbContext类所在的程序集的程序集中实现了IEntityTypeConfiguration的类中加载配置
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
4)执行数据库迁移
5)编写业务代码
ini
using MyDbContext ctx=new MyDbContext();
Rabbit r1=new Rabbit();
r1.Name = "ywl";
ctx.Rabbits.Add(r1);
await ctx.SaveChangesAsync();
分层项目
1、建类库项目,放实体类、DbContext、实体配置类
//安装在类库项目中
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Tools
DbContext
类中不配置数据库连接,不重写OnConfiguring
方法,而是为DbContext增加一个DbContextOptions
类型的构造函数
arduino
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) { }
2、类库项目中创建一个实现了IDesignTimeContextFactory
的类,并且在CreateDbContext
方法返回一个连接开发数据库的DbContext
作用:让开发环境的Add-Migration、Update-Database知道连接哪个数据库,正式项目运行时不会有这个类的事
ini
class MyDbContextDesignFac : IDesignTimeDbContextFactory<MyDbContext> {
public MyDbContext CreateDbContext(string[] args) {
DbContextOptionsBuilder<MyDbContext> builder = new DbContextOptionsBuilder<MyDbContext>();
string ConnStr = ...;
builder.UseSqlServer(ConnStr);
MyDbContext ctx=new MyDbContext(builder.Options);
return ctx;
}
}
关于连接字符串的注意事项
1)如果不在乎连接字符串被上传到Git,就可以把连接字符串直接写死到CreateDbContext
方法中
2)如果在乎,那么CreateDbContext
里面很难读取到VS中通过简单的方法设置的环境变量,所以必须把连接字符串配置到Windows正式的环境变量中,然后再Environment.GetEnvironmentVariable
读取
ini
string ConnStr = Environment.GetEnvironmentVariable("ConnStr");
3、创建ASP.NET Core项目,添加对类库项目的引用。ASP.NET Core项目安装对应数据库的Provider
//安装在ASP.NET Core项目
Microsoft.EntityFrameworkCore.SqlServer
4、ASP.NET Core项目通过AddDbContext
来注册DbContext服务,并对DbContext进行配置
ini
//注入的DbContext不需要再手动New,已经注入DI
builder.Services.AddDbContext<MyDbContext>(opt => {
string ConnStr= builder.Configuration.GetSection("ConnStr").Value;
opt.UseSqlServer(ConnStr);
});
5、ASP.NET Core项目的Controller中注入DbContext类来使用
csharp
private readonly MyDbContext dbCtx;
public Test1Controller(MyDbContext dbCtx) {
this.dbCtx = dbCtx;
}
//使用DbContext
dbCtx.Books.Count();
6、把类库项目设为启动项目,并且在【程序包管理器控制台】中也要选中类库项目。正常执行Add-MIgration、Update-Database迁移即可
数据库迁移
迁移的定义:根据对象的变化,自动更新数据库中的表以及表结构的操作
- 面向对象的ORM开发中,数据库不是程序员手动创建的,而是Migration工具生成的。关系数据库只是盛放数据的一个媒介而已。理想情况下,程序员不用关系数据库的操作
- 迁移可以自动生成数据库及表
- 每次Migration都有其编号和名字
- 迁移可以分为多步(项目进化),也可以回滚
- 执行迁移后,数据库会存在_EFMigrationsHistory表,MigrationId记录当前的数据库曾经应用过的迁移脚本名称,按顺序排列,ProductVersion为EF Core版本
向上迁移:使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做"向上迁移"(Up)
向下迁移:把数据库回退到旧的迁移(Down)
迁移脚本
- 执行数据库迁移后,会生成Migrations文件夹。除非有特殊需要,否则不要删除Migrations文件夹下的代码
- 迁移脚本和数据库相关,因此迁移脚本不能跨数据库
- 迁移脚本中,Up方法中是向上迁移的代码,Down方法中是向下迁移的代码
迁移命令
Add-Migration
:生成一次迁移脚本,会自动在项目的Migrations文件夹中生成操作数据库的C#代码,添加-OutputDir
参数的在同一个项目中为不同的数据库生成不同的迁移脚本
sql
Add-Migration Init //Init为本次迁移的名字,可用于回滚
Add-Migration AddBirth
Remove-migration
:删除最后一次的迁移脚本。可以一直Remove下去
Update-database
:代码需要执行后才会应用对数据库的操作
Update-database XXX
:把数据库回滚或升级到XXX的状态,迁移脚本不动。只适合于开发环境,不适合于生产环境
sql
Update-database Init
Script-Migration
:生成迁移SQL脚本,可以生成指定版本区间的SQL脚本。可以将此脚本直接在数据库执行
r
Script-Migration D:生成版本D到最新版本的SQL脚本
Script-Migration D F:生成版本D到版本F的SQL脚本
数据库迁移步骤
注意,确保程序需要编译通过,没有报错。否则,执行迁移命令会报错Build failed,且没有任何报错信息
安装包
Microsoft.EntityFrameworkCore.Tools //必须安装这个,不然识别不了迁移命令
1)将含有DbContextFactory
的类库项目设为启动项目
2)在程序包管理控制台里输入:Add-Migration Init
注意:项目中有多个DbContext时,迁移要通过-Context指定DbContext
sql
Add-Migration AddPerson -Context My2DbContext
Update-database -Context My2DbContext
dotnet ef migrations add Add_MessageRecord_Entity -c DataSyncServiceDbContext
3)Update-database
分层项目数据库迁移
1)将基础设施层设为启动项目,程序包管理控制台中将基础设施层设为默认项目
2)在通用类库定义一个方法,返回一个包含数据库连接串信息的DbContextOptionsBuilder
对象
csharp
public static class DbContextOptionsBuilderFactory {
//TDbContext代表DbContext的派生类(这只是一个标记,不是实际的类)。从真实的环境变量中读取连接字符串
public static DbContextOptionsBuilder<TDbContext> Create<TDbContext>()
where TDbContext : DbContext {
var connStr = Environment.GetEnvironmentVariable("DefaultDB:ConnStr");
var optionsBuilder = new DbContextOptionsBuilder<TDbContext>();
//optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=YouzackVNextDB;User ID=sa;Password=dLLikhQWy5TBz1uM;");
optionsBuilder.UseSqlServer(connStr);
return optionsBuilder;
}
}
3)在基础设施层添加DesignTimeDbContextFactory
类,实现IDesignTimeDbContextFactory
调用通用类库中的方法拿到DbContextOptionsBuilder
,再返回一个合适的DbContext即可
注:仅在开发时(Add-Migration Update-Database等)运行,正式项目运行时不会有这个类的事
csharp
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<IdDbContext> {
public IdDbContext CreateDbContext(string[] args) {
var optionsBuilder = DbContextOptionsBuilderFactory.Create<IdDbContext>();
return new IdDbContext(optionsBuilder.Options);
}
}
EF Core批量注册上下文
项目中表非常多的话,不应该把所有的表都配置到同一个DbContext中,而是采用"小上下文"策略,一个DbContext只包含几个DbSet。
因此项目中可能存在着多个上下文类,如果每个上下文类都手动AddDbContext就太麻烦。
ini
builder.Services.AddDbContext<MyDbContext>(opt => {
string ConnStr= builder.Configuration.GetSection("ConnStr").Value;
opt.UseSqlServer(ConnStr);
});
builder.Services.AddDbContext<MyDbContext2>(opt => {
//...
});
使用第三方库,当项目中有多个DbContext,但是连接的是同一个数据库时,可以批量注册DbContext
//安装这个第三方库
Zack.Infrastructure
在Program.cs中批量注册上下文
ini
//var asms = new Assembly[] { Assembly.Load("BooksEFCore") };
//原理:通过反射遍历程序集中所有继承自DbContext的类,调用AddDbContetx方法,挨个注册
var asms = ReflectionHelper.GetAllReferencedAssemblies();//遍历所有程序集
builder.Services.AddAllDbContexts(opt => {
string ConnStr = builder.Configuration.GetSection("ConnStr").Value;
opt.UseSqlServer(ConnStr);
}, asms);
分层项目各自进行模块注册
假设有p1、p2、p3三个类库项目、一个asp.net core四个项目,asp.net core项目引用三个类库项目,类库项目中的服务都需要在asp.net core项目中Program.cs中注册,这样写起来Program.cs在代码太多
ini
builder.Services.AddScoped<Class1>();
builder.Services.AddScoped<Class2>();
builder.Services.AddScoped<Class3>();
builder.Services.AddScoped<Class4>();
使用第三方库,在分层项目中,让各个项目负责各自的服务注册。原理是获取项目所有引用的程序集,通过反射的形式调用实现IMoudleInitializer接口的类
//Nuget
Zack.Commons
使用步骤:
1、在每个项目中创建一个实现了IMoudleInitializer
接口的类,在Initialize
方法中可以拿到services
,在这里面注册服务即可
注1:可以创建一个或者多个,不过为了集中管理,还是建议一个项目中只放一个实现了IModuleInitializer的类
注2:当领域层、基础设施层都有实现了IMoudleInitializer接口的类的时候,可能报错,这时尝试将其都放到基础设施层或能解决(因为基础设施层引用领域层)
arduino
public class ModelInitcs: IModuleInitializer
{
public void Initialize(IServiceCollection services){
services.AddScoped<Class1>();
}
}
3、在总的Program.cs中,初始化DI容器
ini
var assembiles=ReflectionHelper.GetAllReferencedAssemblies();
services.RunMoudleInitializers(assembiles);
EF Core处理数据库并发问题
注:采用乐观并发控制。因为EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制
总结
- 如果有一个确定的字段要被进行并发控制,那么使用
IsConcurrencyToken()
把这个字段设置为并发令牌即可 - 如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是SQLServer数据库,那么也可以采用
RowVersion
列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了
并发令牌
原理:把被并发修改的属性使用IsConcurecyToken
设置为并发令牌
当Update的时候,如果数据库的Owner值已经被其他操作者更新为其他值了,那么where语句的值就为false,因此这个Update语句影响的行数就是0,EF Core就知道发生并发冲突了,因此SaveChanges方法就会抛出DbUpdateConcurrencyException
异常
缺点
- 1)不适合于含有多个字段需要处理并发的情况
- 2)会导致ABA的问题,改过去又改回来,会被认为没有修改
ROWVERSION
原理:SQL Server数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()
把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对应ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列为其生成新值
说明
- 1)在SQL Server中,
timestamp
和rowversion
是同一种类型的不同别名而已 - 2)ROWVERSION是SQL Server特有类型。在MYSQL等数据库中虽然也有类似的timestamp类型,但是由于精度不够,并不适合在高并发的系统
- 3)在非SQL SERVER中,可以将并发令牌列的值更新为Guid值。修改其他属性值的时候,使用
h1.RowVersion=Guid.NewGuid()
手动更新并发令牌属性的值
数据软删除
需要使用查询过滤器
步骤
1、为实体类配置软删除相应的属性、方法
csharp
//Zack.DomainCommons.Models包中有这两个接口
//ISoftDelete
public interface ISoftDelete {
bool IsDeleted {
get;
}
void SoftDelete();
}
//IHasDeletionTime
DateTime? DeletionTime {
get;
}
//实现此接口
public class User : IdentityUser<Guid>, IHasDeletionTime, ISoftDelete{
...
}
2、配置查询过滤器
方法一:手动指定实体类
在基础设施层的DbContext直接对实体类添加过滤器
scss
protected override OnModelCreating(ModelBuilder modelBuilder)
{
...
modelBuilder.Entity<Book>().HasQueryFilter(p => !p.Deleted);
}
方法二:判断每个实体类并决定如何配置它
在基础设施层的DbContext的OnModelCreating
,foreach 循环依次遍历每个实体类,检查实体类是否继承了 BaseEntity
基类,如果实现了,它将调用扩展方法来应用正确的软删除过滤器配置
scss
foreach (var entityType in modelBuilder.Model.GetEntityTypes()) {
if (typeof(BaseEntity).IsAssignableFrom(entityType.ClrType)) {
//entityType是IMutableEntityType类型
entityType.AddSoftDeleteQueryFilter();
}
}
下面是扩展类,可以动态创建正确的 LINQ 表达式来配置查询过滤器
csharp
public static class SoftDeleteQueryExtension {
//软删除过滤器,下面这个方法里面没有实体类,通用方法
public static void AddSoftDeleteQueryFilter(this IMutableEntityType entityData) {
var methodToCall = typeof(SoftDeleteQueryExtension)
.GetMethod(nameof(GetSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
.MakeGenericMethod(entityData.ClrType);
var filter = methodToCall.Invoke(null, new object[] { });
entityData.SetQueryFilter((LambdaExpression)filter);
}
//软删除,这个实体类要实现BaseEntity,即含有Deleted属性
//注意:这个不是zack中的BaseEntity
private static LambdaExpression GetSoftDeleteFilter<TEntity>() where TEntity : BaseEntity {
Expression<Func<TEntity, bool>> filter = x => !x.Deleted;
return filter;
}
}
方法三:使用zack的框架
在基础设施层的DbContext的OnModelCreating,启用全局软删除过滤器
arduino
protected override void OnModelCreating(ModelBuilder modelBuilder) {
...
//启用软删除的全局查询过滤器,Zack.Infrastructure中定义的
modelBuilder.EnableSoftDeletionGlobalFilter();
}
其中,EnableSoftDeletionGlobalFilter
是Zack.Infrastructure中定义的扩展方法
csharp
public static void EnableSoftDeletionGlobalFilter(this ModelBuilder modelBuilder) {
//遍历每个实体,找到实现ISoftDelete接口的实体
foreach (IMutableEntityType item in from e in modelBuilder.Model.GetEntityTypes()
where e.ClrType.IsAssignableTo(typeof(ISoftDelete))
select e) {
//对于每个实体,找到IsDeleted属性
IMutableProperty mutableProperty = item.FindProperty("IsDeleted");
//根据IsDeleted属性得到一个LambdaExpression
ParameterExpression parameterExpression = Expression.Parameter(item.ClrType, "p");
LambdaExpression queryFilter = Expression.Lambda
(Expression.Not(Expression.Property(parameterExpression, mutableProperty.PropertyInfo)), parameterExpression);
//SetQueryFilter是EFCore定义的filter,查询时会自动应用此filter,得到过滤后的结果
item.SetQueryFilter(queryFilter);
}
}
3、在控制器的删除方法中,先找到实体,再调用softDelete方法
ini
var album = await repository.GetAlbumByIdAsync(id);
...
album.SoftDelete();//软删除
获取、恢复软删除的数据
ini
//获取时,要IgnoreQueryFilters
var books = ctx.Books.IgnoreQueryFilters()
.Where(x => x.IsDeleted).ToList();
//恢复时,也要IgnoreQueryFilters
var book = ctx.Books.IgnoreQueryFilters()
.Single(x => x.BookId == id);
book.IsDeleted = true;
ctx.SaveChanges();
分页获取数据
原理:利用IQueryable实现分页获取数据。
IQueryable具有延迟特性,所以这个不是先从数据库中取出所有数据再分页,而是直接在sql语句的层面从数据库取部分数据,性能会更高
pageIndex从1开始,pageSize页容量
示例
ini
static void PrintPage(int pageIndex, int pageSize) {
MyDbContext ctx = new MyDbContext();
IQueryable<Article> arts = ctx.Articles.Where(a => !a.Title.Contains("微软"));
//实现分页,最好显示指定排序规则
var items=arts.Skip((pageIndex-1)* pageSize).Take(pageSize);
foreach (var item in items) {
Console.WriteLine(item.Title);
}
//总条数
long count=arts.LongCount();
//总页数
long pageCount=(long)Math.Ceiling(count*1.0/pageSize);
}
完整实现
仓储服务
仓储接口
arduino
public Task<(long count, long pageCount, Equip[])> GetEquipsByPageAsync(int pageIndex, int pageSize,bool orderAsc,string orderField);
仓储服务的实现
排序时可e=>orderField手动设定排序字段为某个字符串变量,可以不通过e=>e.id写法
ini
public async Task<(long count, long pageCount, Equip[])> GetEquipsByPageAsync(int pageIndex, int pageSize, bool orderAsc, string orderField) {
IQueryable<Equip> equips = ctx.Equips.Where(e => 1 == 1);
if(orderAsc) {
//这个东西返回值是Tkey,猜测直接传入字符串也可
equips=equips.OrderBy(e => orderField);
}
else {
equips = equips.OrderByDescending(e => orderField);
}
//总条数
long count = equips.LongCount();
//总页数
long pageCount = (long)Math.Ceiling(count * 1.0 / pageSize);
//数据
var result = await equips.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToArrayAsync();
return (count, pageCount, result);
}
控制器
注意,不能直接返回元组,要手动转换为对象,否则无法序列化为json
csharp
[Authorize]
[HttpGet]
public async Task<EquipResopnse> FindAllByPage(int pageIndex, int pageSize) {
long count=1, pageCount=1;
Equip[] equips;
//...
( count,pageCount,equips)= await repository.GetEquipsByPageAsync(pageIndex, pageSize,true,"id");
return new EquipResopnse(count, pageCount, equips);
}
public record EquipResopnse(long count, long pageCount, Equip[] equips);