1、“迁移”是什么
“迁移”(Migration)我觉得可以理解为将实体类的变化 转换为对数据库修改的方案,应用迁移就是将这个修改方案应用到数据库。其次,迁移也记录了数据库的版本历史等信息。
2、添加迁移
2.1、dotnet cli tool
参考:EF Core 工具参考 (.NET CLI) - EF Core
- 添加迁移等后续操作用到了dotnet的命令行工具,这里记录下工具的安装(前提是已经安装了dotnet)
dotnet tool install --global dotnet-ef
- 更新ef工具
dotnet tool update --global dotnet-ef
- 确保项目中添加了Microsoft.EntityFrameworkCore.Design,可以通过VS的Nuget工具搜索添加,或者通过dotnet安装:
dotnet add package Microsoft.EntityFrameworkCore.Design
- 由于本文示例都是用Sqlite举例的,所以也需要添加Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
2.2、添加用于测试的实体类
分类:
public class Category
{
public int CategoryId { get; set; }
public string? Name { get; set; }
public virtual ObservableCollectionListSource<Product> Products { get; } = new();
}
产品:
public class Product
{
public int ProductId { get; set; }
public string? Name { get; set; }
public int CategoryId { get; set; }
public virtual Category Category { get; set; } = null!;
}
- 添加数据库上下文DbContext
public class ProductsContext : DbContext
{
//DbSet指定 要映射到数据库的实体类
public DbSet<Product> Products { get; set; }
public DbSet<Category> Categories { get; set; }
//数据库
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder.UseSqlite("Data Source=products.db");
//创建时对表做必要的配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Category>().HasData(
new Category { CategoryId = 1, Name = "Cheese" },
new Category { CategoryId = 2, Name = "Meat" },
new Category { CategoryId = 3, Name = "Fish" },
new Category { CategoryId = 4, Name = "Bread" });
modelBuilder.Entity<Product>().HasData(
new Product { ProductId = 1, CategoryId = 1, Name = "Cheddar" },
new Product { ProductId = 2, CategoryId = 1, Name = "Brie" },
new Product { ProductId = 11, CategoryId = 2, Name = "Ham" },
new Product { ProductId = 12, CategoryId = 2, Name = "Beef" },
new Product { ProductId = 13, CategoryId = 2, Name = "Chicken" },
new Product { ProductId = 21, CategoryId = 3, Name = "Salmon" },
new Product { ProductId = 22, CategoryId = 3, Name = "Tuna" },
new Product { ProductId = 24, CategoryId = 4, Name = "Rye" },
new Product { ProductId = 25, CategoryId = 4, Name = "Wheat" }
}
}
以上实体代码也可在微软官方教程中找到:Windows 窗体设计器入门 - EF Core
2.3、添加迁移
在项目所在目录下,启动终端,在终端中执行:
dotnet ef migrations add InitialCreate
InitialCreate是这次迁移的名称,类似于代码通过git提交仓库时的注释性文本,或者理解为一次行动的代号。可自定义,可以简单描述迁移的内容,或者用日期代替也可以。
首次迁移时EF Core 将在项目中创建一个名为“Migrations”的目录,并生成一些文件。
迁移文件:名称如“xxxx_MigrationName.cs”的文件以及名称带“Design”的子文件:“xxxx_MigrationName.Designer.cs”
文件2:名称如“xxxContextModelSnapshot.cs”文件。
增加迁移后,应用迁移就可以创建数据库了,关于应用,见下一节。
xxxContextModelSnapshot.cs文件
是对目前最新,当下的数据库模型,或者说实体的一个快照,主要作用是EF Core以其自身的规则生成对实体模型的描述。当有新的迁移时,新的迁移与这个模型快照进行对比,从而确定修改项以及数据库的升级方案。
xxxx_MigrationName.Designer.cs文件
是基于与快照对比后生成的本次迁移的相关代码,如果是第一次迁移,那么这个文件和模型快照文件基本一致,不同的是模型快照文件仅在生成迁移时有用,后续基本不会执行。而这个“xxx.Design.cs”文件其一方面会协助EFCore生成本次迁移,另一方面在后续的迁移引用时起到一定作用。
xxxx_MigrationName.cs文件
是本次迁移对数据库的具体修改方案。它通常包含两个方法:Up方法用于应用迁移,即将数据库从当前状态迁移到新的状态;Down方法用于回滚迁移,将数据库恢复到迁移前的状态。其代码大致如下:
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Categories",
columns: table => new
{
CategoryId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Categories", x => x.CategoryId);
});
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
ProductId = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: true),
CategoryId = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.ProductId);
table.ForeignKey(
name: "FK_Products_Categories_CategoryId",
column: x => x.CategoryId,
principalTable: "Categories",
principalColumn: "CategoryId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.InsertData(
table: "Categories",
columns: new[] { "CategoryId", "Name" },
values: new object[,]
{
{ 1, "Cheese" },
{ 2, "Meat" },
{ 3, "Fish" },
{ 4, "Bread" }
});
migrationBuilder.InsertData(
table: "Products",
columns: new[] { "ProductId", "CategoryId", "Name" },
values: new object[,]
{
{ 1, 1, "Cheddar" },
//......
{ 33, 4, "Soda" }
});
migrationBuilder.CreateIndex(
name: "IX_Products_CategoryId",
table: "Products",
column: "CategoryId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Products");
migrationBuilder.DropTable(
name: "Categories");
}
}
该文件可以进行修改,比如通过方法的入参 添加一些sql脚本。
migrationBuilder.Sql(
@"
UPDATE Customer
SET FullName = FirstName + ' ' + LastName;
");
2.4、修改实体并添加新迁移
基于以上的例子,假设随着业务发展,要对Product类做简单修改,要增加价钱
public class Product
{
public int ProductId { get; set; }
public string? Name { get; set; }
public int CategoryId { get; set; }
public decimal Price { get; set; }//新增项
public virtual Category Category { get; set; } = null!;
}
接着在终端命令行中,增加迁移
dotnet ef migrations add AddProductPrice
即创建了新的迁移,接下来只需要将迁移应用,就会将修改同步到数据库了。
3、迁移的应用
在添加迁移后,通过dotnet ef工具执行下面(二选一)的指令,即可将迁移应用到数据库。
#更新数据库
dotnet ef database update
# 数据库更新到指定的迁移(也可用于回滚)
dotnet ef database update AddNewTables
但是这样存在问题。即开发过程中我们不可能连接着生产数据库,对于终端本地数据库,我们也不能让用户去执行这样的命令。所以通过dotnet ef工具指令应用仅是和在开发过程中临时使用。正式情况还是需要通过sql脚本或内置代码的方式来应用
3.1、使用Sql脚本
这个是官方比较推荐的方式
通过dotnet ef 工具可以生成sql脚本
# 从创建到最新迁移的所有脚本
dotnet ef migrations script
# 从某一次迁移到最新迁移的脚本
dotnet ef migrations script LastMigration
# 从某一次到指定的另一次迁移(支持from新to旧生成回退脚本)
dotnet ef migrations script FromMigration ToMigration
以前面的示例,第一次生成迁移后,我们 通过上面的指令生成脚本,结果如下:
> dotnet ef migrations script
Build started...
Build succeeded.
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
"ProductVersion" TEXT NOT NULL
);
BEGIN TRANSACTION;
CREATE TABLE "Categories" (
"CategoryId" INTEGER NOT NULL CONSTRAINT "PK_Categories" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NULL
);
CREATE TABLE "Products" (
"ProductId" INTEGER NOT NULL CONSTRAINT "PK_Products" PRIMARY KEY AUTOINCREMENT,
"Name" TEXT NULL,
"CategoryId" INTEGER NOT NULL,
CONSTRAINT "FK_Products_Categories_CategoryId" FOREIGN KEY ("CategoryId") REFERENCES "Categories" ("CategoryId") ON DELETE CASCADE
);
INSERT INTO "Categories" ("CategoryId", "Name")
VALUES (1, 'Cheese');
SELECT changes();
INSERT INTO "Categories" ("CategoryId", "Name")
VALUES (2, 'Meat');
SELECT changes();
INSERT INTO "Categories" ("CategoryId", "Name")
VALUES (3, 'Fish');
SELECT changes();
INSERT INTO "Categories" ("CategoryId", "Name")
VALUES (4, 'Bread');
SELECT changes();
INSERT INTO "Products" ("ProductId", "CategoryId", "Name")
VALUES (1, 1, 'Cheddar');
SELECT changes();
...
...
...
INSERT INTO "Products" ("ProductId", "CategoryId", "Name")
VALUES (33, 4, 'Soda');
SELECT changes();
CREATE INDEX "IX_Products_CategoryId" ON "Products" ("CategoryId");
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
VALUES ('20240821064007_InitialCreate', '8.0.8');
COMMIT;
(注:输出脚本中我删掉了部分重复的INSERT语句以减少篇幅)
从脚本看,除了创建Product和Categories两个表,还创建了__EFMigrationsHistory表并向其插入了本次迁移的名称以及EF Core相关一依赖的版本号
有了Sql脚本,我们一方面就可以通过数据库管理软件来执行并升级数据库,另一方面也可以检查并修改sql脚本以确保迁移的正确性。
3.2、通过代码的方式
当我们的项目是web程序,数据库只有一个(暂不考虑备份啊、多服务器之类的实际详情),我们是应该使用sql脚本来应用迁移的,但是如果我们的项目是一个桌面端程序,数据存储使用的是Sqlite本地Db文件,我们就需要通代码的方式进行应用迁移,当用户运行我们的程序时,程序自动的去升级数据库。
3.2.1、通过EFCore的函数方法
在代码中,我们可以使用DbContext
的Database.Migrate()
方法来应用迁移。以下是一个示例:
var context = new MyDbContext();
//也可能是通过依赖注入的方式从服务中获取MyDbContext↓
//var context = serviceProvider.GetRequiredService<MyDbContext>()
context.Database.Migrate();
当然,也可以指定某个迁移,或者回退到某个迁移
var historyRepository = context.GetService<IHistoryRepository>();
var migrations = historyRepository.GetAppliedMigrations().ToList();
var targetMigration = migrations.LastOrDefault();
context.Database.Migrate(targetMigration.MigrationId);
3.2.1、通过 Sql 脚本的方式
通过Migrate()方法固然简单,单也有点不在掌控的感觉我。当然也是支持通过 SQL 脚本来内部执行的,按照3.1中的方法,先生成sql脚本,将生成的sql脚本写入代码的静态字符串、或者嵌入的资源,或者某个文件中。
通过代码获取sql语句,并执行即可。
string migrationScriptPath = "script.sql";
string scriptContent = File.ReadAllText(migrationScriptPath);
using (var command = context.Database.GetDbConnection().CreateCommand())
{
command.CommandText = scriptContent;
context.Database.OpenConnection();
command.ExecuteNonQuery();
}
4、合并迁移
有时候,我们可能需要合并多个迁移。例如,多人个开发时不同的分支上进行了数据库架构的修改,我们可能需要将这些修改合并到一个迁移中。
这里讨论的合并并不是某个固定的方法或指令。
对于生成环境,我们只需要保留对实体类的修改,并把不同人创建的迁移都删掉,然后重新添加迁移即可。
但对于开发环境,当我拉取到别人的迁移时,我怎么处理已经应用过自己迁移的数据库呢。
可以这么考虑处理:
1、回滚到前一个统一版本,然后删除不同人新增的迁移,生成新的迁移,再重新应用。
2、先合并迁移,创建sql脚本,并修改,删掉自己已经应用了的部分,确保__EFMigrationsHistory中记录与迁移同步。
…