「C#」EF Core的“迁移”(Migration)

1、"迁移"是什么

"迁移"(Migration)我觉得可以理解为将实体类的变化 转换为对数据库修改的方案,应用迁移就是将这个修改方案应用到数据库。其次,迁移也记录了数据库的版本历史等信息。

2、添加迁移

2.1、dotnet cli tool

参考:EF Core 工具参考 (.NET CLI) - EF Core

  1. 添加迁移等后续操作用到了dotnet的命令行工具,这里记录下工具的安装(前提是已经安装了dotnet)
bash 复制代码
dotnet tool install --global dotnet-ef
  1. 更新ef工具
bash 复制代码
dotnet tool update --global dotnet-ef
  1. 确保项目中添加了Microsoft.EntityFrameworkCore.Design,可以通过VS的Nuget工具搜索添加,或者通过dotnet安装:
bash 复制代码
dotnet add package Microsoft.EntityFrameworkCore.Design
  1. 由于本文示例都是用Sqlite举例的,所以也需要添加Microsoft.EntityFrameworkCore.Sqlite
bash 复制代码
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

2.2、添加用于测试的实体类

分类:

csharp 复制代码
public class Category
{
    public int CategoryId { get; set; }
    public string? Name { get; set; }
    public virtual ObservableCollectionListSource<Product> Products { get; } = new();
}

产品:

csharp 复制代码
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!;
}
  1. 添加数据库上下文DbContext
csharp 复制代码
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、添加迁移

在项目所在目录下,启动终端,在终端中执行:

bash 复制代码
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方法用于回滚迁移,将数据库恢复到迁移前的状态。其代码大致如下:

csharp 复制代码
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脚本。

csharp 复制代码
migrationBuilder.Sql(
@"
    UPDATE Customer
    SET FullName = FirstName + ' ' + LastName;
");

2.4、修改实体并添加新迁移

基于以上的例子,假设随着业务发展,要对Product类做简单修改,要增加价钱

csharp 复制代码
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!;
}

接着在终端命令行中,增加迁移

bash 复制代码
dotnet ef migrations add AddProductPrice

即创建了新的迁移,接下来只需要将迁移应用,就会将修改同步到数据库了。

3、迁移的应用

在添加迁移后,通过dotnet ef工具执行下面(二选一)的指令,即可将迁移应用到数据库。

bash 复制代码
#更新数据库
dotnet ef database update
# 数据库更新到指定的迁移(也可用于回滚)
dotnet ef database update AddNewTables

但是这样存在问题。即开发过程中我们不可能连接着生产数据库,对于终端本地数据库,我们也不能让用户去执行这样的命令。所以通过dotnet ef工具指令应用仅是和在开发过程中临时使用。正式情况还是需要通过sql脚本或内置代码的方式来应用

3.1、使用Sql脚本

这个是官方比较推荐的方式

通过dotnet ef 工具可以生成sql脚本

bash 复制代码
# 从创建到最新迁移的所有脚本
dotnet ef migrations script
# 从某一次迁移到最新迁移的脚本
dotnet ef migrations script LastMigration
# 从某一次到指定的另一次迁移(支持from新to旧生成回退脚本)
dotnet ef migrations script FromMigration ToMigration

以前面的示例,第一次生成迁移后,我们 通过上面的指令生成脚本,结果如下:

bash 复制代码
> 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的函数方法

在代码中,我们可以使用DbContextDatabase.Migrate()方法来应用迁移。以下是一个示例:

csharp 复制代码
var context = new MyDbContext();
//也可能是通过依赖注入的方式从服务中获取MyDbContext↓
//var context = serviceProvider.GetRequiredService<MyDbContext>()
context.Database.Migrate();

当然,也可以指定某个迁移,或者回退到某个迁移

csharp 复制代码
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语句,并执行即可。

csharp 复制代码
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中记录与迁移同步。

...

相关推荐
PieroPc29 分钟前
Python 写的 智慧记 进销存 辅助 程序 导入导出 excel 可打印
开发语言·python·excel
Hacker_LaoYi2 小时前
SQL注入的那些面试题总结
数据库·sql
2401_857439693 小时前
SSM 架构下 Vue 电脑测评系统:为电脑性能评估赋能
开发语言·php
SoraLuna3 小时前
「Mac畅玩鸿蒙与硬件47」UI互动应用篇24 - 虚拟音乐控制台
开发语言·macos·ui·华为·harmonyos
xlsw_4 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
Hacker_LaoYi4 小时前
【渗透技术总结】SQL手工注入总结
数据库·sql
独行soc4 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍06-基于子查询的SQL注入(Subquery-Based SQL Injection)
数据库·sql·安全·web安全·漏洞挖掘·hw
Dream_Snowar5 小时前
速通Python 第三节
开发语言·python
高山我梦口香糖6 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
独行soc6 小时前
#渗透测试#漏洞挖掘#红蓝攻防#护网#sql注入介绍08-基于时间延迟的SQL注入(Time-Based SQL Injection)
数据库·sql·安全·渗透测试·漏洞挖掘