Contents

golang-go-zero-教程-Model

go-zero官网

go-zero详细文档

本系列为作者跟着Mikaelemmmm的b站教学视频学习时做的笔记

go-zero官方mysql博客

go-zero官方缓存设计之持久层缓存博客

指令

通过sql或表生成model

1
2
3
goctl model mysql ddl -src="./*.sql" -dir="./sql/model" -c --style=goZero

$ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="*"  -dir="./model" --style=goZero

Mikaelemmmm推荐使用navcat先生成表再通过表生成sql和model

这里我构建好了sql之后使用,注意这里没有cache所以没有-c选项:

1
goctl model mysql ddl -src="./sql/*.sql" -dir="./GenModel" --style=goZero

Model文件

生成的文件放到任意位置

引入Model分4步:

  1. 修改yaml
1
2
DB:
  DataSource: root:xxxxxx@tcp(127.0.0.1:3306)/zero-demo?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
  1. 修改config.go,给Config结构体添加相应字段
1
2
3
DB struct {
  DataSource string
}
  1. 添加中间件客户端到svcCtx结构体字段
1
2
3
4
type ServiceContext struct {
	Config    config.Config
	UserModel model.UserModel
}
  1. 给svcCtx的New函数添加相应的字段值
1
2
3
4
5
6
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		Config:    c,
		UserModel: model.NewUserModel(sqlx.NewMysql(c.DB.DataSource), nil),
	}
}

之后就可以在handler和logic里面使用它们了

源码分析

userModel_gen.go

不要修改这个文件,每次构建api时会覆盖,如果要添加新的方法可以在userModel.go中修改

变量Var

1
2
3
4
5
6
7
8
//结构体字段名切片
userFieldNames          = builder.RawFieldNames(&User{})
//结构体字段使用`,`连接,用于sql语句select 字段生成
userRows                = strings.Join(userFieldNames, ",")
//清除了默认生成字段的select字段
userRowsExpectAutoSet   = strings.Join(stringx.Remove(userFieldNames, "`id`", "`update_at`", "`updated_at`", "`update_time`", "`create_at`", "`created_at`", "`create_time`"), ",")
//清除默认生成字段的update字段
userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "`id`", "`update_at`", "`updated_at`", "`update_time`", "`create_at`", "`created_at`", "`create_time`"), "=?,") + "=?"

对于有缓存的Model

1
2
3
4
//缓存的主键前缀,通常在使用
cacheUserDataIdPrefix     = "cache:userData:id:"
//缓存的普通索引前缀
cacheUserMobilePrefix = "cache:user:mobile:"
接口
1
2
3
4
5
6
7
userModel interface {
  Insert(ctx context.Context, data *User) (sql.Result, error)
  FindOne(ctx context.Context, id int64) (*User, error)
  FindOneByMobile(ctx context.Context, mobile string) (*User, error)
  Update(ctx context.Context, data *User) error
  Delete(ctx context.Context, id int64) error
}

表结构体

User结构体对应表结构

defaultUserModel是userModel接口的实现,其具有默认生成的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
defaultUserModel struct {
  sqlc.CachedConn
  table string
}

User struct {
  Id       int64  `db:"id"`
  Nickname string `db:"nickname"`
  Mobile   string `db:"mobile"`
}

函数

一个工厂函数,用于创建defaultUserModel对象

不带缓存Model

1
2
3
4
5
6
func newUserDataModel(conn sqlx.SqlConn) *defaultUserDataModel {
	return &defaultUserDataModel{
		conn:  conn,
		table: "`user_data`",
	}
}

带缓存Model

1
2
3
4
5
6
func newUserDataModel(conn sqlx.SqlConn, c cache.CacheConf) *defaultUserDataModel {
	return &defaultUserDataModel{
		CachedConn: sqlc.NewConn(conn, c),
		table:      "`user_data`",
	}
}

方法

接口中方法在defaultUserModel对象的实现

 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
73
74
75
76
77
78
79
80
81
82
83
84
85
//删除条目
func (m *defaultUserModel) Delete(ctx context.Context, id int64) error {
  //通过FindOne确定是否有该id的条目,如果没有直接返回数据库给出的错误
	data, err := m.FindOne(ctx, id)
	if err != nil {
		return err
	}
  //如果有该条目
  //生成缓存查询字段
	userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
  //生成数据库查询字段
	userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
  //执行带缓存命令,该命令内部自动识别执行数据库操作,并执行相应的缓存操作,只需要写数据库的逻辑即可
	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
		query := fmt.Sprintf("delete from %s where `id` = ?", m.table)
		return conn.ExecCtx(ctx, query, id)
	}, userIdKey, userMobileKey)
	return err
}

//通过主键查询
func (m *defaultUserModel) FindOne(ctx context.Context, id int64) (*User, error) {
  //缓存查询字段
	userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id)
  //接收的结构体,当不是查询一个的时候可以使用QueryRowsCtx接收一个切片,详见源码或文档
	var resp User
  //执行QueryRowCtx查询
	err := m.QueryRowCtx(ctx, &resp, userIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
		query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
		return conn.QueryRowCtx(ctx, v, query, id)
	})
	switch err {
	case nil:
		return &resp, nil
	case sqlc.ErrNotFound:
		return nil, ErrNotFound
	default:
		return nil, err
	}
}

//插入条目
func (m *defaultUserModel) Insert(ctx context.Context, data *User) (sql.Result, error) {
  //生成缓存主键查询字段
	userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
  //生成缓存mobile索引查询字段
	userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
  //使用ExecCtx函数执行sql查询,其内部封装了缓存操作
	ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
		query := fmt.Sprintf("insert into %s (%s) values (?, ?)", m.table, userRowsExpectAutoSet)
		return conn.ExecCtx(ctx, query, data.Nickname, data.Mobile)
	}, userIdKey, userMobileKey)
	return ret, err
}

//更新条目
func (m *defaultUserModel) Update(ctx context.Context, newData *User) error {
  //查询是否有该主键条目
	data, err := m.FindOne(ctx, newData.Id)
	if err != nil {
		return err
	}
  //生成缓存主键查询字段
	userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
  //生成mobile索引查询字段
	userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
  //通过ExecCtx函数执行更新操作,内部封装了缓存操作
	_, err = m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
		query := fmt.Sprintf("update %s set %s where `id` = ?", m.table, userRowsWithPlaceHolder)
		return conn.ExecCtx(ctx, query, newData.Nickname, newData.Mobile, newData.Id)
	}, userIdKey, userMobileKey)
	return err
}

//传入主键并返回该主键的缓存查询字段
func (m *defaultUserModel) formatPrimary(primary interface{}) string {
	return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary)
}

//与findOne函数一样但是不走缓存,直接查数据库
func (m *defaultUserModel) queryPrimary(ctx context.Context, conn sqlx.SqlConn, v, primary interface{}) error {
	query := fmt.Sprintf("select %s from %s where `id` = ? limit 1", userRows, m.table)
  //这里直接使用conn查询数据库,对于统一事务的查询可以使用同一个conn进行执行
	return conn.QueryRowCtx(ctx, v, query, primary)
}

缓存

Cache仅做单条数据的缓存

通常情况在etc中设置Cache和Redis,Cache是go-zero内置的配置对象,专门用于给Model服务,Redis则使用第三方redis库,作为中间件,可以做缓存也可以做其他作用,比如消息队列

带缓存的所有方法都跟不带缓存的Model不一样

带缓存的数据在缓存中找不到键的值时后才去查询DB

为了防止缓存雪崩,源码中对于未查到的数据会给插入一个*值过期时间是1分钟,后续查询该键的请求发现是*会直接返回。这样当多个请求打到缓存上时1分钟内只会触发一次数据库查询

添加缓存步骤

修改etc/yaml文件

1
2
3
Cache:
  - Host: 192.168.171.3:6379
    Pass: "990910"

修改internal/config.go

1
2
3
4
5
type Config struct {
	...
	Cache       cache.CacheConf
	...
}

修改internal/svc/serviceContext.go,新建Model的时候添加缓存

1
2
3
4
5
6
7
8
func NewServiceContext(c config.Config) *ServiceContext {
	return &ServiceContext{
		...
		UserModel:      model.NewUserModel(sqlx.NewMysql(c.DB.DataSource), c.Cache),
		UserDataModel:  model.NewUserDataModel(sqlx.NewMysql(c.DB.DataSource), c.Cache),
		...
	}
}

索引优化

只有唯一索引可以生成默认优化

userModel.go

该文件用于添加自定义的方法

该文件中的customUserModel继承了userModel_gen.go文件中的defaultUserModel,还有一个UserModel的接口继承了userModel_gen.go文件中的userModel接口

类型type

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type (
	// UserModels是一个接口,在这里添加自定义方法声明
	UserModel interface {
		userModel
	}
  //customUserModel因为继承了userModel_gen.go中的defaultUserModel它能够使用默认给defaultUserModel生成的所有方法,在这里添加新的自定义方法实现
	customUserModel struct {
		*defaultUserModel
	}
)

所以customUserModel可以使用defaultUserModel的所有方法,要添加新方法则直接修改UserModel接口和给customUserModel添加方法就行了

默认生成函数

1
2
3
4
5
6
//UserModel工厂函数
func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf) UserModel {
	return &customUserModel{
		defaultUserModel: newUserModel(conn, c),
	}
}

事务

查看sqlc的源码有如下方法

1
2
3
func (cc CachedConn) TransactCtx(ctx context.Context, fn func(context.Context, sqlx.Session) error) error {
	return cc.db.TransactCtx(ctx, fn)
}

使用该方法来实现事务原子性,参数ctx用于做链路追踪,参数fn表示事务过程

返回err为nil的时提交,否则回滚到函数执行之前的状态

可以在userModel.go中封装这个方法来实现在logic中使用事务

1
2
3
4
5
6
//暴露给logic开启事务
func (m *defaultUserDataModel) TransCtx(ctx context.Context, fn func(ctx context.Context, s sqlx.Session) error) error {
	return m.TransactCtx(ctx, func(ctx context.Context, s sqlx.Session) error {
		return fn(ctx, s)
	})
}

默认生成Model的Insert不支持传入session,可以重写Insert函数,接收一个sqlx.Session参数,并将默认执行sql的对象修改为传入的sqlx.Session对象,这样就能通过一个session多次执行sql

1
2
3
4
5
6
7
8
9
func (m *defaultUserModel) TransInsert(ctx context.Context, session sqlx.Session, data *User) (sql.Result, error) {
	userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id)
	userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile)
	ret, err := m.ExecCtx(ctx, func(ctx context.Context, conn sqlx.SqlConn) (result sql.Result, err error) {
		query := fmt.Sprintf("insert into %s (%s) values (?, ?)", m.table, userRowsExpectAutoSet)
		return session.ExecCtx(ctx, query, data.Nickname, data.Mobile)
	}, userIdKey, userMobileKey)
	return ret, err
}

另外需要使用同一个session去执行db操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if err := l.svcCtx.UserModel.TransCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
  user := &model.User{}
  user.Mobile = req.Mobile
  user.Nickname = req.Nickname
  //添加user
  dbResult, err := l.svcCtx.UserModel.TransInsert(ctx, session, user)
  if err != nil {
    return err
  }
  userId, _ := dbResult.LastInsertId()

  //添加userData
  userData := &model.UserData{}
  userData.UserId = userId
  userData.Data = "xxxx"
  if _, err := l.svcCtx.UserDataModel.TransInsert(ctx, session, userData); err != nil {
    return err
  }
  return nil
}); err != nil {
  fmt.Println(err)
  return nil, errors.New("创建用户失败")
}

可以在函数里面判断一下如果传入的session是否为nil,如果是则直接使用新的连接conn.ExecCtx

TransactCtx源码首先在函数体中开启事务,最后使用defer recover来实现回滚,当执行sql时出现panic或者err时候回滚该事务

缓存的使用

对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。

类型转换规则

文档

DB和Cache的使用

所有的方法都可以在末尾添加Ctx来使用ctx进行链路追踪

DB

go zero博客 mysql用法

特点

  • 完成 queryField -> struct 的自动赋值,反射
  • 批量插入「bulkinserter」
  • 自带熔断
  • API 经过若干个服务的不断考验
  • 提供 partial assignment 特性,不强制 struct 的严格赋值

可以自己新建数据库连接,间原文

CRUD

CUD

1
conn.Exec(sql, args...)

R

1
2
3
conn.QueryRow(&model, querysql, args...)
conn.QueryRows(&users, querysql, sex) //users为slice
conn.QueryRowPartial(&model, querysql, args...)

Cache

go zero博客 go-zero缓存设计之持久层缓存

类型

1
2
3
type QueryFn func(conn sqlx.SqlConn, v interface{}) error
type QueryCtxFn func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error
type ExecFn func(conn sqlx.SqlConn) (sql.Result, error)

方法

  • func (cc CachedConn) QueryRow(v interface{}, key string, query QueryFn) error:查看key是否存在,否则执行query
  • func (cc CachedConn) QueryRowIndex(v interface{}, key string, keyer func(primary interface{}) string, indexQuery IndexQueryFn, primaryQuery PrimaryQueryFn) error:基于唯一索引查询
    • keyer - 用主键生成基于主键缓存的key的方法
    • indexQuery - 用索引从DB读取完整数据的方法,需要返回主键
    • primaryQuery - 用主键从DB获取完整数据的方法
  • func (cc CachedConn) DelCache(keys …string) error
  • func (cc CachedConn) GetCache(key string, v interface{}) error
  • func (cc CachedConn) Exec(exec ExecFn, keys …string) (sql.Result, error)
  • func (cc CachedConn) ExecNoCache(q string, args …interface{}) (sql.Result, error)
  • func (cc CachedConn) QueryRowNoCache(v interface{}, q string, args …interface{}) error
  • func (cc CachedConn) QueryRowsNoCache(v interface{}, q string, args …interface{}) error
  • func (cc CachedConn) SetCache(key string, val interface{}) error
  • func (cc CachedConn) Transact(fn func(sqlx.Session) error) error
 |