資料庫遷移
第 15 章用 EnsureCreated() 快速建立練習資料庫。等應用程式已有資料,再給實體增加屬性時,它不會替你修改現有表。這時需要遷移(migration):把一次表結構變化記錄為可以審查、提交和執行的程式碼。
第 23 章已經包含完整範例。這裡是給 Todos 表增加 Note 列的遷移檔案,由 EF Core 工具產生:
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Deployment.Data.Migrations
{
/// <inheritdoc />
public partial class AddTodoNote : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Note",
table: "Todos",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Note",
table: "Todos");
}
}
}對應的實體完整檔案:
namespace TodoApi.Features.Todos;
public class Todo
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool Done { get; set; }
public string? Note { get; set; }
public int CategoryId { get; set; }
public Category Category { get; set; } = null!;
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<Todo> Todos { get; set; } = [];
}Up 描述升級,Down 描述撤銷。本次增加可空列,因此已有任務的 Note 是 null。撤銷這個遷移會刪掉整列,已經寫入的備註也會丟失;回退結構不等於恢復資料。
執行儲存庫中的遷移
從儲存庫根目錄執行:
cd samples/23-deployment
dotnet tool restore
dotnet ef migrations list
dotnet ef database update本章用本地工具清單固定 dotnet-ef 的版本。第一次更新新的 todos-23.db 時會執行 InitialCreate 和 AddTodoNote 兩個遷移,建立類別和任務表,並加入兩個固定類別。再次執行不會重複建表。
工具輸出帶時間戳的遷移名稱;完整名稱以 Data/Migrations 中的檔案為準。資料庫中的 __EFMigrationsHistory 表記錄哪些遷移已經成功應用程式。
不要重複產生儲存庫裡已有的遷移
InitialCreate 和 AddTodoNote 已經提交在範例中,直接執行即可。只有你再次修改實體結構時,才需要使用 dotnet ef migrations add 產生新的遷移,然後審查產生的檔案。
工具透過設計時工廠建立上下文,不需要先啟動 API 或設定生產身分服務:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace TodoApi.Data;
// dotnet ef only needs a context; authentication and the HTTP service do not need to start.
public class TodoDbContextFactory : IDesignTimeDbContextFactory<TodoDbContext>
{
public TodoDbContext CreateDbContext(string[] args)
{
var config = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json").AddEnvironmentVariables().AddCommandLine(args).Build();
var connectionString = config.GetConnectionString("Todos")
?? throw new InvalidOperationException("Missing ConnectionStrings:Todos configuration.");
return new TodoDbContext(new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(connectionString).Options);
}
}這裡的設計時(design time)指執行 EF 指令產生或應用程式遷移的階段。正常處理 HTTP 請求時,應用程式仍從相依性注入容器取得上下文。
怎麼確認舊資料還在
不能只看到遷移指令成功,就認為升級沒有影響資料。下面的完整測試先建立舊錶、插入任務,再執行新增列的遷移:
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
public class MigrationTests
{
[Fact]
public async Task Adding_note_keeps_existing_rows()
{
var ct = TestContext.Current.CancellationToken;
await using var connection = new SqliteConnection("Data Source=:memory:");
await connection.OpenAsync(ct);
await using var db = new TodoDbContext(new DbContextOptionsBuilder<TodoDbContext>()
.UseSqlite(connection).Options);
var migrator = db.GetService<IMigrator>();
await migrator.MigrateAsync("InitialCreate", ct);
var title = "Created before upgrade";
await db.Database.ExecuteSqlAsync(
$"INSERT INTO Todos (Title, Done, CategoryId) VALUES ({title}, {false}, {1})", ct);
await migrator.MigrateAsync(cancellationToken: ct);
var todo = await db.Todos.SingleAsync(ct);
Assert.Equal(title, todo.Title);
Assert.Null(todo.Note);
Assert.Equal(2, await db.Categories.CountAsync(ct));
todo.Note = "Added after upgrade";
await db.SaveChangesAsync(ct);
await migrator.MigrateAsync(cancellationToken: ct);
Assert.Single(await db.Todos.AsNoTracking().ToListAsync(ct));
Assert.Equal("Added after upgrade", await db.Todos.Select(t => t.Note).SingleAsync(ct));
}
}從本章範例目錄執行:
dotnet test --project Tests/TodoApi.Tests.csproj -c Release -p:TreatWarningsAsErrors=true總計 13 個測試應全部透過。遷移測試確認舊任務和類別仍然存在、新列初始為 null;隨後寫入備註,再次執行遷移,確認不會重複建立記錄或清掉備註。
後續修改資料庫的順序
- 修改實體或模型設定,讓它表達新的結構。
- 用
dotnet ef migrations add加上描述本次變化的名稱,產生遷移。 - 審查
Up、Down和模型快照,再在測試資料庫上驗證。工具看到“舊列消失、新列出現”時,產生的操作不一定就是你想要的重新命名。 - 把遷移檔案與應用程式程式碼一起提交;正式執行前備份資料,安排升級時機。
模型快照儲存上次遷移後的模型,工具透過比較它與目前模型產生下一次變更。不要手動刪除快照來“修復”不一致。
第 23 章用獨立的 --migrate 指令執行升級,成功後才啟動服務。真實系統還可以審查 SQL 指令碼或使用遷移 bundle;部署方式和資料庫提供者會影響可選方案,參見官方遷移應用程式說明。
已有 EnsureCreated 資料庫怎麼辦
EnsureCreated() 不建立遷移歷史。直接對這樣的資料庫執行 InitialCreate,通常會因為表已存在而失敗。
本教學讓第 23 章使用獨立的新檔案,不會覆蓋前面章節的資料。如果舊庫有需要保留的內容,先備份,再製定資料匯入或建立遷移基線的方案,並在副本上演練;不要靠刪除舊庫、偽造遷移記錄來跳過這一步。兩者的適用範圍見官方建表說明。
總結
- 遷移記錄表結構如何變化;
EnsureCreated只適合不需要遷移的建庫場景。 - 修改模型後產生遷移,審查後再執行,遷移和快照要一起提交。
- 用已有記錄驗證升級,不能只測試空資料庫。
- 新增可空列可以保留舊行,但刪除列等操作仍會丟資料。
- 正式升級前備份,並把升級安排在應用程式開始使用新結構之前。
