-
C#EF Core迁移——数据库版本控制
第一部分:C#基础入门
7.EF Core迁移——数据库版本控制
实例介绍
你有没有遇到过这样的场景?
团队协作时,张三在本地加了个Product表的Discount列,李四拉代码后运行程序直接报错——数据库结构不一致;
部署到生产环境时,手动写SQL脚本更新数据库,结果漏了某个索引,导致线上功能异常;
刚上线的新功能发现数据库表设计错了,想回滚却只能手动删表改字段,风险极高。
这些问题的根源,都是数据库版本没有和代码同步。EF Core迁移vb.net教程C#教程python教程SQL教程access 2010教程就是解决这个痛点的“神器”——它把数据库结构的每一次变更都变成代码里的“迁移文件”,像Git管理代码版本一样管理数据库,团队协作同步、部署平滑升级、错误快速回滚,全都搞定。
需求分析
本节要解决的核心问题:
1.数据库版本控制:跟踪每一次结构变更(加表、改列、删索引等);
2.团队协作同步:成员间一键同步数据库结构,避免“本地能跑线上崩”;
3.部署平滑升级:生产环境无需手动写SQL,用迁移自动更新;
4.错误回滚:发现问题时,一键恢复到之前的数据库版本;
5.数据迁移:结构变更时(比如改列类型),安全处理现有数据。
目标:掌握EF Core迁移的全流程,从创建到部署,从基础到高级,避开90%的坑点。
代码实现
前置条件:.NET 6+、EF Core 7+;需安装EF Core工具(dotnet tool install --global dotnet-ef);引用Microsoft.EntityFrameworkCore、Microsoft.EntityFrameworkCore.SqlServer。
准备工作:创建实体与DbContext
先定义两个实体(Category和Product),以及对应的DbContext:
csharp
// 分类实体
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = null!; // 非空标注
public ICollection<Product> Products { get; set; } = new List<Product>(); // 一对多关联
}
// 商品实体
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public int CategoryId { get; set; } // 外键
public Category Category { get; set; } = null!; // 导航属性
}
// DbContext配置
public class ShopDbContext : DbContext
{
public DbSet<Category> Categories { get; set; } = null!;
public DbSet<Product> Products { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
// 替换为你的数据库连接字符串
options.UseSqlServer("Server=.;Database=ShopDB;Trusted_Connection=True;");
}
}
场景1:迁移基本流程(创建→应用→验证)
错误写法1:直接运行程序,EF Core自动创建数据库(无迁移记录)
csharp
// ❌错误:没有迁移时,EF Core会自动创建数据库,但后续无法跟踪变更
public static async Task Main(string[] args)
{
using var db = new ShopDbContext();
await db.Database.EnsureCreatedAsync(); // 自动创建数据库,但无迁移记录
}
问题:后续修改实体(比如给Product加Discount列),EnsureCreatedAsync不会更新数据库,只能手动改。
错误写法2:手动修改数据库后,再生成迁移(导致冲突)
sql
-- ❌错误:手动在数据库加列,和EF Core模型不一致
ALTER TABLE Products ADD Discount DECIMAL(18,2) DEFAULT 0;
问题:生成迁移时,EF Core会认为Discount列不存在(因为模型里还没加),导致迁移脚本重复添加列,运行时报错。
正确流程:用迁移管理所有变更
步骤1:生成第一次迁移
打开命令行,执行:
bash
# 命令行方式(推荐)
dotnet ef migrations add InitialCreate --context ShopDbContext
# Visual Studio Package Manager Console方式
Add-Migration InitialCreate -Context ShopDbContext
执行结果:项目里会生成Migrations文件夹,包含三个文件:
InitialCreate.cs:迁移逻辑(Up=升级,Down=回滚);
InitialCreate.Designer.cs:迁移元数据(EF Core内部用);
ShopDbContextModelSnapshot.cs:当前模型的“快照”(用于对比后续变更)。
步骤2:应用迁移到数据库
执行命令:
bash
# 命令行
dotnet ef database update --context ShopDbContext
# PMC
Update-Database -Context ShopDbContext
执行结果:
数据库ShopDB被创建;
生成__EFMigrationsHistory表(记录已应用的迁移);
生成Categories和Products表(对应实体)。
步骤3:验证迁移
打开SQL Server Management Studio(SSMS),查看ShopDB:
有Categories、Products两个表;
有__EFMigrationsHistory表,里面有一条记录:InitialCreate。
场景2:修改实体后生成新迁移
给Product加Discount列,修改实体:
csharp
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public decimal Price { get; set; }
public decimal Discount { get; set; } = 0; // 新增列
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
生成新迁移:
bash
dotnet ef migrations add AddProductDiscount --context ShopDbContext
迁移文件AddProductDiscount.cs内容:
csharp
public partial class AddProductDiscount : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 升级操作:给Products表加Discount列
migrationBuilder.AddColumn<decimal>(
name: "Discount",
table: "Products",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m); // 默认值0
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 回滚操作:删除Discount列
migrationBuilder.DropColumn(
name: "Discount",
table: "Products");
}
}
应用迁移到数据库:
bash
dotnet ef database update AddProductDiscount --context ShopDbContext
验证:Products表会新增Discount列,__EFMigrationsHistory表新增AddProductDiscount记录。
场景3:回滚迁移(恢复到之前的版本)
如果发现Discount列设计错了(比如应该是int不是decimal),回滚到InitialCreate版本:
bash
# 回滚到InitialCreate(指定迁移名称)
dotnet ef database update InitialCreate --context ShopDbContext
# 或者回滚到上一个版本(不用记名称)
dotnet ef database update --context ShopDbContext --verbose
执行结果:
Products表的Discount列被删除;
__EFMigrationsHistory表中AddProductDiscount记录被移除;
代码里的AddProductDiscount迁移文件还在,需要手动删除(或保留,后续修改后重新生成)。
场景4:生成SQL脚本(用于生产部署)
生产环境可能不允许直接运行dotnet ef命令,这时可以生成SQL脚本,手动执行:
bash
# 生成从InitialCreate到AddProductDiscount的脚本(指定起始和结束迁移)
dotnet ef migrations script InitialCreate AddProductDiscount --context ShopDbContext --output MigrateToAddDiscount.sql --no-build
# 生成所有未应用的迁移脚本(生产环境常用)
dotnet ef migrations script --context ShopDbContext --output LatestMigrations.sql --no-build
脚本内容示例:
sql
BEGIN TRANSACTION;
GO
CREATE TABLE [__EFMigrationsHistory] (
[MigrationId] nvarchar(150) NOT NULL,
[ProductVersion] nvarchar(32) NOT NULL,
CONSTRAINT [PK___EFMigrationsHistory] PRIMARY KEY ([MigrationId])
);
GO
-- 省略InitialCreate的脚本...
ALTER TABLE [Products] ADD [Discount] decimal(18,2) NOT NULL DEFAULT 0.0;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20240520123456_AddProductDiscount', N'7.0.10');
GO
COMMIT;
场景5:高级用法——数据迁移(改列类型)
假设要把Product的Price列从decimal(18,2)改成decimal(18,4)(更精确),直接改模型会导致数据丢失吗?不会,用迁移安全处理:
步骤1:生成迁移(但不要直接应用)
先给Product的Price改类型:
csharp
public class Product
{
// ...其他属性
public decimal Price { get; set; } = 0m; // EF Core默认生成decimal(18,2),需要自定义
}
// 在ShopDbContext的OnModelCreating里配置列类型
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.Property(p => p.Price)
.HasColumnType("decimal(18,4)"); // 改成18,4
}
生成迁移:
bash
dotnet ef migrations add ChangePricePrecision --context ShopDbContext
步骤2:修改迁移文件,添加数据迁移逻辑
默认生成的ChangePricePrecision.cs会直接删除旧列再添加新列(导致数据丢失),需要手动修改:
csharp
public partial class ChangePricePrecision : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. 添加临时列(保存旧数据)
migrationBuilder.AddColumn<decimal>(
name: "TempPrice",
table: "Products",
type: "decimal(18,4)",
nullable: false,
defaultValue: 0m);
// 2. 把旧列数据迁移到临时列
migrationBuilder.Sql(@"
UPDATE Products
SET TempPrice = CAST(Price AS DECIMAL(18,4))
");
// 3. 删除旧列
migrationBuilder.DropColumn(
name: "Price",
table: "Products");
// 4. 重命名临时列为Price
migrationBuilder.RenameColumn(
name: "TempPrice",
table: "Products",
newName: "Price");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 回滚逻辑:反向操作
migrationBuilder.AddColumn<decimal>(
name: "TempPrice",
table: "Products",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
migrationBuilder.Sql(@"
UPDATE Products
SET TempPrice = CAST(Price AS DECIMAL(18,2))
");
migrationBuilder.DropColumn(
name: "Price",
table: "Products");
migrationBuilder.RenameColumn(
name: "TempPrice",
table: "Products",
newName: "Price");
}
}
步骤3:应用迁移
bash
dotnet ef database update ChangePricePrecision --context ShopDbContext
验证:Price列类型变成decimal(18,4),原有数据保留(比如19.99变成19.9900)。
逐行讲解
场景2:迁移文件核心逻辑
以InitialCreate.cs为例:
csharp
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 创建Categories表
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"), // SQL Server自增标识
Name = table.Column<string>(type: "nvarchar(max)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.Id); // 主键
});
// 创建Products表(含外键关联Categories)
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(max)", nullable: false),
Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false),
CategoryId = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
// 外键约束:Products.CategoryId → Categories.Id
table.ForeignKey(
name: "FK_Products_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade); // 删除分类时,自动删除关联商品
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 回滚时,先删外键关联的表(Products),再删主表(Categories)
migrationBuilder.DropTable(
name: "Products");
migrationBuilder.DropTable(
name: "Categories");
}
}
关键说明:
MigrationBuilder:EF Core提供的工具类,用于生成SQL语句;
Up方法:升级数据库时执行的逻辑(创建表、加列等);
Down方法:回滚时执行的逻辑(必须是Up的逆操作,比如删表对应创建表);
Annotation:用于指定数据库特定的特性(比如SQL Server的自增、MySQL的 AUTO_INCREMENT);
ForeignKey:定义外键关联,onDelete: ReferentialAction.Cascade表示级联删除。
场景5:数据迁移逻辑
修改ChangePricePrecision.cs时,为什么要加临时列?
直接删除旧列会丢失数据;
用临时列保存旧数据,迁移后再重命名,确保数据不丢失;
migrationBuilder.Sql:执行自定义SQL语句(EF Core无法自动生成的逻辑,比如数据转换)。
基础知识拓展
-
迁移的工作原理
EF Core迁移的核心是模型快照和迁移历史表:
模型快照:ShopDbContextModelSnapshot.cs文件,记录当前EF Core模型的结构(表、列、关联等);
迁移历史表:数据库中的__EFMigrationsHistory表,记录已应用的迁移名称和EF Core版本;
生成迁移:对比当前模型和快照,找出差异,生成Up/Down逻辑;
应用迁移:执行未应用的迁移的Up方法,更新数据库和历史表;
回滚迁移:执行已应用的迁移的Down方法,恢复数据库和历史表。 -
迁移 vs 手动SQL脚本
对比维度 EF Core迁移 手动SQL脚本
版本控制 和代码绑定,Git管理 独立文件,易丢失/版本不一致
团队协作 一键同步,无冲突 需手动分享,易重复/遗漏
回滚 一键回滚,安全可靠 需手动写回滚脚本,易出错
跨数据库 自动适配(SQL Server/MySQL等) 需针对不同数据库写不同脚本
复杂逻辑 支持自定义SQL,灵活 完全灵活,但需手动维护 -
最佳实践
禁止手动修改数据库:所有变更必须通过迁移,否则会导致模型和数据库不一致;
每个迁移对应一个功能:不要一次生成多个功能的迁移(比如同时加表和改列),方便回滚;
测试迁移:在开发环境测试所有迁移(包括Up和Down),确保无错误;
生产环境用脚本部署:生成SQL脚本后,先在测试环境验证,再手动执行到生产;
备份数据库:应用迁移前,务必备份生产数据库(即使迁移有Down方法,也不能完全保证数据安全);
命名规范:迁移名称要清晰(比如AddProductDiscount、ChangePricePrecision),不要用Migration1这种无意义的名称。 -
常见坑点与解决
迁移冲突:多人同时修改同一实体,生成迁移时会冲突。解决:先pull最新代码,合并模型,再重新生成迁移;
迁移失败:应用迁移时出错(比如SQL语法错误),先回滚到上一个版本,修改迁移文件,再重新应用;
快照文件冲突:Git合并时,ShopDbContextModelSnapshot.cs容易冲突。解决:删除冲突的快照文件,重新生成迁移(dotnet ef migrations add MergeSnapshot --context ShopDbContext);
数据丢失:修改列类型或删除列时,一定要手动添加数据[vb.net教程](https://www.xin3721.com/eschool/vbnetxin3721/)[C#教程](https://www.xin3721.com/eschool/CSharpxin3721/)[python教程](https://www.xin3721.com/eschool/pythonxin3721/)[SQL教程](https://www.xin3721.com/eschool/SQLxin3721/)[access 2010教程](https://www.xin3721.com/eschool/Accessxin3721/)迁移逻辑(比如临时列),不要用EF Core默认生成的逻辑。
总结
EF Core迁移的核心价值,是让数据库结构成为代码的一部分,彻底解决版本不一致的问题。掌握它的关键:
1.核心流程:生成迁移→应用迁移→验证→回滚;
2.关键命令:migrations add(生成)、database update(应用/回滚)、migrations script(生成脚本);
3.高级技巧:自定义迁移逻辑(用Sql方法)、数据迁移(临时列)、生产环境用脚本部署;
4.避坑指南:禁止手动改数据库、测试所有迁移、备份生产数据。
学会EF Core迁移,你就能告别“数据库地狱”,让团队协作和部署变得轻松愉快!
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49455.html










