EntityFrameworkCore 模型自动更新(下)
话题
上一篇我们讨论到获取将要执行的迁移操作,到这一步为止,针对所有数据库都通用,在此之后需要生成SQL脚本对于不同数据库将有不同差异,我们一起来瞅一瞅
SQLite脚本生成差异
在上一篇拿到的迁移操作类即MigrationOperation为执行所有其他操作类的父类,比如添加列操作(AddColumnOperation),修改列操作(AlterColumnOperation)、创建表操作(CreateTableOperation)等等,我们知道SQLite不支持修改列,所以我们需要去除列修改操作,代码如下:
- // Sqlite不支持修改操作,所以需过滤修改迁移操作
- var operations = migrationOperations.Except(migrationOperations.Where(o => o is AlterColumnOperation)).ToList();
- if (!operations.Any())
- {
- return;
- }
然后获取生成SQL脚本接口,拿到执行操作命令类里面的脚本文本即可
- var migrationsSqlGenerator = context.GetService<IMigrationsSqlGenerator>();
- var commandList = migrationsSqlGenerator.Generate(operations);
- if (!commandList.Any())
- {
- return;
- }
- var sqlScript = string.Concat(commandList.Select(c => c.CommandText));
- if (string.IsNullOrEmpty(sqlScript))
- {
- return;
- }
MySQL脚本生成差异
因为我们可能会修改主键,此时Pomelo.EntityFrameworkCore.MySql使用的方式则是创建一个临时存储过程,先删除主键,然后则执行完相关脚本后,最后重建主键,然后删除临时存储过程,临时存储过程如下:
- #region Custom Sql
- #region BeforeDropPrimaryKey
- private const string BeforeDropPrimaryKeyMigrationBegin = @"DROP PROCEDURE IF EXISTS `POMELO_BEFORE_DROP_PRIMARY_KEY`;
- CREATE PROCEDURE `POMELO_BEFORE_DROP_PRIMARY_KEY`(IN `SCHEMA_NAME_ARGUMENT` VARCHAR(255), IN `TABLE_NAME_ARGUMENT` VARCHAR(255))
- BEGIN
- DECLARE HAS_AUTO_INCREMENT_ID TINYINT(1);
- DECLARE PRIMARY_KEY_COLUMN_NAME VARCHAR(255);
- DECLARE PRIMARY_KEY_TYPE VARCHAR(255);
- DECLARE SQL_EXP VARCHAR(1000);
- SELECT COUNT(*)
- INTO HAS_AUTO_INCREMENT_ID
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `Extra` = 'auto_increment'
- AND `COLUMN_KEY` = 'PRI'
- LIMIT 1;
- IF HAS_AUTO_INCREMENT_ID THEN
- SELECT `COLUMN_TYPE`
- INTO PRIMARY_KEY_TYPE
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `COLUMN_KEY` = 'PRI'
- LIMIT 1;
- SELECT `COLUMN_NAME`
- INTO PRIMARY_KEY_COLUMN_NAME
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `COLUMN_KEY` = 'PRI'
- LIMIT 1;
- SET SQL_EXP = CONCAT('ALTER TABLE `', (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA())), '`.`', TABLE_NAME_ARGUMENT, '` MODIFY COLUMN `', PRIMARY_KEY_COLUMN_NAME, '` ', PRIMARY_KEY_TYPE, ' NOT NULL;');
- SET @SQL_EXP = SQL_EXP;
- PREPARE SQL_EXP_EXECUTE FROM @SQL_EXP;
- EXECUTE SQL_EXP_EXECUTE;
- DEALLOCATE PREPARE SQL_EXP_EXECUTE;
- END IF;
- END;";
- private const string BeforeDropPrimaryKeyMigrationEnd = @"DROP PROCEDURE `POMELO_BEFORE_DROP_PRIMARY_KEY`;";
- #endregion BeforeDropPrimaryKey
- #region AfterAddPrimaryKey
- private const string AfterAddPrimaryKeyMigrationBegin = @"DROP PROCEDURE IF EXISTS `POMELO_AFTER_ADD_PRIMARY_KEY`;
- CREATE PROCEDURE `POMELO_AFTER_ADD_PRIMARY_KEY`(IN `SCHEMA_NAME_ARGUMENT` VARCHAR(255), IN `TABLE_NAME_ARGUMENT` VARCHAR(255), IN `COLUMN_NAME_ARGUMENT` VARCHAR(255))
- BEGIN
- DECLARE HAS_AUTO_INCREMENT_ID INT(11);
- DECLARE PRIMARY_KEY_COLUMN_NAME VARCHAR(255);
- DECLARE PRIMARY_KEY_TYPE VARCHAR(255);
- DECLARE SQL_EXP VARCHAR(1000);
- SELECT COUNT(*)
- INTO HAS_AUTO_INCREMENT_ID
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `COLUMN_NAME` = COLUMN_NAME_ARGUMENT
- AND `COLUMN_TYPE` LIKE '%int%'
- AND `COLUMN_KEY` = 'PRI';
- IF HAS_AUTO_INCREMENT_ID THEN
- SELECT `COLUMN_TYPE`
- INTO PRIMARY_KEY_TYPE
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `COLUMN_NAME` = COLUMN_NAME_ARGUMENT
- AND `COLUMN_TYPE` LIKE '%int%'
- AND `COLUMN_KEY` = 'PRI';
- SELECT `COLUMN_NAME`
- INTO PRIMARY_KEY_COLUMN_NAME
- FROM `information_schema`.`COLUMNS`
- WHERE `TABLE_SCHEMA` = (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA()))
- AND `TABLE_NAME` = TABLE_NAME_ARGUMENT
- AND `COLUMN_NAME` = COLUMN_NAME_ARGUMENT
- AND `COLUMN_TYPE` LIKE '%int%'
- AND `COLUMN_KEY` = 'PRI';
- SET SQL_EXP = CONCAT('ALTER TABLE `', (SELECT IFNULL(SCHEMA_NAME_ARGUMENT, SCHEMA())), '`.`', TABLE_NAME_ARGUMENT, '` MODIFY COLUMN `', PRIMARY_KEY_COLUMN_NAME, '` ', PRIMARY_KEY_TYPE, ' NOT NULL AUTO_INCREMENT;');
- SET @SQL_EXP = SQL_EXP;
- PREPARE SQL_EXP_EXECUTE FROM @SQL_EXP;
- EXECUTE SQL_EXP_EXECUTE;
- DEALLOCATE PREPARE SQL_EXP_EXECUTE;
- END IF;
- END;";
- private const string AfterAddPrimaryKeyMigrationEnd = @"DROP PROCEDURE `POMELO_AFTER_ADD_PRIMARY_KEY`;";
- #endregion AfterAddPrimaryKey
- #endregion
我想大部分童鞋使用MySQL时,基本没迁移过,在实际迁移时会可能会抛出如下异常
- Incorrect table definition; there can be only one auto column and it must be defined as a key
此问题一直遗留至今并未得到很好的解决,见链接《https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/issues/711》,根本问题在于主键唯一约束问题,所以我们在得到脚本文本后,进行如下操作变换即可
- var migrationsSqlGenerator = context.GetService<IMigrationsSqlGenerator>();
- var commandList = migrationsSqlGenerator.Generate(migrationOperations);
- if (!commandList.Any())
- {
- return;
- }
- var sqlScript = string.Concat(commandList.Select(c => c.CommandText));
- if (string.IsNullOrEmpty(sqlScript))
- {
- return;
- }
- var builder = new StringBuilder();
- builder.AppendJoin(string.Empty, GetMigrationCommandTexts(migrationOperations, true));
- builder.Append(sqlScript);
- builder.AppendJoin(string.Empty, GetMigrationCommandTexts(migrationOperations, false));
- var sql = builder.ToString();
- sql = sql.Replace("AUTO_INCREMENT", "AUTO_INCREMENT UNIQUE");
PostgreSQL脚本生成差异
要操作PG数据库,我们基本都使用Npgsql.EntityFrameworkCore.PostgreSQL来进行,在查询获取数据库模型时基本也会抛出如下异常
- Cannot parse collation name from annotation: pg_catalog.C.UTF-8
PG数据库基于架构(schema)和排序规则(collation),但在Npg中还不能很好支持,比如PG数据库存在如下架构和排序规则
直到Npg EF Core 7预览版该问题仍未得到解决,作为遗留问题一直存在,当由第一列(schema)和第二列(collation)查询以点组合,在校验时以点分隔,数组长度超过3位,必定抛出异常,所以目前排序规则仅支持default和ci_x_icu,源码如下:
- // TODO: This would be a safer operation if we stored schema and name in the annotation value (see Sequence.cs).
- // Yes, this doesn't support dots in the schema/collation name, let somebody complain first.
- var schemaAndName = annotation.Name.Substring(KdbndpAnnotationNames.CollationDefinitionPrefix.Length).Split('.');
- switch (schemaAndName.Length)
- {
- case 1:
- return (null, schemaAndName[0], elements[0], elements[1], elements[2], isDeterministic);
- case 2:
- return (schemaAndName[0], schemaAndName[1], elements[0], elements[1], elements[2], isDeterministic);
- default:
- throw new ArgumentException($"Cannot parse collation name from annotation: {annotation.Name}");
- }
其他细节考虑
我们知道不同数据库肯定各有差异,差异性主要体现在两点上,其一有大小写区分,比如SQL Server并不区分,而MySQL虽区分但可以在配置文件中设置,人大金仓也好,高斯数据库也好,底层都是基于PG,所以都区分大小写,同时二者在部署时就需明确是否区分大小写,而且对于日期类型还存在时区问题,其二,不同数据库列类型不一样,比如SQLite仅有INTEGER和TEXT等类型,而SQL Server有NVARCHAR和VARCHAR,但PG数据库仅有VARCHAR,若我们对模型列类型以及长度等等不能有统一规范,那么完全通过代码迁移势必会带来一个问题,那就是每次都可能会得出迁移差异。比如我们使用SQL Server数据库,模型如下:
- [Table("test1")]
- public class Test
- {
- [Column("id")]
- public int Id { get; set; }
- [Column("name")]
- public string Name { get; set; }
- }
我们对属性Name类型和长度并未做任何处理,若我们在实际开发过程中,在数据库中将该列类型修改为VARCHAR(30),我们知道EF Core通过命令迁移生成数据库模型时,该列将使用默认约定即映射为NVARCHAR(MAX),通过代码生成的迁移脚本文本则为如下
- DECLARE @var0 sysname;
- SELECT @var0 = [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'[test1]') AND [c].[name] = N'name');
- IF @var0 IS NOT NULL EXEC(N'ALTER TABLE [test1] DROP CONSTRAINT [' + @var0 + '];');
- ALTER TABLE [test1] ALTER COLUMN [name] nvarchar(max) NULL;
- DECLARE @var1 sysname;
- SELECT @var1 = [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'[test1]') AND [c].[name] = N'id');
- IF @var1 IS NOT NULL EXEC(N'ALTER TABLE [test1] DROP CONSTRAINT [' + @var1 + '];');
- ALTER TABLE [test1] ALTER COLUMN [id] int NOT NULL;
所以基于完全通过代码而非命令迁移,前提应该是针对不同数据库定义属于对应数据库支持的列类型格式,如此这般才能避免每次都可能会生成差异性迁移脚本文本从而执行,比如字符串类型可能为中文,此时对于SQL Server就定义为NVARCHAR或其他,而SQLite为TEXT,PG数据库则是VARCHAR或其他,最后再来一下
- if (context.Database.IsSqlite())
- {
- ioTMigrationFactory = new IoTSqlliteMigrationFactory();
- }
- else if (context.Database.IsSqlServer())
- {
- ioTMigrationFactory = new IoTSqlServerMigrationFactory();
- }
- else if (context.Database.IsMySql())
- {
- ioTMigrationFactory = new IoTMySqlMigrationFactory();
- }
- else if (context.Database.IsNpgsql())
- {
- ioTMigrationFactory = new IoTPostgreSQLMigrationFactory();
- }
- else if (context.Database.IsKdbndp())
- {
- ioTMigrationFactory = new IoTKdbndpMigrationFactory();
- }
- if (ioTMigrationFactory == default(IIoTMigrationFactory))
- {
- return;
- }
总结
本文我们重点介绍如何生成脚本文本以及对于不同数据库需要进行对应逻辑处理存在的差异性,以及想完全通过代码而非命令执行迁移所需要遵循对于不同数据库配置不同列类型规范,避免每次都会进行差异性脚本执行,尤其是涉及开发人员手动更改列类型,带来脚文本执行自动覆盖的问题
EntityFrameworkCore 模型自动更新(下)的更多相关文章
- EntityFrameworkCore 模型自动更新(上)
话题 嗯,距离上一次写博文已经过去近整整十个月,还是有一些思考,但还是变得懒惰了,心思也不再那么专注,有点耗费时间,学习也有点停滞不前,那就顺其自然,随心所欲吧,等哪天心血来潮,想写了就写写 模型自动 ...
- EntityFrameworkCore使用Migrations自动更新数据库
EntityFrameworkCore使用Migrations自动更新数据库 系统环境:Win10 IDE:VS2017 RC4 .netcore版本:1.1 一.新建ASP.NET Core Web ...
- C# Winform下一个热插拔的MIS/MRP/ERP框架14(自动更新)
对于软件来说,启用自动更新是非常必要的. 根据软件的应用场景,我们可以设计不同的更新模型. 目前,IMES框架运行在.Net framework 4.0下面,使用的Win系统版本在Win7,域内管控, ...
- Linux下搭建SVN服务器及自动更新项目文件到web目录(www)的方法
首先搭建SVN服务器 1,安装SVN服务端 直接用apt-get或yum安装subversion即可(当然也可以自己去官方下载安装) sudo apt-get install subversion ...
- Cordova webapp实战开发:(5)如何写一个Andorid下自动更新的插件?
在 <Cordova webapp实战开发:(4)Android环境搭建>中我们搭建好了开发环境,也给大家布置了调用插件的预习作业,做得如何了呢?今天我们来学一下如何自己从头建立一个And ...
- windows下svn自动更新
配置hooks下post-commit.bat文件,文件内容如下 @echo offSET REPOS=%1SET REV=%2SET DIR=%REPOS%/hooksSET PATH=%PATH% ...
- 分享下使用 svn,测试服务器代码自动更新、线上服务器代码手动更新的配置经验
分享下使用 svn,测试服务器代码自动更新.线上服务器代码手动更新的配置经验 利用SVN的POST-COMMIT钩子自动部署代码 Linux SVN 命令详解 Linux SVN 命令详解2 使用sv ...
- windows,linux下SVN实现自动更新WEB目录
通过SVN进行版本库管理,每次提交后,都要在SVN服务器更新最新上传的版本到WEB目录进行同步.操作比较烦琐,而且效率也低.使用SVN钩子脚本进行WEB目录同步,可很好的解决这方面的问题.由于测试机器 ...
- Code First 下自动更新数据库结构(Automatic Migrations)
示例 Web.config <?xml version="1.0" encoding="utf-8"?> <configuration> ...
随机推荐
- 02 CSS块级元素和行内元素
02 CSS块级元素和行内元素 划分依据:根据标签内部可以存放的元素内容不同进行划分,它与CSS样式无关. 要先了解这个 得先了解 什么是容器级别的标签和文本级? 容器级标签 什么是容器级标签? 内部 ...
- go-zero微服务实战系列(七、请求量这么高该如何优化)
前两篇文章我们介绍了缓存使用的各种最佳实践,首先介绍了缓存使用的基本姿势,分别是如何利用go-zero自动生成的缓存和逻辑代码中缓存代码如何写,接着讲解了在面对缓存的穿透.击穿.雪崩等常见问题时的解决 ...
- 6G显卡显存不足出现CUDA Error:out of memory解决办法
从6月初开始,6G显存的显卡开始出现CUDA Error:out of memory的问题,这是因为dag文件一直在增加,不过要增加到6G还需要最少两年的时间. 现在出现问题的原因是1.内核太古老 ...
- Linux文件查找实现
文件查找 locate:非实时查找(依赖数据库的方式) find(实时查找) locate:-- 模糊搜索(不适合经常改变的文件) locate 查询系统上预建的文件索引数据库 /var/lib/ml ...
- SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示“正在还原”的解决方法
SQL Server数据库 备份A库,然后删除A库,再还原A库,此时数据库一直显示"正在还原"的解决方法: A库一直显示"正在还原". 在这种状态下,由于未提交 ...
- Linux系列之查找命令
前言 Linux 有四个常用的查找命令:locate.whereis.which 和 find.本文介绍它们的区别和简单用法. locate命令 这个命令将检查你的整个文件系统,并找到该关键词的每一次 ...
- Pytorch从0开始实现YOLO V3指南 part3——实现网络前向传播
本节翻译自:https://blog.paperspace.com/how-to-implement-a-yolo-v3-object-detector-from-scratch-in-pytorch ...
- 【跟着大佬学JavaScript】之节流
前言 js的典型的场景 监听页面的scroll事件 拖拽事件 监听鼠标的 mousemove 事件 ... 这些事件会频繁触发会影响性能,如果使用节流,降低频次,保留了用户体验,又提升了执行速度,节省 ...
- 循环结构-for循环和while循环
循环语句1--for for循环语句格式: for(初始化表达式①; 布尔表达式②; 步进表达式④){ 循环体③ } 执行流程 执行顺序:①②③④>②③④>②③④-②不满足为止. ①负责完 ...
- SpringBoot集成文件 - 集成POI之Excel导入导出
Apache POI 是用Java编写的免费开源的跨平台的 Java API,Apache POI提供API给Java程序对Microsoft Office格式档案读和写的功能.本文主要介绍通过Spr ...