53. [SQL]Druid SQL解析器

53.1. 问题的开始

如何根据数据库中发生的DDL语句,从中解析得到每个表的Comment以及字段的Comment呢?

调研了一下开源的工具有哪些,目前知道阿里JSQLParser、Apache Calcite还有开源的Druid都支持SQL的解析,使用了一下发现JSQLParser很快但是API不好用,Calcite被很多开源工具使用,但是目前还不会具体使用(没做深入研究),听说Druid很强,于是仔细了解了一下,并且实现了在生产环境的应用。下面简单谈一谈Druid的使用。

53.2. 快速上手

我们使用一段简短的SQL来解析并得到字段

    @Test
    public void testCreateParse(){
        String ddl = "CREATE TABLE `tm_bubble_status` (\n" +
                "  `id` bigint(20) NOT NULL COMMENT '主键id',\n" +
                "  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',\n" +
                "  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" +
                "  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\n" +
                "  PRIMARY KEY (`id`)\n" +
                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小气关闭状态表'";
        // 新建 MySQL Parser
        SQLStatementParser parser = new MySqlStatementParser(ddl);

        // 使用Parser解析生成AST,这里SQLStatement就是AST
        SQLStatement statement = parser.parseStatement();

        // 使用visitor来访问AST
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
        statement.accept(visitor);

        // 从visitor中拿出你所关注的信息
        System.out.println(visitor.getColumns());

    }

上面使用MySqlStatementParser解析一个MySQL中的DDL建表语句,拿到字段信息,控制台输出:

[tm_bubble_status.id, tm_bubble_status.user_id, tm_bubble_status.create_time, tm_bubble_status.update_time]

Process finished with exit code 0

53.3. 自定义解析案例

回到文章刚开始的问题,如果想要拿到语句中所有的字段以及它们的comment,该如何做呢?

直接放码吧!

53.3.1. 解析CREATE语句

    @Test
    public void testCreateTable() {
        String ddl = "CREATE TABLE `tm_bubble_status` (\n" +
                "  `id` bigint(20) NOT NULL COMMENT '主键id',\n" +
                "  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',\n" +
                "  `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" +
                "  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',\n" +
                "  PRIMARY KEY (`id`)\n" +
                ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小气关闭状态表'";

        HashMap<String, String> colAndComment = new HashMap<>();
        try {
            MySqlStatementParser parser = new MySqlStatementParser(ddl);
            // 使用Parser解析生成AST,这里SQLStatement就是AST
            SQLCreateTableStatement statement = (SQLCreateTableStatement) parser.parseStatement();
            List<SQLColumnDefinition> columnDefinitions = statement.getColumnDefinitions();

            for (SQLColumnDefinition columnDefinition : columnDefinitions) {
                String colName = columnDefinition.getName().getSimpleName().replaceAll("`", "");
                String comment = columnDefinition.getComment().toString().replaceAll("'", "");
                colAndComment.put(colName, comment);
            }


            System.out.println(colAndComment);

        } catch (Exception e) {
            throw new IllegalArgumentException("DDL语法不正确");

        }
    }

控制台输出:

{update_time=修改时间, create_time=创建时间, user_id=用户id, id=主键id}

53.3.2. 解析ALTER语句

    @Test
    public void testAlterTable() {
        String ddl3 = "ALTER TABLE tm_app_banner\n" +
                "MODIFY `package_name` VARCHAR(100) DEFAULT NULL COMMENT '包名xx',\n" +
                "DROP COLUMN `image1`,\n" +
                "DROP COLUMN `image2`,\n" +
                "ADD COLUMN `images` VARCHAR(255) DEFAULT NULL COMMENT 'URL: xx',\n" +
                "MODIFY `status` TINYINT(1) DEFAULT '2' COMMENT '0:很好的,1:y,不好的:好的 heiei',\n" +
                "ADD COLUMN `white_list_flag` TINYINT(1) DEFAULT '1' COMMENT '好的嘛 OK',\n" +
                "ADD COLUMN `white_list_categorys` VARCHAR(11) DEFAULT NULL COMMENT '白名单状态 并且',\n" +
                "ADD COLUMN `hu_software_version` INT(11) DEFAULT '0' COMMENT 'hu_software_version and other'";
        try {
            MySqlStatementParser parser = new MySqlStatementParser(ddl3);
            // 使用Parser解析生成AST,这里SQLStatement就是AST
            SQLAlterTableStatement statement = (SQLAlterTableStatement) parser.parseStatement();

            List<SQLAlterTableItem> items = statement.getItems();
            HashMap<String, String> colAndComment = new HashMap<>();
            items.forEach(
                    x -> {
                        //如果是ADD语句
                        if (x instanceof SQLAlterTableAddColumn) {
                            for (SQLColumnDefinition column : ((SQLAlterTableAddColumn) x).getColumns()) {
                                String colName = column.getName().getSimpleName().replaceAll("`", "");
                                String comment = column.getComment().toString().replaceAll("'", "");
                                colAndComment.put(colName, comment);
                            }
                        }
                        //如果MODIFY语句
                        if (x instanceof MySqlAlterTableModifyColumn) {
                            SQLColumnDefinition newColumnDefinition = ((MySqlAlterTableModifyColumn) x).getNewColumnDefinition();
                            String colName = newColumnDefinition.getName().getSimpleName();
                            String comment = newColumnDefinition.getComment().toString();
                            colAndComment.put(colName, comment);
                        }
                    }
            );
            System.out.println(colAndComment);
        } catch (Exception e) {
            throw new IllegalArgumentException("DDL语法不正确");

        }
    }

控制台输出:

{images=URL: xx, `status`='0:很好的,1:y,不好的:好的 heiei', white_list_flag=好的嘛 OK, white_list_categorys=白名单状态 并且, `package_name`='包名xx', hu_software_version=hu_software_version and other}

上面之解析了ADD字段的部分和MODIFY字段的部分。

53.4. 拓展

我们以一段代码为例,打一个断点,进入代码深入看看还可以做些什么。

testCreateTable方法中的

SQLCreateTableStatement statement = (SQLCreateTableStatement) parser.parseStatement();

这一行打一个断点。运行。

image-20230714162522111

从断点中可以看到各种信息,再看下每个字段的细节

image-20230714162734744

由此可以看到,如果想要拿到字段及其对应的类型,即可以从SQLColumnDefinition中拿到。