EntityFrameworkCore 模型自動更新(上)

2022-09-08 06:00:12

話題

嗯,距離上一次寫博文已經過去近整整十個月,還是有一些思考,但還是變得懶惰了,心思也不再那麼專注,有點耗費時間,學習也有點停滯不前,那就順其自然,隨心所欲吧,等哪天心血來潮,想寫了就寫寫

模型自動更新方案研究(上)

一般團隊人數很少時,使用EF Core內建遷移基本已滿足,滿足的基本前提首先要生成遷移檔案,然後和資料庫進行對比,但團隊人數一多,遷移檔案等等涉及提交衝突等等,所以大部分情況下,我個人認為EF Core遷移基本沒啥用,這玩意用不起來,尤其涉及到版本分支很多情況下,切換不同分支所使用的資料可能也會不同,我們常用MySql資料庫,同時適配了人大金倉資料庫、高斯資料庫(GaussDb for opengauss),其對應的表結構有一些差異性,列型別也會有很大差異性,這對開發人員和測試人員來講就是深深的折磨和痛苦,大部分時間會花在保持資料庫表結構和模型一致,否則要麼執行不起來,要麼測試功能各種有問題,所以想想通過自動化方式來解決這個問題,本文還是分上下兩篇寫好了。

 

那麼解決此問題的思路是怎樣的呢?同時適配多套資料庫,重寫一套?那是不闊能的,既然我們可以通過dotnet ef命令來進行遷移,通過和資料庫表結構進行對比,從而生成遷移檔案,遷移檔案包含即將要執行的差異性指令碼,從這個角度來看的話,我們從遷移類反堆即可得到生成的指令碼以及和資料庫進行對比操作方法,初步設想理論上應該行得通,只需花時間瞭解下原始碼就好,通過前期兩天的啃原始碼,最終啃出百把行程式碼即可自動更新模型到資料庫,當然這個過程中還涉及一些要考慮的細節,我們一一再敘。接下來我們以MySql為例講講整個過程,其他資料庫依葫蘆畫瓢就好,首先甩出如下程式碼:

var services = new ServiceCollection();

services.AddEntityFrameworkMySql();

services.AddEntityFrameworkDesignTimeServices();

services.AddDbContext<EfCoreDbContext>((serviceProvider, options) =>
{
    options.UseInternalServiceProvider(serviceProvider);

    options.UseMySql("server=localhost;Port=3306;Database=test;Username=root;Password=root;",ServerVersion.AutoDetect("server=localhost;Port=3306;Database=test;Username=root;Password=root;"));
});

services.AddScoped<IDatabaseModelFactory, MySqlDatabaseModelFactory>();

var serviceProvider = services.BuildServiceProvider();

EF Core有屬於它自己的容器,所以我們將全域性容器和上下文所屬容器做了區分,同時呢,我們將遷移中要用到的操作依賴也手動新增,比如上面的設計服務,存在於 Microsoft.EntityFrameworkCore.Design 包內,最後將獲取資料庫表結構模型工廠手動注入即MySqlDatabaseModelFactory。接下來我們要獲取模型定義以及屬性一些定義等等,也就是我們最終要生成的目標模型結構

using var scope = _serviceProvider.CreateScope();

var currentServiceProvider = scope.ServiceProvider;

var context = (DbContext)currentServiceProvider.GetRequiredService<T>();

var connectionString = context.Database.GetDbConnection().ConnectionString;

var targetModel = context.GetService<IDesignTimeModel>().Model.GetRelationalModel();

if (targetModel == null)
{
    return Enumerable.Empty<MigrationOperation>().ToList();
}

接下來則是獲取資料庫表結構也就是資料庫模型

var databaseFactory = currentServiceProvider.GetService<IDatabaseModelFactory>();

var factory = currentServiceProvider.GetService<IScaffoldingModelFactory>();

var tables = context.Model.GetEntityTypes().Select(e => e.GetTableName()).ToList();

if (!tables.Any())
{
    return Enumerable.Empty<MigrationOperation>().ToList();
}

// 僅查詢當前上下文模型所對映表,否則比對資料庫表差異時,將會刪除非當前上下文所有表
var databaseModel = databaseFactory.Create(connectionString, new DatabaseModelFactoryOptions(tables: tables));

if (databaseModel == null)
{
    return Enumerable.Empty<MigrationOperation>().ToList();
}

這裡稍微需要注意的是,若是有多個不同上下文,肯定只查詢當前上下文所對應的模型結構,不然最後生成的指令碼會將其他上下文對應的表結構給刪除。緊接著,我們需要將資料庫模型轉換為上下文中的模型,即型別一致轉換,這就演變成了我們的源模型

var model = factory.Create(databaseModel, new ModelReverseEngineerOptions());

if (model == null)
{
    return Enumerable.Empty<MigrationOperation>().ToList();
}

var soureModel = model.GetRelationalModel();

接下來自熱而然就進行源模型和目標模型差異性比對,得到實際要進行的遷移操作

 var soureModel = model.GetRelationalModel();

//TODO Compare sourceModel vs targetModel

var modelDiffer = context.GetService<IMigrationsModelDiffer>();

var migrationOperations = modelDiffer.GetDifferences(soureModel, targetModel);

那接下來問題來了,拿到差異性遷移操作後,我們應該怎麼處理呢?留著各位思考下,我們下篇見

總結

本文給出了自動更新模型思路以及整個完整實現邏輯,剩餘內容我們下篇再敘,主要是沒心情寫,寫不下去了,今天我們就到此為止~