话题

上一篇我们讨论到获取将要执行的迁移操作,到这一步为止,针对所有数据库都通用,在此之后需要生成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 模型自动更新(下)的更多相关文章

  1. EntityFrameworkCore 模型自动更新(上)

    话题 嗯,距离上一次写博文已经过去近整整十个月,还是有一些思考,但还是变得懒惰了,心思也不再那么专注,有点耗费时间,学习也有点停滞不前,那就顺其自然,随心所欲吧,等哪天心血来潮,想写了就写写 模型自动 ...

  2. EntityFrameworkCore使用Migrations自动更新数据库

    EntityFrameworkCore使用Migrations自动更新数据库 系统环境:Win10 IDE:VS2017 RC4 .netcore版本:1.1 一.新建ASP.NET Core Web ...

  3. C# Winform下一个热插拔的MIS/MRP/ERP框架14(自动更新)

    对于软件来说,启用自动更新是非常必要的. 根据软件的应用场景,我们可以设计不同的更新模型. 目前,IMES框架运行在.Net framework 4.0下面,使用的Win系统版本在Win7,域内管控, ...

  4. Linux下搭建SVN服务器及自动更新项目文件到web目录(www)的方法

    首先搭建SVN服务器 1,安装SVN服务端 直接用apt-get或yum安装subversion即可(当然也可以自己去官方下载安装) sudo apt-get install subversion   ...

  5. Cordova webapp实战开发:(5)如何写一个Andorid下自动更新的插件?

    在 <Cordova webapp实战开发:(4)Android环境搭建>中我们搭建好了开发环境,也给大家布置了调用插件的预习作业,做得如何了呢?今天我们来学一下如何自己从头建立一个And ...

  6. windows下svn自动更新

    配置hooks下post-commit.bat文件,文件内容如下 @echo offSET REPOS=%1SET REV=%2SET DIR=%REPOS%/hooksSET PATH=%PATH% ...

  7. 分享下使用 svn,测试服务器代码自动更新、线上服务器代码手动更新的配置经验

    分享下使用 svn,测试服务器代码自动更新.线上服务器代码手动更新的配置经验 利用SVN的POST-COMMIT钩子自动部署代码 Linux SVN 命令详解 Linux SVN 命令详解2 使用sv ...

  8. windows,linux下SVN实现自动更新WEB目录

    通过SVN进行版本库管理,每次提交后,都要在SVN服务器更新最新上传的版本到WEB目录进行同步.操作比较烦琐,而且效率也低.使用SVN钩子脚本进行WEB目录同步,可很好的解决这方面的问题.由于测试机器 ...

  9. Code First 下自动更新数据库结构(Automatic Migrations)

    示例 Web.config <?xml version="1.0" encoding="utf-8"?> <configuration> ...

随机推荐

  1. js 表面学习 - 认识结构

    JavaScript 语句由以下构成: 值.运算符.表达式.关键词和注释. 这条语句告诉浏览器在 id="demo" 的 HTML 元素中输出 "Hello Kitty. ...

  2. nginx源码层面探究request_time、upstream_response_time、upstream_connect_time与upstream_header_time指标具体含义

    背景概述 最近计划着重分析一下线上各api的HTTP响应耗时情况,检查是否有接口平均耗时.99分位耗时等相关指标过大的情况,了解到nginx统计请求耗时有四个指标:request_time.upstr ...

  3. 5.RDD操作综合实例

    一.词频统计 A. 分步骤实现 1.准备文件 (1)下载小说或长篇新闻稿 (2)上传到hdfs上 2.读文件创建RDD 3.分词 4. ·排除大小写lower(),map() ·标点符号re.spli ...

  4. Tapdata “设擂招贤”携手 LeetCode 举办全球极客技术竞赛

      2021年11月28日 Tapdata 专场全球极客技术竞赛将在 LeetCode 平台开赛,面向程序员"设擂招贤",打擂成功的前50名挑战者将优先获得 Tapdata 高端技 ...

  5. 当mysql表从压缩表变成普通表会发生什么

    前言 本文章做了把mysql表从压缩表过渡到普通表的实验过程,看看压缩表变成普通表会发生什么?本文针对mysql5.7和mysql8分别进行了实验. 1.什么是表压缩 在介绍压缩表变成普通表前,首先给 ...

  6. Solution -「构造」专练

    记录全思路过程和正解分析.全思路过程很 navie,不过很下饭不是嘛.会持续更新的(应该). 「CF1521E」Nastia and a Beautiful Matrix Thought. 要把所有数 ...

  7. Dubbo源码(五) - 服务目录

    前言 本文基于Dubbo2.6.x版本,中文注释版源码已上传github:xiaoguyu/dubbo 今天,来聊聊Dubbo的服务目录(Directory).下面是官方文档对服务目录的定义: 服务目 ...

  8. Frame双向通信插件FrameDataTrans

    FrameDataTrans教程 博客园 乳鸽菌 20220729 核心原理是使用postMessage发送数据,window.addEventListener("message" ...

  9. 众妙之门玄之又玄,游戏系统中的伪随机(Pseudo-Randomization)和真随机(True-Randomization)算法实现Python3

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_212 有人说,如果一个人相信运气,那么他一定参透了人生.想象一下,如果你在某款moba游戏中,在装备平平,队友天坑的情况下,却刀刀 ...

  10. React报错之Style prop value must be an object

    正文从这开始~ 总览 在React中,当我们为元素的style 属性传递字符串时,会产生"Style prop value must be an object"警告.为了解决该警告 ...