﻿# 如何在正式環境套用 Entity Framework Code First 資料庫移轉(Migrations)

要套用 Entity Framework Code First Migrations 資料庫移轉，一共有 5 種方法，每種方法都有著不同的適用情境，有些方法適用於開發測試環境，有些方法則適用於正式部署的環境。今天這篇文章我就來介紹幾種不同的套用資料庫移轉方法。

1.  產生 T-SQL 指令碼（適用於部署正式環境）
    
    從無到有建立全新資料庫的 DDL 指令碼
    
    ```bash
    dotnet ef migrations script
    ```
    
    從特定 MigrationId 產生變更指令碼
    
    ```bash
    dotnet ef migrations script MigrationId
    ```
    
    從特定 MigrationId 到特定 MigrationId 之間的變更指令碼
    
    ```bash
    dotnet ef migrations script MigrationId1 MigrationId2
    ```
    
    他的每一次 Migration 產生的 T-SQL 大概長這樣，每個 Migration 都會套用一次交易：
    
    ```sql
    BEGIN TRANSACTION;
    GO
    
    DECLARE @var3 sysname;
    SELECT @var3 = [d].[name]
    FROM [sys].[default_constraints] [d]
    INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
    WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Course]') AND [c].[name] = N'Title');
    IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Course] DROP CONSTRAINT [' + @var3 + '];');
    ALTER TABLE [Course] ALTER COLUMN [Title] nvarchar(55) NULL;
    GO
    
    INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
    VALUES (N'20230822130004_AddTo55', N'7.0.10');
    GO
    
    COMMIT;
    GO
    ```
    
    這種方法會需要你自行瞭解確切的 Migration 版本差異，並產生相對應的 T-SQL 腳本，使用上較不方便，但產生出來的變更指令碼會比較小，而且產生出來的 T-SQL 可以人工檢視與驗證，避免正式環境上的資料庫在更新的過程受到負面的衝擊。
    
2.  產生冪等的 T-SQL 指令碼（適用於部署正式環境）
    
    所謂的「冪等的 T-SQL 指令碼」(Idempotent SQL Scripts) 就是可以讓你不用擔心正式環境的資料庫目前到底套用了哪些 Migration 版本，他會產生所有的 Migration 歷史紀錄，並且自動找到正式環境上的關聯式資料庫中沒有的 Migration 版本，並且套用變更指令碼上去。
    
    ```bash
    dotnet ef migrations script --idempotent
    ```
    
    他的每一次 Migration 產生的 T-SQL 大概長這樣，每個 Migration 都會套用一次交易：
    
    ```sql
    BEGIN TRANSACTION;
    GO
    
    IF NOT EXISTS(SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20230822130004_AddTo55')
    BEGIN
        DECLARE @var3 sysname;
        SELECT @var3 = [d].[name]
        FROM [sys].[default_constraints] [d]
        INNER JOIN [sys].[columns] [c] ON [d].[parent_column_id] = [c].[column_id] AND [d].[parent_object_id] = [c].[object_id]
        WHERE ([d].[parent_object_id] = OBJECT_ID(N'[Course]') AND [c].[name] = N'Title');
        IF @var3 IS NOT NULL EXEC(N'ALTER TABLE [Course] DROP CONSTRAINT [' + @var3 + '];');
        ALTER TABLE [Course] ALTER COLUMN [Title] nvarchar(55) NULL;
    END;
    GO
    
    IF NOT EXISTS(SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20230822130004_AddTo55')
    BEGIN
        INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
        VALUES (N'20230822130004_AddTo55', N'7.0.10');
    END;
    GO
    
    COMMIT;
    GO
    ```
    
    這種方法你就不需要自行瞭解確切的 Migration 版本差異，但他會產生所有的變更指令碼，缺點就是產生出來的變更指令碼會比較大，對 DBA 來說就比較沒那麼友善，但是對開發人員來說是完美的。
    
3.  直接透過 EF Core CLI 工具更新資料庫（適用於開發測試環境）
    
    套用所有的 Migration 移轉記錄
    
    ```bash
    dotnet ef database update
    ```
    
    從第一版套用到指定的 Migration 版本
    
    ```bash
    dotnet ef database update MigrationId
    ```
    
    > 開發測試環境比較能讓你直接透過 EF Core 的 CLI 工具更新資料庫，正式環境就比較不可能這樣玩。
    
4.  透過 Migration bundles 產生專門用來執行資料庫移轉的執行檔（適用於 CI/CD 環境）
    
    你可以透過以下指令建立一個包含所有資料庫移轉記錄的「單一可執行檔」，而且是支援跨平台的執行檔，幫助你可以透過該執行檔在目標電腦執行程式，連接目標電腦上的資料庫套用資料庫移轉！
    
    例如我們要產生一個可以在 Windows 上面執行的檔案，就可以這樣產生：
    
    ```bash
    dotnet ef migrations bundle --self-contained -r win10-x64
    ```
    
    若要產生一個可以在 Linux 上面執行的檔案，就可以這樣產生：
    
    ```bash
    dotnet ef migrations bundle --self-contained -r linux-x64
    ```
    
    > 如果不加上 `--self-contained` 參數，則輸出的執行檔較小，但目標電腦需事先安裝 .NET Runtime 才能跑。
    
    上述命令執行完之後，會產生一個 `efbundle` 的可執行檔，這個執行檔會包含所有移轉的紀錄，讓你只要擁有這個單一執行檔就可以執行資料庫移轉作業，因此在套用資料庫更新方面非常的方便好用。其執行的方式如下：
    
    ```bash
    efbundle.exe --connection "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=ContosoUniversity;Integrated Security=True"
    ```
    
    若 `efbundle.exe` 執行檔所在目錄有包含 `appsettings.json` 的話，或是 DbContext 資料內容類別中的 `OnConfiguring()` 方法有定義連接字串，那麼 `efbundle` 執行檔就會自動取得連接字串，不用特別再加上 `--connection` 參數。輸入 `efbundle.exe --help` 可查詢完整參數用法。
    
    ![efbundle.exe --help](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/08c451564418d212f6bf-5043854b-fc9e-422d-8e59-97607e5afe18.webp)
    
5.  在應用程式啟動時自動執行資料庫移轉（適用於開發測試環境）
    
    若為 ASP.NET Core 應用程式，你可以在 `Program.cs` 的 `var app = builder.Build();` 之後，執行以下程式碼，就會在每次啟動網站時，自動檢查是否有需要套用資料庫移轉的資料庫更新！
    
    ```csharp
    using (var serviceScope = app.Services.CreateScope())
    {
        var services = serviceScope.ServiceProvider;
        var context = services.GetRequiredService<ContosoUniversityContext>();
        context.Database.Migrate();
    }
    ```
    
    > 請注意：千萬不要在 `context.Database.Migrate();` 之前執行 `context.Database.EnsureCreated()` 方法，否則執行 `Migrate()` 方法時會掛掉！
    
    由於 `Migrate()` 方法是基於 [IMigrator](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.migrations.imigrator) 服務的，你可以透過以下程式碼取得 IMigrator 的物件實例，並可進階的呼叫 [GenerateScript(String, String, MigrationsSqlGenerationOptions)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.migrations.imigrator.generatescript?view=efcore-7.0#microsoft-entityframeworkcore-migrations-imigrator-generatescript\(system-string-system-string-microsoft-entityframeworkcore-migrations-migrationssqlgenerationoptions\)), [Migrate(String)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.migrations.imigrator.migrate?view=efcore-7.0#microsoft-entityframeworkcore-migrations-imigrator-migrate\(system-string\)) 或 [MigrateAsync(String, CancellationToken)](https://learn.microsoft.com/zh-tw/dotnet/api/microsoft.entityframeworkcore.migrations.imigrator.migrateasync?view=efcore-7.0#microsoft-entityframeworkcore-migrations-imigrator-migrateasync\(system-string-system-threading-cancellationtoken\)) 方法：
    
    ```csharp
    var migrator = context.GetInfrastructure().GetService<IMigrator>();
    ```
    

### 相關連結

-   [Migrations Overview](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?WT.mc_id=DT-MVP-4015686&tabs=dotnet-core-cli)
    -   [Applying Migrations - EF Core | Microsoft Learn](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/applying?WT.mc_id=DT-MVP-4015686&tabs=dotnet-core-cli)
-   [Introducing DevOps-friendly EF Core Migration Bundles - .NET Blog](https://devblogs.microsoft.com/dotnet/introducing-devops-friendly-ef-core-migration-bundles/?WT.mc_id=DT-MVP-4015686)
-   [Dependency injection in ASP.NET Core | Resolve a service at app start up](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-6.0&WT.mc_id=DT-MVP-4015686#resolve-a-service-at-app-start-up)
