EF Core在项目中的使用

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中,timestamprowversion是同一种类型的不同别名而已
  • 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);
相关推荐
哎呦没22 分钟前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch41 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码2 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries3 小时前
读《show your work》的一点感悟
后端
A尘埃3 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23073 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code3 小时前
(Django)初步使用
后端·python·django
代码之光_19803 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端
编程老船长3 小时前
第26章 Java操作Mongodb实现数据持久化
数据库·后端·mongodb
IT果果日记4 小时前
DataX+Crontab实现多任务顺序定时同步
后端