diff --git a/tools/goctl/goctl.go b/tools/goctl/goctl.go index da1fa19c..c813f6fc 100644 --- a/tools/goctl/goctl.go +++ b/tools/goctl/goctl.go @@ -19,6 +19,7 @@ import ( "github.com/tal-tech/go-zero/tools/goctl/configgen" "github.com/tal-tech/go-zero/tools/goctl/docker" "github.com/tal-tech/go-zero/tools/goctl/kube" + "github.com/tal-tech/go-zero/tools/goctl/model/mongo" model "github.com/tal-tech/go-zero/tools/goctl/model/sql/command" "github.com/tal-tech/go-zero/tools/goctl/plugin" rpc "github.com/tal-tech/go-zero/tools/goctl/rpc/cli" @@ -28,7 +29,7 @@ import ( ) var ( - buildVersion = "1.1.5" + buildVersion = "1.1.6" commands = []cli.Command{ { Name: "upgrade", @@ -447,6 +448,29 @@ var ( }, }, }, + { + Name: "mongo", + Usage: `generate mongo model`, + Flags: []cli.Flag{ + cli.StringSliceFlag{ + Name: "type, t", + Usage: "specified model type name", + }, + cli.BoolFlag{ + Name: "cache, c", + Usage: "generate code with cache [optional]", + }, + cli.StringFlag{ + Name: "dir, d", + Usage: "the target dir", + }, + cli.StringFlag{ + Name: "style", + Usage: "the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md]", + }, + }, + Action: mongo.Action, + }, }, }, { diff --git a/tools/goctl/model/mongo/generate/generate.go b/tools/goctl/model/mongo/generate/generate.go new file mode 100644 index 00000000..ce58a451 --- /dev/null +++ b/tools/goctl/model/mongo/generate/generate.go @@ -0,0 +1,69 @@ +package generate + +import ( + "errors" + "path/filepath" + + "github.com/tal-tech/go-zero/tools/goctl/config" + "github.com/tal-tech/go-zero/tools/goctl/model/mongo/template" + "github.com/tal-tech/go-zero/tools/goctl/util" + "github.com/tal-tech/go-zero/tools/goctl/util/format" +) + +// Context defines the model generation data what they needs +type Context struct { + Types []string + Cache bool + Output string + Cfg *config.Config +} + +// Do executes model template and output the result into the specified file path +func Do(ctx *Context) error { + if ctx.Cfg == nil { + return errors.New("missing config") + } + + err := generateModel(ctx) + if err != nil { + return err + } + + return generateError(ctx) +} + +func generateModel(ctx *Context) error { + for _, t := range ctx.Types { + fn, err := format.FileNamingFormat(ctx.Cfg.NamingFormat, t+"_model") + if err != nil { + return err + } + + text, err := util.LoadTemplate(category, modelTemplateFile, template.Text) + if err != nil { + return err + } + + output := filepath.Join(ctx.Output, fn+".go") + err = util.With("model").Parse(text).GoFmt(true).SaveTo(map[string]interface{}{ + "Type": t, + "Cache": ctx.Cache, + }, output, false) + if err != nil { + return err + } + } + + return nil +} + +func generateError(ctx *Context) error { + text, err := util.LoadTemplate(category, errTemplateFile, template.Error) + if err != nil { + return err + } + + output := filepath.Join(ctx.Output, "error.go") + + return util.With("error").Parse(text).GoFmt(true).SaveTo(ctx, output, false) +} diff --git a/tools/goctl/model/mongo/generate/generate_test.go b/tools/goctl/model/mongo/generate/generate_test.go new file mode 100644 index 00000000..4e651083 --- /dev/null +++ b/tools/goctl/model/mongo/generate/generate_test.go @@ -0,0 +1,34 @@ +package generate + +import ( + "io/ioutil" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tal-tech/go-zero/tools/goctl/config" +) + +var testTypes = ` + type User struct{} + type Class struct{} +` + +func TestDo(t *testing.T) { + cfg, err := config.NewConfig(config.DefaultFormat) + assert.Nil(t, err) + + tempDir := t.TempDir() + typesfile := filepath.Join(tempDir, "types.go") + err = ioutil.WriteFile(typesfile, []byte(testTypes), 0666) + assert.Nil(t, err) + + err = Do(&Context{ + Types: []string{"User", "Class"}, + Cache: false, + Output: tempDir, + Cfg: cfg, + }) + + assert.Nil(t, err) +} diff --git a/tools/goctl/model/mongo/generate/template.go b/tools/goctl/model/mongo/generate/template.go new file mode 100644 index 00000000..03bbce18 --- /dev/null +++ b/tools/goctl/model/mongo/generate/template.go @@ -0,0 +1,50 @@ +package generate + +import ( + "fmt" + + "github.com/tal-tech/go-zero/tools/goctl/model/mongo/template" + "github.com/tal-tech/go-zero/tools/goctl/util" + "github.com/urfave/cli" +) + +const ( + category = "mongo" + modelTemplateFile = "model.tpl" + errTemplateFile = "err.tpl" +) + +var templates = map[string]string{ + modelTemplateFile: template.Text, + errTemplateFile: template.Error, +} + +func Category() string { + return category +} + +func Clean() error { + return util.Clean(category) +} + +func Templates(_ *cli.Context) error { + return util.InitTemplates(category, templates) +} + +func RevertTemplate(name string) error { + content, ok := templates[name] + if !ok { + return fmt.Errorf("%s: no such file name", name) + } + + return util.CreateTemplate(category, name, content) +} + +func Update() error { + err := Clean() + if err != nil { + return err + } + + return util.InitTemplates(category, templates) +} diff --git a/tools/goctl/model/mongo/mongo.go b/tools/goctl/model/mongo/mongo.go new file mode 100644 index 00000000..39d96281 --- /dev/null +++ b/tools/goctl/model/mongo/mongo.go @@ -0,0 +1,39 @@ +package mongo + +import ( + "errors" + "path/filepath" + "strings" + + "github.com/tal-tech/go-zero/tools/goctl/config" + "github.com/tal-tech/go-zero/tools/goctl/model/mongo/generate" + "github.com/urfave/cli" +) + +// Command provides the entry for goctl +func Action(ctx *cli.Context) error { + tp := ctx.StringSlice("type") + c := ctx.Bool("cache") + o := strings.TrimSpace(ctx.String("dir")) + s := ctx.String("style") + if len(tp) == 0 { + return errors.New("missing type") + } + + cfg, err := config.NewConfig(s) + if err != nil { + return err + } + + a, err := filepath.Abs(o) + if err != nil { + return err + } + + return generate.Do(&generate.Context{ + Types: tp, + Cache: c, + Output: a, + Cfg: cfg, + }) +} diff --git a/tools/goctl/model/mongo/readme.md b/tools/goctl/model/mongo/readme.md new file mode 100644 index 00000000..5f607e24 --- /dev/null +++ b/tools/goctl/model/mongo/readme.md @@ -0,0 +1,210 @@ +# mongo生成model + +## 背景 + +在业务务开发中,model(dao)数据访问层是一个服务必不可缺的一层,因此数据库访问的CURD也是必须要对外提供的访问方法, 而CURD在go-zero中就仅存在两种情况 + +* 带缓存model +* 不带缓存model + +从代码结构上来看,C-U-R-D四个方法就是固定的结构,因此我们可以将其交给goctl工具去完成,帮助我们提升开发效率。 + +## 方案设计 + +mongo的生成不同于mysql,mysql可以从scheme_information库中读取到一张表的信息(字段名称,数据类型,索引等), +而mongo是文档型数据库,我们暂时无法从db中读取某一条记录来实现字段信息获取,就算有也不一定是完整信息(某些字段可能是omitempty修饰,可有可无), 这里采用type自己编写+代码生成方式实现 + +## 使用示例 + +假设我们需要生成一个usermodel.go的代码文件,其包含用户信息字段有 + +|字段名称|字段类型| +|---|---| +|_id|bson.ObejctId| +|name|string| + +### 编写types.go + +```shell +$ vim types.go +``` + +```golang +package model + +//go:generate goctl model mongo -t User +import "github.com/globalsign/mgo/bson" + +type User struct { + ID bson.ObjectId `bson:"_id"` + Name string `bson:"name"` +} +``` + +### 生成代码 + +生成代码的方式有两种 + +* 命令行生成 在types.go所在文件夹执行命令 + ```shell + $ goctl model mongo -t User -style gozero + ``` +* 在types.go中添加`//go:generate`,然后点击执行按钮即可生成,内容示例如下: + ```golang + //go:generate goctl model mongo -t User + ``` + +### 生成示例代码 + +* usermodel.go + + ```golang + package model + + import ( + "context" + + "github.com/globalsign/mgo/bson" + cachec "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/core/stores/mongoc" + ) + + type UserModel interface { + Insert(data *User, ctx context.Context) error + FindOne(id string, ctx context.Context) (*User, error) + Update(data *User, ctx context.Context) error + Delete(id string, ctx context.Context) error + } + + type defaultUserModel struct { + *mongoc.Model + } + + func NewUserModel(url, collection string, c cachec.CacheConf) UserModel { + return &defaultUserModel{ + Model: mongoc.MustNewModel(url, collection, c), + } + } + + func (m *defaultUserModel) Insert(data *User, ctx context.Context) error { + if !data.ID.Valid() { + data.ID = bson.NewObjectId() + } + + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + return m.GetCollection(session).Insert(data) + } + + func (m *defaultUserModel) FindOne(id string, ctx context.Context) (*User, error) { + if !bson.IsObjectIdHex(id) { + return nil, ErrInvalidObjectId + } + + session, err := m.TakeSession() + if err != nil { + return nil, err + } + + defer m.PutSession(session) + var data User + + err = m.GetCollection(session).FindOneIdNoCache(&data, bson.ObjectIdHex(id)) + switch err { + case nil: + return &data, nil + case mongoc.ErrNotFound: + return nil, ErrNotFound + default: + return nil, err + } + } + + func (m *defaultUserModel) Update(data *User, ctx context.Context) error { + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + + return m.GetCollection(session).UpdateIdNoCache(data.ID, data) + } + + func (m *defaultUserModel) Delete(id string, ctx context.Context) error { + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + + return m.GetCollection(session).RemoveIdNoCache(bson.ObjectIdHex(id)) + } + ``` + +* error.go + + ```golang + package model + + import "errors" + + var ErrNotFound = errors.New("not found") + var ErrInvalidObjectId = errors.New("invalid objectId") + ``` + +### 文件目录预览 + +```text +. +├── error.go +├── types.go +└── usermodel.go + +``` + +## 命令预览 + +```text +NAME: + goctl model - generate model code + +USAGE: + goctl model command [command options] [arguments...] + +COMMANDS: + mysql generate mysql model + mongo generate mongo model + +OPTIONS: + --help, -h show help +``` + +```text +NAME: + goctl model mongo - generate mongo model + +USAGE: + goctl model mongo [command options] [arguments...] + +OPTIONS: + --type value, -t value specified model type name + --cache, -c generate code with cache [optional] + --dir value, -d value the target dir + --style value the file naming format, see [https://github.com/tal-tech/go-zero/tree/master/tools/goctl/config/readme.md] + +``` + +> 温馨提示 +> +> `--type` 支持slice传值,示例 `goctl model mongo -t=User -t=Class` +## 注意事项 + +types.go本质上与xxxmodel.go无关,只是将type定义部分交给开发人员自己编写了,在xxxmodel.go中,mongo文档的存储结构必须包含 +`_id`字段,对应到types中的field为`ID`,model中的findOne,update均以data.ID来进行操作的,当然,如果不符合你的命名风格,你也 可以修改模板,只要保证`id` +在types中的field名称和模板中一致就行。 \ No newline at end of file diff --git a/tools/goctl/model/mongo/template/template.go b/tools/goctl/model/mongo/template/template.go new file mode 100644 index 00000000..1862d029 --- /dev/null +++ b/tools/goctl/model/mongo/template/template.go @@ -0,0 +1,111 @@ +package template + +// Text provides the default template for model to generate +var Text = `package model + +import ( + "context" + + "github.com/globalsign/mgo/bson" + cachec "github.com/tal-tech/go-zero/core/stores/cache" + "github.com/tal-tech/go-zero/core/stores/mongoc" +) + +{{if .Cache}}var prefix{{.Type}}CacheKey = "cache#{{.Type}}#"{{end}} + +type {{.Type}}Model interface{ + Insert(ctx context.Context,data *{{.Type}}) error + FindOne(ctx context.Context,id string) (*{{.Type}}, error) + Update(ctx context.Context,data *{{.Type}}) error + Delete(ctx context.Context,id string) error +} + +type default{{.Type}}Model struct { + *mongoc.Model +} + +func New{{.Type}}Model(url, collection string, c cachec.CacheConf) {{.Type}}Model { + return &default{{.Type}}Model{ + Model: mongoc.MustNewModel(url, collection, c), + } +} + + +func (m *default{{.Type}}Model) Insert(ctx context.Context, data *{{.Type}}) error { + if !data.ID.Valid() { + data.ID = bson.NewObjectId() + } + + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + return m.GetCollection(session).Insert(data) +} + +func (m *default{{.Type}}Model) FindOne(ctx context.Context, id string) (*{{.Type}}, error) { + if !bson.IsObjectIdHex(id) { + return nil, ErrInvalidObjectId + } + + session, err := m.TakeSession() + if err != nil { + return nil, err + } + + defer m.PutSession(session) + var data {{.Type}} + {{if .Cache}}key := prefix{{.Type}}CacheKey + id + err = m.GetCollection(session).FindOneId(&data, key, bson.ObjectIdHex(id)) + {{- else}} + err = m.GetCollection(session).FindOneIdNoCache(&data, bson.ObjectIdHex(id)) + {{- end}} + switch err { + case nil: + return &data,nil + case mongoc.ErrNotFound: + return nil,ErrNotFound + default: + return nil,err + } +} + +func (m *default{{.Type}}Model) Update(ctx context.Context, data *{{.Type}}) error { + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex() + return m.GetCollection(session).UpdateId(data.ID, data, key) + {{- else}} + return m.GetCollection(session).UpdateIdNoCache(data.ID, data) + {{- end}} +} + +func (m *default{{.Type}}Model) Delete(ctx context.Context, id string) error { + session, err := m.TakeSession() + if err != nil { + return err + } + + defer m.PutSession(session) + {{if .Cache}}key := prefix{{.Type}}CacheKey + id + return m.GetCollection(session).RemoveId(bson.ObjectIdHex(id), key) + {{- else}} + return m.GetCollection(session).RemoveIdNoCache(bson.ObjectIdHex(id)) + {{- end}} +} +` + +var Error = ` +package model + +import "errors" + +var ErrNotFound = errors.New("not found") +var ErrInvalidObjectId = errors.New("invalid objectId") +` diff --git a/tools/goctl/model/sql/gen/update.go b/tools/goctl/model/sql/gen/update.go index 9e80689b..4fb2bc1e 100644 --- a/tools/goctl/model/sql/gen/update.go +++ b/tools/goctl/model/sql/gen/update.go @@ -3,6 +3,7 @@ package gen import ( "strings" + "github.com/tal-tech/go-zero/core/collection" "github.com/tal-tech/go-zero/tools/goctl/model/sql/template" "github.com/tal-tech/go-zero/tools/goctl/util" "github.com/tal-tech/go-zero/tools/goctl/util/stringx" @@ -23,6 +24,15 @@ func genUpdate(table Table, withCache bool) (string, string, error) { expressionValues = append(expressionValues, "data."+camel) } + keySet := collection.NewSet() + keyVariableSet := collection.NewSet() + keySet.AddStr(table.PrimaryCacheKey.DataKeyExpression) + keyVariableSet.AddStr(table.PrimaryCacheKey.KeyLeft) + for _, key := range table.UniqueCacheKey { + keySet.AddStr(key.DataKeyExpression) + keyVariableSet.AddStr(key.KeyLeft) + } + expressionValues = append(expressionValues, "data."+table.PrimaryKey.Name.ToCamel()) camelTableName := table.Name.ToCamel() text, err := util.LoadTemplate(category, updateTemplateFile, template.Update) @@ -35,6 +45,8 @@ func genUpdate(table Table, withCache bool) (string, string, error) { Execute(map[string]interface{}{ "withCache": withCache, "upperStartCamelObject": camelTableName, + "keys": strings.Join(keySet.KeysStr(), "\n"), + "keyValues": strings.Join(keyVariableSet.KeysStr(), ", "), "primaryCacheKey": table.PrimaryCacheKey.DataKeyExpression, "primaryKeyVariable": table.PrimaryCacheKey.KeyLeft, "lowerStartCamelObject": stringx.From(camelTableName).Untitle(), diff --git a/tools/goctl/model/sql/template/update.go b/tools/goctl/model/sql/template/update.go index 530f008f..17486623 100644 --- a/tools/goctl/model/sql/template/update.go +++ b/tools/goctl/model/sql/template/update.go @@ -3,11 +3,11 @@ package template // Update defines a template for generating update codes var Update = ` func (m *default{{.upperStartCamelObject}}Model) Update(data {{.upperStartCamelObject}}) error { - {{if .withCache}}{{.primaryCacheKey}} + {{if .withCache}}{{.keys}} _, err := m.Exec(func(conn sqlx.SqlConn) (result sql.Result, err error) { query := fmt.Sprintf("update %s set %s where {{.originalPrimaryKey}} = ?", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder) return conn.Exec(query, {{.expressionValues}}) - }, {{.primaryKeyVariable}}){{else}}query := fmt.Sprintf("update %s set %s where {{.originalPrimaryKey}} = ?", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder) + }, {{.keyValues}}){{else}}query := fmt.Sprintf("update %s set %s where {{.originalPrimaryKey}} = ?", m.table, {{.lowerStartCamelObject}}RowsWithPlaceHolder) _,err:=m.conn.Exec(query, {{.expressionValues}}){{end}} return err } diff --git a/tools/goctl/tpl/templates.go b/tools/goctl/tpl/templates.go index 05b38ab4..48cba9df 100644 --- a/tools/goctl/tpl/templates.go +++ b/tools/goctl/tpl/templates.go @@ -8,6 +8,7 @@ import ( "github.com/tal-tech/go-zero/tools/goctl/api/gogen" "github.com/tal-tech/go-zero/tools/goctl/docker" "github.com/tal-tech/go-zero/tools/goctl/kube" + mongogen "github.com/tal-tech/go-zero/tools/goctl/model/mongo/generate" modelgen "github.com/tal-tech/go-zero/tools/goctl/model/sql/gen" rpcgen "github.com/tal-tech/go-zero/tools/goctl/rpc/generator" "github.com/tal-tech/go-zero/tools/goctl/util" @@ -34,6 +35,9 @@ func GenTemplates(ctx *cli.Context) error { func() error { return kube.GenTemplates(ctx) }, + func() error { + return mongogen.Templates(ctx) + }, ); err != nil { return err } @@ -61,6 +65,15 @@ func CleanTemplates(_ *cli.Context) error { func() error { return rpcgen.Clean() }, + func() error { + return docker.Clean() + }, + func() error { + return kube.Clean() + }, + func() error { + return mongogen.Clean() + }, ) if err != nil { return err @@ -90,6 +103,8 @@ func UpdateTemplates(ctx *cli.Context) (err error) { return rpcgen.Update() case modelgen.Category(): return modelgen.Update() + case mongogen.Category(): + return mongogen.Update() default: err = fmt.Errorf("unexpected category: %s", category) return @@ -116,6 +131,8 @@ func RevertTemplates(ctx *cli.Context) (err error) { return rpcgen.RevertTemplate(filename) case modelgen.Category(): return modelgen.RevertTemplate(filename) + case mongogen.Category(): + return mongogen.RevertTemplate(filename) default: err = fmt.Errorf("unexpected category: %s", category) return