背景
現在信創是搞得如火如荼,在這個浪潮下,數據庫也是從之前熟悉的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;
可以解析爲如下的類及屬性:
如上就包含了索引、列定義、建表選項等等。
我們接下來就只需要根據這些字段,獲取數據並轉換爲對應的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就是註釋值。
提取出來後,格式化爲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,大致如下:
以上僅是部分,具體查看代碼
-
默認值處理
這塊也比較麻煩,比如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 |