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

项目源码地址: github.com/wangyuheng/…

背景

利用plantuml绘制架构评审图时,发现数据库ER图手写字段信息成本太大,需要一个把DB表结构转换为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图文件

整体交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码storage "Context" {
node ddl
node db_schema
node ER
node template
node table
usecase parser
usecase reader
usecase writer
}
ddl -down-|> db_schema
db_schema -down-> reader
reader -> table
table -> parser
parser <-up- template
parser -down-> writer
writer -> ER

design

选型

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

编码实现

Reader

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码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

1
2
3
4
5
6
7
8
9
10
复制代码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) }

}

效果

1
复制代码java -jar ddl2plantuml.jar ddl.sql er.puml

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码@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

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%