ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EF Core迁移记录合并实战:从历史包袱到干净基线

EF Core迁移记录合并实战:从历史包袱到干净基线 很多人把EF Core的迁移机制当成项目里的常规操作加个表执行dotnet ef migrations add部署时跑一遍database update一切看起来都有章可循。但真实项目跑到一年以上打开Migrations目录数一数四十个文件一点也不稀奇。我也是在这种背景下开始研究迁移记录合并的——不是简单的删掉重来而是在不破坏已有生产库的前提下把手上一堆碎迁移压缩成干净利落的几条。这个过程里踩了不少坑其中一些做法和官方文档里的标准答案很不一样说是非常规操作一点也不夸张。这篇文章把我两次合并迁移记录的经历完整写出来包括开发期的常规流程、生产环境上线后的替代方案、合并时容易丢失的内容以及合并完成后的验证思路希望对正准备收拾迁移历史的同行有帮助。1. 迁移记录是怎么堆积起来的以及合并解决什么问题1.1 打开Migrations文件夹后的真实名场面一年前接手某个老项目时Migrations目录里已经有三十多个文件。明明是一次很小的需求往往会因为赶工细分出好几条迁移先AddOrderStatusField过两天发现类型不对又FixOrderStatusType再后来为了加个索引又AddOrderStatusIndex。这些迁移单独看都没问题但堆在一起就成了历史包袱。更麻烦的是分支合并。你和同事同时改了模型git合并代码时冲突最多的地方往往就是迁移文件——因为迁移文件带时间戳且依赖前一个迁移的Snapshot。两个人各自生成一条迁移合并到主干时EF Core会认为两条迁移都要执行顺序还得手动调整。这在小团队里还能忍项目一大人一多每次处理迁移冲突都能耗掉半天。1.2 合并能解决什么不能解决什么先说合并带来的实际好处。初始迁移从几十个压缩到一个部署脚本和代码审查都干净很多。减少PR里因迁移文件时间戳冲突导致的无效改动。新成员从零搭建本地环境时执行一次迁移就能拿到完整结构不用像看电影一样追几十个碎迁移。ModelSnapshot的对比路径变短后续生成迁移时的diff效率更高也更不容易误判。但我要泼一盆冷水合并迁移记录不解决任何数据问题。如果某个迁移当年做过一次不可逆的数据修复合并后这条逻辑就找不回来了。也不能根治模型本身的不合理设计那需要重构代码不是整理迁移文件能搞定的。1.3 动手前必须回答的三个问题很多人上来就问怎么合并我一般先反问三个问题。目标数据库有没有真正上线如果只是本地开发或预生产路子可以很野如果线上有用户数据路子必须很稳。有没有别的分支也在频繁改迁移有的话先合并完代码再动手否则你会体会一把迁移合并完直接冲突爆炸的滋味。当前代码模型和数据库结构是否完全一致不一致的情况下做合并后面查问题会非常痛苦。这三个问题分别对应了后面章节里两条完全不同的操作路线先想清楚再动手别急着删文件。2. 开发期合并的标准套路与四个容易翻车的细节2.1 先搞懂EF Core在迁移里到底记住了什么要合并迁移至少要明白迁移文件里的三件套是什么。一个迁移20250101000000_AddOrders.cs看起来是一个类实际上背后关联三样东西Up和Down方法定义了应用和回滚该迁移时的具体数据库操作。Designer.cs里的[DbContext]特性和BuildTargetModel方法保存了应用该迁移后数据库对应的完整模型快照。整个Migrations目录里的{DcContext}ModelSnapshot.cs相当于所有迁移的总和基线下一个新迁移就是拿当前模型和这个快照做差异对比生成的。这就是关键所在EF Core生成新迁移从来不是全量建库而是增量对比。当你删掉所有老迁移再重新add一个InitialCreate时相当于把快照归零让EF认为所有表都是新增的一次性全量生成建库脚本。开发期的合并利用的正是这个机制。2.2 标准命令序列回滚、清空、重建如果确定数据库还没上线或者可以接受推倒重建操作路径非常直接。# 1. 把所有迁移回滚到空状态 dotnet ef database update 0 \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api # 2. 从最新开始逐个删除迁移 dotnet ef migrations remove \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api # 重复执行直到Migrations目录为空 # 3. 生成全新的初始迁移 dotnet ef migrations add InitialCreate \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api # 4. 在空库上应用 dotnet ef database update \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api流程本身不难但有几个细节值得展开说。第一database update 0并不等于删库它是按顺序执行所有迁移的Down方法把数据库结构一步步回退到没有迁移的状态。如果某个老迁移的Down方法写得不完善比如删除列后又建了依赖它的索引执行到一半就可能报错。我在一个老项目里遇到过Down方法里直接drop table的情况回滚非常酸爽。第二migrations remove也得按顺序从新到旧执行每次移除一条并同步更新Snapshot。理论上你可以直接暴力删掉整个Migrations目录再重新add一个这样也能用代价是Snapshot文件整个重新生成命名空间和引用关系如果原本有多处自定义配置可能留下隐性坑。第三开发库如果保留了老历史执行完新迁移后可能因为表已存在直接报错。最省事的做法是把开发库整个drop掉重新create反正在开发初期数据库里没什么不能丢的东西。第四重新生成的InitialCreate迁移里所有表结构、索引、外键都是当时模型的全量快照。如果之前有某个迁移手写过migrationBuilder.Sql(...)去创建存储过程或视图这些内容在这个新迁移里是不存在的。2.3 为什么删文件夹重建这条路只适合开发期删掉所有迁移文件重新跑一次migrations add这个思路在开发环境没有任何问题。但到了生产环境就完全行不通因为线上数据库结构已经应用了老迁移且__EFMigrationsHistory表里存着完整的迁移ID列表。如果你把代码里的迁移清空重来生产库的表结构还在但EF跑database update时会发现新基线迁移没有记录于是尝试全量建库结果每建一张表都报对象已存在。数据库结构被搞乱还是小事更怕的是中间有迁移做过数据回填一旦重跑结构会造成不可挽回的重复操作。所以生产环境的合并要换一种思路不是让EF真的去执行基线迁移而是让EF以为基线迁移早就执行过了。3. 生产环境不能回滚基线迁移加历史表手工衔接3.1 先想通一个反直觉的点迁移历史不一定要连续EF Core在运行时判断哪些迁移需要执行逻辑其实很简单它加载程序集里所有迁移类和数据库中__EFMigrationsHistory表里的MigrationId列表做差集差集里的迁移才会被应用。注意这里没有要求代码里的迁移必须完整覆盖历史表里的每条记录也没有要求中间不能有空档。也就是说你在历史表里手动插入一条新基线迁移的记录只要这条记录的MigrationId和代码里存在的迁移ID一致并且数据库结构和该迁移生成的结构一致整个状态就是合法的。这有点像档案室改台账账本上补了一条2025年初已经完成整体初始化后面查账的人不会质疑中间那几十个历史批次为什么不见了。这就是生产环境合并的核心原理。掌握它之后操作路径就变得非常清晰。3.2 生产环境合并的完整五步这里给出我验证过的完整操作序列以SQL Server为例。第一步备份生产库。这不是例行公事而是整个操作的保险绳。建议在备份前先确认当前实例没有被长事务卡住备份文件单独存到异地。第二步在代码中精简迁移记录。最简单的方式是把Migrations目录里所有旧迁移文件删除保留{DbContext}ModelSnapshot.cs其实也可以一起删migrations add会重新生成然后执行dotnet ef migrations add InitialConsolidated \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api这步生成的InitialConsolidated里包含了当前模型的全部建库逻辑。第三步验证新基线迁移和生产库结构一致。这个步骤如果跳过后面会在运行时莫名其妙报列名无效之类的错误。验证方式我推荐两种找一台空数据库从零执行新基线迁移得到一张新库。然后把新库和生产库的Schema做一次对比可以用数据库工具自带的Schema Compare没有就手动抽查关键表和索引。用Scaffold-DbContext把生产库反推成EF模型再用一个临时上下文对比模型和代码模型的差异。第四步在生产库的历史表里插入基线迁移记录INSERT INTO __EFMigrationsHistory (MigrationId, ProductVersion) VALUES (20250301120000_InitialConsolidated, 9.0.5);这里两个细节必须说清楚。MigrationId的格式是时间戳下划线迁移名时间戳部分不能随便编最好和你代码里生成的InitialConsolidated类名完全一致ProductVersion要取项目当前实际引用的EF Core版本可以从csproj文件或dotnet ef --version输出里查。如果项目自定义了历史表名比如在UseSqlServer里配置了MigrationsHistoryTable(__EFMigrationsHistory, schema)插入语句要跟着改。我一同事就因为没看历史表配置把SQL插到了默认表里结果项目在另一个Schema查历史记录直接抓瞎。第五步执行一次空操作验证dotnet ef database update \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api \ --connection 生产库连接串如果一切正确控制台会提示没有待应用的迁移。此时合并就算完成了。3.3 旧的历史记录删不删我的建议是不删生产库历史表里的几十条旧迁移ID在新合并后会不会有问题我在实践中确认不会。原因还是差集判断逻辑EF只关心程序集里存在的迁移ID是否都已在历史表里出现多出来的旧ID不会触发任何操作也不影响下一次增量迁移的生成。那我为什么不建议顺手清掉旧记录因为旧ID本身是一份审计痕迹。哪天你需要排查某个数据变更是什么时候发生的历史表里的记录配合迁移文件哪怕是Git历史里的老文件能帮你快速定位。清理它们除了让表好看点没有任何实际收益反而增加了出事的窗口。3.4 它的局限性在哪这个方案要求代码中的模型快照和生产库实际结构完全对齐否则插入历史记录等于自欺欺人后续每次查询和迁移都会漏风。所以合并前一定要把一致性验证做扎实。另外如果生产库有多个比如租户分库或只读副本每个库都得执行一遍第五步的验证和第四步的插入。千万不要只改主库就完事我见过分库项目只插了主库、结果从库和代码对不上导致报表查询崩溃的真实案例。4. 合并时最容易丢的东西自定义Sql与种子数据处理4.1 重新生成的初始迁移并不包含Sql写进去的内容EF Core自动生成的迁移文件只会反映模型层面的结构变更比如创建表、加字段、加索引、建外键。但真实项目里很多人会直接在迁移文件里手写SQL做以下事情创建存储过程、视图、函数给已有表做数据回填修改排序规则或者打上触发器和审计字段插入权限数据这些内容只存在于特定迁移文件的Up方法里一旦删掉迁移文件这些SQL就跟着没了。而重新生成的InitialConsolidated是根据模型快照来的根本不知道这些自定义操作曾经存在过。我在第二次合并迁移时就踩过这个坑。项目里有个sp_GetDashboardData存储过程是在第三十七条迁移里用migrationBuilder.Sql建进去的。合并后代码跑了几个星期一切正常直到有一天报表模块调用存储过程报对象不存在系统拉出警报我才反应过来。4.2 合并前先做一次自定义内容抢救动手清空Migrations目录前我建议做这么几步逐个打开迁移文件搜索migrationBuilder.Sql(把找到的SQL全部复制到一个独立文档里按功能分好类。搜索HasData(虽然它是模型层面的种子数据通常不需要手工处理但问题在于合并后种子数据是否还保留在初始迁移里会影响后面的数据一致性。把存储过程、视图这类结构对象整理成一个新的独立迁移放在InitialConsolidated之后。比如public partial class AddCriticalStoredProcedures : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.Sql( CREATE OR ALTER PROCEDURE [dbo].[sp_GetDashboardData] AS BEGIN SET NOCOUNT ON; SELECT [Status], COUNT(*) AS [Count] FROM [Orders] GROUP BY [Status]; END ); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.Sql(DROP PROCEDURE [dbo].[sp_GetDashboardData]); } }注意一个坑migrationBuilder.Sql()默认不支持GO批处理命令。如果你从SSMS里把脚本直接复制过来里面带着GO执行时就可能因为批处理解析错误而失败。需要把GO拆成多条migrationBuilder.Sql调用或者改写成以分号结尾的多语句脚本。而对于数据回填类SQL我的建议是单独创建一个数据修复迁移语义上明确这是数据操作不是结构操作并且在日志里留好说明。这样即使在非常规合并后后人追查数据变化也有据可依。4.3 HasData与生产库的重复执行问题HasData种子数据在重新生成的基线迁移里会保留。有人会担心生产库里这些数据已经有了基线迁移虽然被手工标记为已应用但如果以后其他开发环境从零执行基线迁移会不会又插一遍种子数据答案是分环境看。新环境从零执行基线迁移时没有任何数据种子数据正常插入没问题。生产环境因为基线迁移已经被标记为已应用根本不会执行Up方法所以也不会重复插入。真正需要小心的是那些开发库不想重建只插了历史记录的情况——如果开发库结构是用老迁移一路升上来的表里已经有了种子数据此时新基线迁移被标记为已应用那数据不会重复插入但如果结构有出入问题就会在migrations add生成下一个增量时暴露出来。稳妥起见开发库要么推倒重建要么自动应用一次全面的对比校验。5. 多程序集、多DbContext场景下合并操作的注意事项5.1 迁移分散在类库时命令别漏参数很多项目的DbContext并不在Web启动项目里而是放在独立的类库比如src/MyApp.Infrastructure。这种情况下执行迁移命令一定要带上--project和--startup-project否则EF会默认去启动项目里找DbContext大概率直接报错或生成到错误的位置。dotnet ef migrations add InitialConsolidated \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api \ --output-dir Migrations如果连这个都配不对执行前可以先跑一下dotnet ef migrations list看EF最终是从哪个程序集加载迁移的。5.2 多DbContext各管一摊时合并要分开做一个比较容易被忽略的场景是项目里同时有AppDbContext和IdentityDbContext它们的迁移分别存在不同的目录。合并时必须按DbContext分别操作因为每个DbContext都有自己的ModelSnapshot文件混淆之后会生成牛头不对马嘴的迁移。操作时记得指定上下文名称dotnet ef migrations add InitialConsolidated \ --context AppDbContext \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api \ --output-dir Migrations/App dotnet ef migrations add InitialConsolidated \ --context IdentityDbContext \ --project src/MyApp.Infrastructure \ --startup-project src/MyApp.Api \ --output-dir Migrations/Identity如果两个DbContext共用同一个物理库生产库历史表插入记录时也要一条一条把对应ID插进去别插串了。5.3 分支合并的窗口期管理迁移合并最容易被低估的其实是协作流程问题。一旦你删掉老迁移并推送了新基线其他还停留在老迁移版本上的同事pull到代码后运行migrations add会看到系统认为不存在任何迁移因为新基线已应用但本地数据库历史表又对不上各种奇异报错接踵而至。我现在的做法是合并迁移前确认所有特性分支都先并入主干并清理掉那些上游已经被重置的旧迁移。然后在合并完成后的同一天通知所有相关同事执行一次数据库重建或按新基线重新同步。过了这个窗口期再去rebase旧分支代价会小很多。6. 合并完成后的验证清单以及什么情况下别合并6.1 一份可以照着检查的验证清单合并完成后我每次都会做下面这套验证建议原样照抄。用dotnet ef migrations list检查代码里的迁移链是否符合预期基线迁移排在最前。在一台空库上执行dotnet ef database update检查能否从零构建完整结构并且无报错。对生产库执行一次dotnet ef database update确认输出没有待应用的迁移。对比生产库历史表和代码迁移ID集合确保代码里每条迁移都有对应记录不多不少。把模型里一个字段类型随手改一下执行dotnet ef migrations add VerifyIncremental打开新生成的迁移文件确认里面只包含你对字段的那条AlterColumn操作而不是整个库的全量建表脚本。验证完立刻删除这个测试迁移保持目录干净。这一步是整个验证里最有效的能一下子暴露快照对不上的问题。让测试环境跑一轮完整的冒烟测试重点覆盖存储过程、视图和种子数据相关功能。6.2 我踩过的坑ProductVersion写错导致运行时异常有一次我在插入历史记录时图省事把ProductVersion写成了自己记忆里的一个版本号结果项目引用的EF Core实际是另一个版本。当时表面上database update提示无待应用迁移以为没问题了结果程序启动时EF做迁移装配校验发现历史记录里的ProductVersion与当前运行时版本不一致抛了异常。虽然报错信息比较直观但这种低级失误完全可以通过从csproj中确认版本号的方式避免。还有一次是在没有备份的情况下直接操作好在当时是预生产环境数据库能重建换成生产环境就是事故了。6.3 什么时候不要合并或者说只合并到某个阶段并不是所有项目都适合把迁移记录合并成一个。如果历史迁移里有大量数据回填脚本、临时修复逻辑、按季度执行的数据归档操作把这些全部压进一个基线会让后人完全无法判断某张表的数据为什么和别的不一样。这种情况下我更推荐阶段式基线策略保留最近几个版本的迁移作为增量把最早的一段历史合并到一个基线比如InitialCreateConsolidated_2025然后后续继续以增量迁移前进。说到底合并迁移是一个代码整理动作不是数据库重构动作。它最大的价值是减少噪音、降低新人上手成本、缓解分支冲突而不是帮你修复任何结构或数据上的历史问题。每次动手前先想清楚这三个问题数据库能不能重建同事分支有没有同步结构是否一致。三个问题都有明确答案了再按生产环境和开发环境选择对应的操作路线就不会出大乱子。
返回列表