Repository: zeromicro/go-zero
Pull request #2002 (2022-06-11 15:14:55)

feat: Replace mongo package with monc & mon


Patch:
diff --git a/tools/goctl/model/mongo/generate/generate.go b/tools/goctl/model/mongo/generate/generate.go
index 6bc8c005..bbdf9123 100644
--- a/tools/goctl/model/mongo/generate/generate.go
+++ b/tools/goctl/model/mongo/generate/generate.go
@@ -9,6 +9,7 @@ import (
 	"github.com/zeromicro/go-zero/tools/goctl/util"
 	"github.com/zeromicro/go-zero/tools/goctl/util/format"
 	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+	"github.com/zeromicro/go-zero/tools/goctl/util/stringx"
 )
 
 // Context defines the model generation data what they needs
@@ -25,8 +26,15 @@ func Do(ctx *Context) error {
 		return errors.New("missing config")
 	}
 
-	err := generateModel(ctx)
-	if err != nil {
+	if err := generateTypes(ctx); err != nil {
+		return err
+	}
+
+	if err := generateModel(ctx); err != nil {
+		return err
+	}
+
+	if err := generateCustomModel(ctx); err != nil {
 		return err
 	}
 
@@ -34,21 +42,47 @@ func Do(ctx *Context) error {
 }
 
 func generateModel(ctx *Context) error {
+	for _, t := range ctx.Types {
+		fn, err := format.FileNamingFormat(ctx.Cfg.NamingFormat, t+"_model_gen")
+		if err != nil {
+			return err
+		}
+
+		text, err := pathx.LoadTemplate(category, modelTemplateFile, template.ModelText)
+		if err != nil {
+			return err
+		}
+
+		output := filepath.Join(ctx.Output, fn+".go")
+		if err = util.With("model").Parse(text).GoFmt(true).SaveTo(map[string]interface{}{
+			"Type":      stringx.From(t).Title(),
+			"lowerType": stringx.From(t).Untitle(),
+			"Cache":     ctx.Cache,
+		}, output, true); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func generateCustomModel(ctx *Context) error {
 	for _, t := range ctx.Types {
 		fn, err := format.FileNamingFormat(ctx.Cfg.NamingFormat, t+"_model")
 		if err != nil {
 			return err
 		}
 
-		text, err := pathx.LoadTemplate(category, modelTemplateFile, template.Text)
+		text, err := pathx.LoadTemplate(category, modelCustomTemplateFile, template.ModelCustomText)
 		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,
+			"Type":      stringx.From(t).Title(),
+			"lowerType": stringx.From(t).Untitle(),
+			"Cache":     ctx.Cache,
 		}, output, false)
 		if err != nil {
 			return err
@@ -58,6 +92,29 @@ func generateModel(ctx *Context) error {
 	return nil
 }
 
+func generateTypes(ctx *Context) error {
+	for _, t := range ctx.Types {
+		fn, err := format.FileNamingFormat(ctx.Cfg.NamingFormat, t+"types")
+		if err != nil {
+			return err
+		}
+
+		text, err := pathx.LoadTemplate(category, modelTypesTemplateFile, template.ModelTypesText)
+		if err != nil {
+			return err
+		}
+
+		output := filepath.Join(ctx.Output, fn+".go")
+		if err = util.With("model").Parse(text).GoFmt(true).SaveTo(map[string]interface{}{
+			"Type": stringx.From(t).Title(),
+		}, output, false);err!=nil{
+			return err
+		}
+	}
+
+	return nil
+}
+
 func generateError(ctx *Context) error {
 	text, err := pathx.LoadTemplate(category, errTemplateFile, template.Error)
 	if err != nil {
diff --git a/tools/goctl/model/mongo/generate/template.go b/tools/goctl/model/mongo/generate/template.go
index a86f96d8..1adbddcb 100644
--- a/tools/goctl/model/mongo/generate/template.go
+++ b/tools/goctl/model/mongo/generate/template.go
@@ -8,14 +8,18 @@ import (
 )
 
 const (
-	category          = "mongo"
-	modelTemplateFile = "model.tpl"
-	errTemplateFile   = "err.tpl"
+	category                = "mongo"
+	modelTemplateFile       = "model.tpl"
+	modelCustomTemplateFile = "model_custom.tpl"
+	modelTypesTemplateFile  = "model_types.tpl"
+	errTemplateFile         = "err.tpl"
 )
 
 var templates = map[string]string{
-	modelTemplateFile: template.Text,
-	errTemplateFile:   template.Error,
+	modelTemplateFile:       template.ModelText,
+	modelCustomTemplateFile: template.ModelCustomText,
+	modelTypesTemplateFile:  template.ModelTypesText,
+	errTemplateFile:         template.Error,
 }
 
 // Category returns the mongo category.
diff --git a/tools/goctl/model/mongo/mongo.go b/tools/goctl/model/mongo/mongo.go
index ab3d4ae6..022c47b0 100644
--- a/tools/goctl/model/mongo/mongo.go
+++ b/tools/goctl/model/mongo/mongo.go
@@ -38,12 +38,14 @@ func Action(_ *cobra.Command, _ []string) error {
 	home := VarStringHome
 	remote := VarStringRemote
 	branch := VarStringBranch
+	
 	if len(remote) > 0 {
 		repo, _ := file.CloneIntoGitHome(remote, branch)
 		if len(repo) > 0 {
 			home = repo
 		}
 	}
+	
 	if len(home) > 0 {
 		pathx.RegisterGoctlHome(home)
 	}
@@ -62,6 +64,10 @@ func Action(_ *cobra.Command, _ []string) error {
 		return err
 	}
 
+	if err = pathx.MkdirIfNotExist(a); err != nil {
+		return err
+	}
+
 	return generate.Do(&generate.Context{
 		Types:  tp,
 		Cache:  c,
diff --git a/tools/goctl/model/mongo/readme.md b/tools/goctl/model/mongo/readme.md
index 9e2bb80e..3317cce3 100644
--- a/tools/goctl/model/mongo/readme.md
+++ b/tools/goctl/model/mongo/readme.md
@@ -15,196 +15,194 @@ mongo的生成不同于mysql，mysql可以从scheme_information库中读取到
 而mongo是文档型数据库，我们暂时无法从db中读取某一条记录来实现字段信息获取，就算有也不一定是完整信息（某些字段可能是omitempty修饰，可有可无）， 这里采用type自己编写+代码生成方式实现
 
 ## 使用示例
+为 User 生成 mongo model
+```bahs
+$ goctl model mongo -t User -c --dir .
+```
 
-假设我们需要生成一个usermodel.go的代码文件，其包含用户信息字段有
+### 生成示例代码
 
-|字段名称|字段类型|
-|---|---|
-|_id|bson.ObejctId|
-|name|string|
+#### usermodel.go
+```go
+package model
 
-### 编写types.go
+import (
+	"github.com/zeromicro/go-zero/core/stores/cache"
+	"github.com/zeromicro/go-zero/core/stores/monc"
+)
+
+var _ UserModel = (*customUserModel)(nil)
+
+type (
+	// UserModel is an interface to be customized, add more methods here,
+	// and implement the added methods in customUserModel.
+	UserModel interface {
+		userModel
+	}
+
+	customUserModel struct {
+		*defaultUserModel
+	}
+)
+
+// NewUserModel returns a model for the mongo.
+func NewUserModel(url, db, collection string, c cache.CacheConf) UserModel {
+	conn := monc.MustNewModel(url, db, collection, c)
+	return &customUserModel{
+		defaultUserModel: newDefaultUserModel(conn),
+	}
+}
 
-```shell
-$ vim types.go
 ```
 
-```golang
+#### usermodelgen.go
+```go
+// Code generated by goctl. DO NOT EDIT!
 package model
 
-//go:generate goctl model mongo -t User
-import "github.com/globalsign/mgo/bson"
+import (
+	"context"
+	"time"
 
-type User struct {
-	ID   bson.ObjectId `bson:"_id"`
-	Name string        `bson:"name"`
+	"github.com/zeromicro/go-zero/core/stores/monc"
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+var prefixUserCacheKey = "cache:user:"
+
+type userModel interface {
+	Insert(ctx context.Context, data *User) error
+	FindOne(ctx context.Context, id string) (*User, error)
+	Update(ctx context.Context, data *User) error
+	Delete(ctx context.Context, id string) error
 }
-```
 
-### 生成代码
+type defaultUserModel struct {
+	conn *monc.Model
+}
 
-生成代码的方式有两种
+func newDefaultUserModel(conn *monc.Model) *defaultUserModel {
+	return &defaultUserModel{conn: conn}
+}
 
-* 命令行生成 在types.go所在文件夹执行命令
-    ```shell
-    $ goctl model mongo -t User -style gozero
-    ```
-* 在types.go中添加`//go:generate`，然后点击执行按钮即可生成，内容示例如下：
-  ```golang
-  //go:generate goctl model mongo -t User
-  ```
+func (m *defaultUserModel) Insert(ctx context.Context, data *User) error {
+	if !data.ID.IsZero() {
+		data.ID = primitive.NewObjectID()
+		data.CreateAt = time.Now()
+		data.UpdateAt = time.Now()
+	}
 
-### 生成示例代码
+	key := prefixUserCacheKey + data.ID.Hex()
+	_, err := m.conn.InsertOne(ctx, key, data)
+	return err
+}
 
-* usermodel.go
-
-  ```golang
-  package model
-  
-  import (
-      "context"
-  
-      "github.com/globalsign/mgo/bson"
-      cachec "github.com/zeromicro/go-zero/core/stores/cache"
-      "github.com/zeromicro/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")
-  ```
+func (m *defaultUserModel) FindOne(ctx context.Context, id string) (*User, error) {
+	oid, err := primitive.ObjectIDFromHex(id)
+	if err != nil {
+		return nil, ErrInvalidObjectId
+	}
+
+	var data User
+	key := prefixUserCacheKey + data.ID.Hex()
+	err = m.conn.FindOne(ctx, key, &data, bson.M{"_id": oid})
+	switch err {
+	case nil:
+		return &data, nil
+	case monc.ErrNotFound:
+		return nil, ErrNotFound
+	default:
+		return nil, err
+	}
+}
 
-### 文件目录预览
+func (m *defaultUserModel) Update(ctx context.Context, data *User) error {
+	data.UpdateAt = time.Now()
+	key := prefixUserCacheKey + data.ID.Hex()
+	_, err := m.conn.ReplaceOne(ctx, key, bson.M{"_id": data.ID}, data)
+	return err
+}
 
-```text
-.
-├── error.go
-├── types.go
-└── usermodel.go
+func (m *defaultUserModel) Delete(ctx context.Context, id string) error {
+	oid, err := primitive.ObjectIDFromHex(id)
+	if err != nil {
+		return ErrInvalidObjectId
+	}
+	key := prefixUserCacheKey + id
+	_, err = m.conn.DeleteOne(ctx, key, bson.M{"_id": oid})
+	return err
+}
 
 ```
 
-## 命令预览
+#### usertypes.go
+```go
+package model
 
-```text
-NAME:
-   goctl model - generate model code
+import (
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type User struct {
+	ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
+	// TODO: Fill your own fields
+	UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
+	CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
+}
+
+```
 
-USAGE:
-   goctl model command [command options] [arguments...]
+#### error.go
+```go
+package model
+
+import (
+	"errors"
 
-COMMANDS:
-   mysql  generate mysql model
-   mongo  generate mongo model
+	"github.com/zeromicro/go-zero/core/stores/mon"
+)
 
-OPTIONS:
-   --help, -h  show help
+var (
+	ErrNotFound        = mon.ErrNotFound
+	ErrInvalidObjectId = errors.New("invalid objectId")
+)
 ```
 
+### 文件目录预览
+
 ```text
-NAME:
-   goctl model mongo - generate mongo model
+.
+├── error.go
+├── usermodel.go
+├── usermodelgen.go
+└── usertypes.go
+```
 
-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/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
+```text
+Generate mongo model
+
+Usage:
+  goctl model mongo [flags]
+
+Flags:
+      --branch string   The branch of the remote repo, it does work with --remote
+  -c, --cache           Generate code with cache [optional]
+  -d, --dir string      The target dir
+  -h, --help            help for mongo
+      --home string     The goctl home path of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
+      --remote string   The remote git repo of the template, --home and --remote cannot be set at the same time, if they are, --remote has higher priority
+                                The git repo directory must be consistent with the https://github.com/zeromicro/go-zero-template directory structure
+      --style string    The file naming format, see [https://github.com/zeromicro/go-zero/tree/master/tools/goctl/config/readme.md]
+  -t, --type strings    Specified model type name
 
 ```
 
 > 温馨提示
 > 
 > `--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名称和模板中一致就行。
diff --git a/tools/goctl/model/mongo/template/error.tpl b/tools/goctl/model/mongo/template/error.tpl
index cb8be412..27d92449 100644
--- a/tools/goctl/model/mongo/template/error.tpl
+++ b/tools/goctl/model/mongo/template/error.tpl
@@ -1,6 +1,12 @@
 package model
 
-import "errors"
+import (
+	"errors"
 
-var ErrNotFound = errors.New("not found")
-var ErrInvalidObjectId = errors.New("invalid objectId")
+	"github.com/zeromicro/go-zero/core/stores/mon"
+)
+
+var (
+	ErrNotFound        = mon.ErrNotFound
+	ErrInvalidObjectId = errors.New("invalid objectId")
+)
diff --git a/tools/goctl/model/mongo/template/model.tpl b/tools/goctl/model/mongo/template/model.tpl
index bf807d01..c88f1462 100644
--- a/tools/goctl/model/mongo/template/model.tpl
+++ b/tools/goctl/model/mongo/template/model.tpl
@@ -1,98 +1,77 @@
+// Code generated by goctl. DO NOT EDIT!
 package model
 
 import (
     "context"
+    "time"
 
-    "github.com/globalsign/mgo/bson"
-     {{if .Cache}}cachec "github.com/zeromicro/go-zero/core/stores/cache"
-	"github.com/zeromicro/go-zero/core/stores/mongoc"{{else}}"github.com/zeromicro/go-zero/core/stores/mongo"{{end}}
+    {{if .Cache}}"github.com/zeromicro/go-zero/core/stores/monc"{{else}}"github.com/zeromicro/go-zero/core/stores/mon"{{end}}
+    "go.mongodb.org/mongo-driver/bson"
+    "go.mongodb.org/mongo-driver/bson/primitive"
 )
 
-{{if .Cache}}var prefix{{.Type}}CacheKey = "cache:{{.Type}}:"{{end}}
+{{if .Cache}}var prefix{{.Type}}CacheKey = "cache:{{.lowerType}}:"{{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 {{.lowerType}}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 {
-    {{if .Cache}}*mongoc.Model{{else}}*mongo.Model{{end}}
+    conn {{if .Cache}}*monc.Model{{else}}*mon.Model{{end}}
 }
 
-func New{{.Type}}Model(url, collection string{{if .Cache}}, c cachec.CacheConf{{end}}) {{.Type}}Model {
-	return &default{{.Type}}Model{
-		Model: {{if .Cache}}mongoc.MustNewModel(url, collection, c){{else}}mongo.MustNewModel(url, collection){{end}},
-	}
+func newDefault{{.Type}}Model(conn {{if .Cache}}*monc.Model{{else}}*mon.Model{{end}}) *default{{.Type}}Model {
+    return &default{{.Type}}Model{conn: conn}
 }
 
 
 func (m *default{{.Type}}Model) Insert(ctx context.Context, data *{{.Type}}) error {
-    if !data.ID.Valid() {
-        data.ID = bson.NewObjectId()
+    if !data.ID.IsZero() {
+        data.ID = primitive.NewObjectID()
+        data.CreateAt = time.Now()
+        data.UpdateAt = time.Now()
     }
 
-    session, err := m.TakeSession()
-    if err != nil {
-        return err
-    }
-
-    defer m.PutSession(session)
-    return m.GetCollection(session).Insert(data)
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
+    _, err := m.conn.InsertOne(ctx, {{if .Cache}}key, {{end}} data)
+    return err
 }
 
 func (m *default{{.Type}}Model) FindOne(ctx context.Context, id string) (*{{.Type}}, error) {
-    if !bson.IsObjectIdHex(id) {
-        return nil, ErrInvalidObjectId
-    }
-
-    session, err := m.TakeSession()
+    oid, err := primitive.ObjectIDFromHex(id)
     if err != nil {
-        return nil, err
+        return nil, ErrInvalidObjectId
     }
 
-    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).FindId(bson.ObjectIdHex(id)).One(&data)
-	{{- end}}
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
+    err = m.conn.FindOne(ctx, {{if .Cache}}key, {{end}}&data, bson.M{"_id": oid})
     switch err {
     case nil:
-        return &data,nil
-    case {{if .Cache}}mongoc.ErrNotFound{{else}}mongo.ErrNotFound{{end}}:
-        return nil,ErrNotFound
+        return &data, nil
+    case {{if .Cache}}monc{{else}}mon{{end}}.ErrNotFound:
+        return nil, ErrNotFound
     default:
-        return nil,err
+        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).UpdateId(data.ID, data)
-	{{- end}}
+    data.UpdateAt = time.Now()
+    {{if .Cache}}key := prefix{{.Type}}CacheKey + data.ID.Hex(){{end}}
+    _, err := m.conn.ReplaceOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": data.ID}, data)
+    return err
 }
 
 func (m *default{{.Type}}Model) Delete(ctx context.Context, id string) error {
-    session, err := m.TakeSession()
+    oid, err := primitive.ObjectIDFromHex(id)
     if err != nil {
-        return err
+        return ErrInvalidObjectId
     }
-
-    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).RemoveId(bson.ObjectIdHex(id))
-	{{- end}}
+	{{if .Cache}}key := prefix{{.Type}}CacheKey +id{{end}}
+    _, err = m.conn.DeleteOne(ctx, {{if .Cache}}key, {{end}}bson.M{"_id": oid})
+	return err
 }
diff --git a/tools/goctl/model/mongo/template/model_custom.tpl b/tools/goctl/model/mongo/template/model_custom.tpl
new file mode 100644
index 00000000..244e5c5a
--- /dev/null
+++ b/tools/goctl/model/mongo/template/model_custom.tpl
@@ -0,0 +1,28 @@
+package model
+
+{{if .Cache}}import (
+    "github.com/zeromicro/go-zero/core/stores/cache"
+    "github.com/zeromicro/go-zero/core/stores/monc"
+){{else}}import "github.com/zeromicro/go-zero/core/stores/mon"{{end}}
+
+var _ {{.Type}}Model = (*custom{{.Type}}Model)(nil)
+
+type (
+    // {{.Type}}Model is an interface to be customized, add more methods here,
+    // and implement the added methods in custom{{.Type}}Model.
+    {{.Type}}Model interface {
+        {{.lowerType}}Model
+    }
+
+    custom{{.Type}}Model struct {
+        *default{{.Type}}Model
+    }
+)
+
+// New{{.Type}}Model returns a model for the mongo.
+func New{{.Type}}Model(url, db, collection string{{if .Cache}}, c cache.CacheConf{{end}}) {{.Type}}Model {
+    conn := {{if .Cache}}monc{{else}}mon{{end}}.MustNewModel(url, db, collection{{if .Cache}}, c{{end}})
+    return &custom{{.Type}}Model{
+        default{{.Type}}Model: newDefault{{.Type}}Model(conn),
+    }
+}
diff --git a/tools/goctl/model/mongo/template/template.go b/tools/goctl/model/mongo/template/template.go
index 9477f5f2..72e160e3 100644
--- a/tools/goctl/model/mongo/template/template.go
+++ b/tools/goctl/model/mongo/template/template.go
@@ -2,10 +2,20 @@ package template
 
 import _ "embed"
 
-// Text provides the default template for model to generate.
-//go:embed model.tpl
-var Text string
+var (
+	// ModelText provides the default template for model to generate.
+	//go:embed model.tpl
+	ModelText string
 
-// Error provides the default template for error definition in mongo code generation.
-//go:embed error.tpl
-var Error string
+	// ModelCustomText provides the default template for model to generate.
+	//go:embed model_custom.tpl
+	ModelCustomText string
+
+	// ModelTypesText provides the default template for model to generate.
+	//go:embed types.tpl
+	ModelTypesText string
+
+	// Error provides the default template for error definition in mongo code generation.
+	//go:embed error.tpl
+	Error string
+)
diff --git a/tools/goctl/model/mongo/template/types.tpl b/tools/goctl/model/mongo/template/types.tpl
new file mode 100644
index 00000000..8da006f4
--- /dev/null
+++ b/tools/goctl/model/mongo/template/types.tpl
@@ -0,0 +1,14 @@
+package model
+
+import (
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type {{.Type}} struct {
+	ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
+	// TODO: Fill your own fields
+	UpdateAt time.Time `bson:"updateAt,omitempty" json:"updateAt,omitempty"`
+	CreateAt time.Time `bson:"createAt,omitempty" json:"createAt,omitempty"`
+}


Test patch:
diff --git a/tools/goctl/model/mongo/generate/generate_test.go b/tools/goctl/model/mongo/generate/generate_test.go
index c214ac74..3586b60a 100644
--- a/tools/goctl/model/mongo/generate/generate_test.go
+++ b/tools/goctl/model/mongo/generate/generate_test.go
@@ -16,20 +16,53 @@ var testTypes = `
 `
 
 func TestDo(t *testing.T) {
-	cfg, err := config.NewConfig(config.DefaultFormat)
-	assert.Nil(t, err)
-
-	tempDir := pathx.MustTempDir()
-	typesfile := filepath.Join(tempDir, "types.go")
-	err = ioutil.WriteFile(typesfile, []byte(testTypes), 0o666)
-	assert.Nil(t, err)
-
-	err = Do(&Context{
-		Types:  []string{"User", "Class"},
-		Cache:  false,
-		Output: tempDir,
-		Cfg:    cfg,
+	t.Run("should generate model", func(t *testing.T) {
+		cfg, err := config.NewConfig(config.DefaultFormat)
+		assert.Nil(t, err)
+
+		tempDir := pathx.MustTempDir()
+		typesfile := filepath.Join(tempDir, "types.go")
+		err = ioutil.WriteFile(typesfile, []byte(testTypes), 0o666)
+		assert.Nil(t, err)
+
+		err = Do(&Context{
+			Types:  []string{"User", "Class"},
+			Cache:  false,
+			Output: tempDir,
+			Cfg:    cfg,
+		})
+
+		assert.Nil(t, err)
+	})
+
+	t.Run("missing config", func(t *testing.T) {
+		tempDir := t.TempDir()
+		typesfile := filepath.Join(tempDir, "types.go")
+		err := ioutil.WriteFile(typesfile, []byte(testTypes), 0o666)
+		assert.Nil(t, err)
+
+		err = Do(&Context{
+			Types:  []string{"User", "Class"},
+			Cache:  false,
+			Output: tempDir,
+			Cfg:    nil,
+		})
+		assert.Error(t, err)
 	})
 
-	assert.Nil(t, err)
+	t.Run("invalid config", func(t *testing.T) {
+		cfg := &config.Config{NamingFormat: "foo"}
+		tempDir := t.TempDir()
+		typesfile := filepath.Join(tempDir, "types.go")
+		err := ioutil.WriteFile(typesfile, []byte(testTypes), 0o666)
+		assert.Nil(t, err)
+
+		err = Do(&Context{
+			Types:  []string{"User", "Class"},
+			Cache:  false,
+			Output: tempDir,
+			Cfg:    cfg,
+		})
+		assert.Error(t, err)
+	})
 }
diff --git a/tools/goctl/model/mongo/generate/template_test.go b/tools/goctl/model/mongo/generate/template_test.go
new file mode 100644
index 00000000..ea580fb2
--- /dev/null
+++ b/tools/goctl/model/mongo/generate/template_test.go
@@ -0,0 +1,41 @@
+package generate
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/zeromicro/go-zero/tools/goctl/util/pathx"
+)
+
+func TestTemplate(t *testing.T) {
+	tempDir := t.TempDir()
+	pathx.RegisterGoctlHome(tempDir)
+	t.Cleanup(func() {
+		pathx.RegisterGoctlHome("")
+	})
+
+	t.Run("Category", func(t *testing.T) {
+		assert.Equal(t, category, Category())
+	})
+
+	t.Run("Clean", func(t *testing.T) {
+		err := Clean()
+		assert.NoError(t, err)
+	})
+
+	t.Run("Templates", func(t *testing.T) {
+		err := Templates()
+		assert.NoError(t, err)
+		assert.True(t, pathx.FileExists(filepath.Join(tempDir, category, modelTemplateFile)))
+	})
+
+	t.Run("RevertTemplate", func(t *testing.T) {
+		assert.NoError(t, RevertTemplate(modelTemplateFile))
+		assert.Error(t, RevertTemplate("foo"))
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		assert.NoError(t, Update())
+	})
+}
diff --git a/tools/goctl/test/common/echo.sh b/tools/goctl/test/common/echo.sh
new file mode 100644
index 00000000..3314209a
--- /dev/null
+++ b/tools/goctl/test/common/echo.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+function console_red() {
+	echo -e "\033[31m "$*" \033[0m"
+}
+
+function console_green() {
+	echo -e "\033[32m "$*" \033[0m"
+}
+
+function console_yellow() {
+	echo -e "\033[33m "$*" \033[0m"
+}
+
+function console_blue() {
+	echo -e "\033[34m "$*" \033[0m"
+}
+
+function console_tip() {
+   console_blue "========================== $* =============================="
+}
+
+function console_step() {
+   console_blue "<<<<<<<<<<<<<<<< $* >>>>>>>>>>>>>>>>"
+}
diff --git a/tools/goctl/test/integration/model/mongo/Dockerfile b/tools/goctl/test/integration/model/mongo/Dockerfile
new file mode 100644
index 00000000..4116de77
--- /dev/null
+++ b/tools/goctl/test/integration/model/mongo/Dockerfile
@@ -0,0 +1,12 @@
+FROM golang:1.18
+
+ENV TZ Asia/Shanghai
+ENV GOPROXY https://goproxy.cn,direct
+
+WORKDIR /app
+ADD goctl /usr/bin/goctl
+ADD cmd.sh .
+
+RUN chmod +x /usr/bin/goctl
+RUN chmod +x cmd.sh
+CMD ["/bin/bash", "cmd.sh"]
diff --git a/tools/goctl/test/integration/model/mongo/cmd.sh b/tools/goctl/test/integration/model/mongo/cmd.sh
new file mode 100644
index 00000000..c8ba2edb
--- /dev/null
+++ b/tools/goctl/test/integration/model/mongo/cmd.sh
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+wd=$(dirname $0)
+project=test
+testDir=$wd/$project
+mkdir -p $testDir
+
+cd $testDir
+
+# go mod init
+go mod init $project
+
+# generate cache code
+goctl model mongo -t User -c --dir cache
+if [ $? -ne 0 ]; then
+	exit 1
+fi
+
+# generate non-cache code
+goctl model mongo -t User --dir nocache
+if [ $? -ne 0 ]; then
+	exit 1
+fi
+
+# go mod tidy
+GOPROXY=https://goproxy.cn && go mod tidy
+
+# code inspection
+go test -race ./...
+if [ $? -ne 0 ]; then
+	echo
+fi
diff --git a/tools/goctl/test/integration/model/mongo/mongo.sh b/tools/goctl/test/integration/model/mongo/mongo.sh
new file mode 100644
index 00000000..73c80ef3
--- /dev/null
+++ b/tools/goctl/test/integration/model/mongo/mongo.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+
+cd $(dirname $0)
+
+# source functions
+source ../../../common/echo.sh
+
+console_tip "mongo  test"
+
+# build goctl
+console_step "goctl building"
+
+buildFile=goctl
+CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o $buildFile ../../../../goctl.go
+image=goctl-mongo:latest
+
+# docker build
+console_step "docker building"
+docker build -t $image .
+if [ $? -ne 0 ]; then
+	rm -f $buildFile
+	console_red "docker build failed"
+	exit 1
+fi
+
+# run docker image
+console_step "docker running"
+docker run $image
+if [ $? -ne 0 ]; then
+	rm -f $buildFile
+	console_red "docker run failed"
+	docker image rm -f $image
+	exit 1
+fi
+
+rm -f $buildFile
+console_green "PASS"
+docker image rm -f $image > /dev/null 2>&1
diff --git a/tools/goctl/test/main.sh b/tools/goctl/test/main.sh
new file mode 100644
index 00000000..a9d94cea
--- /dev/null
+++ b/tools/goctl/test/main.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+
+# main.sh is the entry point for the goctl tests.
+
+# testing mongo code generation.
+/bin/bash integration/model/mongo/mongo.sh