這一切都要從多年前提及。html
那時候剛入職一家新公司,項目經理給我分配了一個比較簡單的工做,爲全部的數據庫字段整理一張元數據表。java
由於不少接手的項目文檔都不全,因此須要統一整理一份基本的字典表。mysql
若是是你,你會怎麼處理這個任務呢?git
一開始我是直接準備人工把全部的字段整理一遍,而後整理出對應的 SQL 插入到元數據庫管理表中。github
meta_table 元數據表信息web
meta_field 元數據字段信息sql
一開始還有點激情,後來就是無盡的重複,感受十分無聊。數據庫
因而,我本身動手寫了一個開源的小工具。markdown
metadata 能夠自動將全部的表信息和字段信息存入元數據表中,便於統一查閱。
(註釋須要保證庫自己已經包含了對於表和字段的註釋)
一開始實現了 3 種常見的數據庫:mysql oracle sql-server。
以 mysql 爲例,對應的建表語句爲:
drop table if exists meta_field; drop table if exists meta_model; /*==============================================================*/ /* Table: meta_field */ /*==============================================================*/ create table meta_field ( ID int not null auto_increment comment '自增加主鍵', uid varchar(36) comment '惟一標識', name varchar(125) comment '名稱', dbObjectName varchar(36) comment '數據庫表名', alias varchar(125) comment '別名', description varchar(255) comment '描述', isNullable bool comment '是否可爲空', dataType varchar(36) comment '數據類型', createTime datetime comment '建立時間', updateTime datetime comment '更新時間', primary key (ID) ) auto_increment = 1000 DEFAULT CHARSET=utf8; alter table meta_field comment '元數據字段表'; /*==============================================================*/ /* Table: meta_model */ /*==============================================================*/ create table meta_model ( ID int not null auto_increment comment '自增加主鍵', uid varchar(36) comment '惟一標識', name varchar(125) comment '名稱', dbObjectName varchar(36) comment '數據庫表名', alias varchar(125) comment '別名', description varchar(255) comment '描述', category varchar(36) comment '分類', isVisible bool comment '是否可查詢', isEditable bool comment '是否可編輯', createTime datetime comment '建立時間', updateTime datetime comment '更新時間', primary key (ID) ) DEFAULT CHARSET=utf8; alter table meta_model comment '元數據實體表';
metadata 是一個 web 應用,部署啓動後,頁面指定數據庫鏈接信息,就能夠完成全部數據的初始化。
以測試腳本
CREATE DATABASE `metadata-test` DEFAULT CHARACTER SET UTF8; USE `metadata-test`; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '惟一標識', `username` varchar(255) DEFAULT NULL COMMENT '用戶名', `password` varchar(255) DEFAULT NULL COMMENT '密碼', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=UTF8 COMMENT='用戶表';
爲例,能夠將對應的表和字段信息所有初始化到對應的表中。
一切看起來都很棒,幾分鐘就搞定了。不是嗎?
原本 metadata 沒有意外的話,我幾乎不會再去修改他了。
直接前不久,我基於 mybatis-plus-generator 實現了一個代碼自動生成的低代碼平臺。
開源地址以下:
我發現了 metadata 這個應用雖然做爲 web 應用還不錯,可是自己的複用性不好,我沒法在這個基礎上實現一個代碼生成工具。
因而,就誕生了實現一個最基礎的 jdbc 元數據管理工具的想法。
他山之石,能夠攻玉。
咱們就直接以 MPG 的源碼爲例,學習而且改造。
元數據管理最核心的一點在於全部的數據庫自己就有元數據管理。
咱們以 mysql 爲例,查看全部表信息。
show table status;
以下:
+------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------------+----------+----------------+--------------+ | Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time | Update_time | Check_time | Collation | Checksum | Create_options | Comment | +------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------------+----------+----------------+--------------+ | word | InnoDB | 10 | Compact | 0 | 0 | 16384 | 0 | 0 | 0 | 1 | 2021-07-22 19:39:13 | NULL | NULL | utf8_general_ci | NULL | | 敏 感詞表 | +------+--------+---------+------------+------+----------------+-------------+-----------------+--------------+-----------+----------------+---------------------+-------------+------------+-----------------+----------+----------------+--------------+ 1 row in set (0.00 sec)
對應的字段信息查看
show full fields from word;
輸出以下:
mysql> show full fields from word; +-------------+------------------+-----------------+------+-----+-------------------+-----------------------------+---------------------------------+--------------------+ | Field | Type | Collation | Null | Key | Default | Extra | Privileges | Comment | +-------------+------------------+-----------------+------+-----+-------------------+-----------------------------+---------------------------------+--------------------+ | id | int(10) unsigned | NULL | NO | PRI | NULL | auto_increment | select,insert,update,references | 應用自增主鍵 | | word | varchar(128) | utf8_general_ci | NO | UNI | NULL | | select,insert,update,references | 單詞 | | type | varchar(8) | utf8_general_ci | NO | | NULL | | select,insert,update,references | 類型 | | status | char(1) | utf8_general_ci | NO | | S | | select,insert,update,references | 狀態 | | remark | varchar(64) | utf8_general_ci | NO | | | | select,insert,update,references | 配置描述 | | operator_id | varchar(64) | utf8_general_ci | NO | | system | | select,insert,update,references | 操做員名稱 | | create_time | timestamp | NULL | NO | | CURRENT_TIMESTAMP | | select,insert,update,references | 建立時間戳 | | update_time | timestamp | NULL | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | select,insert,update,references | 更新時間戳 | +-------------+------------------+-----------------+------+-----+-------------------+-----------------------------+---------------------------------+--------------------+ 8 rows in set (0.01 sec)
能夠獲取到很是全面的信息,代碼生成就是基於這些基本信息,生成對應的代碼文本。
其中,word 的建表語句以下:
create table word ( id int unsigned auto_increment comment '應用自增主鍵' primary key, word varchar(128) not null comment '單詞', type varchar(8) not null comment '類型', status char(1) not null default 'S' comment '狀態', remark varchar(64) not null comment '配置描述' default '', operator_id varchar(64) not null default 'system' comment '操做員名稱', create_time timestamp default CURRENT_TIMESTAMP not null comment '建立時間戳', update_time timestamp default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新時間戳' ) comment '敏感詞表' ENGINE=Innodb default charset=UTF8 auto_increment=1; create unique index uk_word on word (word) comment '惟一索引';
雖然上面介紹元數據獲取,是以 mysql 爲例。
可是咱們在實現工具的時候,必定要考慮對應的可拓展性。
能夠是 mysql,也能夠是常見的 oracle/sql-server。
每一種數據庫的獲取方式都是不一樣的,因此須要根據配置不一樣,實現也要不一樣。
獲取到元數據以後,處理的方式也能夠很是多樣化。
能夠控臺輸出,能夠入庫,能夠生成對應的 markdown/html/pdf/word/excel 不一樣形式的文檔等。
好的工具,應該對用戶屏蔽複雜的實現細節。
用戶只須要簡單的指定配置信息,想要獲取的表,處理方式便可。
至於如何實現,用戶能夠不關心。
接下來,咱們結合 MPG 的源碼,抽取最核心的部分進行講解。
如何根據鏈接信息獲取 connection?
但願常用 mybatis 等工具的你還記得:
public class DbConnection implements IDbConnection { /** * 驅動鏈接的URL */ private String url; /** * 驅動名稱 */ private String driverName; /** * 數據庫鏈接用戶名 */ private String username; /** * 數據庫鏈接密碼 */ private String password; //getter&setter @Override public Connection getConnection() { Connection conn = null; try { Class.forName(driverName); conn = DriverManager.getConnection(url, username, password); } catch (ClassNotFoundException | SQLException e) { throw new JdbcMetaException(e); } return conn; } }
IDbConnection 接口的定義很是簡單:
public interface IDbConnection { /** * 獲取數據庫鏈接 * @return 鏈接 * @since 1.0.0 */ Connection getConnection(); }
這樣便於後期替換實現,你甚至可使用數據庫鏈接池:
對於不一樣的數據庫,查詢的方式不一樣。
以 mysql 爲例,實現以下:
public class MySqlQuery extends AbstractDbQuery { @Override public DbType dbType() { return DbType.MYSQL; } @Override public String tablesSql() { return "show table status"; } @Override public String tableFieldsSql() { return "show full fields from `%s`"; } @Override public String tableName() { return "NAME"; } @Override public String tableComment() { return "COMMENT"; } @Override public String fieldName() { return "FIELD"; } @Override public String fieldType() { return "TYPE"; } @Override public String fieldComment() { return "COMMENT"; } @Override public String fieldKey() { return "KEY"; } @Override public boolean isKeyIdentity(ResultSet results) throws SQLException { return "auto_increment".equals(results.getString("Extra")); } @Override public String nullable() { return "Null"; } @Override public String defaultValue() { return "Default"; } }
其中 show table status
用於查看全部的表元數據;show full fields from %s
能夠查看具體表的字段元數據。
nullable() 和 defaultValue() 這兩個屬性是老馬新增的,MPG 中是沒有的,由於代碼生成不關心這兩個字段。
作好上面的準備工做以後,咱們能夠開始進行核心代碼編寫。
@Override public List<TableInfo> getTableList(TableInfoContext context) { // 鏈接 Connection connection = getConnection(context); DbType dbType = DbTypeUtils.getDbType(context.getDriverName()); IDbQuery dbQuery = DbTypeUtils.getDbQuery(dbType); // 構建元數據查詢 SQL String tableSql = buildTableSql(context); // 執行查詢 List<TableInfo> tableInfoList = queryTableInfos(connection, tableSql, dbQuery, context); return tableInfoList; }
具體數據庫的實現是不一樣的,能夠根據 driverName 獲取。
DbTypeUtils 的實現以下:
/** * @author binbin.hou * @since 1.0.0 */ public final class DbTypeUtils { private DbTypeUtils(){} /** * 根據驅動獲取 dbType * @param driverName 驅動信息 * @return 結果 * @since 1.1.0 */ public static DbType getDbType(final String driverName) { DbType dbType = null; if (driverName.contains("mysql")) { dbType = DbType.MYSQL; } else if (driverName.contains("oracle")) { dbType = DbType.ORACLE; } else if (driverName.contains("postgresql")) { dbType = DbType.POSTGRE_SQL; } else { throw new JdbcMetaException("Unknown type of database!"); } return dbType; } /** * 獲取對應的數據庫查詢類型 * @param dbType 數據庫類型 * @return 結果 * @since 1.0.0 */ public static IDbQuery getDbQuery(final DbType dbType) { IDbQuery dbQuery = null; switch (dbType) { case ORACLE: dbQuery = new OracleQuery(); break; case SQL_SERVER: dbQuery = new SqlServerQuery(); break; case POSTGRE_SQL: dbQuery = new PostgreSqlQuery(); break; default: // 默認 MYSQL dbQuery = new MySqlQuery(); break; } return dbQuery; } }
根據對應的 IDbQuery 構建表數據查詢的 sql。
/** * 構建 table sql * @param context 上下文 * @return 結果 * @since 1.0.0 */ private String buildTableSql(final TableInfoContext context) { // 獲取 dbType & DbQuery final String jdbcUrl = context.getDriverName(); DbType dbType = DbTypeUtils.getDbType(jdbcUrl); IDbQuery dbQuery = DbTypeUtils.getDbQuery(dbType); String tablesSql = dbQuery.tablesSql(); if (DbType.POSTGRE_SQL == dbQuery.dbType()) { //POSTGRE_SQL 使用 tablesSql = String.format(tablesSql, "public"); } // 簡化掉 oracle 的特殊處理 return tablesSql; }
直接獲取對應的 tablesSql 便可,很是簡答。
直接根據構建好的 tableSql 查詢,而後構建最基本的表信息。
try(PreparedStatement preparedStatement = connection.prepareStatement(tablesSql);) { List<TableInfo> tableInfoList = new ArrayList<>(); ResultSet results = preparedStatement.executeQuery(); TableInfo tableInfo; while (results.next()) { String tableName = results.getString(dbQuery.tableName()); if (StringUtil.isNotEmpty(tableName)) { String tableComment = results.getString(dbQuery.tableComment()); tableInfo = new TableInfo(); tableInfo.setName(tableName); tableInfo.setComment(tableComment); tableInfoList.add(tableInfo); } else { System.err.println("當前數據庫爲空!!!"); } } } catch (SQLException e) { throw new JdbcMetaException(e); }
此處省去對錶信息的過濾。
表信息構建爲完成後,構建具體的字段信息。
try { String tableFieldsSql = dbQuery.tableFieldsSql(); if (DbType.POSTGRE_SQL == dbQuery.dbType()) { tableFieldsSql = String.format(tableFieldsSql, "public", tableInfo.getName()); } else { tableFieldsSql = String.format(tableFieldsSql, tableInfo.getName()); } PreparedStatement preparedStatement = connection.prepareStatement(tableFieldsSql); ResultSet results = preparedStatement.executeQuery(); while (results.next()) { TableField field = new TableField(); // 省略 ID 相關的處理 // 省略自定義字段查詢 // 處理其它信息 field.setName(results.getString(dbQuery.fieldName())); field.setType(results.getString(dbQuery.fieldType())); String propertyName = getPropertyName(field.getName()); DbColumnType dbColumnType = typeConvert.getTypeConvert(field.getType()); field.setPropertyName(propertyName); field.setColumnType(dbColumnType); field.setComment(results.getString(dbQuery.fieldComment())); field.setNullable(results.getString(dbQuery.nullable())); field.setDefaultValue(results.getString(dbQuery.defaultValue())); fieldList.add(field); } } catch (SQLException e) { throw new JdbcMetaException(e); }
字段信息的實現也比較簡單,直接根據對應的 sql 進行查詢,而後構建便可。
在通過大量的刪減以後,咱們能夠獲取最基礎的表元數據信息。
可是要怎麼處理這個列表信息呢?
咱們能夠定義一個接口:
public interface IResultHandler { /** * 處理 * @param context 上下文 * @since 1.0.0 */ void handle(final IResultHandlerContext context); }
context 的屬性比較簡單,目前就是 List<TableInfo>
。
咱們能夠實現一個控臺輸出:
public class ConsoleResultHandler implements IResultHandler { @Override public void handle(IResultHandlerContext context) { List<TableInfo> tableInfoList = context.tableInfoList(); for(TableInfo tableInfo : tableInfoList) { // 數據 System.out.println("> " + tableInfo.getName() + " " + tableInfo.getComment()); System.out.println(); List<TableField> tableFields = tableInfo.getFields(); System.out.println("| 序列 | 列名 | 類型 | 是否爲空 | 缺省值 | 描述 |"); System.out.println("|:---|:---|:---|:---|:---|:---|"); String format = "| %d | %s | %s | %s | %s | %s |"; int count = 1; for (TableField field : tableFields) { String info = String.format(format, count, field.getName(), field.getType(), field.getNullable(), field.getDefaultValue(), field.getComment()); System.out.println(info); count++; } System.out.println("\n\n"); } } }
在控臺輸出對應的 markdown 字段信息。
你也能夠實現本身的 html/pdf/word/excel 等等。
咱們前面寫了這麼多主要是原理實現。
那麼工具是否好用,仍是要體驗一下。
JdbcMetadataBs.newInstance() .url("jdbc:mysql://127.0.0.1:3306/test") .includes("word") .execute();
指定輸出 test.word 的表信息。
對應的日誌以下:
> word 敏感詞表 | 序列 | 列名 | 類型 | 是否爲空 | 缺省值 | 描述 | |:---|:---|:---|:---|:---|:---| | 1 | id | int(10) unsigned | NO | null | 應用自增主鍵 | | 2 | word | varchar(128) | NO | null | 單詞 | | 3 | type | varchar(8) | NO | null | 類型 | | 4 | status | char(1) | NO | S | 狀態 | | 5 | remark | varchar(64) | NO | | 配置描述 | | 6 | operator_id | varchar(64) | NO | system | 操做員名稱 | | 7 | create_time | timestamp | NO | CURRENT_TIMESTAMP | 建立時間戳 | | 8 | update_time | timestamp | NO | CURRENT_TIMESTAMP | 更新時間戳 |
這個就是簡單的 markdown 格式,實際效果以下:
word 敏感詞表
序列 | 列名 | 類型 | 是否爲空 | 缺省值 | 描述 |
---|---|---|---|---|---|
1 | id | int(10) unsigned | NO | null | 應用自增主鍵 |
2 | word | varchar(128) | NO | null | 單詞 |
3 | type | varchar(8) | NO | null | 類型 |
4 | status | char(1) | NO | S | 狀態 |
5 | remark | varchar(64) | NO | 配置描述 | |
6 | operator_id | varchar(64) | NO | system | 操做員名稱 |
7 | create_time | timestamp | NO | CURRENT_TIMESTAMP | 建立時間戳 |
8 | update_time | timestamp | NO | CURRENT_TIMESTAMP | 更新時間戳 |
這樣,咱們就擁有了一個最簡單的 jdbc 元數據管理工具。
固然,這個只是 v1.0.0 版本,後續還有許多特性須要添加。
MPG 基本上每個使用 mybatis 必備的工具,大大提高了咱們的效率。
知道對應的實現原理,可讓咱們更好的使用它,而且在其基礎上,實現本身的腦洞。
我是老馬,期待與你的下次重逢。
備註:涉及的代碼較多,文中作了簡化。若你對源碼感興趣,能夠關註{老馬嘯西風},後臺回復{代碼生成}便可得到。