ddl工具与代码生成

go-doudou ddl命令是表结构同步和生成单表dao层代码的命令行工具。其中表结构同步支持双向同步,即支持从go语言结构体创建和更新数据库表结构和从数据库表结构生成go语言结构体。

特性

  • 从Go语言结构体类型创建或更新表结构,仅新增和更新字段,不删字段
  • 从表结构生成Go语言结构体
  • 生成支持单表CRUD操作的Dao层代码
  • Dao层代码支持数据库事务
  • 支持索引的创建和更新
  • 支持外键的创建和更新

命令行参数

➜  go-doudou git:(main) go-doudou ddl -h 
migration tool between database table structure and golang struct

Usage:
  go-doudou ddl [flags]

Flags:
  -d, --dao             If true, generate dao code.
      --df string       Name of dao folder. (default "dao")
      --domain string   Path of domain folder. (default "domain")
      --env string      Environment name such as dev, uat, test, prod, default is dev (default "dev")
  -h, --help            help for ddl
      --pre string      Table name prefix. e.g.: prefix biz_ for biz_product.
  -r, --reverse         If true, generate domain code from database. If false, update or create database tables from domain code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14

表结构同步

从go语言结构体创建和更新数据库表结构命令示例:go-doudou ddl --pre=ddl_--pre表示表名称前缀。

从数据库表结构生成go语言结构体代码命令示例:go-doudou ddl --reverse --pre=ddl_,必须加--reverse-r

下面我们看一下从go语言结构体同步数据库表结构所需加的结构体标签。

示例

package domain

import "time"

type Base struct {
	CreateAt *time.Time `dd:"default:CURRENT_TIMESTAMP"`
	UpdateAt *time.Time `dd:"default:CURRENT_TIMESTAMP;extra:ON UPDATE CURRENT_TIMESTAMP"`
	DeleteAt *time.Time
}
1
2
3
4
5
6
7
8
9
package domain

//dd:table
type Book struct {
  ID          int `dd:"pk;auto"`
  UserId      int `dd:"type:int"`
  PublisherId int	`dd:"fk:ddl_publisher,id,fk_publisher,ON DELETE CASCADE ON UPDATE NO ACTION"`

  Base
}
1
2
3
4
5
6
7
8
9
10
package domain

//dd:table
type Publisher struct {
  ID   int `dd:"pk;auto"`
  Name string

  Base
}
1
2
3
4
5
6
7
8
9
package domain

import "time"

//dd:table
type User struct {
  ID         int    `dd:"pk;auto"`
  Name       string `dd:"index:name_phone_idx,2;default:'jack'"`
  Phone      string `dd:"index:name_phone_idx,1;default:'13552053960';extra:comment '手机号'"`
  Age        int    `dd:"unsigned"`
  No         int    `dd:"type:int;unique"`
  UniqueCol  int    `dd:"type:int;unique:unique_col_idx,1"`
  UniqueCol2 int    `dd:"type:int;unique:unique_col_idx,2"`
  School     string `dd:"null;default:'harvard';extra:comment '学校'"`
  IsStudent  bool
  ArriveAt *time.Time `dd:"type:datetime;extra:comment '到货时间'"`
  Status   int8       `dd:"type:tinyint(4);extra:comment '0进行中
1完结
2取消'"`

  Base
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

pk

表示主键

auto

表示自增

type

字段类型,非必须。如果没有显式设置,默认的对应规则如表格所示

Go语言类型(包括指针类型)Mysql字段类型
int, int16, int32int
int64bigint
float32float
float64double
stringvarchar(255)
bool, int8tinyint
time.Timedatetime
decimal.Decimaldecimal(6,2)

default

默认值。如果是mysql数据库内置的函数或由内置函数构成的表达式,则不需要单引号。如果是字面值,则需要单引号。

extra

额外定义。示例:"on update CURRENT_TIMESTAMP","comment 'cellphone number'"
注意:在comment里不要出现英文分号;和英文冒号:

index

设置索引。

  • 格式:"index:Name,Order,Sort" or "index"
  • Name: 索引名称,字符串类型。如果有多个字段设置了相同的索引名称,则会在该表中创建复合索引。非必须。默认值为字段名_idx
  • Order: 顺序,int类型
  • Sort: 排序规则,字符串类型。仅接受两种值:ascdesc。非必须。默认值是asc

unique

唯一索引,用法同索引。

null

可接受null值. 注意:如果字段类型是指针类型,则默认可接受null

unsigned

无符号

fk

设置外键

  • 格式:"fk:ReferenceTableName,ReferenceTablePrimaryKey,Constraint,Action"
  • ReferenceTableName:关联表名称
  • ReferenceTablePrimaryKey:关联表主键,如id
  • Constraint:外键名称,如fk_publisher
  • Action:示例:ON DELETE CASCADE ON UPDATE NO ACTION

Dao层代码生成

生成单表dao层代码时需要加上--dao,示例:go-doudou ddl --dao --pre=ddl_

单表CRUD

package dao

import (
	"context"
	"github.com/unionj-cloud/go-doudou/v2/toolkit/sqlext/query"
)

type Base interface {
	Insert(ctx context.Context, data interface{}) (int64, error)
	Upsert(ctx context.Context, data interface{}) (int64, error)
	UpsertNoneZero(ctx context.Context, data interface{}) (int64, error)
	Update(ctx context.Context, data interface{}) (int64, error)
	UpdateNoneZero(ctx context.Context, data interface{}) (int64, error)
	BeforeSaveHook(ctx context.Context, data interface{})
	AfterSaveHook(ctx context.Context, data interface{}, lastInsertID int64, affected int64)

	UpdateMany(ctx context.Context, data interface{}, where query.Q) (int64, error)
	UpdateManyNoneZero(ctx context.Context, data interface{}, where query.Q) (int64, error)
	BeforeUpdateManyHook(ctx context.Context, data interface{}, where query.Q)
	AfterUpdateManyHook(ctx context.Context, data interface{}, where query.Q, affected int64)

	DeleteMany(ctx context.Context, where query.Q) (int64, error)
	DeleteManySoft(ctx context.Context, where query.Q) (int64, error)
	BeforeDeleteManyHook(ctx context.Context, data interface{}, where query.Q)
	AfterDeleteManyHook(ctx context.Context, data interface{}, where query.Q, affected int64)

	SelectMany(ctx context.Context, where ...query.Q) (interface{}, error)
	CountMany(ctx context.Context, where ...query.Q) (int, error)
	PageMany(ctx context.Context, page query.Page, where ...query.Q) (query.PageRet, error)
	BeforeReadManyHook(ctx context.Context, page *query.Page, where ...query.Q)
	
	Get(ctx context.Context, id interface{}) (interface{}, error)
}
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

数据库事务

示例:

func (receiver *StockImpl) processExcel(ctx context.Context, f multipart.File, sheet string) (err error) {
	types := []string{"food", "tool"}
	var (
		xlsx *excelize.File
		rows [][]string
		tx   ddl.Tx
	)
	xlsx, err = excelize.OpenReader(f)
	if err != nil {
		return errors.Wrap(err, "")
	}
	rows, err = xlsx.GetRows(sheet)
	if err != nil {
		return errors.Wrap(err, "")
	}
	colNum := len(rows[0])
	rows = rows[1:]
	// 封装数据库连接实例到GddDB类型
    gdddb := wrapper.NewGddDB(db, wrapper.WithLogger(logger.NewSqlLogger(log.Default())))
	// 开启事务
	tx, err = gdddb.BeginTxx(ctx, nil)
	if err != nil {
		return errors.Wrap(err, "")
	}
	defer func() {
		if r := recover(); r != nil {
			_ = tx.Rollback()
			if e, ok := r.(error); ok {
				err = errors.Wrap(e, "")
			} else {
				err = errors.New(fmt.Sprint(r))
			}
		}
	}()
	// 将tx作为ddl.Querier实例注入dao层的工厂方法创建dao实例
	mdao := dao.NewMaterialDao(tx)
	for _, item := range rows {
		if len(item) == 0 {
			goto END
		}
		row := make([]string, colNum)
		copy(row, item)
		name := row[0]
		price := cast.ToFloat32(row[1])
		spec := row[2]
		pieces := cast.ToInt(row[3])
		amount := cast.ToInt(row[4])
		note := row[5]
		totalMount := pieces * amount
		if _, err = mdao.Upsert(ctx, domain.Material{
			Name:        name,
			Amount:      amount,
			Price:       price,
			TotalAmount: totalMount,
			Spec:        spec,
			Pieces:      pieces,
			Type:        int8(sliceutils.IndexOf(sheet, types)),
			Note:        note,
		}); err != nil {
			// 如果有错误,则回滚
			_ = tx.Rollback()
			return errors.Wrap(err, "")
		}
	}
END:
	// 提交事务
	if err = tx.Commit(); err != nil {
        _ = tx.Rollback()
		return errors.Wrap(err, "")
	}
	return err
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

钩子函数

ddl工具生成的dao层代码里提供了以下7个钩子函数,需用户自定义实现业务逻辑。

// 在 insert/upsert/update 操作中自动调用
BeforeSaveHook(ctx context.Context, data interface{})
AfterSaveHook(ctx context.Context, data interface{}, lastInsertID int64, affected int64)

// 在 update many 操作中自动调用
BeforeUpdateManyHook(ctx context.Context, data interface{}, where query.Q)
AfterUpdateManyHook(ctx context.Context, data interface{}, where query.Q, affected int64)

// 在 delete many 操作中自动调用
BeforeDeleteManyHook(ctx context.Context, data interface{}, where query.Q)
AfterDeleteManyHook(ctx context.Context, data interface{}, where query.Q, affected int64)

// 在 read many 操作中自动调用, 例如 SelectMany/CountMany/PageMany
BeforeReadManyHook(ctx context.Context, page *query.Page, where ...query.Q)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

query.Q是接口类型的参数,建议传入指针类型的实现类型,方便修改查询条件。

示例代码:

func (receiver UserDaoImpl) BeforeReadManyHook(ctx context.Context, page *query.Page, where ...query.Q) {
	// implement your business logic
	if len(where) > 0 {
		if criteria, ok := where[0].(*query.Criteria); ok {
			*criteria = criteria.Col("delete_at").IsNull()
		} else if w, ok := where[0].(*query.Where); ok {
			*w = w.And(query.C().Col("delete_at").IsNull())
		}
	}
}
1
2
3
4
5
6
7
8
9
10

新增dao层代码

实际开发中,我们一定需要自己编写一些更复杂的CRUD代码。怎么做呢?下面我们以user表为例来说明开发步骤:

  • 首先需要在dao文件夹下的userdao.go文件里的UserDao接口里定义方法,例如:
type UserDao interface {
	Base
	FindUsersByHobby(ctx context.Context, hobby string) ([]domain.User, error)
}
1
2
3
4

我们这里加了一个FindUsersByHobby方法

  • 然后我们需要在dao文件夹下新建一个文件userdaoimplext.go,文件名任意,但推荐以去掉前缀的表名 + daoimplext.go的方式命名

  • 在新创建的文件里编写FindUsersByHobby方法的实现

func (receiver UserDaoImpl) FindUsersByHobby(ctx context.Context, hobby string) (users []domain.User, err error) {
	sqlStr := `select * from ddl_user where hobby = ? and delete_at is null`
	err = receiver.db.SelectContext(ctx, &users, receiver.db.Rebind(sqlStr), hobby)
	return
}
1
2
3
4
5
  • 我们新建一个测试文件userdaoimplext_test.go,编写单元测试
func TestUserDaoImpl_FindUsersByHobby(t *testing.T) {
	t.Parallel()
	u := dao.NewUserDao(db)
	users, err := u.FindUsersByHobby(context.Background(), "football")
	require.NoError(t, err)
	require.NotEqual(t, 0, len(users))
}
1
2
3
4
5
6
7

最佳实践

下面说的几点最佳实践只是作者总结的,仅供参考。

  • 先通过Navicat或者Mysql Workbench之类的数据库设计工具整体设计表结构
  • 再通过命令go-doudou ddl --reverse --dao命令一把生成Go代码,--reverse参数仅在初始化项目时使用
  • 后续开发迭代过程中,修改domain文件夹的代码以后,须先将dao文件夹中的以sql.go为后缀的文件,如userdaosql.go删掉,再通过命令go-doudou ddl --dao将修改同步到数据库表结构,同时重新生成sql.go为后缀的文件。如果修改了表名称或者表前缀,则dao文件夹中的以daoimpl.go为后缀的文件,如userdaoimpl.go也需要删掉并重新生成。
  • 新增dao层代码一定要在新建的文件里编写,一定不要人工修改dao文件夹里的base.go、以daoimpl.go为后缀和以daosql.go为后缀的这三类文件的代码,在整个项目生命周期里这三类文件都必须是可以随时删除随时重新生成且不影响程序功能的