從代碼生成提及,帶你深刻理解 mybatis generator 源碼

枯燥的任務

這一切都要從多年前提及。html

那時候剛入職一家新公司,項目經理給我分配了一個比較簡單的工做,爲全部的數據庫字段整理一張元數據表。java

由於不少接手的項目文檔都不全,因此須要統一整理一份基本的字典表。mysql

若是是你,你會怎麼處理這個任務呢?git

重複的工做

一開始我是直接準備人工把全部的字段整理一遍,而後整理出對應的 SQL 插入到元數據庫管理表中。github

meta_table 元數據表信息web

meta_field 元數據字段信息sql

一開始還有點激情,後來就是無盡的重複,感受十分無聊。數據庫

因而,我本身動手寫了一個開源的小工具。markdown

https://github.com/houbb/metadatamybatis

元數據管理

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 應用,部署啓動後,頁面指定數據庫鏈接信息,就能夠完成全部數據的初始化。

metadata

以測試腳本

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 實現了一個代碼自動生成的低代碼平臺。

開源地址以下:

http://github.com/houbb/low-code

我發現了 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();

}

這樣便於後期替換實現,你甚至可使用數據庫鏈接池:

https://github.com/houbb/jdbc-pool

元數據查詢腳本

對於不一樣的數據庫,查詢的方式不一樣。

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

}

表數據查詢 sql

根據對應的 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 必備的工具,大大提高了咱們的效率。

知道對應的實現原理,可讓咱們更好的使用它,而且在其基礎上,實現本身的腦洞。

我是老馬,期待與你的下次重逢。

備註:涉及的代碼較多,文中作了簡化。若你對源碼感興趣,能夠關註{老馬嘯西風},後臺回復{代碼生成}便可得到。

相關文章
相關標籤/搜索