EntityFrameworkCore数据迁移(一)
.net core出来已经有很长一段时间了,而EentityFrameworkCore(后面简称EFCore)是.net framework的EntityFramework在.net core中的实现,至于EntityFramework是什么,这里就不介绍了。
本文主要介绍EFCore的CodeFirst方式下的数据迁移。
一、创建项目
首先创建项目结构如下:
说明:
EFCoreDemo.EntityFrameworkCore:这个是一个标准类库,主要一些EFCore的一些ORM实体与配置。
EFCoreDemo.ConsoleApp:这个是一个控制台项目,主要用于使用EFCore的使用,因为一般都是使用WebApi或者MVC,然后后使用三层架构,或者一些其他的架构诸如ABP等,使用仓储等模式使用EFCore,但这里为了简单,直接使用一个控制台程序模拟。
EFCoreDemo.EntityFrameworkCore.Host:这个也是一个控制台程序,主要用于EFCore的数据迁移。
于是乎,我们的项目引用关系是:
EFCoreDemo.ConsoleApp 引用 EFCoreDemo.EntityFrameworkCore
EFCoreDemo.EntityFrameworkCore.Host 引用 EFCoreDemo.EntityFrameworkCore
二、创建实体及上下文
对EFCoreDemo.EntityFrameworkCore项目使用nuget安装EFCore所需的包:
# 如果使用MySql,安装
Microsoft.EntityFrameworkCore Pomelo.EntityFrameworkCore.MySql
# 如果使用SqlServer,安装
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
这里使用的是MySql
现在假设我们要创建3张表,用户表(Account),活动表(Activity),活动记录表(ActivityRecord)
于是我们分别创建3个实体与它对应
/// <summary> /// 用户表 /// </summary> public class Account : BaseEntity { /// <summary> /// 姓名 /// </summary> public string Name { get; set; } /// <summary> /// 手机号码 /// </summary> public string Phone { get; set; } /// <summary> /// 年龄 /// </summary> public int Age { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreationTime { get; set; } }
/// <summary> /// 活动表 /// </summary> public class Activity : BaseEntity { /// <summary> /// 活动名称 /// </summary> public string Name { get; set; } /// <summary> /// 开始时间 /// </summary> public DateTime StartTime { get; set; } /// <summary> /// 结束时间 /// </summary> public DateTime EndTime { get; set; } /// <summary> /// 活动状态 /// </summary> public int Status { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreationTime { get; set; } }
/// <summary> /// 活动记录表 /// </summary> public class ActivityRecord : BaseEntity { /// <summary> /// 活动Id /// </summary> public int ActivityId { get; set; } /// <summary> /// 用户Id /// </summary> public int AccountId { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreationTime { get; set; } }
其中BaseEntity是一个抽象类,主要用来记录主键Id的,推荐每个表都有单列主键,尽量不要使用复合主键,如果有其他唯一标识,可以使用唯一值索引
public abstract class BaseEntity { /// <summary> /// Primary Key /// </summary> public int Id { get; set; } }
这里设置主键类型是int,当然也可以使用其他数据类型,只需将BaseEntity写成泛型类就可以了,如:
public abstract class BaseEntity<T> { /// <summary> /// Primary Key /// </summary> public T Id { get; set; } }
接下来,创建实体映射配置类,同样的,这里也创建一个基类,基类主要做一些通用的配置:
public abstract class BaseEntityTypeConfiguration<TEntity> : IEntityTypeConfiguration<TEntity> where TEntity : class { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public virtual void Configure(EntityTypeBuilder<TEntity> builder) { //映射表名 builder.ToTable("demo_" + typeof(TEntity).Name.ToLower()); //映射主键 if (typeof(BaseEntity).IsAssignableFrom(typeof(TEntity))) { builder.HasKey(nameof(BaseEntity.Id)); builder.Property(nameof(BaseEntity.Id)).ValueGeneratedOnAdd();//自增 } //种子数据 var seeds = this.GetSeeds(); if (seeds != null && seeds.Length > 0) { builder.HasData(seeds); } } /// <summary> /// 种子数据 /// </summary> /// <returns></returns> public virtual TEntity[] GetSeeds() { return new TEntity[0]; } }
然后分别对用户表(Account),活动表(Activity),活动记录表(ActivityRecord)三个实体类创建映射配置类
public class AccountEntityTypeConfiguration : BaseEntityTypeConfiguration<Account> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<Account> builder) { base.Configure(builder); } /// <summary> /// 种子数据 /// </summary> /// <returns></returns> public override Account[] GetSeeds() { return new Account[] { new Account(){ Id=1, Name="admin", Phone="110", Age=100, CreationTime=new DateTime(2020,02,02)//注意,种子数据不要使用DateTime.Now之类的,避免每次都会迁移数据 } }; } }
public class ActivityEntityTypeConfiguration : BaseEntityTypeConfiguration<Activity> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<Activity> builder) { base.Configure(builder); } }
public class ActivityRecordEntityTypeConfiguration : BaseEntityTypeConfiguration<ActivityRecord> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<ActivityRecord> builder) { base.Configure(builder); } }
到这里,有些朋友可能会觉着这样做很麻烦,倒不如直接使用特性标识实体类来的简单,但笔者认为,使用特性标识确实简单,但它的局限性太大:
1、使用特性让实体看起来不干净,而且当描述实体关系时不是很方便
2、特性功能有限,很多复杂的关系实现不了,而初始化数据也不方便实现
3、当特性不能满足我们的需求是,我们可能需要将配置移到其他地方去,如DbContext的OnModelCreating方法,这样就造成了配置的分散
总之,当使用CodeFirst方式开发时,推荐使用单独的映射配置类去实现,尽可能不要使用特性
接着就可以创建DbContext上下文了:
public class DemoDbContext : DbContext { public DemoDbContext(DbContextOptions options) : base(options) { } #region Method /// <summary> /// 配置 /// </summary> /// <param name="optionsBuilder"></param> protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.EnableSensitiveDataLogging(); base.OnConfiguring(optionsBuilder); } /// <summary> /// 初始化 /// </summary> /// <param name="modelBuilder"></param> protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyFromAssembly(typeof(DemoDbContext)); } #endregion }
这里的DbContext没有创建那些DbSet属性,不表示不能创建,只是为了说明EFCore的迁移不依赖那些而已。
其中modelBuilder.ApplyFromAssembly是一个拓展方法:
public static class EntityFrameworkCoreExtensions { /// <summary> /// 从程序集加载配置 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="modelBuilder"></param> /// <returns></returns> public static ModelBuilder ApplyFromAssembly<T>(this ModelBuilder modelBuilder) => modelBuilder.ApplyFromAssembly(typeof(T)); /// <summary> /// 从程序集加载配置 /// </summary> /// <param name="modelBuilder"></param> /// <param name="type"></param> /// <returns></returns> public static ModelBuilder ApplyFromAssembly(this ModelBuilder modelBuilder, Type type) => modelBuilder.ApplyFromAssembly(type.Assembly); /// <summary> /// 从程序集加载配置 /// </summary> /// <param name="modelBuilder"></param> /// <param name="assembly"></param> /// <returns></returns> public static ModelBuilder ApplyFromAssembly(this ModelBuilder modelBuilder, Assembly assembly) { return modelBuilder.ApplyConfigurationsFromAssembly(assembly, t => !t.IsAbstract); } }
到这里,EFCoreDemo.EntityFrameworkCore项目就基本上完成了,项目结构如下:
三、开始迁移
注意,这里说明一下,其实数据迁移也是可以在EFCoreDemo.EntityFrameworkCore中完成的,但是数据迁移不是我们业务上下文的的一部分,他只是我们开发过程中用到的,所以没有必要将它们放在一起,最好将它独立出来,比如这里,将它独立到一个单独的控制台程序中,这个就是EFCoreDemo.EntityFrameworkCore.Host项目:
首先,对EFCoreDemo.EntityFrameworkCore.Host使用nuget安装:
Microsoft.EntityFrameworkCore.Design Microsoft.EntityFrameworkCore.Tools
这两个包是迁移所需的工具包。
创建一个迁移上下文:
public class MigrationDbContext : DemoDbContext { public MigrationDbContext(DbContextOptions options) : base(options) { } }
可以看到,这个迁移数据上下文其实是继承了DemoDbContext类,然后其他的什么都没做,那为什么不直接使用DemoDbContext?
当然,使用DemoDbContext也是可以的,甚至可以将MigrationDbContext继承DbContext,然后将DemoDbContext中的内容复制一份到MigrationDbContext也是可以的。
而这里单独创建一个MigrationDbContext主要是为了迁移的方便,因为EFCore生成迁移时默认是从启动项目中去查找DbContext类的,如果DbContext类在其他类库中,那么就需要自己指定-Context参数。
其次,单独的MigrationDbContext上下文,也能保证生成的迁移文件在当前项目,而不会跑到其他项目中去!
接着创建一个迁移时使用的上下文工厂类,这个类需要实现IDesignTimeDbContextFactory<DbContext>接口:
public class DemoMigrationsDbContextFactory : IDesignTimeDbContextFactory<MigrationDbContext> { public MigrationDbContext CreateDbContext(string[] args) {var builder = new DbContextOptionsBuilder<MigrationDbContext>() .UseMySql("Server=192.168.209.128;Port=3306;Database=demodb;Uid=root;Pwd=123456"); return new MigrationDbContext(builder.Options); } }
到这里,EFCoreDemo.EntityFrameworkCore.Host项目就完成了,项目结构如下:
到这里,就可以生成迁移文件了。
通过【导航栏=》工具=》NuGet包管理器=》程序包管理器控制台】打开控制台:
然后输入 Add-Migration init_20200727
这里使用的 Add-Migration 命令生成迁移文件,其中 init_20200727 是迁移名称。
Add-Migration命令常用参数如下:
-o|--output-dir <PATH> 存放迁移文件的相对路径,默认是Migrations -c|--context <DBCONTEXT> 迁移使用的上下文 -a|--assembly <PATH> 迁移使用的程序集 -s|--startup-assembly <PATH> 迁移时的启动程序集 --project-dir <PATH> 项目路径 --language <LANGUAGE> 语言,默认是C#
注意,要将 EFCoreDemo.EntityFrameworkCore.Host 设置成启动项目,然后将控制台的默认项目也设置成 EFCoreDemo.EntityFrameworkCore.Host ,否则生成迁移文件的是否会报错
Your startup project 'EFCoreDemo.EntityFrameworkCore' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project is correct, install the package, and try again.
More than one DbContext was found. Specify which one to use. Use the '-Context' parameter for PowerShell commands and the '--context' parameter for dotnet commands.
Add-Migration 命令执行成功后,会生成一个Migrations目录,目录下会有三个文件:
20200727075220_init_20200727.cs:这个是我们的迁移文件,其中两个方法,Up方法时往数据库迁移数据时执行,Down方法时撤销迁移时执行
20200727075220_init_20200727.Designer.cs:这个设计文件,记录的是当前迁移之后实体映射的一个快照
MigrationDbContextModelSnapshot.cs:这个是当前实体映射的快照
注意,上面的文件名中,20200727075220是时间戳(UTC时间),后面的init_20200727是迁移名称,EFCore的迁移执行顺序是按照时间戳的顺序执行的。
如果需要修改迁移的执行顺序,可以自行改变这个时间戳,但是不是改变文件名,而是对应的Designer.cs文件中的Migration特性标识的那个时间戳!
生成迁移文件之后,如果要撤销此次的迁移文件,可以使用 Remove-Migration 命令
接着我们可以使用 Update-Database 命令开始执行迁移:
注意:Update-Database命令会执行所有的数据迁移,可以指定一个迁移名称去执行单个数据迁移
执行完Update-Database后,可以看看数据库:
其中除了我们业务需要的表之外,还有一张名称为__EFMigrationsHistory的表,这个表其实就是EFCore的迁移记录表
注:这里说的是使用Nuget命令来生成迁移和执行迁移,但是如果要上线,总不能使用nuget连接线上数据库操作吧?这里有两种解决办法:
1、在nuget中使用Script-Migration命令将迁移生成脚本,然后到线上去执行
2、使用程序去执行迁移,比如控制台程序,或者在项目启动时执行,具体可参考下一篇:EntityFrameworkCore数据迁移(二)
四、再次迁移
上面的迁移其实是生成了数据库和其中的一些表,但是我们的需求是不断变化的。
前面我们虽然创建了表,但是里面字段约束等等基本上都放过了,比如字段长度,外键约束等等,现在我们补上:
1、Account中的姓名和手机号码长度不超过100,而且姓名为必须(非空);
2、Activity中名称长度也不超过100,且必须(非空);
3、Activity中增加备注列,长度不超过1000
4、ActivityRecord对ActivityId和AccountId增加外键约束
首先,Activity增加列,只需要在实体中添加对应的属性就可以了:
/// <summary> /// 活动表 /// </summary> public class Activity : BaseEntity { /// <summary> /// 活动名称 /// </summary> public string Name { get; set; } /// <summary> /// 开始时间 /// </summary> public DateTime StartTime { get; set; } /// <summary> /// 结束时间 /// </summary> public DateTime EndTime { get; set; } /// <summary> /// 活动状态 /// </summary> public int Status { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreationTime { get; set; } /// <summary> /// 备注 /// </summary> public string Remark { get; set; } }
ActivityRecord要增加外键约束,首先在ActivityRecord中新增Activity和Account的类型引用:
/// <summary> /// 活动记录表 /// </summary> public class ActivityRecord : BaseEntity { /// <summary> /// 活动Id /// </summary> public int ActivityId { get; set; } /// <summary> /// 用户Id /// </summary> public int AccountId { get; set; } /// <summary> /// 创建时间 /// </summary> public DateTime CreationTime { get; set; } public Activity Activity { get; set; } public Account Account { get; set; } }
其他的,为实现这几项约束,我不使用特性,只需要在上面的实体映射配置中用代码实现就可以了:
public class AccountEntityTypeConfiguration : BaseEntityTypeConfiguration<Account> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<Account> builder) { base.Configure(builder); builder.Property(p => p.Name).HasMaxLength(100).IsRequired(true); builder.Property(p => p.Phone).HasMaxLength(100); } /// <summary> /// 种子数据 /// </summary> /// <returns></returns> public override Account[] GetSeeds() { return new Account[] { new Account(){ Id=1, Name="admin", Phone="110", Age=100, CreationTime=new DateTime(2020,02,02)//注意,种子数据不要使用DateTime.Now之类的,避免每次都会迁移数据 } }; } }
public class ActivityEntityTypeConfiguration : BaseEntityTypeConfiguration<Activity> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<Activity> builder) { base.Configure(builder); builder.Property(p => p.Name).HasMaxLength(100).IsRequired(true); } }
public class ActivityRecordEntityTypeConfiguration : BaseEntityTypeConfiguration<ActivityRecord> { /// <summary> /// 配置实体类型 /// </summary> /// <param name="builder"></param> public override void Configure(EntityTypeBuilder<ActivityRecord> builder) { base.Configure(builder); builder.HasOne(p => p.Activity).WithMany().HasForeignKey(p => p.ActivityId); builder.HasOne(p => p.Account).WithMany().HasForeignKey(p => p.AccountId); } }
然后,接着在程序包管理器控制台输入命令生成迁移文件:
注意设置项目启动项和默认程序
执行成功后,在Migrations目录下又会多两个迁移文件。
另外,说明一下,上面的迁移执行抛出了警告,这个是因为我的迁移中修改了数据列的长度,可能会导致数据丢失所致。
接着执行Update-Database命令执行迁移,将其更新至数据库:
最后,忘了说明了,前面说了,如果想撤销迁移,可以使用Remove-Migration命令,但是如果已经执行Update-Database将迁移更新至数据库后,Remove-Migration命令将会抛出异常:
The migration '20200727094748_alter_20200727' has already been applied to the database. Revert it and try again. If the migration has been applied to other databases, consider reverting its changes using a new migration.
此时只需要先执行 Update-Database -Migration <migration> ,这个命令后面携带的参数是迁移名称(可以去数据库中__EFMigrationsHistory表查看),执行这个命令表示将数据库迁移执行到指定名称的迁移处,后续全部的迁移都会被撤销!
比如,如果我们想撤销20200727094748_alter_20200727这个迁移,可以先执行 Update-Database -Migration 20200727092850_init_20200727 ,注意,这里的迁移名称是要撤销的迁移的上一个!也就是20200727092850_init_20200727!
执行完上面命令后再执行 Remove-Migration 就可以了
五、应用使用
实体模型创建好了,数据迁移也完成了,可以到应用了吧。
上面可以看到,我们将实体模型与数据迁移分成了两个项目,这样EFCoreDemo.ConsoleApp只需要引用EFCoreDemo.EntityFrameworkCore去使用实体模型就够了,因为它跟数据迁移完全没关系!
修改Program:
class Program { static void Main(string[] args) { var builder = new DbContextOptionsBuilder<DemoDbContext>() .UseMySql("Server=192.168.209.128;Port=3306;Database=demodb;Uid=root;Pwd=123456"); using (var db = new DemoDbContext(builder.Options)) { //查询管理员信息 var admin = db.Set<Account>().Find(1); //新建活动 var activity = new Activity() { Name = "活动1", StartTime = DateTime.Now, EndTime = DateTime.Now.AddMonths(1), Status = 1, Remark = "备注", CreationTime = DateTime.Now }; db.Set<Activity>().Add(activity); //新增活动记录 var record = new ActivityRecord() { Account = admin, Activity = activity, CreationTime = DateTime.Now }; db.Set<ActivityRecord>().Add(record); db.SaveChanges(); } using (var db = new DemoDbContext(builder.Options)) { var records = db.Set<ActivityRecord>().Include(f => f.Activity).Include(f => f.Account).ToArray(); foreach (var record in records) { Console.WriteLine($"{record.Account.Name}参加了{record.Activity.Name}"); } } Console.ReadKey(); } }
执行后输出: