导出数据表结构到Excel
最近项目复盘,需要提供的资料包括数据字典,也就是项目的表结构,注释,类型,是否可空等信息。表要是少的话,一个一个填倒也没啥。 表一多的话,填写起来就很费劲了。于是顺手写了个小玩意儿,用来导出表结构到Excel。具体思路就是
information_schema数据库中的COLUMNS表 记录了所有的表的信息,查询然后填充到Excel中就好了。
package main
import "fmt"
type Config struct {
User string `json:"user"`
Password string `json:"password"`
Host string `json:"host"`
Port int `json:"port"`
Database string `json:"database"`
Include []string `json:"include"`
Exclude []string `json:"exclude"`
ProjectName string `json:"projectName"`
ProjectVersion string `json:"projectVersion"`
Author string `json:"author"`
WriteAt string `json:"writeAt"`
ExpectedRelease string `json:"expectedRelease"`
Reader string `json:"reader"`
Purpose string `json:"purpose"`
Histories []struct{
Version string `json:"version"`
Date string `json:"date"`
Content string `json:"content"`
Reason string `json:"reason"`
Author string `json:"author"`
} `json:"histories"`
}
func (c Config) getDsn() string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8mb4", c.User, c.Password, c.Host, c.Port)
}
:::
// db.go
package main
import (
"github.com/jinzhu/gorm"
)
type Column struct {
TableCatalog string `gorm:"column:TABLE_CATALOG;type:varchar(64)"`
TableSchema string `gorm:"column:TABLE_SCHEMA;type:varchar(64)"`
TableNameReal string `gorm:"column:TABLE_NAME;type:varchar(64)"`
ColumnName string `gorm:"column:COLUMN_NAME;type:varchar(64)"`
OrdinalPosition uint `gorm:"column:ORDINAL_POSITION"`
ColumnDefault *string `gorm:"column:COLUMN_DEFAULT;type:text"`
IsNullable string `gorm:"column:IS_NULLABLE;type:varchar(3)"`
DataType string `gorm:"column:DATA_TYPE;type:longtext"`
CharacterMaximumLength *int64 `gorm:"column:CHARACTER_MAXIMUM_LENGTH;type:bigint"`
CharacterOctetLength *int64 `gorm:"column:CHARACTER_OCTET_LENGTH;type:bigint"`
NumericPrecision *uint64 `gorm:"column:NUMERIC_PRECISION;type:unsigned bigint"`
NumericScale *uint64 `gorm:"column:NUMERIC_SCALE;type:unsigned bigint"`
DatetimePrecision *uint32 `gorm:"column:DATETIME_PRECISION"`
CharacterSetName *string `gorm:"column:CHARACTER_SET_NAME;type:varchar(64)"`
CollationName *string `gorm:"column:COLLATION_NAME;type:varchar(64)"`
ColumnType string `gorm:"column:COLUMN_TYPE;type:mediumtext"`
ColumnKey string `gorm:"column:COLUMN_KEY;type:enum('','PRI','UNI','MUL')"` // ???
Extra string `gorm:"column:EXTRA;type:varchar(256)"`
Privileges string `gorm:"column:PRIVILEGES;type:varchar(154)"`
ColumnComment string `gorm:"column:COLUMN_COMMENT;type:text"`
GenerationExpression string `gorm:"column:GENERATION_EXPRESSION;type:longtext"`
SrsId *uint32 `gorm:"column:SRS_ID;type:unsigned int"`
}
var DB *gorm.DB
func connect(dsn string) {
var err error
DB, err = gorm.Open("mysql", dsn)
if err != nil {
panic("连接数据库失败")
}
}
// 获取该数据库下所有的列信息
// @return 返回的是按表名分组的列信息
func getTables(dbName string, include, exclude []string) map[string][]Column {
var columns []Column
var count = 100
if len(include) > 0 {
DB.Where("TABLE_SCHEMA = ? and TABLE_NAME in (?)", dbName, include).Find(&columns)
} else if len(exclude) > 0 {
DB.Where("TABLE_SCHEMA = ? and TABLE_NAME not in (?)", dbName, exclude).Find(&columns)
} else {
DB.Where("TABLE_SCHEMA = ?", dbName).Find(&columns)
}
var maps = make(map[string][]Column, count)
for _, c := range columns {
if _, ok := maps[c.TableNameReal]; !ok {
maps[c.TableNameReal] = []Column{c}
} else {
maps[c.TableNameReal] = append(maps[c.TableNameReal], c)
}
}
return maps
}
func (c Column) TableName() string {
return "COLUMNS"
}
// main.go
package main
import (
"encoding/json"
"errors"
"fmt"
"github.com/360EntSecGroup-Skylar/excelize/v2"
_ "github.com/jinzhu/gorm/dialects/mysql"
"github.com/urfave/cli/v2"
"io/ioutil"
"log"
"os"
"strconv"
"time"
)
func main() {
app := &cli.App{
Name: "exporter",
Usage: "导出指定数据库的表元数据",
Compiled: time.Now(),
Authors: []*cli.Author{
{Name: "xycc", Email: "[email protected]"},
},
Commands: []*cli.Command{
{
Name: "json",
Category: "config",
Usage: "生成所需的配置文件模板",
Aliases: []string{"j"},
Action: func(context *cli.Context) error {
f, err := os.Create("config.json")
if err != nil {
return errors.New(fmt.Sprintf("创建配置文件失败,请确认对当前目录有读写权限: %v\n", err))
}
_, err = f.WriteString(`{
"user": "root",
"password": "password",
"host": "127.0.0.1",
"port": 3306,
"database": "xxxx",
"include": [],
"exclude": [],
"projectName": "xxxxx",
"projectVersion": "1.0.0",
"author": "作者,例如:xycc",
"writeAt": "文档撰写时间例如:2009/05/04",
"expectedRelease": "预计上线时间,例如:2009/06/13",
"reader": "读者,例如:xxx项目和XXXX业务项目",
"purpose": "目的,例如本数据字典是为了干嘛的,哪个项目的",
"histories": [
{
"version": "文档版本编号,这个数组是历史修订的数组",
"date": "修订日期",
"content": "修订内容",
"reason": "修订原因",
"author": "修订人"
}
]
}`)
if err != nil {
return errors.New(fmt.Sprintf("写入配置文件失败,请确保对当前目录有读写权限: %v\n", err))
}
log.Println(`已将示例配置文件写入了当前目录下的"config.json"文件中,修改配置文件,然后运行generate命令生成文档
注意: 1. include配置项的作用为只解析此数组内的表, exclude配置项的作用为此数组内的表除外都解析
2. include与exclude配置互斥,同时存在的情况下只有include会生效
3. exclude与include最好二选一, include以上的选项都必填`)
return nil
},
},
{
Name: "generate",
Category: "action",
Aliases: []string{"g"},
Usage: "此子命令的作用为根据当前目录下的config.json文件生成数据字典的excel文件",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "此选项为指定配置文件,默认为当前目录下的config.json文件, 仅支持json文件且格式与示例文件的格式相同",
Value: "config.json",
Required: false,
},
},
Action: func(context *cli.Context) error {
// 链接数据库 并且获取到指定数据库的指定表的元数据
fileName := context.String("config")
file, err := os.OpenFile(fileName, os.O_RDONLY, 0600)
if err != nil {
return errors.New(fmt.Sprintf("不能打开文件:[%s]的内容,请检查, 错误原因: %v\n", fileName, err))
}
data, err := ioutil.ReadAll(file)
if err != nil {
return errors.New(fmt.Sprintf("不能读取文件: [%s]的内容,请检查,错误原因: %v\n", fileName, err))
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return errors.New(fmt.Sprintf("不能反序列化json文件: [%s]的内容, 请检查格式是否正确,可以使用json命令生成示例配置文件, 错误原因: %v\n", fileName, err))
}
connect(config.getDsn())
defer func() {
_ = DB.Close()
}()
f := excelize.NewFile()
sheetName := config.ProjectName + "数据字典"
f.DeleteSheet("sheet1")
index := f.NewSheet(sheetName)
titleStyle, _ := f.NewStyle(`{"font":{"bold":true,"size":24}}`)
f, startRow := setTitle(f, sheetName, config, titleStyle)
// 然后插入每张表的数据
columnMap := getTables(config.Database, config.Include, config.Exclude)
f = parseColumnMap(f, sheetName, columnMap, startRow)
f.SetActiveSheet(index)
_ = f.SaveAs(sheetName + ".xlsx")
return nil
},
},
},
}
err := app.Run(os.Args)
if err != nil {
log.Fatalln(err)
}
}
func setTitle(f *excelize.File, sheetName string, config Config, titleStyle int) (*excelize.File, int) {
_ = f.MergeCell(sheetName, "A1", "E1")
f.SetCellValue(sheetName, "A1", "")
f.SetCellStyle(sheetName, "A1", "A1", titleStyle)
f.SetCellValue(sheetName, "A2", "文档名称")
f.MergeCell(sheetName, "B2", "E2")
f.SetCellValue(sheetName, "B2", config.ProjectName + "项目数据字典")
f.SetCellValue(sheetName, "A3", "产品版本")
f.MergeCell(sheetName, "B3", "E3")
f.SetCellValue(sheetName, "B3", config.ProjectVersion)
f.SetCellValue(sheetName, "A4", "撰写人")
f.MergeCell(sheetName, "B4", "E4")
f.SetCellValue(sheetName, "B4", config.Author)
f.SetCellValue(sheetName, "A5", "撰写时间")
f.MergeCell(sheetName, "B5", "E5")
f.SetCellValue(sheetName, "B5", config.WriteAt)
f.SetCellValue(sheetName, "A6", "预计上线时间")
f.MergeCell(sheetName, "B6", "E6")
f.SetCellValue(sheetName, "B6", config.ExpectedRelease)
f.SetCellValue(sheetName, "A7", "读者")
f.MergeCell(sheetName, "B7", "E7")
f.SetCellValue(sheetName, "B7", config.Reader)
f.SetCellValue(sheetName, "A8", "目的")
f.MergeCell(sheetName, "B8", "E8")
f.SetCellValue(sheetName, "B8", config.Purpose)
f.SetRowHeight(sheetName, 4, 32)
f.SetRowHeight(sheetName, 8, 36)
f.SetRowHeight(sheetName, 2, 28)
f.SetRowHeight(sheetName, 3, 28)
f.SetRowHeight(sheetName, 5, 28)
f.SetRowHeight(sheetName, 6, 28)
f.SetRowHeight(sheetName, 7, 28)
f.SetCellValue(sheetName, "A10", "历史修订")
f.MergeCell(sheetName, "A10", "E10")
f.SetCellValue(sheetName, "A11", "文档版本编号")
f.SetCellValue(sheetName, "B11", "修订日期")
f.SetCellValue(sheetName, "C11", "修订内容")
f.SetCellValue(sheetName, "D11", "修订原因")
f.SetCellValue(sheetName, "E11", "修订人")
startRow := 12
for _, x := range config.Histories {
cell := strconv.Itoa(startRow)
f.SetCellValue(sheetName, "A"+cell, x.Version)
f.SetCellValue(sheetName, "B"+cell, x.Date)
f.SetCellValue(sheetName, "C"+cell, x.Content)
f.SetCellValue(sheetName, "D"+cell, x.Reason)
f.SetCellValue(sheetName, "E"+cell, x.Author)
startRow += 1
}
startRow += 1
return f,startRow
}
// 组装每一行的数据
func parseColumnMap(f *excelize.File, sheetName string, columnMap map[string][]Column, startRow int) *excelize.File {
for tableName, cols := range columnMap {
f.SetCellValue(sheetName, "A" + strconv.Itoa(startRow), tableName)
startRow += 1
cell := strconv.Itoa(startRow)
f.SetCellValue(sheetName, "A" + cell, "序号")
f.SetCellValue(sheetName, "B" + cell, "名称")
f.SetCellValue(sheetName, "C" + cell, "字段(代码)")
f.SetCellValue(sheetName, "D" + cell, "数据类型(长度)")
f.SetCellValue(sheetName, "E" + cell, "空")
f.SetCellValue(sheetName, "F" + cell, "说明")
f.SetCellValue(sheetName, "G" + cell, "是否必填")
startRow += 1
for i, col := range cols {
cell = strconv.Itoa(startRow)
f.SetCellValue(sheetName, "A" + cell, i+1)
f.SetCellValue(sheetName, "B"+cell, col.ColumnName)
f.SetCellValue(sheetName, "C"+cell, col.ColumnName)
f.SetCellValue(sheetName, "D"+cell, col.ColumnType)
f.SetCellValue(sheetName, "E"+cell, col.IsNullable)
f.SetCellValue(sheetName, "F"+cell, col.ColumnComment)
f.SetCellValue(sheetName, "G"+cell, col.IsNullable)
startRow += 1
}
startRow += 2 // 空一行
}
return f
}