diff --git a/doc/goctl-model-sql.md b/doc/goctl-model-sql.md new file mode 100644 index 00000000..62e3fb9a --- /dev/null +++ b/doc/goctl-model-sql.md @@ -0,0 +1,274 @@ +# Goctl Model + +goctl model 为go-zero下的工具模块中的组件之一,目前支持识别mysql ddl进行model层代码生成,通过命令行或者idea插件(即将支持)可以有选择地生成带redis cache或者不带redis cache的代码逻辑。 + +# 快速开始 + +* 通过ddl生成 + + ```shell script + $ goctl model mysql ddl -src="./sql/user.sql" -dir="./sql/model" -c=true + ``` + + 执行上述命令后即可快速生成CURD代码。 + + ``` + model + │   ├── error.go + │   └── usermodel.go + ``` +* 通过datasource生成 + + ```shell script + $ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model" + ``` + + +* 生成代码示例 + + ``` go + package model + + import ( + "database/sql" + "fmt" + "strings" + "time" + + "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/core/stores/sqlc" + "github.com/tal-tech/go-zero/core/stores/sqlx" + "github.com/tal-tech/go-zero/core/stringx" + "github.com/tal-tech/go-zero/tools/goctl/model/sql/builderx" + ) + + var ( + userFieldNames = builderx.FieldNames(&User{}) + userRows = strings.Join(userFieldNames, ",") + userRowsExpectAutoSet = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), ",") + userRowsWithPlaceHolder = strings.Join(stringx.Remove(userFieldNames, "id", "create_time", "update_time"), "=?,") + "=?" + + cacheUserMobilePrefix = "cache#User#mobile#" + cacheUserIdPrefix = "cache#User#id#" + cacheUserNamePrefix = "cache#User#name#" + ) + + type ( + UserModel struct { + sqlc.CachedConn + table string + } + + User struct { + Id int64 `db:"id"` + Name string `db:"name"` // 用户名称 + Password string `db:"password"` // 用户密码 + Mobile string `db:"mobile"` // 手机号 + Gender string `db:"gender"` // 男|女|未公开 + Nickname string `db:"nickname"` // 用户昵称 + CreateTime time.Time `db:"create_time"` + UpdateTime time.Time `db:"update_time"` + } + ) + + func NewUserModel(conn sqlx.SqlConn, c cache.CacheConf, table string) *UserModel { + return &UserModel{ + CachedConn: sqlc.NewConn(conn, c), + table: table, + } + } + + func (m *UserModel) Insert(data User) (sql.Result, error) { + query := `insert into ` + m.table + `(` + userRowsExpectAutoSet + `) value (?, ?, ?, ?, ?)` + return m.ExecNoCache(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname) + } + + func (m *UserModel) FindOne(id int64) (*User, error) { + userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) + var resp User + err := m.QueryRow(&resp, userIdKey, func(conn sqlx.SqlConn, v interface{}) error { + query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1` + return conn.QueryRow(v, query, id) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } + } + + func (m *UserModel) FindOneByName(name string) (*User, error) { + userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, name) + var resp User + err := m.QueryRowIndex(&resp, userNameKey, func(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary) + }, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := `select ` + userRows + ` from ` + m.table + ` where name = ? limit 1` + if err := conn.QueryRow(&resp, query, name); err != nil { + return nil, err + } + return resp.Id, nil + }, func(conn sqlx.SqlConn, v, primary interface{}) error { + query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1` + return conn.QueryRow(v, query, primary) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } + } + + func (m *UserModel) FindOneByMobile(mobile string) (*User, error) { + userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, mobile) + var resp User + err := m.QueryRowIndex(&resp, userMobileKey, func(primary interface{}) string { + return fmt.Sprintf("%s%v", cacheUserIdPrefix, primary) + }, func(conn sqlx.SqlConn, v interface{}) (i interface{}, e error) { + query := `select ` + userRows + ` from ` + m.table + ` where mobile = ? limit 1` + if err := conn.QueryRow(&resp, query, mobile); err != nil { + return nil, err + } + return resp.Id, nil + }, func(conn sqlx.SqlConn, v, primary interface{}) error { + query := `select ` + userRows + ` from ` + m.table + ` where id = ? limit 1` + return conn.QueryRow(v, query, primary) + }) + switch err { + case nil: + return &resp, nil + case sqlc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } + } + + func (m *UserModel) Update(data User) error { + userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, data.Id) + _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { + query := `update ` + m.table + ` set ` + userRowsWithPlaceHolder + ` where id = ?` + return conn.Exec(query, data.Name, data.Password, data.Mobile, data.Gender, data.Nickname, data.Id) + }, userIdKey) + return err + } + + func (m *UserModel) Delete(id int64) error { + data, err := m.FindOne(id) + if err != nil { + return err + } + userIdKey := fmt.Sprintf("%s%v", cacheUserIdPrefix, id) + userNameKey := fmt.Sprintf("%s%v", cacheUserNamePrefix, data.Name) + userMobileKey := fmt.Sprintf("%s%v", cacheUserMobilePrefix, data.Mobile) + _, err = m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { + query := `delete from ` + m.table + ` where id = ?` + return conn.Exec(query, id) + }, userIdKey, userNameKey, userMobileKey) + return err + } + ``` + +# 用法 + +``` +$ goctl model mysql -h +``` + +``` +NAME: + goctl model mysql - generate mysql model" + +USAGE: + goctl model mysql command [command options] [arguments...] + +COMMANDS: + ddl generate mysql model from ddl" + datasource generate model from datasource" + +OPTIONS: + --help, -h show help +``` + +# 生成规则 + +* 默认规则 + + 我们默认用户在建表时会创建createTime、updateTime字段(忽略大小写、下划线命名风格)且默认值均为`CURRENT_TIMESTAMP`,而updateTime支持`ON UPDATE CURRENT_TIMESTAMP`,对于这两个字段生成`insert`、`update`时会被移除,不在赋值范畴内,当然,如果你不需要这两个字段那也无大碍。 +* 带缓存模式 + * ddl + + ```shell script + $ goctl model mysql -src={filename} -dir={dir} -cache=true + ``` + * datasource + + ```shell script + $ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=true + ``` + + 目前仅支持redis缓存,如果选择带缓存模式,即生成的`FindOne(ByXxx)`&`Delete`代码会生成带缓存逻辑的代码,目前仅支持单索引字段(除全文索引外),对于联合索引我们默认认为不需要带缓存,且不属于通用型代码,因此没有放在代码生成行列,如example中user表中的`id`、`name`、`mobile`字段均属于单字段索引。 + +* 不带缓存模式 + + * ddl + + ```shell script + $ goctl model -src={filename} -dir={dir} + ``` + * datasource + + ```shell script + $ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} + ``` + or + * ddl + + ```shell script + $ goctl model -src={filename} -dir={dir} -cache=false + ``` + * datasource + + ```shell script + $ goctl model mysql datasource -url={datasource} -table={tables} -dir={dir} -cache=false + ``` + + 生成代码仅基本的CURD结构。 + +# 缓存 + + 对于缓存这一块我选择用一问一答的形式进行罗列。我想这样能够更清晰的描述model中缓存的功能。 + +* 缓存会缓存哪些信息? + + 对于主键字段缓存,会缓存整个结构体信息,而对于单索引字段(除全文索引)则缓存主键字段值。 + +* 数据有更新(`update`)操作会清空缓存吗? + + 会,但仅清空主键缓存的信息,why?这里就不做详细赘述了。 + +* 为什么不按照单索引字段生成`updateByXxx`和`deleteByXxx`的代码? + + 理论上是没任何问题,但是我们认为,对于model层的数据操作均是以整个结构体为单位,包括查询,我不建议只查询某部分字段(不反对),否则我们的缓存就没有意义了。 + +* 为什么不支持`findPageLimit`、`findAll`这么模式代码生层? + + 目前,我认为除了基本的CURD外,其他的代码均属于业务型代码,这个我觉得开发人员根据业务需要进行编写更好。 + +# QA + +* goctl model除了命令行模式,支持插件模式吗? + + 很快支持idea插件。 + + + + + + diff --git a/doc/goctl-rpc.md b/doc/goctl-rpc.md new file mode 100644 index 00000000..9ac26164 --- /dev/null +++ b/doc/goctl-rpc.md @@ -0,0 +1,223 @@ +# Rpc Generation +Goctl Rpc是`goctl`脚手架下的一个rpc服务代码生成模块,支持proto模板生成和rpc服务代码生成,通过此工具生成代码你只需要关注业务逻辑编写而不用去编写一些重复性的代码。这使得我们把精力重心放在业务上,从而加快了开发效率且降低了代码出错率。 + +# 特性 +* 简单易用 +* 快速提升开发效率 +* 出错率低 + +# 快速开始 + +### 方式一:快速生成greet服务 + + 通过命令 `goctl rpc new ${servieName}`生成 + + 如生成greet rpc服务: + + ```shell script + $ goctl rpc new greet + ``` + + 执行后代码结构如下: + + ```golang + └── greet + ├── etc + │   └── greet.yaml + ├── go.mod + ├── go.sum + ├── greet + │   ├── greet.go + │   ├── greet_mock.go + │   └── types.go + ├── greet.go + ├── greet.proto + ├── internal + │   ├── config + │   │   └── config.go + │   ├── logic + │   │   └── pinglogic.go + │   ├── server + │   │   └── greetserver.go + │   └── svc + │   └── servicecontext.go + └── pb + └── greet.pb.go + ``` + +rpc一键生成常见问题解决见 常见问题解决 +### 方式二:通过指定proto生成rpc服务 + +* 生成proto模板 + + ```shell script + $ goctl rpc template -o=user.proto + ``` + + ```golang + syntax = "proto3"; + + package remote; + + message Request { + // 用户名 + string username = 1; + // 用户密码 + string password = 2; + } + + message Response { + // 用户名称 + string name = 1; + // 用户性别 + string gender = 2; + } + + service User { + // 登录 + rpc Login(Request)returns(Response); + } + ``` +* 生成rpc服务代码 + + ``` + $ goctl rpc proto -src=user.proto + ``` + + 代码tree + + ``` + user + ├── etc + │   └── user.json + ├── internal + │   ├── config + │   │   └── config.go + │   ├── handler + │   │   ├── loginhandler.go + │   ├── logic + │   │   └── loginlogic.go + │   └── svc + │   └── servicecontext.go + ├── pb + │   └── user.pb.go + ├── shared + │   ├── mockusermodel.go + │   ├── types.go + │   └── usermodel.go + ├── user.go + └── user.proto + + ``` +# 准备工作 + +* 安装了go环境 +* 安装了protoc&protoc-gen-go,并且已经设置环境变量 +* mockgen(可选,将移除) +* 更多问题请见 注意事项 + +# 用法 + +### rpc服务生成用法 + +```shell script +$ goctl rpc proto -h +``` + +```shell script +NAME: + goctl rpc proto - generate rpc from proto + +USAGE: + goctl rpc proto [command options] [arguments...] + +OPTIONS: + --src value, -s value the file path of the proto source file + --dir value, -d value the target path of the code,default path is "${pwd}". [option] + --service value, --srv value the name of rpc service. [option] + --shared[已废弃] value the dir of the shared file,default path is "${pwd}/shared. [option]" + --idea whether the command execution environment is from idea plugin. [option] + +``` + +### 参数说明 + +* --src 必填,proto数据源,目前暂时支持单个proto文件生成,这里不支持(不建议)外部依赖 +* --dir 非必填,默认为proto文件所在目录,生成代码的目标目录 +* --service 服务名称,非必填,默认为proto文件所在目录名称,但是,如果proto所在目录为一下结构: + ```shell script + user + ├── cmd + │   └── rpc + │   └── user.proto + ``` + 则服务名称亦为user,而非proto所在文件夹名称了,这里推荐使用这种结构,可以方便在同一个服务名下建立不同类型的服务(api、rpc、mq等),便于代码管理与维护。 +* --shared[⚠️已废弃] 非必填,默认为$dir(xxx.proto)/shared,rpc client逻辑代码存放目录。 + + > 注意:这里的shared文件夹名称将会是代码中的package名称。 + +* --idea 非必填,是否为idea插件中执行,保留字段,终端执行可以忽略 + +# 开发人员需要做什么 + +关注业务代码编写,将重复性、与业务无关的工作交给goctl,生成好rpc服务代码后,开饭人员仅需要修改 +* 服务中的配置文件编写(etc/xx.json、internal/config/config.go) +* 服务中业务逻辑编写(internal/logic/xxlogic.go) +* 服务中资源上下文的编写(internal/svc/servicecontext.go) + +# 扩展 +对于需要进行rpc mock的开发人员,在安装了`mockgen`工具的前提下可以在rpc的shared文件中生成好对应的mock文件。 + +# 注意事项 +* `google.golang.org/grpc`需要降级到v1.26.0,且protoc-gen-go版本不能高于v1.3.2(see [https://github.com/grpc/grpc-go/issues/3347](https://github.com/grpc/grpc-go/issues/3347))即 + ``` + replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 + ``` +* proto不支持暂多文件同时生成 +* proto不支持外部依赖包引入,message不支持inline +* 目前main文件、shared文件、handler文件会被强制覆盖,而和开发人员手动需要编写的则不会覆盖生成,这一类在代码头部均有 + ```shell script + // Code generated by goctl. DO NOT EDIT! + // Source: xxx.proto + ``` + 的标识,请注意不要将也写业务性代码写在里面。 + +# 常见问题解决(go mod工程) + +* 错误一: + + ```golang + pb/xx.pb.go:220:7: undefined: grpc.ClientConnInterface + pb/xx.pb.go:224:11: undefined: grpc.SupportPackageIsVersion6 + pb/xx.pb.go:234:5: undefined: grpc.ClientConnInterface + pb/xx.pb.go:237:24: undefined: grpc.ClientConnInterface + ``` + 解决方法:请将`protoc-gen-go`版本降至v1.3.2及一下 + +* 错误二: + + ```golang + + # go.etcd.io/etcd/clientv3/balancer/picker + ../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/err.go:25:9: cannot use &errPicker literal (type *errPicker) as type Picker in return argument:*errPicker does not implement Picker (wrong type for Pick method) + have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) + want Pick(balancer.PickInfo) (balancer.PickResult, error) + ../../../go/pkg/mod/go.etcd.io/etcd@v0.0.0-20200402134248-51bdeb39e698/clientv3/balancer/picker/roundrobin_balanced.go:33:9: cannot use &rrBalanced literal (type *rrBalanced) as type Picker in return argument: + *rrBalanced does not implement Picker (wrong type for Pick method) + have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) + want Pick(balancer.PickInfo) (balancer.PickResult, error) + #github.com/tal-tech/go-zero/rpcx/internal/balancer/p2c + ../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/rpcx/internal/balancer/p2c/p2c.go:41:32: not enough arguments in call to base.NewBalancerBuilder + have (string, *p2cPickerBuilder) + want (string, base.PickerBuilder, base.Config) + ../../../go/pkg/mod/github.com/tal-tech/go-zero@v1.0.12/rpcx/internal/balancer/p2c/p2c.go:58:9: cannot use &p2cPicker literal (type *p2cPicker) as type balancer.Picker in return argument: + *p2cPicker does not implement balancer.Picker (wrong type for Pick method) + have Pick(context.Context, balancer.PickInfo) (balancer.SubConn, func(balancer.DoneInfo), error) + want Pick(balancer.PickInfo) (balancer.PickResult, error) + ``` + + 解决方法: + + ```golang + replace google.golang.org/grpc => google.golang.org/grpc v1.26.0 + ``` diff --git a/tools/goctl/model/sql/README.MD b/tools/goctl/model/sql/README.MD index d525a696..62e3fb9a 100644 --- a/tools/goctl/model/sql/README.MD +++ b/tools/goctl/model/sql/README.MD @@ -22,9 +22,6 @@ goctl model 为go-zero下的工具模块中的组件之一,目前支持识别m ```shell script $ goctl model mysql datasource -url="user:password@tcp(127.0.0.1:3306)/database" -table="table1,table2" -dir="./model" ``` - - -> 详情用法请参考[example](https://github.com/tal-tech/go-zero/tree/master/tools/goctl/model/sql/example) * 生成代码示例