GORM GEN的代码生成

1,967 阅读10分钟

上一篇:GORM GEN 快速开始

下一篇:GORM GEN的CRUD

推荐有时间的同学可以将系列文章都过一遍有个印象、没有时间的同学可以通过关键词搜索找到自己需要的内容。

image.png

前面介绍了GEN的基础,以及快速开始相关的内容,相信你已经了解了GEN的能力和最简单的使用。本文是GEN教程系列的第二篇,将详细介绍代码生成的能力,包括直接同步数据库表生成、基于已有的model生成以及通过建表SQL生成三部分。

同步数据库表生成

1.1 配置介绍

一下介绍的配置都是全局配置,对所有表都生效,主要包括生成的模式、生成的路径、类型和名称等配置。


// GenerateMode generate mode
type GenerateMode uint

const (
   // 自动生成全局变量(推荐)
   WithDefaultQuery GenerateMode = 1 << iota

   // 不强制使用Context(公司内不推荐)
   WithoutContext

   // 为dao生成接口,有mock需求的可以用
   WithQueryInterface
)

// Config generator's basic configuration
type Config struct {
   db *gorm.DB // 需要从数据库同步的时候需要,同步g.UseDB(db)写入

   OutPath      string // 代码生成的路径
   OutFile      string // gen文件名 default: gen.go
   ModelPkgPath string // 指定model的包,默认 model
   WithUnitTest bool   // 是否生成单元测试

   // generate model global configuration
   FieldNullable     bool // 允许为null的字段生成指针类型
   FieldCoverable    bool // 存在非零默认值的字段生成指针类型, to fix problem zero value cannot be assign: https://gorm.io/docs/create.html#Default-Values
   FieldSignable     bool // 是否生成无符号类型(uint\uint32\uint64 等)
   FieldWithIndexTag bool // 是否生成索引的tag(idx_xxx)
   FieldWithTypeTag  bool // 是否生成db类型tag(vachar(64))

   Mode GenerateMode // 最前面的定义,指定多个 WithDefaultQuery|WithQueryInterface
}

//1. 全局可选项,对所有表都生效
WithOpts(opts ...ModelOpt) 
// 比如指定字段类型,gen.FieldType("is_deleted","gorm.DeletedAt")
// g.WithOpts(gen.FieldType("deleted_at","gorm.DeletedAt"))


//2. 指定表名称策略
WithTableNameStrategy(ns func(tableName string) (targetTableName string)) 
//比如过滤某些表,不生成
/*
g.WithTableNameStrategy(func(tableName string) (targetTableName string) {
   if strings.HasPrefix(tableName, "_") { //忽略下划线开头的表
      return ""
   }
   return tableName
})
*/

//3. 指定表生成的model的struct的名称,默认是转大驼峰
WithModelNameStrategy(ns func(tableName string) (modelName string)) 

//4. 指定表生成的文件名称
WithFileNameStrategy(ns func(tableName string) (fileName string))

//5. db类型和go类型映射关系,默认映射不满足需求的时候可以用
WithDataTypeMap(newMap map[string]func(columnType gorm.ColumnType)
//比如 tinyint(1)不想生成bool而是int32类型
/*
g.WithDataTypeMap(map[string]func(columnType gorm.ColumnType) (dataType string){
   "tinyint": func(columnType gorm.ColumnType) (dataType string) {
      ct, _ := columnType.ColumnType()
      if strings.HasPrefix(ct, "tinyint(1)") {
         return "int8"
      }
      return "int16"
   },
})
*/

//6. 指定生成的json tag的名称,默认是db定义的字段名
WithJSONTagNameStrategy(ns func(columnName string) (tagContent string)) 

//7. 指定自定义类型/也可以是类型冲突时自定义
WithImportPkgPath(paths ...string) 
//比如依赖了一个其他包的model
// g.WithImportPkgPath("xxmodel xxx.xxx.model")

1.2 可选项介绍

这部分主要是model生成的一些可选项,用于一些表字段的特殊需求,比如某个字段需要生成自定义类型、需要修改生成的Field、需要修改字段tag、需要忽略些字段、新增一些非DB字段以及生成关联关系等。这部分选项一般是对某个表的字段做修改,所以是作用在GenerateModel/GenerateModelAs上的,当然也可能有需求要适用所有的表,比如忽略下划线开头的字段或者是删除字段统一用gorm的软删除等等,这种全局生效的场景可以用前面配置提到的WithOpts。

   //1. 通用的选项,允许你对整个字段进行修改(名字、类型、标签、关联关系等)
   //下面其他的选项都是对这个封装/简化
   FieldModify 
//比如针对id字段,删除default标签
/*
g.GenerateModel("user", gen.FieldModify(func(f gen.Field) gen.Field {
   if f.ColumnName == "id" {
      f.GORMTag.Remove(field.TagKeyGormDefault)
   }
   return f
}))
*/
   // 2. 新增一个数据库不存在,但是model上需要的字段
   FieldNew 
  
   // 3. 根据名称忽略字段
   FieldIgnore 
    //gen.FieldIgnore("create_time") //忽略创建时间字段
    
   // 4. 根据正则表达式忽略字段
   FieldIgnoreReg 
   
   // 5. 自定义字段的model属性名
   FieldRename 
   //gen.FieldRename("_id","ID") //指定id字段的在model上的名称
   
   // 6. 自定义字段的注释信息,默认会同步数据库的备注信息
   FieldComment
   
   // 7. 字段名称指定字段的类型
   FieldType
   //gen.FieldType("created_time", "int64") // 创建时间戳字段生成int64
   //gen.FieldType("status", "BindStatus") // 状态生成自定义类型字段,需要实现Scan/Value
   
   // 8. 根据正则指定字段的类型
   FieldTypeReg
   
   // 9. 指定生成dao的类型
   FieldGenType 
   //比如自定义枚举字段默认都会生成一个通用的Field类型,你可以指定成Int64
   //gen.FieldGenType("status", "Int64")
   
   // 10. 根据正则指定dao属性类型
   FieldGenTypeReg 
   
   // 11. 自定义标签
   FieldTag
   //比如一个加密字段,需要加上kms的标签
   /*
   gen.FieldTag("phone", func(tag field.Tag) field.Tag {
   //tag.Set("kms","xxx")
   return tag.Set("kms","xxx")
})
*/
   // 12. 自定义json标签
   FieldJSONTag 
   //比如我的字段是int类型,数据源是string类型
   // gen.FieldJSONTag("id","id,string,omitempty")
   
   // 13. 自定义json标签
   FieldJSONTagWithNS 
   //比如含有时间的字段,都加上omitempty
   /*
   gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
   if strings.Contains(columnName, "time") {
      return columnName + ",omitempty"
   }
   return columnName
})
   */
   
   // 14. 自定义GROM标签
   FieldGORMTag 
   //比如给某些时间/自定义类型字段加序列化标签
   /*
   gen.FieldGORMTag("extra", func(tag field.GormTag) field.GormTag {
   return tag.Set("serializer", "json")
})
*/
   // 15. 根据正则自定义GORM标签
   FieldGORMTagReg 
   
   // 16. 新增tag
   FieldNewTag 
   //gen.FieldNewTag("phone", field.Tag{"kms": "xxx"})
  
   // 17. 新增tag
   FieldNewTagWithNS
   //比如统一加上thrif/form标签
   /*
   gen.FieldNewTagWithNS("form", func(columnName string) (tagContent string) {
   return columnName
})
   */
   
   // 18. 删除属性前缀
   FieldTrimPrefix 
   
   // 19. 删除属性后缀
   FieldTrimSuffix
   
   // 20. 增加属性前缀
   FieldAddPrefix 
   
   // 21. 增加属性后缀
   FieldAddSuffix 
   
   // 22. 定义关联关系
   FieldRelate 
   //比如在用户model上生成关联的角色属性
   /*
   role := g.GenerateModel("role")
g.GenerateModel("user", gen.FieldRelate(field.HasMany, "Roles", role,
   &field.RelateConfig{
      // RelateSlice: true,
      GORMTag: field.GormTag{"foreignKey": []string{"RoleRefer"}},
   }),
)
   */
   
   // 23. 用已有的model定义关联关系
   FieldRelateModel 
   //和上面的区别是role不是同步数据库表生成的model,而是历史代码中已有的model
   
   // 24. 给生成的model添加一些通用的方法
   WithMethod
   //比如加上判断空或者获取名称的方法
   /*
   type CommonMethod struct {
    ID   int32
    Name *string
}

func (m *CommonMethod) IsEmpty() bool {
    if m == nil {
        return true
    }
    return m.ID == 0
}

func (m *CommonMethod) GetName() string {
    if m == nil || m.Name == nil {
        return ""
    }
    return *m.Name
}

// 给生成的People model增加 IsEmpty方法
g.GenerateModel("people", gen.WithMethod(CommonMethod{}.IsEmpty))

// 给生成的User model增加 CommonMethod的所有方法
g.GenerateModel("user", gen.WithMethod(CommonMethod))
   */
)

1.3 完整示例

github.com/go-gorm/gen…

package main

import (
   "context"
   "strings"

   "github.com/go-gorm/gendemo/mysql"
   "gorm.io/gen"
   "gorm.io/gen/field"
   "gorm.io/gorm"
)

// GEN Guideline: https://gorm.io/gen/index.html

// generate code
func main() {
   //init db
   mysql.Init()
   db := mysql.DB(context.Background())

   // specify the output directory (default: "./query")
   // ### if you want to query without context constrain, set mode gen.WithoutContext ###
   g := gen.NewGenerator(gen.Config{
      OutPath: "../../biz/dal/query",
      Mode:    gen.WithDefaultQuery | gen.WithQueryInterface,
      /* Mode: gen.WithoutContext,*/
      //if you want the nullable field generation property to be pointer type, set FieldNullable true
      /* FieldNullable: true,*/
   })
   g.WithOpts(gen.FieldType("deleted_at", "gorm.DeletedAt"))
   g.WithTableNameStrategy(func(tableName string) (targetTableName string) {
      if strings.HasPrefix(tableName, "_") { //忽略下划线开头的表
         return ""
      }
      return tableName
   })
   g.WithDataTypeMap(map[string]func(columnType gorm.ColumnType) (dataType string){
      "tinyint": func(columnType gorm.ColumnType) (dataType string) {
         ct, _ := columnType.ColumnType()
         if strings.HasPrefix(ct, "tinyint(1)") {
            return "int8"
         }
         return "int16"
      },
   })

   // reuse the database connection in Project or create a connection here
   // if you want to use GenerateModel/GenerateModelAs, UseDB is necessray or it will panic
   g.UseDB(db)
   // apply basic crud api on structs or table models which is specified by table name with function
   // GenerateModel/GenerateModelAs. And generator will generate table models' code when calling Excute.
   // g.ApplyBasic(model.User{}, g.GenerateModel("company"), g.GenerateModelAs("people", "Person", gen.FieldIgnore("address")))
   // apply diy interfaces on structs or table models
   // g.ApplyInterface(func(method model.Method) {}, model.User{}, g.GenerateModel("company"))
   g.ApplyBasic(
      g.GenerateModel("user", gen.FieldModify(func(f gen.Field) gen.Field {
         if f.ColumnName == "id" {
            f.GORMTag.Remove(field.TagKeyGormDefault)
         }
         return f
      }),
         gen.FieldTag("phone", func(tag field.Tag) field.Tag {
            //tag.Set("kms","xxx")
            return tag.Set("encrypt", "xxx")
         }),

         gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
            if strings.Contains(columnName, "time") {
               return columnName + ",omitempty"
            }
            return columnName
         }),
         gen.FieldJSONTag("id", "id,string,omitempty"),
         gen.FieldIgnore("create_time"),
         gen.FieldNewTag("phone", field.Tag{"encrypt": "xxx"}),
         gen.FieldNewTagWithNS("form", func(columnName string) (tagContent string) {
            return columnName
         }),
      ),
      g.GenerateModel("role", gen.FieldType("created_time", "int64"),
         gen.FieldGORMTag("extra", func(tag field.GormTag) field.GormTag {
            return tag.Set("serializer", "json")
         }),
         gen.WithMethod(gen.DefaultMethodTableWithNamer),
      ),
   )

   //role := g.GenerateModel("role")
   //g.GenerateModel("user", gen.FieldRelate(field.HasMany, "Roles", role,
   // &field.RelateConfig{
   //    // RelateSlice: true,
   //    GORMTag: field.GormTag{"foreignKey": []string{"RoleRefer"}},
   // }),
   //)

   // execute the action of code generation
   g.Execute()
}

建表SQL生成

2.1 介绍/使用

背景:一般我们用数据库直接生成就可以了,也没有额外的维护成本。但是由于公司内基本上boe都是用的一个数据库,有些业务会存在同时多个需求都新增字段;需求A上线的时候,会将需求B增加的字读放到model里面带上线,导致线上出现字段不存在的错误。针对这个问题,对于一般的业务其实PPE验证的时候应该就能发现了,然后顺带提下rds工单也能搞定;有些特殊的业务没有PPE验证的逻辑,会导致上线才发现问题,因此有了SQL生成的方式。业务在代码中维护需求设计的建表SQL,然后根据SQL文件生成model。

使用:根据建表SQL生成和同步数据库生成的方式是一致的,唯一不同的是替换db为一个db即可。支持多种方式:

  • 建表SQL字符串

  • 建表SQL文件

  • 建表SQL文件夹

db, _ := gorm.Open(rawsql.New(rawsql.Config{
   //SQL:      rawsql,                      //指定建表sql字符串也行
   FilePath: []string{
      //"./sql/user.sql", // 可以是sql文件,文件里可以是单个建表语句,也可以是多个
      "../../conf/sql/", // 指定一个sql文件路径也可以
   },
}))

2.2 完整示例

github.com/go-gorm/gen…

package main

import (
   "strings"

   "gorm.io/gen"
   "gorm.io/gen/field"
   "gorm.io/gorm"
   "gorm.io/rawsql"
)

// GEN Guideline: https://gorm.io/gen/index.html

// generate code
func main() {
   //init db
   //mysql.Init()
   //db := mysql.DB(context.Background())
   db, _ := gorm.Open(rawsql.New(rawsql.Config{
      //SQL:      rawsql,                      //指定建表sql字符串也行
      FilePath: []string{
         //"./sql/user.sql", // 可以是sql文件,文件里可以是单个建表语句,也可以是多个
         "../sql/", // 指定一个sql文件路径也可以
      },
   }))
   // specify the output directory (default: "./query")
   // ### if you want to query without context constrain, set mode gen.WithoutContext ###
   g := gen.NewGenerator(gen.Config{
      OutPath: "../../biz/dal/query",
      Mode:    gen.WithDefaultQuery | gen.WithQueryInterface,
      /* Mode: gen.WithoutContext,*/
      //if you want the nullable field generation property to be pointer type, set FieldNullable true
      /* FieldNullable: true,*/
   })
   g.WithOpts(gen.FieldType("deleted_at", "gorm.DeletedAt"))
   g.WithTableNameStrategy(func(tableName string) (targetTableName string) {
      if strings.HasPrefix(tableName, "_") { //忽略下划线开头的表
         return ""
      }
      return tableName
   })
   g.WithDataTypeMap(map[string]func(columnType gorm.ColumnType) (dataType string){
      "tinyint": func(columnType gorm.ColumnType) (dataType string) {
         ct, _ := columnType.ColumnType()
         if strings.HasPrefix(ct, "tinyint(1)") {
            return "int8"
         }
         return "int16"
      },
   })

   // reuse the database connection in Project or create a connection here
   // if you want to use GenerateModel/GenerateModelAs, UseDB is necessray or it will panic
   g.UseDB(db)
   // apply basic crud api on structs or table models which is specified by table name with function
   // GenerateModel/GenerateModelAs. And generator will generate table models' code when calling Excute.
   // g.ApplyBasic(model.User{}, g.GenerateModel("company"), g.GenerateModelAs("people", "Person", gen.FieldIgnore("address")))
   // apply diy interfaces on structs or table models
   // g.ApplyInterface(func(method model.Method) {}, model.User{}, g.GenerateModel("company"))
   g.ApplyBasic(
      g.GenerateModel("user", gen.FieldModify(func(f gen.Field) gen.Field {
         if f.ColumnName == "id" {
            f.GORMTag.Remove(field.TagKeyGormDefault)
         }
         return f
      }),
         gen.FieldTag("phone", func(tag field.Tag) field.Tag {
            //tag.Set("kms","xxx")
            return tag.Set("encrypt", "xxx")
         }),

         gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
            if strings.Contains(columnName, "time") {
               return columnName + ",omitempty"
            }
            return columnName
         }),
         gen.FieldJSONTag("id", "id,string,omitempty"),
         gen.FieldIgnore("create_time"),
         gen.FieldNewTag("phone", field.Tag{"encrypt": "xxx"}),
         gen.FieldNewTagWithNS("form", func(columnName string) (tagContent string) {
            return columnName
         }),
      ),
      g.GenerateModel("role", gen.FieldType("created_time", "int64"),
         gen.FieldGORMTag("extra", func(tag field.GormTag) field.GormTag {
            return tag.Set("serializer", "json")
         }),
         gen.WithMethod(gen.DefaultMethodTableWithNamer),
      ),
   )

   //role := g.GenerateModel("role")
   //g.GenerateModel("user", gen.FieldRelate(field.HasMany, "Roles", role,
   // &field.RelateConfig{
   //    // RelateSlice: true,
   //    GORMTag: field.GormTag{"foreignKey": []string{"RoleRefer"}},
   // }),
   //)

   // execute the action of code generation
   g.Execute()
}

已有的model生成

3.1 介绍/使用

背景:这种场景主要针对,历史项目已经写好了model,想要直接复用;GEN提供这种支持,不需要通过表或者SQL生成model,仅仅是通过model生成对应的dao逻辑。

使用:使用还是参考第一部分,只是前面提到的选项和配置基本都不可用了,当然对于自定义的model已经没有意义了,所有的类型标签等体现在model上。

g.ApplyBasic(
   model.User{},
   model.Role{},
)

3.2 完整示例

github.com/go-gorm/gen…

package main

import (
   "github.com/go-gorm/gendemo/biz/dal/model"
   "gorm.io/gen"
   "gorm.io/gorm"
   "gorm.io/rawsql"
)

// GEN Guideline: https://gorm.io/gen/index.html

// generate code
func main() {
   //init db
   // mysql.Init()
   //db := mysql.DB(context.Background())
  
   // specify the output directory (default: "./query")
   // ### if you want to query without context constrain, set mode gen.WithoutContext ###
   g := gen.NewGenerator(gen.Config{
      OutPath: "../../biz/dal/query",
      Mode:    gen.WithDefaultQuery | gen.WithQueryInterface,
      /* Mode: gen.WithoutContext,*/
      //if you want the nullable field generation property to be pointer type, set FieldNullable true
      /* FieldNullable: true,*/
   })
   g.ApplyBasic(
      model.User{},
      model.Role{},
   )

   // execute the action of code generation
   g.Execute()
}

生成Tips

4.1 为生成的model生成额外的方法

你是否也想生成的model可以带上一些通用的简单方法,比如判断是否为空等

package diy

import "gorm.io/gen"

type Querier interface {
   // SELECT * FROM @@table WHERE id=@id
   GetByID(id int) (gen.T, error) // GetByID query data by id and return it as *struct*

   // SELECT * FROM @@table WHERE id IN @ids
   MGet(ids ...string) ([]*gen.T, error)

   // QueryWith
   //SELECT * FROM @@table
   //  {{if p != nil}}
   //      {{if p.ID > 0}}
   //          WHERE id=@p.ID
   //      {{else if p.Name != ""}}
   //          WHERE name=@p.Name
   //      {{end}}
   //  {{end}}
   QueryWith(p *gen.T) (gen.T, error)
}

4.2 动态SQL,自定义dao方法

你是否有时候感觉,基础的方法还是不够用,希望能自定义一些dao方法?

完整 demo:github.com/go-gorm/gen…

更多模版参考:gorm.io/gen/dynamic…

package main

import (
   "context"
   "strings"

   "github.com/go-gorm/gendemo/biz/dal/diy"
   "github.com/go-gorm/gendemo/biz/dal/model"
   "github.com/go-gorm/gendemo/mysql"
   "gorm.io/gen"
   "gorm.io/gen/field"
   "gorm.io/gorm"
)

// GEN Guideline: https://gorm.io/gen/index.html

// generate code
func main() {
   //init db
   mysql.Init()
   db := mysql.DB(context.Background())

   // specify the output directory (default: "./query")
   // ### if you want to query without context constrain, set mode gen.WithoutContext ###
   g := gen.NewGenerator(gen.Config{
      OutPath: "../../biz/dal/query",
      Mode:    gen.WithDefaultQuery | gen.WithQueryInterface,
      /* Mode: gen.WithoutContext,*/
      //if you want the nullable field generation property to be pointer type, set FieldNullable true
      /* FieldNullable: true,*/
   })
   g.WithOpts(gen.FieldType("deleted_at", "gorm.DeletedAt"))
   g.WithTableNameStrategy(func(tableName string) (targetTableName string) {
      if strings.HasPrefix(tableName, "_") { //忽略下划线开头的表
         return ""
      }
      return tableName
   })
   g.WithDataTypeMap(map[string]func(columnType gorm.ColumnType) (dataType string){
      "tinyint": func(columnType gorm.ColumnType) (dataType string) {
         ct, _ := columnType.ColumnType()
         if strings.HasPrefix(ct, "tinyint(1)") {
            return "int8"
         }
         return "int16"
      },
   })

   // reuse the database connection in Project or create a connection here
   // if you want to use GenerateModel/GenerateModelAs, UseDB is necessray or it will panic
   g.UseDB(db)
   // apply basic crud api on structs or table models which is specified by table name with function
   // GenerateModel/GenerateModelAs. And generator will generate table models' code when calling Excute.
   // g.ApplyBasic(model.User{}, g.GenerateModel("company"), g.GenerateModelAs("people", "Person", gen.FieldIgnore("address")))
   // apply diy interfaces on structs or table models
   // g.ApplyInterface(func(method model.Method) {}, model.User{}, g.GenerateModel("company"))
   g.ApplyInterface(func(diy.Querier) {},
      g.GenerateModel("user", gen.FieldModify(func(f gen.Field) gen.Field {
         if f.ColumnName == "id" {
            f.GORMTag.Remove(field.TagKeyGormDefault)
         }
         return f
      }),
         gen.FieldTag("phone", func(tag field.Tag) field.Tag {
            //tag.Set("kms","xxx")
            return tag.Set("encrypt", "xxx")
         }),

         gen.FieldJSONTagWithNS(func(columnName string) (tagContent string) {
            if strings.Contains(columnName, "time") {
               return columnName + ",omitempty"
            }
            return columnName
         }),
         gen.FieldJSONTag("id", "id,string,omitempty"),
         gen.FieldIgnore("create_time"),
         gen.FieldNewTag("phone", field.Tag{"encrypt": "xxx"}),
         gen.FieldNewTagWithNS("form", func(columnName string) (tagContent string) {
            return columnName
         }),
      ),
      model.Role{},
   )

   //role := g.GenerateModel("role")
   //g.GenerateModel("user", gen.FieldRelate(field.HasMany, "Roles", role,
   // &field.RelateConfig{
   //    // RelateSlice: true,
   //    GORMTag: field.GormTag{"foreignKey": []string{"RoleRefer"}},
   // }),
   //)

   // execute the action of code generation
   g.Execute()
}
package diy

import "gorm.io/gen"

type Querier interface {
   // SELECT * FROM @@table WHERE id=@id
   GetByID(id int) (gen.T, error) // GetByID query data by id and return it as *struct*


   // SELECT * FROM @@table WHERE id IN @ids
   MGet(ids ...string) ([]*gen.T, error)

   // QueryWith
   //SELECT * FROM @@table
   //  {{if p != nil}}
   //      {{if p.ID > 0}}
   //          WHERE id=@p.ID
   //      {{else if p.ShopID != ""}}
   //          WHERE created_time=@p.ShopID
   //      {{end}}
   //  {{end}}
   QueryWith(p *gen.T) (gen.T, error)
}

下一篇:GORM GEN的CRUD