SpringBoot + Beetl 实现动态数据库DDL

序言

最近公司里有一个新的需求,需要导出数据库元数据表中存储的表的 DDL 语句,而在元数据表中数据源的类型庞大,少则十几种多则达到几十种,各种数据库类型都有,比如常见就有 MySQL PostgreSQL Oracle SQL Server等等,而不同的数据库其数据类型和语法都存在一些差异,如果在代码中去处理工作量就太大了。所以我想到使用模版引擎来处理,现在市面上出现了一个新的模版引擎 Beetl。本篇文章就从这个模版引擎来入手介绍如何实现功能。代码已经提交到了我的 GitHub 仓库

Beetl

Beetl 的使用方式和 ThymeleafFreemarker 差不多,而且其语法简单,性能也优于这两者,目前在国内已经有一定的用户群体。话不多说,具体的使用方式非常简单,大家看一下官网就会了,下面来说功能怎么实现

代码

使用前在SpringBoot中使用的时候需要先导入依赖,我测试的 SpringBoot 的版本为 3.2.1

xml 复制代码
<dependency>
    <groupId>com.ibeetl</groupId>
    <artifactId>beetl-spring-boot-starter-classic</artifactId>
    <version>3.19.1.RELEASE</version>
</dependency>

创建一下库表,因为模版文件是存储在数据库中,方便后期维护

sql 复制代码
create table ddl_template
(
    id          int auto_increment
        primary key,
    name        varchar(255) not null,
    description varchar(255) null,
    created_at  timestamp    not null,
    updated_at  timestamp    null,
    created_by  varchar(255) null,
    updated_by  varchar(255) null,
    version     float        not null,
    db_type     varchar(255) not null,
    template    text         not null
)
    comment 'ddl模板表' charset = utf8mb4;

表创建完成后,我事先往这个表中添加了三条数据,分别代表 MySQL、Oracle、PgSQL 的DDL 模版

MySQL:

sql 复制代码
drop table if exists `${table.tableCode}`;
create table ${table.tableCode}(
    <%
    for(field in fieldList){
    var code = field.code;
    var comment = field.comment;
    var type = field.type;
    var len = field.len;
    var scale = field.scale;
    var notNull = field.notNull;
    var autoIncrement = field.autoIncrement;
    var defaultValue = field.defaultValue;

    var last =  fieldLP.last;
    if (len != null) {
        if (len > 0) {
            var temp = "(" + len;
            if (scale != null) {
                if (scale > 0) {
                    temp = temp + "," + scale;
                }
            }
            temp = temp + ")";
            type = type + temp;
        }
    }
    %>
    `${field.code}` ${type}${autoIncrement ? " auto_increment":""}${notNull ? " not null":" null"}${defaultValue != null ? " default '" + defaultValue + "'" : "" }
     ${comment != null ? " comment '" + comment + "'":""}${!last ? ",":""}
    <%
    }
     %>
    <%
    if (primaryKey != null){
    %>
,primary key (${primaryKey})
    <%
    }
    %>
) comment = '${table.tableComment}';

Oracle:

sql 复制代码
create table "${table.tableCode}"(
    <%
    for(field in fieldList){
    var code = field.code;
    var comment = field.comment;
    var type = field.type;
    var len = field.len;
    var scale = field.scale;
    var primaryKey = field.primaryKey;
    var notNull = field.notNull;
    var defaultValue = field.defaultValue;

    var last =  fieldLP.last;
    if (len != null && type != 'date') {
        if (len > 0) {
            var temp = "(" + len;
            if (scale != null) {
                if (scale > 0) {
                    temp = temp + "," + scale;
                }
            }
            temp = temp + ")";
            type = type + temp;
        }
    }
    if (type == 'date') {
            defaultValue = null;
            notNull = false;
    }
    %>
    "${field.code}" ${type}${defaultValue != null ? " default '" + defaultValue + "'" : "" }${notNull ? " not null":" null"}${!last ? ",":""}
    <%
    }
     %>
     <%
     if (primaryKey != null){
     %>
,primary key ("${primaryKey}")
     <%
     }
     %>
);

comment on table "${table.tableCode}" is '${table.tableComment}';

<%
    for(field in fieldList){
    var code = field.code;
    var comment = field.comment;
    var last =  fieldLP.last;
    %>
comment on column "${table.tableCode}"."${code}" is '${comment}';

    <%
    }
     %>

PgSQL:

SQL 复制代码
drop table if exists "${table.tableCode}";

create table "${table.tableCode}"(
    <%
    for(field in fieldList){
    var code = field.code;
    var comment = field.comment;
    var type = field.type;
    var len = field.len;
    var scale = field.scale;
    var notNull = field.notNull;
    var autoIncrement = field.autoIncrement;
    var defaultValue = field.defaultValue;

    var last =  fieldLP.last;

    if (len != null) {
        if (len > 0) {
            var temp = "(" + len;
            if (scale != null) {
                if (scale > 0) {
                    temp = temp + "," + scale;
                }
            }
            temp = temp + ")";
            type = type + temp;
        }
    }

    %>
    "${code}" ${type}${autoIncrement ? " generated always as identity" : ""}${notNull ? " not null" : ""}${defaultValue != null ? " default '" + defaultValue + "'" : "" }
    ${!last ? "," : ""}
    <%
    }
    %>
    <%
    if (primaryKey != null){
    %>
    ,primary key (${primaryKey})
    <%
    }
    %>
);

comment on table "${table.tableCode}" is '${table.tableComment}';

<%
for(field in fieldList){
    var code = field.code;
    var comment = field.comment;
    if (comment != null) {
%>
comment on column "${table.tableCode}"."${code}" is '${comment}';
<%
    }
}
%>

准备工作完成后,接下来开发代码,具体代码如下:

java 复制代码
@Bean
public GroupTemplate groupTemplate() throws Exception {
    StringTemplateResourceLoader loader = new StringTemplateResourceLoader();
    org.beetl.core.Configuration cfg = org.beetl.core.Configuration.defaultConfiguration();
    GroupTemplate gt = new GroupTemplate(loader, cfg);
    return gt;
}

这里使用的 StringTemplateResourceLoader 表示从字符串中来加载模板,也有其他的 ResourceLoader,比如 FileResourceLoader 表示从文件中加载模版,具体的看官网就可以了

java 复制代码
/**
 * 单张表的ddl语句创建
 *
 * @return
 */
@PostMapping("/createDdl")
public void createDdl(@RequestParam("version") String version,@RequestParam("dbType") String dbType) {
    ddlTemplateService.createDdl(version, dbType);
}
java 复制代码
@Override
public void createDdl(String version, String dbType) {
    DbEnum dbEnum = DbEnum.fromString(dbType);
    Assert.notNull(dbType, "数据库类型不支持");

    switch (dbEnum) {
        case MYSQL:
            generateMysqlDdl(version, dbType, buildMySQLData());
            break;
        case ORACLE:
            generateOracleDdl(version, dbType, buildOracleData());
            break;
        case POSTGRESQL:
            generatePostgresqlDdl(version, dbType, buildPostgresqlData());
            break;
    }
}

这里支持的数据库类型还比较少,大家可以后面扩充

java 复制代码
/**
 * 这里只是模拟一下,实际情况应该是从数据库中查询的对应的表及字段
 *
 * @return
 */
private Map<String, Object> buildMySQLData() {
    MySQLTable tableVO = MySQLTable.builder().tableCode("t_user").tableComment("用户表").build();
    List<MySQLField> fieldVOList = List.of(
            MySQLField.builder().code("id").comment("主键").type("int").len(11).scale(0.0).autoIncrement(true).notNull(true).build(),
            MySQLField.builder().code("name").comment("姓名").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            MySQLField.builder().code("age").comment("年龄").type("int").len(11).scale(0.0).autoIncrement(false).notNull(true).build(),
            MySQLField.builder().code("email").comment("邮箱").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            MySQLField.builder().code("create_time").comment("创建时间").type("date").len(0).scale(0.0).autoIncrement(false).notNull(true).notNull(true).build()
    );
    return Map.of("table", tableVO, "fieldList", fieldVOList, "primaryKey", "id");
}

private Map<String, Object> buildOracleData() {
    OracleTable tableVO = OracleTable.builder().tableCode("t_user").tableComment("用户表").build();
    List<OracleField> fieldVOList = List.of(
            OracleField.builder().code("id").comment("主键").type("NUMBER").len(11).scale(0.0).autoIncrement(true).notNull(true).build(),
            OracleField.builder().code("name").comment("姓名").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            OracleField.builder().code("age").comment("年龄").type("NUMBER").len(11).scale(0.0).autoIncrement(false).notNull(true).build(),
            OracleField.builder().code("email").comment("邮箱").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            OracleField.builder().code("create_time").comment("创建时间").type("date").len(0).scale(0.0).autoIncrement(false).notNull(true).notNull(true).build()
    );
    return Map.of("table", tableVO, "fieldList", fieldVOList, "primaryKey", "id");
}

private Map<String, Object> buildPostgresqlData() {
    PostgresqlTable tableVO = PostgresqlTable.builder().tableCode("t_user").tableComment("用户表").build();
    List<PostgresqlField> fieldVOList = List.of(
            PostgresqlField.builder().code("id").comment("主键").type("integer").autoIncrement(true).notNull(true).build(),
            PostgresqlField.builder().code("name").comment("姓名").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            PostgresqlField.builder().code("age").comment("年龄").type("int").autoIncrement(false).notNull(true).build(),
            PostgresqlField.builder().code("email").comment("邮箱").type("varchar").len(50).scale(0.0).autoIncrement(false).defaultValue("").notNull(true).build(),
            PostgresqlField.builder().code("create_time").comment("创建时间").type("timestamp").len(0).scale(0.0).autoIncrement(false).notNull(true).notNull(true).build()
    );
    return Map.of("table", tableVO, "fieldList", fieldVOList, "primaryKey", "id");
}


private void generateMysqlDdl(String version, String dbType, Map<String, Object> data) {
    try {
        // 可以考虑将模版文件缓存
        Template template = groupTemplate.getTemplate(queryByUnique(version, dbType).getTemplate());
        template.binding(data);
        String render = template.render();
        log.info("生成MysqlDDL语句成功");
        FileUtil.writeUtf8String(render, "D:/ddl.sql");
    } catch (Exception e) {
        throw new RuntimeException("生成DDL语句失败", e);
    }
}

private void generateOracleDdl(String version, String dbType, Map<String, Object> data) {
    try {
        // 可以考虑将模版文件缓存
        Template template = groupTemplate.getTemplate(queryByUnique(version, dbType).getTemplate());
        template.binding(data);
        String render = template.render();
        log.info("生成OracleDDL语句成功");
        FileUtil.writeUtf8String(render, "D:/ddl.sql");
    } catch (Exception e) {
        throw new RuntimeException("生成DDL语句失败", e);
    }
}

private void generatePostgresqlDdl(String version, String dbType, Map<String, Object> data) {
    // 可以考虑将模版文件缓存
    Template template = groupTemplate.getTemplate(queryByUnique(version, dbType).getTemplate());
    template.binding(data);
    String render = template.render();
    log.info("生成PgSQLDDL语句成功");
    FileUtil.writeUtf8String(render, "D:/ddl.sql");
}

我这里运行后会分别针对不同的数据库类型来生成对应的表 DDL 语句,效果如下:

MySQL

SQL 复制代码
drop table if exists `t_user`;
create table t_user(
    `id` int(11) auto_increment not null
      comment '主键',
    `name` varchar(50) not null default ''
      comment '姓名',
    `age` int(11) not null
      comment '年龄',
    `email` varchar(50) not null default ''
      comment '邮箱',
    `create_time` date not null
      comment '创建时间'
,primary key (id)
) comment = '用户表';

Oracle

sql 复制代码
create table "t_user"(
    "id" NUMBER(11) not null,
    "name" varchar(50) default '' not null,
    "age" NUMBER(11) not null,
    "email" varchar(50) default '' not null,
    "create_time" date null
,primary key ("id")
);

comment on table "t_user" is '用户表';

comment on column "t_user"."id" is '主键';

comment on column "t_user"."name" is '姓名';

comment on column "t_user"."age" is '年龄';

comment on column "t_user"."email" is '邮箱';

comment on column "t_user"."create_time" is '创建时间';

PgSQL

SQL 复制代码
drop table if exists "t_user";

create table "t_user"(
    "id" integer generated always as identity not null
    ,
    "name" varchar(50) not null default ''
    ,
    "age" int not null
    ,
    "email" varchar(50) not null default ''
    ,
    "create_time" timestamp not null
    
    ,primary key (id)
);

comment on table "t_user" is '用户表';

comment on column "t_user"."id" is '主键';
comment on column "t_user"."name" is '姓名';
comment on column "t_user"."age" is '年龄';
comment on column "t_user"."email" is '邮箱';
comment on column "t_user"."create_time" is '创建时间';

这三个 DDL 语句在对应的数据库里面是可以运行

总结

这里采用数据库存储模版的方式比在代码中硬编码维护性要好,不然模版有改动的话,还需要去修改代码重新打包,总体下来并不是很难。

相关推荐
是紫焅呢8 分钟前
I排序算法.go
开发语言·后端·算法·golang·排序算法·学习方法·visual studio code
lovebugs18 分钟前
Java线上死锁问题实战:从定位到解决的全链路指南
java·后端·面试
飞飞帅傅26 分钟前
go语言位运算
开发语言·后端·golang
kong@react1 小时前
使用springboot实现过滤敏感词功能
java·spring boot·后端·spring
丘山子2 小时前
一些 Python 字典(dict)的常见使用误区
后端·python·面试
31535669133 小时前
我开源了一套springboot3快速开发模板
后端·github
我崽不熬夜3 小时前
为什么Java中的设计模式会让你的代码更优雅?
java·后端·设计模式
先做个垃圾出来………3 小时前
简单的 Flask 后端应用
后端·python·flask
音元系统4 小时前
项目开发中途遇到困难的解决方案
后端·目标跟踪·中间件·服务发现