抽象思维实践——ddl2plantuml 开发记录

本贴最后更新于 1840 天前,其中的信息可能已经时异事殊

项目源码地址: github

背景

利用 plantuml 绘制架构评审图时,发现数据库 ER 图手写字段信息成本太大,想要一个把表结构转换为 plantuml 格式的工具。
搜索了一番,没有发现支持工具,所以准备手撸一个,并记录下设计及编码实现的过程。

需求

一句话需求: 读取数据库结构,转换为 plantuml 格式的 ER 图。

设计

根据需求抽象出下列概念

基础元素(一个输入一个输出)

  1. ER -> plantuml 格式 er
  2. db_schema -> 数据库结构,ddl 语句是其中一种实现形式

扩展概念

  1. table -> 表结构信息
  2. template -> 模板,定义 er 图输出格式

操作行为

  1. reader -> 读取数据库结构,识别 table
  2. parser -> 根据 tabletemplate 转换为 ER
  3. writer -> 输出 ER 图文件

整体交互

design

选型

都是基本的文件及 String 操作,通过 druid 进行 sql 解析

编码实现

Reader

读取 ddl.sql 文件,解析成 table

interface Reader {

    fun read(dbType: String? = DEFAULT_DB_TYPE): Iterable<Table>

    fun extract(dbType: String, sql: String): Table {
    ...    
    }
}

class FileReader(private val path: String) : Reader {

    override fun read(dbType: String?): Iterable<Table> {
        return Files.readAllLines(Paths.get(path))
                .filter { !it.startsWith("#") }
                .joinToString("")
                .split(";")
                .filter { it.isNotBlank() }
                .map { extract(dbType?: DEFAULT_DB_TYPE, it) }
                .toList()
    }

}

Writer

template 通过 resource 文件管理,接收 table 输出 plantuml 格式 ER

interface Writer {

    fun write(tables: Iterable<Table>)

    fun parse(tables: Iterable<Table>): String {
        val template = Thread.currentThread().contextClassLoader.getResource("dot.template")!!.readText()

        val content = tables.joinToString("") { table ->
            val columns = table.columnList.joinToString("\n") { "${it.notNullNameWrapper()} ${it.type} ${it.defaultValue} ${it.comment}" }
            "Table(${table.name}, \"${table.name}\\n(${table.comment})\"){ \n $columns + \n } \n"
        }

        return template.replace("__content__", content)
    }

    private fun Column.notNullNameWrapper(): String {
        return if (this.notNull) {
            "not_null(${this.name})"
        } else {
            this.name
        }
    }
}

class FileWriter(private val path: String) : Writer {

    override fun write(tables: Iterable<Table>) {
        Files.write(Paths.get(path), parse(tables).toByteArray())
    }

}

Main

fun main(args: Array<String>) {

    val inPath = args[0]
    val outPath = args[1]
    val dbType = args.getOrNull(2)

    FileReader(inPath).read(dbType)
            .apply { FileWriter(outPath).write(this) }

}

效果

java -jar ddl2plantuml.jar ddl.sql er.puml

程序会读取当前目录下的 ddl.sql 文件,并转换生成 er.puml 文件。

@startuml !define Table(name,desc) class name as "desc" << (T,#FFAAAA) >> !define primary_key(x) <color:red><b>x</b></color> !define unique(x) <color:green>x</color> !define not_null(x) <u>x</u> hide methods hide stereotypes Table(table_1, "table_1\n(This is table 1)"){ not_null(id) bigint column_1 not_null(prod_name) varchar column_2 not_null(prod_type) tinyint '0' column_3 0:活期 1:定期 not_null(start_time) time 停止交易开始时间 not_null(end_time) time 停止交易结束时间 not_null(online_type) tinyint '0' 0:上线 1:未上线 not_null(prod_info) varchar '' 产品介绍 not_null(over_limit) tinyint '0' 超额限制 0:限制 1:不限制 not_null(created_time) datetime CURRENT_TIMESTAMP not_null(updated_time) datetime CURRENT_TIMESTAMP } Table(table_2, "table_2\n(This is table 2)"){ not_null(id) bigint not_null(user_id) bigint 用户id not_null(user_name) varchar 用户名称 not_null(prod_id) bigint 产品id interest_date dateNULL计息日期 not_null(created_time) datetime CURRENT_TIMESTAMP 创建时间 not_null(updated_time) datetime CURRENT_TIMESTAMP 更新时间 } Table(table_3, "table_3\n(This is table 3)"){ not_null(id) bigint not_null(user_id) bigint 用户id not_null(user_name) varchar 用户名称 not_null(prod_id) bigint 产品id interest_date dateNULL计息日期 not_null(created_time) datetime CURRENT_TIMESTAMP 创建时间 not_null(updated_time) datetime CURRENT_TIMESTAMP 更新时间 } @enduml

result

  • PlantUML
    4 引用 • 6 回帖 • 1 关注
  • 创造

    你创造的作品可能会帮助到很多人,如果是开源项目的话就更赞了!

    178 引用 • 997 回帖
2 操作
crick77 在 2019-12-08 23:52:59 置顶了该帖
crick77 在 2019-12-08 23:52:47 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...