純分享:將MySql的建表DDL轉爲PostgreSql的DDL

背景

現在信創是搞得如火如荼,在這個浪潮下,數據庫也是從之前熟悉的Mysql換到了某國產數據庫。

該數據庫我倒是想吐槽吐槽,它是基於Postgre 9.x的基礎上改的,至於改了啥,我也沒去詳細瞭解,當初的數據庫POC測試和後續的選型沒太參與,但對於我一個開發人員的角度來說,它給我帶來的不便主要是客戶端GUI工具這塊。

我們讀寫數據庫,程序這塊還好,CURD代碼用到的語法,基本是sql標準兼容的那些,沒用多少mysql的特殊語法,所以這塊沒啥感覺。

客戶端GUI這塊,pg的客戶端軟件目前知道幾個:

  • navicat,公司沒采購正版,用不了,替代軟件是開源的dbeaver
  • pgAdmin,pg官方客戶端,結果不知道這個國產化過程中改了啥,用pgAdmin連上就各種報錯,放棄
  • dbeaver,這個倒是可以用,就是我感覺操作太麻煩了,太繁瑣

基於以上原因,一直用dbeaver來着,之前兩次把mysql項目的表結構換成pg,一次是寫了個亂七八糟的代碼來做建表語句轉換,一次是用dbeaver建的,太繁瑣了。

這次又來了個項目,我就換回了我熟悉的sqlyog(一款mysql客戶端),幾下就把表建好了(mysql版本),然後寫了個工具代碼,來把mysql的DDL轉換成pg的。

下面簡單介紹下這個轉換代碼。

技術選型

以前寫這種代碼,都是各種字符串操作(正則、匹配、替換等等),反正代碼最終是非常難以維護。這次就先去網上查了下,發現有人有類似需求,還發了文章:https://zhuanlan.zhihu.com/p/314069540

我發現其中利用了一個java庫,JSqlParser(https://github.com/JSQLParser/JSqlParser),我在網上也找了下其他的庫,java這塊沒有更好的了,遙遙領先。

其官方說明:

JSqlParser parses an SQL statement and translate it into a hierarchy of Java classes.

它支持解析sql語句這種非結構化文本爲結構化數據,比如,針對如下的一個建庫sql:

CREATE TABLE `xxl_job_log_report` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `trigger_day` datetime DEFAULT NULL COMMENT '調度-時間',
  `running_count` int(11) NOT NULL DEFAULT '0' COMMENT '運行中-日誌數量',
  `suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '執行成功-日誌數量',
  `fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '執行失敗-日誌數量',
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

可以解析爲如下的類及屬性:

image-20230902221909233

如上就包含了索引、列定義、建表選項等等。

我們接下來就只需要根據這些字段,獲取數據並轉換爲對應的Postgre的語法即可。

轉換效果

源碼:https://github.com/cctvckl/convertMysqlDdlToPostgre.git

對於以上的類,給大家看看轉換效果:

CREATE TABLE xxl_job_log_report (
    id serial PRIMARY KEY,
    trigger_day timestamp NULL,
    running_count int NOT NULL DEFAULT '0',
    suc_count int NOT NULL DEFAULT '0',
    fail_count int NOT NULL DEFAULT '0',
    update_time timestamp NULL
);
COMMENT ON COLUMN xxl_job_log_report.trigger_day IS '調度-時間';
COMMENT ON COLUMN xxl_job_log_report.running_count IS '運行中-日誌數量';
COMMENT ON COLUMN xxl_job_log_report.suc_count IS '執行成功-日誌數量';
COMMENT ON COLUMN xxl_job_log_report.fail_count IS '執行失敗-日誌數量';

這個sql,基本都滿足我們的要求了。

當然,我這個工具類,還沒特別完善,對於索引這塊,只支持了主鍵索引,其他索引類型,後面空了我補一下。

支持的DDL類型,目前僅限於create table和drop table,目前能滿足我個人需求了,反正mysqldump那些導出來的sql結構基本就這樣。

暫不支持DML,如insert那些。

代碼要點

整體邏輯

Statements statements = CCJSqlParserUtil.parseStatements(sqlContent);
for (Statement statement : statements.getStatements()) {
    if (statement instanceof CreateTable) {
        String sql = ProcessSingleCreateTable.process((CreateTable) statement);
        totalSql.append(sql).append("\n");
    } else if (statement instanceof Drop) {
        String sql = ProcessSingleDropTable.process((Drop) statement);
        totalSql.append(sql).append("\n");
    } else {
        throw new UnsupportedOperationException();
    }
}

如上,CCJSqlParserUtil 是 JSqlParser 的工具類,將我們的sql轉換爲一個一個的statement(即sql語句),我這邊利用instanceof檢查屬於哪種DDL,再調用對應的代碼進行處理,設計模式也懶得弄,if else寫起來多快。

數據準備:表註釋

List<String> tableOptionsStrings = createTable.getTableOptionsStrings();
String tableCommentSql = null;
int commentIndex = tableOptionsStrings.indexOf("COMMENT");
if (commentIndex != -1) {
    tableCommentSql = String.format("COMMENT ON TABLE %s IS %s;", tableFullyQualifiedName,tableOptionsStrings.get(commentIndex + 2));
}

解析出的表的相關屬性,全都被放在一個list中,我們根據COMMENT關鍵字定位索引,然後找後兩個,即是表註釋具體值。

數據準備:列註釋

由於我是直接在作者基礎上改的,https://zhuanlan.zhihu.com/p/314069540,所以也是像他那樣,複用了其代碼,提取每一列的註釋,邏輯也是根據COMMENT關鍵字找到index,然後index+1就是註釋值。

image-20230902224226311

提取出來後,格式化爲pg語法:

String.format("COMMENT ON COLUMN %s.%s IS %s;", table, column, commentValue);

數據準備:提取主鍵

Index primaryKey = createTable.getIndexes().stream()
    .filter((Index index) -> Objects.equals("PRIMARY KEY", index.getType()))
    .findFirst().orElse(null);

組裝sql:建表第一行

String createTableFirstLine = String.format("CREATE TABLE %s (", tableFullyQualifiedName);

組裝sql:主鍵列

這裏涉及數據類型轉換,如mysql中的bigint,在pg中,使用bigserial即可:

String dataType = primaryKeyColumnDefinition.getColDataType().getDataType();
if (Objects.equals("bigint", dataType)) {
    primaryKeyType = "bigserial";
} else if (Objects.equals("int", dataType)) {
    primaryKeyType = "serial";
} else if (Objects.equals("varchar", dataType)){
    primaryKeyType = primaryKeyColumnDefinition.getColDataType().toString();
}

String sql = String.format("%s %s PRIMARY KEY", primaryKeyColumnName, primaryKeyType);

組裝sql:其他列

這部分有幾塊:

  • 類型轉換,mysql的類型,轉換爲pg的,我這邊定義了一個map,大致如下:

    image-20230902224901408

​ 以上僅是部分,具體查看代碼

  • 默認值處理

    這塊也比較麻煩,比如mysql中的函數這種,如CURRENT_TIMESTAMP這種默認值,轉換爲pg中的對應函數,我大概定義了幾個,滿足當前需要:

    static {
        MYSQL_DEFAULT_TO_POSTGRE_DEFAULT.put("NULL", "NULL");
        MYSQL_DEFAULT_TO_POSTGRE_DEFAULT.put("CURRENT_TIMESTAMP", "CURRENT_TIMESTAMP");
        MYSQL_DEFAULT_TO_POSTGRE_DEFAULT.put("CURRENT_DATE", "CURRENT_DATE");
        MYSQL_DEFAULT_TO_POSTGRE_DEFAULT.put("CURRENT_TIME", "CURRENT_TIME");
    }
    
  • 刪除pg不支持的mysql語法

    // postgre不支持unsigned
    sourceSpec = sourceSpec.replaceAll("unsigned", "");
    // postgre不支持ON UPDATE CURRENT_TIMESTAMP
    sourceSpec = sourceSpec.replaceAll("ON UPDATE CURRENT_TIMESTAMP", "");
    
    

打印完整的pg語法sql

這塊就不說了,上面效果展示部分有。

生成出來的sql,會在項目根路徑下的target.sql文件中

總結

生成的target.sql文件,在idea中打開,如果有語法錯誤會飄紅,如果大家有java開發能力,直接debug改就行,不行就提issue,我看到了空了就改;

我之前拿着有語法錯誤的sql就去dbeaver執行了,報錯也不詳細,看得一臉懵,idea還是厲害。

參考資料

mysql官方的遷移指南,裏面包含了pg的各種類型對應到mysql的什麼類型
https://dev.mysql.com/doc/workbench/en/wb-migration-database-postgresql-typemapping.html
mysql中的各種類型查閱
https://dev.mysql.com/doc/refman/8.0/en/data-types.html
pg中的各種類型查閱,我看得低版本的,誰讓我們的信創數據庫是基於pg 9版本的呢
https://www.postgresql.org/docs/11/datatype-numeric.html#DATATYPE-INT

這邊直接貼一下吧,方便大家看:

Pg Source Type Taret MySQL Type Comment
INT INT
SMALLINT SMALLINT
BIGINT BIGINT
SERIAL INT Sets AUTO_INCREMENT in its table definition.
SMALLSERIAL SMALLINT Sets AUTO_INCREMENT in its table definition.
BIGSERIAL BIGINT Sets AUTO_INCREMENT in its table definition.
BIT BIT
BOOLEAN TINYINT(1)
REAL FLOAT
DOUBLE PRECISION DOUBLE
NUMERIC DECIMAL
DECIMAL DECIMAL
MONEY DECIMAL(19,2)
CHAR CHAR/LONGTEXT
NATIONAL CHARACTER CHAR/LONGTEXT
VARCHAR VARCHAR/MEDIUMTEXT/LONGTEXT
NATIONAL CHARACTER VARYING VARCHAR/MEDIUMTEXT/LONGTEXT
DATE DATE
TIME TIME
TIMESTAMP DATETIME
INTERVAL TIME
BYTEA LONGBLOB
TEXT LONGTEXT
CIDR VARCHAR(43)
INET VARCHAR(43)
MACADDR VARCHAR(17)
UUID VARCHAR(36)
XML LONGTEXT
JSON LONGTEXT
TSVECTOR LONGTEXT
TSQUERY LONGTEXT
ARRAY LONGTEXT
POINT POINT
LINE LINESTRING
LSEG LINESTRING
BOX POLYGON
PATH LINESTRING
POLYGON POLYGON
CIRCLE POLYGON
TXID_SNAPSHOT VARCHAR
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章