乐趣区

关于mybatis:从代码生成说起带你深入理解-mybatis-generator-源码

干燥的工作

这所有都要从多年前说起。

那时候刚入职一家新公司,项目经理给我调配了一个比较简单的工作,为所有的数据库字段整顿一张元数据表。

因为很多接手的我的项目文档都不全,所以须要对立整顿一份根本的字典表。

如果是你,你会怎么解决这个工作呢?

反复的工作

一开始我是间接筹备人工把所有的字段整顿一遍,而后整顿出对应的 SQL 插入到元数据库治理表中。

meta_table 元数据表信息

meta_field 元数据字段信息

一开始还有点激情,起初就是无尽的反复,感觉非常无聊。

于是,我本人入手写了一个开源的小工具。

https://github.com/houbb/metadata

元数据管理

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 实现了一个代码主动生成的低代码平台。

开源地址如下:

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 必备的工具,大大晋升了咱们的效率。

晓得对应的实现原理,能够让咱们更好的应用它,并且在其根底上,实现本人的脑洞。

我是老马,期待与你的下次重逢。

备注:波及的代码较多,文中做了简化。若你对源码感兴趣,能够關註 {老马啸东风},後臺回復{代码生成} 即可取得。

退出移动版