EntityFrameworkCore 模型自动更新(上)
话题
嗯,距离上一次写博文已经过去近整整十个月,还是有一些思考,但还是变得懒惰了,心思也不再那么专注,有点耗费时间,学习也有点停滞不前,那就顺其自然,随心所欲吧,等哪天心血来潮,想写了就写写
模型自动更新方案研究(上)
一般团队人数很少时,使用EF Core内置迁移基本已满足,满足的基本前提首先要生成迁移文件,然后和数据库进行对比,但团队人数一多,迁移文件等等涉及提交冲突等等,所以大部分情况下,我个人认为EF Core迁移基本没啥用,这玩意用不起来,尤其涉及到版本分支很多情况下,切换不同分支所使用的数据可能也会不同,我们常用MySql数据库,同时适配了人大金仓数据库、高斯数据库(GaussDb for opengauss),其对应的表结构有一些差异性,列类型也会有很大差异性,这对开发人员和测试人员来讲就是深深的折磨和痛苦,大部分时间会花在保持数据库表结构和模型一致,否则要么运行不起来,要么测试功能各种有问题,所以想想通过自动化方式来解决这个问题,本文还是分上下两篇写好了。
那么解决此问题的思路是怎样的呢?同时适配多套数据库,重写一套?那是不阔能的,既然我们可以通过dotnet ef命令来进行迁移,通过和数据库表结构进行对比,从而生成迁移文件,迁移文件包含即将要执行的差异性脚本,从这个角度来看的话,我们从迁移类反堆即可得到生成的脚本以及和数据库进行对比操作方法,初步设想理论上应该行得通,只需花时间了解下源码就好,通过前期两天的啃源码,最终啃出百把行代码即可自动更新模型到数据库,当然这个过程中还涉及一些要考虑的细节,我们一一再叙。接下来我们以MySql为例讲讲整个过程,其他数据库依葫芦画瓢就好(比如使用SqlServer,将对应后缀MySql替换为SqlServer就好,同理对于Npgsql等等亦是如此),首先甩出如下代码:
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);
那接下来问题来了,拿到差异性迁移操作后,我们应该怎么处理呢?留着各位思考下,我们下篇见
总结
本文给出了自动更新模型思路以及整个完整实现逻辑,剩余内容我们下篇再叙,主要是没心情写,写不下去了,今天我们就到此为止~