自适应企业建站企业,成都网站建设推,搭建商城哪家好怎么样,做淘宝客导购网站#xff08;Migrations#xff09;是个啥玩意#xff1f;IT 界从来不缺造词人才#xff0c;总喜欢造各种各样的词。之所以叫迁移#xff0c;大概是因为使用它可以创建并在后期修订数据库。总之#xff0c;说人话就是迁移可以生成一系列的 .NET 类#xff0c;每个类代表一…Migrations是个啥玩意IT 界从来不缺造词人才总喜欢造各种各样的词。之所以叫迁移大概是因为使用它可以创建并在后期修订数据库。总之说人话就是迁移可以生成一系列的 .NET 类每个类代表一个修订版本。开发者可以在多个版本之间“进”或“退”——可以修改数据库之后可以撤销前一次修改。注意这里说的修改 / 修订不是指数据而是数据库的基础结构比如某个表后面由于某些原因要添加一列或要删除一列。大伙伴都知道调用 dbContext.Database.EnsureCreated 方法可以根据配置的 Model 创建数据库它与迁移最大的区别就是EnsureCreated 方法创建的数据库在后期是不能修改的可以手动执行 SQL 语句来修改。而迁移在创建数据库时它会顺便把当前迁移的版本信息保存到数据库实体类 HistoryRow 类包含两个属性MigrationId 表示迁移IDProductVersion 表示 EF Core 版本这样可以通过版本对比来确定版本的前进和回退也可依此判定哪些迁移已应用到数据库哪些还没同步到数据库。为了能友好地分辨出迁移版本在生成迁移代码时开发者可以自定义一个命名。其格式是“当前日期_自定义名称”。例如“20251213102915_abc”即开发者实际指定的命名是“abc”前缀的时间和下画线是 EF Core 设计时服务自动加的。生成此名称是由 IMigrationsIdGenerator 服务接口负责的默认的实现类是 MigrationsIdGenerator。咱们不妨看看 GenerateId 方法的源码复制代码public virtual string GenerateId(string name){var now DateTime.UtcNow;var timestamp new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);……// 这里拼接新名称return timestamp.ToString(Format, CultureInfo.InvariantCulture) _ name;}复制代码很简单粗暴吧就是获取当前时间精确到秒足够了你该不会每秒生成一次这么无聊吧然后加上“_”字符再加上你给的名字。有大伙伴会问我要是不喜欢这种命名方式我自己写个类实现 IMigrationsIdGenerator 接口注册到服务容器中替换到框架的默认实现那是不是可以实现以自己喜欢的方式生成迁移命名呢答案是肯定的。迁移是由一系列 Operation 组成用 MigrationOperation 类表示。依据修改数据库的各种骚操作派生出相应的类。如 AlterTableOperation 表它代表 SQL 语句ALTER TABLE ...再比如 AddColumnOperation 类它代表 ALTER TABLE 表 ADD 新列 语句SqlServerCreateDatabaseOperation 类代表 CREATE DATABASE 数据库名 语句等等。这些类都能在 Microsoft.EntityFrameworkCore.Migrations.Operations 命名空间下找到。在迁移的时候会根据这些 Operation 生成关联的 SQL 语句。例如下面源码是生成添加新列的 SQL。复制代码protected virtual void Generate(AddColumnOperation operation,IModel? model,MigrationCommandListBuilder builder,bool terminate true){if (operation[RelationalAnnotationNames.ColumnOrder] ! null){Dependencies.MigrationsLogger.ColumnOrderIgnoredWarning(operation);}builder.Append(ALTER TABLE ).Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(operation.Table, operation.Schema)).Append( ADD );ColumnDefinition(operation, model, builder);if (terminate){builder.AppendLine(Dependencies.SqlGenerationHelper.StatementTerminator);EndStatement(builder);}}protected virtual void ColumnDefinition(string? schema,string table,string name,ColumnOperation operation,IModel? model,MigrationCommandListBuilder builder){if (operation.ComputedColumnSql ! null){ComputedColumnDefinition(schema, table, name, operation, model, builder);return;}var columnType operation.ColumnType ?? GetColumnType(schema, table, name, operation, model);builder.Append(Dependencies.SqlGenerationHelper.DelimitIdentifier(name)).Append( ).Append(columnType);if (operation.Collation ! null){builder.Append( COLLATE ).Append(operation.Collation);}builder.Append(operation.IsNullable ? NULL : NOT NULL);DefaultValue(operation.DefaultValue, operation.DefaultValueSql, columnType, builder);}复制代码迁移代码的基类是 Migration它是抽象类。派生类通常要实现以下成员1、向上版本即新版本被应用到数据库时要修改的内容。protected abstract void Up(MigrationBuilder migrationBuilder);2、向下版本即回退功能当此版本的迁移被取消时要撤销的内容。protected virtual void Down(MigrationBuilder migrationBuilder);什么是向上向下呢举个例子假如你一开始的数据库中A表只有三个列。后来由于客户兽性大发要改需求随后你给A表添加了一个新列 F。于是生成了迁移。此时该迁移的向上操作升级就是给 A 表 add column但是过了一个星期后客户逐渐恢复人性又要求你改回去。这时候你要回到上一个迁移最新一个迁移所做的修改要撤回这就是向下操作降级。这时向下操作要把 F 列删除。说一句话就是UP 加 F 列DOWN 删 F 列。3、构建数据库模型。protected virtual void BuildTargetModel(ModelBuilder modelBuilder);这个其实和 DbContext.OnModelCreating 方法中实现数据库模型配置的逻辑相同。区别是迁移这里的 ModelBuilder 实例是没有添加预置约定集合。也就是说它不能自动帮你识别实体的属性不能自动识别导航属性不能自动分析主键。你必须一五一十老老实实地完成所有配置。实际上这里的模型配置既麻烦且重复的所以才用工具生成而不是你动手去写。毕竟写重复代码意义不大之所以要在这里重新配置一遍是出于优化和效率。这里的 ModelBuilder 没有约定类没有 ModelCustomer 等服务精简了许多但信息量又比运行时模型完整些。为了让代码生成器了解这个从 Migration 派生的类就是一个迁移还要在这个类上应用 [Migration] 特性MigrationAttribute 类并设置 Id 属性即指定迁移的 ID。前文老周已经介绍了ID的格式是 当前时间_名称。就算你闲着没事想自己手动写一个迁移类你的 Id 也必须遵守这个格式否则代码生成器是找不到迁移的因为它在查找迁移时同样用到了 IMigrationsIdGenerator 服务来获取迁移名称。所以你不按这个格式套的话正则表达式无法匹配就找不到了。最后还要在迁移类上加上 [DbContext] 特性表明这个迁移是与哪个 dbContext 关联的。除了迁移类一般会伴随一个快照类——该类从 ModelSnapshot 派生。这个类里又要配置一次数据库模型。protected abstract void BuildModel(ModelBuilder modelBuilder);你说如果手动写代码是不是很烦老是重复配置模型。和 Migration 类一样快照类的 ModelBuilder 也是空的没有添加预置约定类所以它不能自动完成某些通用配置的也是手动档的。这个快照类等于你给 dbContext 的最新模型拍个照生成迁移时候用于对比新旧版本产生了哪些变化。同理快照类上也要用 [DbContext] 特性标记它与哪个 dbContext 类相关联。和上一篇介绍从数据库生成模型一样根据实体 Model 生成更新数据库的迁移代码也要依赖一些服务。接下来老周简单介绍一下这些服务大伙伴们了解一下就好了因为我们一般不会直接调用它们不一般的情况极少见。1、当前 DbContext 实例如何获取。用 ICurrentDbContext 服务咱们现在是直接编程了不是用 dotnet-ef 工具所以不要反射那么麻烦了直接实例化你用的 dbContext然后把它的服务搬到设计时服务集合中就行了。这个你不要紧张不用咱们写代码EF Core 已经封装好了稍后介绍。2、当前上下文的数据库模型IModel这个也是直接从 DbContext 实例搬过来就行同样EF Core 已经封装好了你不用写一行代码。3、IMigrationsModelDiffer这是今天四大天皇巨星之一。它用于分析两个数据库模型之间的差异并创建一个用于更新数据库的 MigrationOperation 列表。4、IMigrationsIdGenerator四大天皇巨星之二它是核心服务用于生成迁移代码。默认实现类是 MigrationsCodeGenerator它是抽象类不同编程语言可以继承并实现生成的代码。目前内置的只有 CSharpMigrationsGenerator 类所以只能生成 C# 代码。5、IMigrationsCodeGeneratorSelector四大天皇巨星之三用于选择使用哪个 MigrationsCodeGenerator目前只不过是根据“C#”选择 CSharpMigrationsGenerator以后可能有其他扩展。6、IHistoryRepository四大天皇巨星之四它用来访问创、删、读数据库中的迁移版本历史表。这个和生成代码关系不大但它和删除迁移代码有关用于获取已经应用到数据库的迁移版本。以上服务仅作了解大伙伴们忘了也无所谓但下面这个重量级巨星大伙得记住它。它就是 IMigrationsScaffolder 服务默认实现类是 MigrationsScaffolder。它定义了以下方法1、生成代码不保存。ScaffoldedMigration ScaffoldMigration(string migrationName,string? rootNamespace,string? subNamespace null,string? language null,bool dryRun false);这是核心功能生成迁移代码。只是生成了代码未保存到文件。migrationName 参数指定迁移名称rootNamespace 指定项目的根命名空间可以为空。subNamespace 参数是子命名空间即迁移类所在的命名空间。language 参数是编程语言反正都是“C#”目前这个参数可以忽略。dryRun 是啥“干运行”不用管它在这个方法中它没有用上。2、保存生成的代码到文件。MigrationFiles Save(string projectDir,ScaffoldedMigration migration,string? outputDir,bool dryRun false);projectDir参数是项目所在目录。migration 参数传的是上面 ScaffoldMigration 方法返回的对象。outputDir 参数是输出目录它相对于 projectDir 指定的目录。dryRun你看干运行又来了。这次它起作用了如果为 true则不会真正保存文件。默认为 false会保存文件。这个你可以看看源代码。复制代码if (!dryRun){Directory.CreateDirectory(migrationDirectory);File.WriteAllText(migrationFile, migration.MigrationCode, Encoding.UTF8);File.WriteAllText(migrationMetadataFile, migration.MetadataCode, Encoding.UTF8);Dependencies.OperationReporter.WriteVerbose(DesignStrings.WritingSnapshot(modelSnapshotFile));Directory.CreateDirectory(modelSnapshotDirectory);File.WriteAllText(modelSnapshotFile, migration.SnapshotCode, Encoding.UTF8);}复制代码总共产生三个文件迁移类迁移类元数据其实和迁移类是同一个类声明为部分类以及快照类。好了到了这里相信悟性惊人的大伙伴可能知道怎么用了。咱们来演示一下。先弄些实体和上下文。复制代码/// summary/// 动物/// /summarypublic class Animal{public Guid AnId { get; set; }/// summary/// 昵称/// /summarypublic string Nick { get; set; } default!;/// summary/// 年龄/// /summarypublic int Age { get; set; }/// summary/// 详细信息/// /summarypublic AnimalDetail? Details { get; set; }}/// summary/// 动物详细信息/// /summarypublic class AnimalDetail{public int AniID { get; set; }/// summary/// 门/// /summarypublic string? Phylum { get; set; }/// summary/// 纲/// /summarypublic string? Class { get; set; }/// summary/// 目/// /summarypublic string? Order { get; set; } default!;/// summary/// 科/// /summarypublic string Family { get; set; } default!;/// summary/// 属/// /summarypublic string Genus { get; set; } default!;}public class DemoDbContext : DbContext{// 数据集合public DbSetAnimal Animals { get; set; }protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder){optionsBuilder.UseSqlServer(server(localdb)\mssqllocaldb;databaseAnimalDB);}protected override void OnModelCreating(ModelBuilder modelBuilder){// 详细类var entAnmDetail modelBuilder.EntityAnimalDetail();// 属性entAnmDetail.Property(b b.AniID).HasColumnName(detail_id);entAnmDetail.Property(b b.Phylum).HasColumnName(phylum).HasMaxLength(20);entAnmDetail.Property(f f.Class).HasColumnName(class).HasMaxLength(32);entAnmDetail.Property(d d.Order).HasColumnName(order).HasMaxLength(32);entAnmDetail.Property(g g.Family).HasColumnName(family).HasMaxLength(32);entAnmDetail.Property(m m.Genus).HasColumnName(genus).HasMaxLength(32);// 影子属性作为外键entAnmDetail.PropertyGuid(Animal_id).HasColumnName(animal_id);// 主键entAnmDetail.HasKey(k k.AniID).HasName(PK_Animal_details);// 动物类var entAnimal modelBuilder.EntityAnimal();entAnimal.Property(a a.AnId).HasColumnName(anl_id);entAnimal.Property(a a.Nick).HasColumnName(nick).HasMaxLength(15);entAnimal.Property(d d.Age).HasColumnName(age);// 主键entAnimal.HasKey(x x.AnId).HasName(PK_Animal);// 一对一详细表引用动物表entAnimal.HasOne(p p.Details).WithOne()// 定义外键.HasForeignKeyAnimalDetail(Animal_id).HasConstraintName(FK_Animal)// AnimalDetail - Animal.HasPrincipalKeyAnimal(n n.AnId);}}复制代码迁移功能是给开发者用的程序正常启动不应该去生成迁移代码所以和上一篇水文一样咱们用条件编译。复制代码#define GEN_MIGRATION……static void Main(string[] args){#if GEN_MIGRATIONMakeMigration(test666);return;#endif// 应用程序主代码……}复制代码下面咱们完成 MakeMigration 方法实现迁移类和快照类的生成。复制代码#if GEN_MIGRATIONstatic void MakeMigration(string migName){// 项目目录const string ProjectDir ..\..\..\;// 输出目录//const string OutputDir MyMigrations;// 迁移命名空间const string migNs DBUpdates;// 项目根命名空间const string rootNs nameof(MigrationGenApp);// 正常操作using var context new DemoDbContext();// 服务容器var servicesCollection new ServiceCollection();// 添加设计时基础服务servicesCollection.AddEntityFrameworkDesignTimeServices();// 把dbcontext的服务也添加进去servicesCollection.AddDbContextDesignTimeServices(context);// 添加数据库相关的设计时服务string providerAssemblyName context.Database.ProviderName!;// 找到设计时服务类Assembly providerAss Assembly.Load(providerAssemblyName);var designtimeSvcAttr providerAss.GetCustomAttributeDesignTimeProviderServicesAttribute();Type svcType providerAss.GetType(designtimeSvcAttr!.TypeName)!;// 动态实例化IDesignTimeServices designtimeSvc (IDesignTimeServices)Activator.CreateInstance(svcType)!;// 配置服务designtimeSvc.ConfigureDesignTimeServices(servicesCollection);// 构建服务var services servicesCollection.BuildServiceProvider();// 获取迁移服务IMigrationsScaffolder migscaff services.GetRequiredServiceIMigrationsScaffolder();// 直接开干var migres migscaff.ScaffoldMigration(migrationName: migName, // 名称rootNamespace: rootNs, // 根命名空间subNamespace: migNs // 迁移类的命名空间);// 保存var saveres migscaff.Save(ProjectDir, migres, null/*, OutputDir*/);Console.WriteLine($快照文件{saveres.SnapshotFile});Console.WriteLine($元数据文件{saveres.MetadataFile});Console.WriteLine($迁移类文件{saveres.MigrationFile});}#endif复制代码上一篇水文中咱们是没有 DbContext 的而是根据现有数据库来生成的。但这次不同咱们有 DbContext 的所以操作上有一点点不同。1、正常方式 new 一个数据库上下文本示例中是 DemoDbContext 类。2、实例化服务容器集合这个和上次一样。3、调用 AddEntityFrameworkDesignTimeServices 扩展方法添加设计时相关的基础服务这个和上次一样。4、注意这次咱们多了这一步调用 AddDbContextDesignTimeServices 扩展方法把 DbContext 实例的服务也添加到服务容器中。这是因为咱们生成迁移代码需要用到数据库上下文以及它里面的某些服务。5、这一步和上次一样通过数据库提供者库中应用到程序集的 [DesignTimeProviderServices] 特性得到设计时服务类的类型。创建实例调用 ConfigureDesignTimeServices 方法完成配置。6、BuildServiceProvider 方法调用后产生完整的服务容器。7、获取 IMigrationsScaffolder 服务。8、调用 ScaffoldMigration 方法生成代码未保存。9、调用 Save 方法真正保存。注意这里 outputDir 参数有个超级大坑。注释里面说它是相对于项目目录的而实际上并不是。从源代码中找到这个坑。复制代码public virtual MigrationFiles Save(string projectDir, ScaffoldedMigration migration, string? outputDir, bool dryRun){var lastMigrationFileName migration.PreviousMigrationId migration.FileExtension;// 这里是保存迁移类的路径注意“??”运算符var migrationDirectory outputDir ?? GetDirectory(projectDir, lastMigrationFileName, migration.MigrationSubNamespace);var migrationFile Path.Combine(migrationDirectory, migration.MigrationId migration.FileExtension);var migrationMetadataFile Path.Combine(migrationDirectory, migration.MigrationId .Designer migration.FileExtension);var modelSnapshotFileName migration.SnapshotName migration.FileExtension;var modelSnapshotDirectory GetDirectory(projectDir, modelSnapshotFileName, migration.SnapshotSubnamespace);var modelSnapshotFile Path.Combine(modelSnapshotDirectory, modelSnapshotFileName);Dependencies.OperationReporter.WriteVerbose(DesignStrings.WritingMigration(migrationFile));if (!dryRun){Directory.CreateDirectory(migrationDirectory);File.WriteAllText(migrationFile, migration.MigrationCode, Encoding.UTF8);File.WriteAllText(migrationMetadataFile, migration.MetadataCode, Encoding.UTF8);Dependencies.OperationReporter.WriteVerbose(DesignStrings.WritingSnapshot(modelSnapshotFile));Directory.CreateDirectory(modelSnapshotDirectory);File.WriteAllText(modelSnapshotFile, migration.SnapshotCode, Encoding.UTF8);}return new MigrationFiles{MigrationFile migrationFile,MetadataFile migrationMetadataFile,SnapshotFile modelSnapshotFile,Migration migration};}复制代码仔细看这里var migrationDirectory outputDir ?? ...也就是说如果你的 outputDir 参数不是 null 的话它直接用来作为迁移类输出目录这就不是相对于项目目录了而是相对于程序运行的当前目录了。只有当 outputDir 参数给了 null 后它才调用 GetDirectory 在项目目录同级目录下以分隔子命名空间的结构来拼接目录。比如我这里的项目根命名空间为 MigrationGenApp 我指定的迁移类子命名空间为 DBUpdates于是若 outputDir 参数为 null 时它生成相对于当前项目目录的目录 DBUpdates。如下图所示。image所以要让迁移类和快照类放一起outputDir 参数还得刻意配置一下。复制代码static void MakeMigration(string migName){// 项目目录const string ProjectDir ..\..\..\;// 输出目录const string OutputDir ..\..\..\MyMigrations;……// 保存var saveres migscaff.Save(ProjectDir, migres, OutputDir);……}复制代码这样才算把迁移类的代码放到项目目录下了。image接下来咱们回到 Animal 实体加一个 Remark 属性。复制代码public class Animal{……/// summary/// 备注/// /summarypublic string? Remark { get; set; }}复制代码再到 DemoDbContext 类的 OnModelCreating 方法配置改一下。entAnimal.Property(n n.Remark).HasColumnName(remarks).HasMaxLength(250);最后在 Main 方法中把刚才 test666 的迁移名称改为 test777。复制代码static void Main(string[] args){#if GEN_MIGRATIONMakeMigration(test777);return;#endif// 应用程序主代码……}复制代码第一次生成的迁移代码不要删除否则不能比较了。再运行一下看看新生成的迁移类。我们来对比一下因为 test666 是第一个迁移等于是全新创建数据库所以要 CREATE TABLE。复制代码protected override void Up(MigrationBuilder migrationBuilder){migrationBuilder.CreateTable(name: Animals,columns: table new{anl_id table.ColumnGuid(type: uniqueidentifier, nullable: false),nick table.Columnstring(type: nvarchar(15), maxLength: 15, nullable: false),age table.Columnint(type: int, nullable: false)},constraints: table {table.PrimaryKey(PK_Animal, x x.anl_id);});migrationBuilder.CreateTable(name: AnimalDetail,columns: table new{detail_id table.Columnint(type: int, nullable: false).Annotation(SqlServer:Identity, 1, 1),phylum table.Columnstring(type: nvarchar(20), maxLength: 20, nullable: true),class table.Columnstring(name: class, type: nvarchar(32), maxLength: 32, nullable: true),order table.Columnstring(type: nvarchar(32), maxLength: 32, nullable: true),family table.Columnstring(type: nvarchar(32), maxLength: 32, nullable: false),genus table.Columnstring(type: nvarchar(32), maxLength: 32, nullable: false),animal_id table.ColumnGuid(type: uniqueidentifier, nullable: false)},constraints: table {table.PrimaryKey(PK_Animal_details, x x.detail_id);table.ForeignKey(name: FK_Animal,column: x x.animal_id,principalTable: Animals,principalColumn: anl_id,onDelete: ReferentialAction.Cascade);});migrationBuilder.CreateIndex(name: IX_AnimalDetail_animal_id,table: AnimalDetail,column: animal_id,unique: true);}/// inheritdoc /protected override void Down(MigrationBuilder migrationBuilder){migrationBuilder.DropTable(name: AnimalDetail);migrationBuilder.DropTable(name: Animals);}复制代码而 test777 只是添加了一列所以是 ADD COLUMN。复制代码protected override void Up(MigrationBuilder migrationBuilder){migrationBuilder.AddColumnstring(name: remarks,table: Animals,type: nvarchar(250),maxLength: 250,nullable: true);}/// inheritdoc /protected override void Down(MigrationBuilder migrationBuilder){migrationBuilder.DropColumn(name: remarks,table: Animals);}复制代码现在迁移代码已生成咱们不需要再运行它们了把条件编译注释掉。//#define GEN_MIGRATION调用 Migrate 方法有两个重载。如果传递迁移名称那么只会把那个迁移应用到数据库如果无参数调用就会把所有未应用的迁移全部同步到数据库。由于咱们刚刚创建了两个迁移而且没有数据库所以咱们是从零构建要调用无参数的 Migrate 方法。复制代码static void Main(string[] args){