異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單

青石路發表於2024-05-06

開心一刻

今天坐沙發上看電視,旁邊的老婆拿著手機貼了過來
老婆:老公,這次出門旅遊,機票我準備買了哈
我:嗯
老婆:你、我、你爸媽、我爸媽,一共六張票
老婆:這上面還有意外保險,要不要買?
我:都特麼團滅了,還買啥保險?

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單

異構資料來源同步

概念介紹

  • 資料來源,不只是包含關係型資料庫,還包括 NoSQL、數倉、中介軟體、ftp 等等,凡是有儲存功能的都算
  • 異構,兩端的資料來源的結構存在差異,比如列數不一致、列型別不一致等等
  • 同步,將源資料來源的資料同步到目標資料來源,包括資料讀取、轉換和寫入過程

所以,異構資料來源同步就是指在不同型別或格式的資料來源之間傳輸和同步資料的過程

同步策略

主要有兩種同步策略:離線同步實時同步 ,各有其特點和適用場景
但是,這些我今天都不講,就吊吊你們胃口

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
如果你們想了解,自己去查吧 今天我就要逆襲一把,將 `離線同步` 中的一個小配角轉正成主角!

表結構同步

異構資料來源同步 整個主線劇情中,資料同步 才是真正的主角

表結構同步 只能算活不過三集的那種配角

但今天不拍主線劇情,我要拍個番外篇來重點講 表結構同步 ,我是導演嘛,當然我說了算

背景說明

主要是針對關係型資料庫,當目標資料來源的表不存在時,則先在目標資料來源建立目標表,然後進行資料的同步

比如:從 MySQL 的表 tbl_t1 同步到 SQL Server 的表 tbl_tt ,若 tbl_tt 不存在,則根據 tbl_t1 的表結構建立 tbl_tt

所以這裡就涉及到表結構的同步,也正是本文的主角!

如何實現

透過 jdbc 來實現,具體實現步驟如下

  1. 透過 jdbc 獲取後設資料資訊:表後設資料、列後設資料、主鍵後設資料、索引後設資料

  2. 根據後設資料拼接目標表的建表 SQL

  3. 透過 jdbc ,根據建表 SQL,在目標資料來源建立目標表

第 3 步實現比較容易,難得是第 1、2步
雖然前路坑很多,但你們不要慌,我已經替你們趟掉很多了

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
我們以 `MySQL ` 為例,假設我們庫 `test` 下有表 `tbl_sync`
CREATE TABLE `tbl_sync` (
  `c_bigint_auto` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT 'bigint 型別',
  `c_bigint` bigint DEFAULT NULL COMMENT 'bigint 型別',
  `c_vachar` varchar(100) NOT NULL COMMENT 'varchar 型別',
  `c_char` char(32) NOT NULL COMMENT 'char 型別',
  `c_text` text NOT NULL COMMENT 'text 型別',
  `c_decimal_4` decimal(15,4) NOT NULL DEFAULT '5000.0000' COMMENT 'decimal 型別',
  `c_decimal_0` decimal(10,0) DEFAULT NULL COMMENT 'decimal 型別',
  `c_blob` blob COMMENT 'blob 型別',
  `c_bit` bit(1) DEFAULT NULL COMMENT 'bit 型別',
  `c_tinyint` tinyint DEFAULT NULL COMMENT 'tinyint 型別',
  `c_binary` binary(10) DEFAULT NULL COMMENT 'binary 型別',
  `c_float` float(13,0) DEFAULT NULL COMMENT 'float 型別',
  `c_double` double(23,0) DEFAULT NULL COMMENT 'double 型別',
  `c_varbinary` varbinary(20) DEFAULT NULL COMMENT 'varbinary 型別',
  `c_longblob` longblob COMMENT 'longblob 型別',
  `c_longtext` longtext COMMENT 'longtext 型別',
  `c_json` json DEFAULT NULL COMMENT 'json 型別',
  `c_date` date DEFAULT NULL COMMENT 'date 型別',
  `c_time` time(2) DEFAULT NULL COMMENT 'time 型別',
  `c_datetime` datetime(3) DEFAULT NULL COMMENT 'datetime 型別',
  `c_timestamp` timestamp(4) NULL DEFAULT NULL COMMENT 'timestamp 型別',
  `c_year` year DEFAULT NULL COMMENT 'year 型別',
  PRIMARY KEY (`c_vachar`,`c_char`,`c_bigint_auto`),
  UNIQUE KEY `uk_id` (`c_bigint_auto`),
  KEY `idx_name_salary` (`c_vachar`,`c_decimal_4`)
) COMMENT='包含各種型別列的同步表';

現在需要將其同步到另一個 MySQLobj_db

表後設資料

表的元資訊比較少,包括表名、表型別、表說明(表註釋)等,其他的,類似字符集、排序規則等,就繼承資料庫的

表名,我想你們都知道,也就是對應上面的 tbl_sync

表說明(表註釋)你們肯定也知道,對應上面的 包含各種型別列的同步表

那表型別是什麼,你們還知道嗎?

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
我們通常說的表是狹義上的表,也就是 `基本表`,是最常見的表型別,用於儲存具有明確定義的列和資料型別的資料

tbl_sync 就是 基本表 ,但廣義上的表還包括 檢視臨時表系統表 等等

下文都是基於 基本表 ,大家需要注意這個前提

透過 jdbc 獲取 表後設資料,非常簡單,直接看程式碼

Connection connection = dataSource.getConnection();
DatabaseMetaData databaseMetaData = connection.getMetaData();
ResultSet tableResultSet = databaseMetaData.getTables(connection.getCatalog(), connection.getSchema(),
		"tbl_sync", new String[]{"TABLE"});
while (tableResultSet.next()) {
	System.out.println("tableCatalog = " + tableResultSet.getString("TABLE_CAT"));
	System.out.println("tableSchema = " + tableResultSet.getString("TABLE_SCHEM"));
	System.out.println("tableName = " + tableResultSet.getString("TABLE_NAME"));
	System.out.println("tableType = " + tableResultSet.getString("TABLE_TYPE"));
	System.out.println("remarks = " + tableResultSet.getString("REMARKS"));
}

輸出結果

tableCatalog = test
tableSchema = null
tableName = tbl_sync
tableType = TABLE
remarks = 包含各種型別列的同步表

一般我們只需要關注: TABLE_NAMETABLE_TYPEREMARKS

我們看下 java.sql.DatabaseMetaData#getTables 說明

點選檢視程式碼
/**
 * Retrieves a description of the tables available in the given catalog.
 * Only table descriptions matching the catalog, schema, table
 * name and type criteria are returned.  They are ordered by
 * <code>TABLE_TYPE</code>, <code>TABLE_CAT</code>,
 * <code>TABLE_SCHEM</code> and <code>TABLE_NAME</code>.
 * <P>
 * Each table description has the following columns:
 *  <OL>
 *  <LI><B>TABLE_CAT</B> String {@code =>} table catalog (may be <code>null</code>)
 *  <LI><B>TABLE_SCHEM</B> String {@code =>} table schema (may be <code>null</code>)
 *  <LI><B>TABLE_NAME</B> String {@code =>} table name
 *  <LI><B>TABLE_TYPE</B> String {@code =>} table type.  Typical types are "TABLE",
 *                  "VIEW", "SYSTEM TABLE", "GLOBAL TEMPORARY",
 *                  "LOCAL TEMPORARY", "ALIAS", "SYNONYM".
 *  <LI><B>REMARKS</B> String {@code =>} explanatory comment on the table
 *  <LI><B>TYPE_CAT</B> String {@code =>} the types catalog (may be <code>null</code>)
 *  <LI><B>TYPE_SCHEM</B> String {@code =>} the types schema (may be <code>null</code>)
 *  <LI><B>TYPE_NAME</B> String {@code =>} type name (may be <code>null</code>)
 *  <LI><B>SELF_REFERENCING_COL_NAME</B> String {@code =>} name of the designated
 *                  "identifier" column of a typed table (may be <code>null</code>)
 *  <LI><B>REF_GENERATION</B> String {@code =>} specifies how values in
 *                  SELF_REFERENCING_COL_NAME are created. Values are
 *                  "SYSTEM", "USER", "DERIVED". (may be <code>null</code>)
 *  </OL>
 *
 * <P><B>Note:</B> Some databases may not return information for
 * all tables.
 *
 * @param catalog a catalog name; must match the catalog name as it
 *        is stored in the database; "" retrieves those without a catalog;
 *        <code>null</code> means that the catalog name should not be used to narrow
 *        the search
 * @param schemaPattern a schema name pattern; must match the schema name
 *        as it is stored in the database; "" retrieves those without a schema;
 *        <code>null</code> means that the schema name should not be used to narrow
 *        the search
 * @param tableNamePattern a table name pattern; must match the
 *        table name as it is stored in the database
 * @param types a list of table types, which must be from the list of table types
 *         returned from {@link #getTableTypes},to include; <code>null</code> returns
 * all types
 * @return <code>ResultSet</code> - each row is a table description
 * @exception SQLException if a database access error occurs
 * @see #getSearchStringEscape
 */

相信你們都能看懂,我只強調下 TABLE_TYPE

其值包括

  • TABLE
  • VIEW
  • SYSTEM TABLE
  • GLOBAL TEMPORARY,LOCAL TEMPORARY
  • ALIAS
  • SYNONYM

列後設資料

列元資訊比較多一點,包括列名、列型別、列型別名、是否自增、是否允許NULL、列大小、小數位數、預設值、列說明(列註釋)等

透過 jdbc 獲取 列後設資料 也很簡單,直接看程式碼

Connection connection = dataSource.getConnection();
DatabaseMetaData databaseMetaData = connection.getMetaData();
ResultSet columnResultSet = databaseMetaData.getColumns(connection.getCatalog(), connection.getSchema(),
		"tbl_sync", null);
while (columnResultSet.next()) {
	System.out.println("ColumnName = " + columnResultSet.getString("COLUMN_NAME"));
	System.out.println("ColumnType = " + columnResultSet.getInt("DATA_TYPE"));
	System.out.println("ColumnTypeName = " + columnResultSet.getString("TYPE_NAME"));
	System.out.println("isAutoIncrement = " + columnResultSet.getString("IS_AUTOINCREMENT"));
	System.out.println("isNullable = " + columnResultSet.getString("IS_NULLABLE"));
	System.out.println("Precision = " + columnResultSet.getInt("COLUMN_SIZE"));
	System.out.println("Scale = " + columnResultSet.getInt("DECIMAL_DIGITS"));
	System.out.println("DefaultValue = " + columnResultSet.getString("COLUMN_DEF"));
	System.out.println("Remarks = " + columnResultSet.getString("REMARKS"));
	System.out.println("===================================");
}

輸出結果

ColumnName = c_bigint_auto
ColumnType = -5
ColumnTypeName = BIGINT UNSIGNED
isAutoIncrement = YES
isNullable = NO
Precision = 20
Scale = 0
DefaultValue = null
Remarks = bigint 型別
===================================
ColumnName = c_bigint
ColumnType = -5
ColumnTypeName = BIGINT
isAutoIncrement = NO
isNullable = YES
Precision = 19
Scale = 0
DefaultValue = null
Remarks = bigint 型別
===================================
ColumnName = c_vachar
ColumnType = 12
ColumnTypeName = VARCHAR
isAutoIncrement = NO
isNullable = NO
Precision = 100
Scale = 0
DefaultValue = null
Remarks = varchar 型別
===================================
...

ColumnType 的值是 java.sql.Types-5 即是 java.sql.Types#BIGINT

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
那是不是根據 `ColumnType` 就可以推斷出資料庫列型別了?

我們看下如下輸出

ColumnName = c_longtext
ColumnType = -1
ColumnTypeName = LONGTEXT
isAutoIncrement = NO
isNullable = YES
Precision = 2147483647
Scale = 0
DefaultValue = null
Remarks = longtext 型別
===================================
ColumnName = c_json
ColumnType = -1
ColumnTypeName = JSON
isAutoIncrement = NO
isNullable = YES
Precision = 1073741824
Scale = 0
DefaultValue = null
Remarks = json 型別
===================================

ColumnType = -1 是對應 LONGTEXT ,還是對應 JSON

我們再看一個

ColumnName = c_datetime
ColumnType = 93
ColumnTypeName = DATETIME
isAutoIncrement = NO
isNullable = YES
Precision = 23
Scale = 0
DefaultValue = null
Remarks = datetime 型別
===================================
ColumnName = c_timestamp
ColumnType = 93
ColumnTypeName = TIMESTAMP
isAutoIncrement = NO
isNullable = YES
Precision = 24
Scale = 0
DefaultValue = null
Remarks = timestamp 型別
===================================

ColumnType = 93 是對應 DATETIME,還是對應 TIMESTAMP

這說明不能透過 java.sql.Types 精準確認列的資料庫型別!!!

那怎麼辦?

我相信你們已經看到了列的另一個後設資料:ColumnTypeName

它不就是 源資料來源 中列列型別嗎?

比如列 c_timestamp 的型別不就是 TIMESTAMP 嗎,絲毫不差,準確的很!

但是我們不能忘了我們的初衷:拼接目標表的建表 SQL

透過 ColumnTypeName 能不能對應到目標表的列型別?

直接使用,肯定是不行的,關係型資料庫之間的型別不是完全一一對應的,比如 MySQLDATETIMEOracle 是沒有的

那可不可以透過 ColumnTypeName 來對映了,比如 DATETIME 對映到 OracleDATE

理論上來說是可行的,但是,問題又來了!

我們是透過 jdbc 來完成對映的,它只提供了int 型別的 java.sql.Types ,並未提供 String 型別的 java.sql.Types

莫非你要自實現 String 型別的 java.sql.Types ? 你窮舉的過來嗎?

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
所以我們需要根據 `java.sql.Types` 對源資料來源的列型別最大相容性獲取,而不是百分之百的精準獲取

例如:java.sql.Types#LONGVARCHAR 就當作列型別 LONGTEXT ,然後向目標資料來源對映

如果想更精準,則再結合 ColumnTypeName 的值向目標資料來源對映

總之一句話:ColumnType 主導,ColumnTypeName 輔助,完成目標資料來源列對映

java.sql.DatabaseMetaData#getColumns 能獲取的後設資料不侷限於上述示例中的那些

大家可以去看下其原始碼註釋,因為太長了,我就不貼了,我們重點看下 COLUMN_SIZE

* The COLUMN_SIZE column specifies the column size for the given column.
* For numeric data, this is the maximum precision.  For character data, this is the length in characters.
* For datetime datatypes, this is the length in characters of the String representation (assuming the
* maximum allowed precision of the fractional seconds component). For binary data, this is the length in bytes.  For the ROWID datatype,
* this is the length in bytes. Null is returned for data types where the
* column size is not applicable.

我給你們逐行翻譯下

/**
 *  For numeric data, this is the maximum precision => 對於數值資料,表示最大精度
 *  For character data, this is the length in characters => 對於字元資料,表示字元長度
 *  For datetime datatypes, this is the length in characters of the String representation(assuming the maximum allowed precision of the fractional seconds component )
 *      => 對於日期時間資料型別,表示字串表示形式的最大長度(假設最大允許的分秒小數部分的精度)
 *      例如:"2024-04-30 14:00:00" => 19,"2024-04-30 14:00:00.234" => 23
 *      "14:00:00" => 8,"14:00:00.234" => 11
 *  For binary data, this is the length in bytes => 對於二進位制資料,表示位元組長度
 *  For the ROWID datatype, this is the length in bytes => 對於 ROWID 型別,表示位元組長度
 *  0 is returned for data types where the column size is not applicable => 對於列大小不適用的資料型別,返回0
 */

主鍵後設資料

主鍵元資訊就比較少了,我們一般只關注主鍵名、列名、列序號

透過 jdbc 程式碼獲取,示例程式碼如下

Connection connection = dataSource.getConnection();
DatabaseMetaData databaseMetaData = connection.getMetaData();
ResultSet primaryKeysResultSet = databaseMetaData.getPrimaryKeys(connection.getCatalog(), connection.getSchema(), "tbl_sync");
while (primaryKeysResultSet.next()) {
	String columnName = primaryKeysResultSet.getString("COLUMN_NAME");
	short keySeq = primaryKeysResultSet.getShort("KEY_SEQ");
	String pkName = primaryKeysResultSet.getString("PK_NAME");
	System.out.println(columnName + " - " + keySeq + " - " + pkName);
}

輸出結果

c_vachar - 1 - PRIMARY
c_char - 2 - PRIMARY
c_bigint_auto - 3 - PRIMARY

不用過多說明了吧,你們肯定都能看懂

索引後設資料

與主鍵後設資料類似,關注的後設資料主要包括索引名、列名、列序號,同時多了一個 是否非唯一

透過 jdbc 獲取,程式碼如下

Connection connection = dataSource.getConnection();
DatabaseMetaData databaseMetaData = connection.getMetaData();
ResultSet indexResultSet = databaseMetaData.getIndexInfo(connection.getCatalog(), connection.getSchema(), "tbl_sync", false, false);
while (indexResultSet.next()) {
	String indexName = indexResultSet.getString("INDEX_NAME");
	String columnName = indexResultSet.getString("COLUMN_NAME");
	boolean nonUnique = indexResultSet.getBoolean("NON_UNIQUE");
	short ordinalPosition = indexResultSet.getShort("ORDINAL_POSITION");
	System.out.println(columnName + " - " + ordinalPosition + " - " + indexName +  " - " + nonUnique);
}

輸出結果

c_vachar - 1 - PRIMARY - false
c_char - 2 - PRIMARY - false
c_bigint_auto - 3 - PRIMARY - false
c_bigint_auto - 1 - uk_id - false
c_vachar - 1 - idx_name_salary - true
c_decimal_4 - 2 - idx_name_salary - true

建表 SQL

當相關後設資料都獲取到之後,就萬事俱備,只欠東風了

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
我們將 `test` 庫下的表 `tbl_sync` 同步到另一個 `MySQL` 庫 `obj_db` 中

SQL 拼接如下

點選檢視程式碼
public String getCreateTableSql(String schemaName, TableMeta tableMeta, List<ColumnMeta> columnMetas,
								IndexMeta primaryKeyMeta, Map<String, IndexMeta> indexMetaMap) {
	StringBuilder createSql = new StringBuilder("CREATE TABLE " + schemaName + "." + tableMeta.getTableName() + " ( ");
	for (ColumnMeta columnMeta : columnMetas) {
		createSql.append(columnMeta.getColumnName()).append(" ").append(getColumnType(columnMeta));
		if (columnMeta.getIfUnsigned()) {
			createSql.append(" UNSIGNED");
		}
		if (columnMeta.getIfNullable() == 0) {
			createSql.append(" NOT NULL");
		}
		if (StrUtil.isNotBlank(columnMeta.getDefaultValue())) {
			createSql.append(" DEFAULT '").append(columnMeta.getDefaultValue()).append("'");
		}
		if (columnMeta.getIfAutoIncrement()) {
			createSql.append(" AUTO_INCREMENT");
		}
		if (StrUtil.isNotBlank(columnMeta.getRemarks())) {
			createSql.append(" COMMENT '").append(columnMeta.getRemarks()).append("'");
		}
		createSql.append(",");
	}
	// 主鍵處理
	if (ObjectUtil.isNotNull(primaryKeyMeta)) {
		List<IndexColumnMeta> indexColumns = primaryKeyMeta.getIndexColumns();
		indexColumns.sort(Comparator.comparingInt(IndexColumnMeta::getOrdinalPosition));
		createSql.append(" PRIMARY KEY (");
		for (int i=0; i<indexColumns.size(); i++) {
			if (i>0) {
				createSql.append(",");
			}
			createSql.append(indexColumns.get(i).getColumnName());
		}
		createSql.append("),");
	}
	if (CollectionUtil.isNotEmpty(indexMetaMap)) {
		for (IndexMeta indexMeta : indexMetaMap.values()) {
			if (indexMeta.getIndexType() == IndexTypeEnum.UNIQUE) {
				// 唯一索引
				createSql.append("UNIQUE ");
			}
			createSql.append("KEY ").append(indexMeta.getIndexName()).append(" (");
			List<IndexColumnMeta> indexColumns = indexMeta.getIndexColumns();
			indexColumns.sort(Comparator.comparingInt(IndexColumnMeta::getOrdinalPosition));
			for (int i=0; i<indexColumns.size(); i++) {
				if (i>0) {
					createSql.append(",");
				}
				createSql.append(indexColumns.get(i).getColumnName());
			}
			createSql.append("),");
		}
	}
	// 刪除最後一個逗號
	createSql.deleteCharAt(createSql.length()-1);
	createSql.append(")");
	if (StrUtil.isNotBlank(tableMeta.getRemarks())) {
		createSql.append(" COMMENT '").append(tableMeta.getRemarks()).append("'");
	}
	return createSql.toString();
}

/**
 * 獲取表 列型別
 * @param columnMeta 列後設資料
 * @return mysql 列型別
 */
private String getColumnType(ColumnMeta columnMeta) {
	switch (columnMeta.getColumnType()) {
		// 數值型別
		case Types.TINYINT:
			return "TINYINT";
		case Types.SMALLINT:
			return "SMALLINT";
		case Types.INTEGER:
			return "INT";
		case Types.BIGINT:
			return "BIGINT";
		case Types.FLOAT:
		case Types.REAL:
			return columnMeta.getPrecision() > 0 ? "FLOAT(" + columnMeta.getPrecision() + "," + columnMeta.getScale() + ")" : "FLOAT";
		case Types.DOUBLE:
			return columnMeta.getPrecision() > 0 ? "DOUBLE(" + columnMeta.getPrecision() + "," + columnMeta.getScale() + ")" : "DOUBLE";
		case Types.DECIMAL:
			return "DECIMAL(" + columnMeta.getPrecision() + "," + columnMeta.getScale() + ")";
		case Types.NUMERIC:
			return columnMeta.getScale() <= 0 ? "BIGINT" : "DECIMAL(" + columnMeta.getPrecision() + "," + columnMeta.getScale() + ")";
		// 字元與字串型別
		case Types.CHAR:
		case Types.NCHAR:
			return columnMeta.getPrecision() > 0 ? "CHAR(" + columnMeta.getPrecision() + ")" : "CHAR";
		case Types.VARCHAR:
		case Types.NVARCHAR:
			return columnMeta.getPrecision() > 0 ? "VARCHAR(" + columnMeta.getPrecision() + ")" : "VARCHAR";
		case Types.LONGVARCHAR:
		case Types.LONGNVARCHAR:
			switch (columnMeta.getColumnTypeName()) {
				case "TINYTEXT":
					return "TINYTEXT";
				case "MEDIUMTEXT":
					return "MEDIUMTEXT";
				case "LONGTEXT":
					return "LONGTEXT";
				case "JSON":
					return "JSON";
				default:
					return "TEXT";
			}
		case Types.CLOB:
		case Types.NCLOB:
			return "LONGTEXT";
		// 日期和時間型別
		case Types.DATE:
			switch (columnMeta.getColumnTypeName()) {
				case "YEAR":
					return "YEAR";
				default:
					return "DATE";
			}
		case Types.TIME:
			return "TIME" + (columnMeta.getPrecision() > 8 ? "(" + (columnMeta.getPrecision() - 9) + ")" : "");
		case Types.TIMESTAMP:
			switch (columnMeta.getColumnTypeName()) {
				case "DATETIME":
					return "DATETIME" + (columnMeta.getPrecision() > 19 ? "(" + (columnMeta.getPrecision() - 20) + ")" : "");
				case "DATE":
					// oracle 的 DATE
					return "DATETIME";
				default:
					return "TIMESTAMP"+ (columnMeta.getPrecision() > 19 ? "(" + (columnMeta.getPrecision() - 20) + ")" : "");
			}
		// 二進位制型別
		case Types.BIT:
		case Types.BOOLEAN:
			return columnMeta.getPrecision() > 0 ? "BIT(" + columnMeta.getPrecision() + ")" : "BIT";
		case Types.BINARY:
			return columnMeta.getPrecision() > 0 ? "BINARY(" + columnMeta.getPrecision() + ")" : "BINARY";
		case Types.VARBINARY:
			return columnMeta.getPrecision() > 0 ? "VARBINARY(" + columnMeta.getPrecision() + ")" : "VARBINARY";
		case Types.BLOB:
		case Types.LONGVARBINARY:
			switch (columnMeta.getColumnTypeName()) {
				case "TINYBLOB":
					return "TINYBLOB";
				case "MEDIUMBLOB":
					return "MEDIUMBLOB";
				case "LONGBLOB":
					return "LONGBLOB";
				default:
					return "BLOB";
			}
		case Types.OTHER:
			if (columnMeta.getColumnTypeName().contains("VARCHAR")) {
				return "VARCHAR" + (columnMeta.getPrecision() > 0 ? "(" + columnMeta.getPrecision() + ")" : "");
			} else if (columnMeta.getColumnTypeName().contains("TIMESTAMP")) {
				return "TIMESTAMP" + (columnMeta.getScale() > 0 ? "(" + columnMeta.getScale() + ")" : "");
			}
			else {
				throw new SyncException("不支援的型別:" + columnMeta.getColumnTypeName());
			}
		default:
			throw new SyncException("不支援的型別:" + columnMeta.getColumnTypeName());
	}
}

結合後設資料的獲取

點選檢視程式碼
@Test
public void getMySQLCreateTableSql() throws SQLException {
	Connection connection = dataSource.getConnection();
	DatabaseMetaData databaseMetaData = connection.getMetaData();
	ResultSet tableResultSet = databaseMetaData.getTables(connection.getCatalog(), connection.getSchema(), "tbl_sync", new String[]{"TABLE"});
	TableMeta tableMeta = new TableMeta();
	while (tableResultSet.next()) {
		tableMeta.setTableName(tableResultSet.getString("TABLE_NAME"));
		tableMeta.setTableType(tableResultSet.getString("TABLE_TYPE"));
		tableMeta.setRemarks(tableResultSet.getString("REMARKS"));
	}
	// 獲取列後設資料
	ResultSet columnResultSet = databaseMetaData.getColumns(connection.getCatalog(), connection.getSchema(), "tbl_sync", null);
	List<ColumnMeta> columnMetas = new ArrayList<>();
	while (columnResultSet.next()) {
		ColumnMeta columnMeta = new ColumnMeta();
		columnMeta.setColumnName(columnResultSet.getString("COLUMN_NAME"));
		columnMeta.setColumnType(columnResultSet.getInt("DATA_TYPE"));
		columnMeta.setColumnTypeName(columnResultSet.getString("TYPE_NAME"));
		columnMeta.setIfAutoIncrement("YES".equalsIgnoreCase(columnResultSet.getString("IS_AUTOINCREMENT")));
		columnMeta.setIfNullable("YES".equalsIgnoreCase(columnResultSet.getString("IS_NULLABLE")) ? 1 : 0);
		columnMeta.setPrecision(columnResultSet.getInt("COLUMN_SIZE"));
		columnMeta.setScale(columnResultSet.getInt("DECIMAL_DIGITS"));
		columnMeta.setDefaultValue(columnResultSet.getString("COLUMN_DEF"));
		columnMeta.setRemarks(columnResultSet.getString("REMARKS"));
		columnMeta.setIfUnsigned(columnMeta.getColumnTypeName().contains("UNSIGNED"));
		columnMetas.add(columnMeta);
	}
	columnResultSet.close();
	// 獲取主鍵後設資料
	ResultSet primaryKeyResultSet = databaseMetaData.getPrimaryKeys(connection.getCatalog(), connection.getSchema(), "tbl_sync");
	IndexMeta primaryKeyMeta = new IndexMeta();
	while (primaryKeyResultSet.next()) {
		IndexColumnMeta indexColumnMeta = new IndexColumnMeta(primaryKeyResultSet.getString("COLUMN_NAME"), primaryKeyResultSet.getShort("KEY_SEQ"));
		primaryKeyMeta.setIndexName(primaryKeyResultSet.getString("PK_NAME"));
		primaryKeyMeta.getIndexColumns().add(indexColumnMeta);
	}
	primaryKeyResultSet.close();
	// 獲取索引後設資料
	ResultSet indexResultSet = databaseMetaData.getIndexInfo(connection.getCatalog(), connection.getSchema(), "tbl_sync", false, false);
	Map<String, IndexMeta> indexMetaMap = new HashMap<>();
	while (indexResultSet.next()) {
		String indexName = indexResultSet.getString("INDEX_NAME");
		if (indexName.equals(primaryKeyMeta.getIndexName())) {
			continue;
		}
		IndexMeta indexMeta = indexMetaMap.get(indexName);
		if (ObjectUtil.isNull(indexMeta)) {
			indexMeta = new IndexMeta(indexName);
			indexMetaMap.put(indexName, indexMeta);
		}
		indexMeta.setIndexType(indexResultSet.getBoolean("NON_UNIQUE") ? IndexTypeEnum.NORMAL : IndexTypeEnum.UNIQUE);
		indexMeta.getIndexColumns().add(new IndexColumnMeta(indexResultSet.getString("COLUMN_NAME"), indexResultSet.getShort("ORDINAL_POSITION")));
	}
	indexResultSet.close();

	MysqlSql mysqlSql = new MysqlSql();
	String createTableSql = mysqlSql.getCreateTableSql("obj_db", tableMeta, columnMetas, primaryKeyMeta, indexMetaMap);
	System.out.println(SQLUtils.formatMySql(createTableSql));
}

得到的建表 SQL 如下

CREATE TABLE obj_db.tbl_sync (
	c_bigint_auto BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'bigint 型別',
	c_bigint BIGINT COMMENT 'bigint 型別',
	c_vachar VARCHAR(100) NOT NULL COMMENT 'varchar 型別',
	c_char CHAR(32) NOT NULL COMMENT 'char 型別',
	c_text TEXT NOT NULL COMMENT 'text 型別',
	c_decimal_4 DECIMAL(15, 4) NOT NULL DEFAULT '5000.0000' COMMENT 'decimal 型別',
	c_decimal_0 DECIMAL(10, 0) COMMENT 'decimal 型別',
	c_blob BLOB COMMENT 'blob 型別',
	c_bit BIT(1) COMMENT 'bit 型別',
	c_tinyint TINYINT COMMENT 'tinyint 型別',
	c_binary BINARY(10) COMMENT 'binary 型別',
	c_float FLOAT(13, 0) COMMENT 'float 型別',
	c_double DOUBLE(23, 0) COMMENT 'double 型別',
	c_varbinary VARBINARY(20) COMMENT 'varbinary 型別',
	c_longblob LONGBLOB COMMENT 'longblob 型別',
	c_longtext LONGTEXT COMMENT 'longtext 型別',
	c_json JSON COMMENT 'json 型別',
	c_date DATE COMMENT 'date 型別',
	c_time TIME(2) COMMENT 'time 型別',
	c_datetime DATETIME(3) COMMENT 'datetime 型別',
	c_timestamp TIMESTAMP(4) COMMENT 'timestamp 型別',
	c_year YEAR COMMENT 'year 型別',
	PRIMARY KEY (c_vachar, c_char, c_bigint_auto),
	UNIQUE KEY uk_id (c_bigint_auto),
	KEY idx_name_salary (c_vachar, c_decimal_4)
) COMMENT '包含各種型別列的同步表'
異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
可以看出,與原表的結構是一致的!

此處應該有掌聲

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單

同源同步

何謂同源?

就是資料庫型別相同的資料來源,例如從 MySQL 同步到 MySQL

這種情況還有必要進行 SQL 拼接嗎?

異構資料來源同步之表結構同步 → 透過 jdbc 實現,沒那麼簡單
還記得怎麼檢視 MySQL 表的完整定義嗎

SHOW CREATE TABLE test.tbl_sync

這是不是就可以獲取到表的 DDL

所以同源的表結構同步,就不用拼接 SQL 那麼複雜了,直接獲取 DDL 後在目標資料來源建表即可

總結

  • 異構資料來源同步的策略有兩種:離線同步 和 實時同步,各自的特點及使用場景需要區分清楚
  • 關係型資料庫的後設資料有很多種,大家可以仔細看看 java.sql.DatabaseMetaData
  • 同源表結構同步,可以不用拼接建表 SQL,可以直接獲取建表 DDL
  • 異源表結構同步,需要先獲取源表的相關後設資料,然後再拼接目標表的建表 SQL,最後在目標資料來源執行 SQL 建立目標表
  • COLUMN_SIZE 針對不同的列型別,它的含義不同,文中已經詳細說明,值得大家注意

相關文章