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;
}

總結

本文我們重點介紹如何生成腳本文本以及對於不同數據庫需要進行對應邏輯處理存在的差異性,以及想完全通過代碼而非命令執行遷移所需要遵循對於不同數據庫配置不同列類型規範,避免每次都會進行差異性腳本執行,尤其是涉及開發人員手動更改列類型,帶來腳文本執行自動覆蓋的問題

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章