GORM 强大的代码生成工具 —— gorm/gen

2022年11月1日 410点热度 0人点赞 0条评论

来源:juejin.cn/post/7133150674400837668


GORM 的那些困扰


GORM 进入到 2.0 时代之后解决了很多 jinzhu/gorm 时代的问题,整体的扩展性以及功能也更强大。但总有一些绕不开的问题困扰着我们。


?点击领取

Go后端开发大厂面试题手册

SQL 注入


Object Relational Mapping 的定位就是帮助开发者减轻心智负担,你不用再去思考业务 object 和 数据表 relation 之间的对应,ORM 框架来帮你完成。我们只需要简单的在 object 上加上 tag,剩下怎么拼 SQL,怎么 Scan 数据后写入 object 就交给 ORM 来完成。业务开发者不需要操心这个。


问题就在这里,这样的定位势必导致 ORM 被反射和 interface{} 满天飞,你既然要通用,按照 Golang 目前的能力,就势必要在运行时做类型转换,用各种反射黑科技。


但是,反射顶多能告诉你当前是什么,不能来校验。因为 ORM 是不感知业务的。

要求它来校验输入数据的类型,格式,合法性是不现实的。使用方法十分灵活的查询接口很容易造成研发对接口的误用,从而导致SQL注入。


复杂 SQL


GORM作为ORM框架并没有提供任何辅助代码开发的功能,导致面对较为复杂的数据库表查询场景时,开发者需逐条手写数据表中的列与对应结构体的成员变量,单调且重复的查询功能也需要手动复制,稍不注意就会造成不易察觉的拼写错误。


其实在 Golang 泛型比较弱的情况下,使用【代码生成】依然是解决个性化场景的经典方案,这样绕开了 interface{},我们就可以做更多校验,也省去了断言。


GORM 其实也是基于这个思路,推出了自己的【代码生成工具】:Gen。


gorm-gen


Gen: Friendly & Safer GORM powered by Code Generation

这里需要说明,Gen 并不是一个三方突发奇想做的库,而是作为 GORM 的官方工具,在 go-gorm 组织下提供的。本身也是由 jinzhu 大佬和相关同学一起维护,所以大家可以放心,这是个官方的解决方案。


我们可以使用 GORM,也可以用 Gen 来生成代码,只是 API 层的两种实现,底层的能力都是一样的。


gen[1] 对自己的定位就是通过代码生成,让 GORM 更加友好(针对复杂SQL场景也能处理),也更加安全(增加类型校验)。


  • CRUD or DIY query method code generation
  • Auto migration from database to code
  • Transactions, Nested Transactions, Save Point, RollbackTo to Saved Point
  • Competely compatible with GORM
  • Developer Friendly
  • Multiple Generate modes


从真正使用上来说,我觉得最核心的 feature 在于:


  1. 字段类型校验,过滤参数错误,为数字、字符串、布尔类型、时间类型硬编码制定差异化类型安全的表达式方法,杜绝了 SQL 注入的风险,能跑就安全;

  2. 映射数据库表像,DB 里面有数据表就能生成对应的 Golang 结构体;

  3. 用注释的形式描述查询的逻辑后,一键即可生成对应的安全可靠查询API。

此外还有一个好处是,我们用 GORM 来 Find 数据时,总还是要先声明结果,然后把指针传入 API,由 GORM 进行填充,而有了 Gen 之后,直接返回对应的数据结构,免于提前实例化数据后在注入API的繁琐。


复杂 SQL 怎么解


通过 interface 指明我们希望查询的语义,自动生成查询代码,这个可以说是 gorm-gen 最香的能力了。原因很简单:


  • 根据表结构倒回来生成结构体,这件事情非常低频,大多数情况下我们是先有一个 Persistent Object,再去创建表;

  • 类型安全,很重要,但对业务本身的能力上没有加成,也很难量化怎样算做的好,大家感触不深。


所以,大家最关心的能力还是,能不能我定义个接口,说清楚我需要什么数据(或者 sql 提供出来),你自己来生成查询代码,gorm 的封装,类型安全,数据转换等等,一切都由工具搞定,作为业务开发者,我只管调用从你生成的方法就行。能不能做到?


能!这就是 gorm-gen 带来的能力。这一节我们直接来实战演练一下。


本节实例源码在 db-demo[2] 感兴趣的同学可以看一下 gendemo 目录下的代码。


我们还是通过 go get 添加 gen 的依赖

go get -u gorm.io/gen


然后在项目中 import "gorm.io/gen" 进来即可。


首先我们创建一个 gendemo 目录,准备一些业务结构体,这些就是我们的 PO(需要持久化的对象)。目录结构如下:


  • cmd/generate:用于存放 gorm-gen 的代码生成逻辑;
  • dal/model:我们的业务结构定义(model.go),以及希望 gorm-gen 生成实现的接口定义(method.go);
  • generate.sh:一个bash 脚本,启动代码生成。


我们来看看每个文件干了什么。


  • dal.go


这里很简单,只是维护了内存中的数据库连接,完成初始化,和业务无关。

package dal
import ( "fmt" "sync"
"gorm.io/gorm"
"gorm.io/driver/sqlite"
"github.com/ag9920/db-demo/gendemo/dal/model")
var DB *gorm.DBvar once sync.Once
func init() { once.Do(func() { DB = ConnectDB().Debug() _ = DB.AutoMigrate(&model.User{}, &model.Passport{}) })}
func ConnectDB() (conn *gorm.DB) { conn, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) if err != nil { panic(fmt.Errorf("cannot establish db connection: %w", err)) } return conn}

  • model.go


这里我们定义了两个业务模型:User 以及 Passport。

package model
import ( "database/sql/driver" "fmt" "strings" "time"
"gorm.io/gorm")
type Username string
type Password string
func (p *Password) Scan(src interface{}) error { *p = Password(fmt.Sprintf("@%v@", src)) return nil}
func (p *Password) Value() (driver.Value, error) { *p = Password(strings.Trim(string(*p), "@")) return p, nil}
type User struct { gorm.Model // ID uint CreatAt time.Time UpdateAt time.Time DeleteAt gorm.DeleteAt If it is repeated with the definition will be ignored ID uint `gorm:"primary_key"` Name string `gorm:"column:name"` Age int `gorm:"column:age;type:varchar(64)"` Role string `gorm:"column:role;type:varchar(64)"` Friends []User `gorm:"-"` // only local used gorm ignore}
type Passport struct { ID int Username Username // will be field.String Password Password // will be field.Field because type Password set Scan and Value LoginTime time.Time}

  • method.go


这里定义了我们希望实现的接口定义。这里本质上就是通过【注释】告诉 gen,我们希望获取什么样的数据,sql 怎么生成。所以注释的写法很重要。大家先看下代码,我们下面会说:

package model
import "gorm.io/gen"
type Method interface { // Where("name=@name and age=@age") FindByNameAndAge(name string, age int) (gen.T, error) //sql(select id,name,age from users where age>18) FindBySimpleName() ([]gen.T, error)
//sql(select id,name,age from @@table where age>18 //{{if cond1}}and id=@id {{end}} //{{if name == ""}}and @@col=@name{{end}}) FindByIDOrName(cond1 bool, id int, col, name string) (gen.T, error)
//sql(select * from users) FindAll() ([]gen.M, error)
//sql(select * from users limit 1) FindOne() gen.M
//sql(select address from users limit 1) FindAddress() (gen.T, error)}
// only used to Usertype UserMethod interface { //where(id=@id) FindByID(id int) (gen.T, error)
//select * from users where age>18 FindAdult() ([]gen.T, error)
//select * from @@table // {{where}} // {{if role=="user"}} // id=@id // {{else if role=="admin"}} // role="user" or rule="normal-admin" // {{else}} // role="user" or role="normal-admin" or role="admin" // {{end}} // {{end}} FindByRole(role string, id int)
//update users // {{set}} // update_time=now(), // {{if name != ""}} // name=@name // {{end}} // {{end}} // where id=@id UpdateUserName(name string, id int) error}


注释的内容可以描述gorm的Where查询内容,也可以是一个完整的SQL查询语句。


  • 最简单的注释可以直接用 Where 来指明对应关系即可,如:
// Where("name=@name and age=@age")FindByNameAndAge(name string, age int) (gen.T, error)

  • 直接写 sql
//sql(select id,name,age from users where age>18)FindBySimpleName() ([]gen.T, error)

  • sql 带子句
//sql(select id,name,age from @@table where age>18//{{if cond1}}and id=@id {{end}}//{{if name == ""}}and @@col=@name{{end}})FindByIDOrName(cond1 boolid intcolname string) (gen.T, error)

下面两个小节我们来看一下注释的规则:

占位符


  • gen.T 用于返回数据的结构体,会根据生成结构体或者数据库表结构自动生成

  • gen.M 表示map[string]interface{},用于返回数据

  • gen.RowsAffected 用于执行SQL进行更新或删除时候,用于返回影响行数

  • @@table 查询的表名,如果没有传参,会根据结构体或者表名自动生成

  • @@<name> 当表名或者字段名可控时候,用@@占位,name为可变参数名,需要函数传入。

  • @<name> 当数据可控时候,用@占位,name为可变参数名,需要函数传入

  • 出于安全拼接考虑,like查询不支持在SQL中拼接%,如需要拼接,需要在调用函数参数中拼接好。


子句


  • 逻辑操作必须包裹在{{}}中,如{{if}},结束语句必须是 {{end}}, 所有的语句都可以嵌套。{{}}中的语法除了{{end}}其它的都是Golang语法;

  • {{if}} 支持通过满足条件拼接字符串到SQL;

  • where 只有在where子句不为空时候插入where,若子句的开头为 where连接关键字AND 或 OR,会将它们去除。

  • set 只有在set子句不为空时候插入set,若子句的开头为,会将它们去除。

  • for 通过遍历数组并将其内容插入到SQL中,需要注意之前的连接词。

  • 所有子句需要用{{end}} 结束子句,支持嵌套使用

OK,现在我们有了业务 Model,有了我们希望生成的接口。该让 gorm-gen 出场了!

首先我们切换到 cmd/generate 包,看看我们需要做什么来告诉 gorm-gen 如何生成:


  • generate.go
package main
import ( "github.com/ag9920/db-demo/gendemo/dal/model" "gorm.io/gen")
func main() {
g := gen.NewGenerator(gen.Config{ OutPath: "../../dal/query", Mode: gen.WithDefaultQuery, })
g.ApplyBasic(model.Passport{}, model.User{})
g.ApplyInterface(func(model.Method) {}, model.User{}) g.ApplyInterface(func(model.UserMethod) {}, model.User{})
g.Execute()}

  • 我们通过 gen.NewGenerator 来构造一个【代码生成器】,指定我们要生成的代码要放到 dal 下面的 query 子包,生成模式暂时用 default 就ok。

  • 调用 ApplyBasic 基于两个 model 来生成基础 DAL 代码;

  • 调用 ApplyInterface,指明我们希望基于什么 model 和 interface 来生成自定义的接口实现。

  • 最后调用 Execute 方法来触发生成。

好了,我们切换到外层的 generate.sh

#!/bin/bash
PROJECT_DIR=$(dirname "$0")GENERATE_DIR="$PROJECT_DIR/cmd/generate"
cd "$GENERATE_DIR" || exit
echo "Start Generating"go run .

这里来调用 go run 启动我们的 main 函数即可。

万事俱备,我们来执行一下:

$ ./generate.sh
Start Generating2022/08/18 17:12:01 Start generating code.2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/passports.gen.go2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/users.gen.go2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/gen.go2022/08/18 17:12:01 Generate code done.

Bingo,任务完成。此时,我们再来看看 dal 目录,你会发现多了个 query 文件夹


其中,passports.gen.go 以及 users.gen.go 分别对应我们的两个model,很直观。而 gen.go 则是通用的查询代码。我们来看看里面有什么:

  • gen.go
// Code generated by gorm.io/gen. DO NOT EDIT.// Code generated by gorm.io/gen. DO NOT EDIT.// Code generated by gorm.io/gen. DO NOT EDIT.
package query
import ( "context" "database/sql"
"gorm.io/gorm")
var ( Q = new(Query) Passport *passport User *user)
func SetDefault(db *gorm.DB) { *Q = *Use(db) Passport = &Q.Passport User = &Q.User}
func Use(db *gorm.DB) *Query { return &Query{ db: db, Passport: newPassport(db), User: newUser(db), }}
type Query struct { db *gorm.DB
Passport passport User user}
func (q *Query) Available() bool { return q.db != nil }
func (q *Query) clone(db *gorm.DB) *Query { return &Query{ db: db, Passport: q.Passport.clone(db), User: q.User.clone(db), }}
type queryCtx struct { Passport *passportDo User *userDo}
func (q *Query) WithContext(ctx context.Context) *queryCtx { return &queryCtx{ Passport: q.Passport.WithContext(ctx), User: q.User.WithContext(ctx), }}
func (q *Query) Transaction(fc func(tx *Query) error, opts ...*sql.TxOptions) error { return q.db.Transaction(func(tx *gorm.DB) error { return fc(q.clone(tx)) }, opts...)}
func (q *Query) Begin(opts ...*sql.TxOptions) *QueryTx { return &QueryTx{q.clone(q.db.Begin(opts...))}}
type QueryTx struct{ *Query }
func (q *QueryTx) Commit() error { return q.db.Commit().Error}
func (q *QueryTx) Rollback() error { return q.db.Rollback().Error}
func (q *QueryTx) SavePoint(name string) error { return q.db.SavePoint(name).Error}
func (q *QueryTx) RollbackTo(name string) error { return q.db.RollbackTo(name).Error}

这里很好理解,其实就是把我们的 DAL 操作都封装了起来,提供了常见的 WithContext, Transaction 等方法。业务只需要构造出一个 gorm 链接,传入 SetDefault 就能使用。


  • user.go

自动生成的数据访问方法比较多,而且还有我们指定的两个接口实现。这里我们就不贴完整代码了,感兴趣的同学可以到上面的源码仓库了解。这里我们抽出几个典型的代码片段看一下。


//Where("name=@name and age=@age")func (u userDo) FindByNameAndAge(name string, age int) (result *model.User, err error) { params := make(map[string]interface{}, 0)
var generateSQL strings.Builder params["name"] = name params["age"] = age generateSQL.WriteString("name=@name and age=@age ")
var executeSQL *gorm.DB if len(params) > 0 { executeSQL = u.UnderlyingDB().Where(generateSQL.String(), params).Take(&result) } else { executeSQL = u.UnderlyingDB().Where(generateSQL.String()).Take(&result) } err = executeSQL.Error return}
//sql(select id,name,age from users where age>18)func (u userDo) FindBySimpleName() (result []*model.User, err error) { var generateSQL strings.Builder generateSQL.WriteString("select id,name,age from users where age>18 ")
var executeSQL *gorm.DB executeSQL = u.UnderlyingDB().Raw(generateSQL.String()).Find(&result) err = executeSQL.Error return}
//sql(select id,name,age from @@table where age>18//{{if cond1}}and id=@id {{end}}//{{if name == ""}}and @@col=@name{{end}})func (u userDo) FindByIDOrName(cond1 bool, id int, col string, name string) (result *model.User, err error) { params := make(map[string]interface{}, 0)
var generateSQL strings.Builder generateSQL.WriteString("select id,name,age from users where age>18 ") if cond1 { params["id"] = id generateSQL.WriteString("and id=@id ") } if name == "" { params["name"] = name generateSQL.WriteString("and " + u.Quote(col) + "=@name ") }
var executeSQL *gorm.DB if len(params) > 0 { executeSQL = u.UnderlyingDB().Raw(generateSQL.String(), params).Take(&result) } else { executeSQL = u.UnderlyingDB().Raw(generateSQL.String()).Take(&result) } err = executeSQL.Error return}

看到了么?这就是基于我们的 interface 生成的实现。gorm-gen 很贴心的把我们的注释也搬了过来,我们可以参照着对比一下。


其实本质就是用我们给的 SQL, 对变量进行填充,通过 GORM 提供的 Raw 和 Exec 接口拿到最后的结果。属于最上层的封装,但可以大大减轻我们的负担,简单的 SQL 可能还看不出来,我们对比一下复杂的:


//select * from @@table// {{where}}//  {{if role=="user"}}//   id=@id//  {{else if role=="admin"}}//   role="user" or rule="normal-admin"//  {{else}}//   role="user" or role="normal-admin" or role="admin"//  {{end}}// {{end}}func (u userDo) FindByRole(role string, id int) { params := make(map[string]interface{}, 0)
var generateSQL strings.Builder generateSQL.WriteString("select * from users ") var whereSQL0 strings.Builder if role == "user" { params["id"] = id whereSQL0.WriteString("id=@id ") } else if role == "admin" { whereSQL0.WriteString("role=\"user\" or rule=\"normal-admin\" ") } else { whereSQL0.WriteString("role=\"user\" or role=\"normal-admin\" or role=\"admin\" ") } helper.JoinWhereBuilder(&generateSQL, whereSQL0)
if len(params) > 0 { _ = u.UnderlyingDB().Exec(generateSQL.String(), params) } else { _ = u.UnderlyingDB().Exec(generateSQL.String()) } return}
//update users// {{set}}// update_time=now(),// {{if name != ""}}// name=@name// {{end}}// {{end}}//where id=@idfunc (u userDo) UpdateUserName(name string, id int) (err error) { params := make(map[string]interface{}, 0)
var generateSQL strings.Builder generateSQL.WriteString("update users ") var setSQL0 strings.Builder setSQL0.WriteString("update_time=now(), ") if name != "" { params["name"] = name setSQL0.WriteString("name=@name ") } helper.JoinSetBuilder(&generateSQL, setSQL0) params["id"] = id generateSQL.WriteString("where id=@id ")
var executeSQL *gorm.DB if len(params) > 0 { executeSQL = u.UnderlyingDB().Exec(generateSQL.String(), params) } else { executeSQL = u.UnderlyingDB().Exec(generateSQL.String()) } err = executeSQL.Error return}

Where,Set 现在都可以根据实际的数据情况进行调整。只要我们把注释写对,生成的代码就是安全的,非常方便。


这里也可以看出,gorm-gen 提供的【SQL模板】 => 【接口实现】的能力还是非常灵活的,子句和占位符同时使用,基本上大部分场景都可以覆盖。


基础 API


除此之外,我们通过 ApplyBasic 生成的基础的访问代码也非常有用,这是对 GORM API 的加强,还是基于users.gen.go,我们看一下生成的代码什么样:


func (u userDo) Create(values ...*model.User) error { if len(values) == 0 {  return nil } return u.DO.Create(values)}
func (u userDo) CreateInBatches(values []*model.User, batchSize int) error { return u.DO.CreateInBatches(values, batchSize)}
// Save : !!! underlying implementation is different with GORM// The method is equivalent to executing the statement: db.Clauses(clause.OnConflict{UpdateAll: true}).Create(values)func (u userDo) Save(values ...*model.User) error { if len(values) == 0 { return nil } return u.DO.Save(values)}
func (u userDo) First() (*model.User, error) { if result, err := u.DO.First(); err != nil { return nil, err } else { return result.(*model.User), nil }}
func (u userDo) Take() (*model.User, error) { if result, err := u.DO.Take(); err != nil { return nil, err } else { return result.(*model.User), nil }}
func (u userDo) Last() (*model.User, error) { if result, err := u.DO.Last(); err != nil { return nil, err } else { return result.(*model.User), nil }}
func (u userDo) Find() ([]*model.User, error) { result, err := u.DO.Find() return result.([]*model.User), err}
func (u userDo) FindInBatch(batchSize int, fc func(tx gen.Dao, batch int) error) (results []*model.User, err error) { buf := make([]*model.User, 0, batchSize) err = u.DO.FindInBatches(&buf, batchSize, func(tx gen.Dao, batch int) error { defer func() { results = append(results, buf...) }() return fc(tx, batch) }) return results, err}
func (u userDo) FindInBatches(result *[]*model.User, batchSize int, fc func(tx gen.Dao, batch int) error) error { return u.DO.FindInBatches(result, batchSize, fc)}

调用生成的代码


其实这一步就更简单了,我们在 dal/query 目录下已经有了生成的代码,回忆一下,在 gen.go 里面我们还有对外暴露的方法来获取到这个 DAO:


import ( "context" "database/sql"
"gorm.io/gorm")
var ( Q = new(Query) Passport *passport User *user)
func SetDefault(db *gorm.DB) { *Q = *Use(db) Passport = &Q.Passport User = &Q.User}
func Use(db *gorm.DB) *Query { return &Query{ db: db, Passport: newPassport(db), User: newUser(db), }}
type Query struct { db *gorm.DB
Passport passport User user}

最终我们是靠这个 Query 对象来作为 DAO,对外提供查询,更新能力。所以这里我们有两种方案:


  1. 调用 SetDefault 之后,直接引用两个对象对应的分别的 DAO:Passport 或 User。

  2. 通过 Use 方法,传入一个 gorm.DB 链接,拿到一个 *Query 对象,这里已经包含了两个模型的 DAO,也可以直接使用。


这里我们引用官方的最佳实践,来看看结合生成的代码,可以如何完成增删改查,非常方便:


import ( "context" "fmt"
"gorm.io/hints"
"github.com/ag9920/db-demo/gendemo/dal" "github.com/ag9920/db-demo/gendemo/dal/model" "github.com/ag9920/db-demo/gendemo/dal/query")
var q = query.Use(dal.DB.Debug())
func Create(ctx context.Context) { var err error ud := q.User.WithContext(ctx)
userData := &model.User{ID: 1, Name: "modi"} // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.389','2021-09-13 20:05:51.389',NULL,'modi',0,'',1) err = ud.Create(userData)
userDataArray := []*model.User{{ID: 2, Name: "A"}, {ID: 3, Name: "B"}} err = ud.CreateInBatches(userDataArray, 2) // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.403','2021-09-13 20:05:51.403',NULL,'A',0,'',2),('2021-09-13 20:05:51.403','2021-09-13 20:05:51.403',NULL,'B',0,'',3)
userData.Name = "new name" err = ud.Save(userData) // INSERT INTO `users` (`created_at`,`updated_at`,`deleted_at`,`name`,`age`,`role`,`id`) VALUES ('2021-09-13 20:05:51.389','2021-09-13 20:05:51.409',NULL,'new name',0,'',1) ON DUPLICATE KEY UPDATE `updated_at`=VALUES(`updated_at`),`deleted_at`=VALUES(`deleted_at`),`name`=VALUES(`name`),`age`=VALUES(`age`),`role`=VALUES(`role`)}
func Delete(ctx context.Context) { var err error u, ud := q.User, q.User.WithContext(ctx)
_, err = ud.Where(u.ID.Eq(1)).Delete() // UPDATE `users` SET `deleted_at`='2021-09-13 20:05:51.418' WHERE `users`.`id` = 1 AND `users`.`deleted_at` IS NULL
_, err = ud.Where(u.ID.In(2, 3)).Delete() // UPDATE `users` SET `deleted_at`='2021-09-13 20:05:51.428' WHERE `users`.`id` IN (2,3) AND `users`.`deleted_at` IS NULL
_, err = ud.Where(u.ID.Gt(100)).Unscoped().Delete() // DELETE FROM `users` WHERE `users`.`id` > 100}

func Query(ctx context.Context) { var err error var user *model.User var users []*model.User
u, ud := q.User, q.User.WithContext(ctx)
/*--------------Basic query-------------*/ user, err = ud.Take() // SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL LIMIT 1 fmt.Printf("query 1 item: %+v", user)
user, err = ud.Where(u.ID.Gt(100), u.Name.Like("%T%")).Take() // SELECT * FROM `users` WHERE `users`.`id` > 100 AND `users`.`name` LIKE '%T%' AND `users`.`deleted_at` IS NULL LIMIT 1 fmt.Printf("query conditions got: %+v", user)
user, err = ud.Where(ud.Columns(u.ID).In(ud.Select(u.ID.Min()))).First() // SELECT * FROM `users` WHERE `users`.`id` IN (SELECT MIN(`users`.`id`) FROM `users` WHERE `users`.`deleted_at` IS NULL) AND `users`.`deleted_at` IS NULL // ORDER BY `users`.`id` LIMIT 1 fmt.Printf("subquery 1 got item: %+v", user)
user, err = ud.Where(ud.Columns(u.ID).Eq(ud.Select(u.ID.Max()))).First() // SELECT * FROM `users` WHERE `users`.`id` = (SELECT MAX(`users`.`id`) FROM `users` WHERE `users`.`deleted_at` IS NULL) AND `users`.`deleted_at` IS NULL // ORDER BY `users`.`id` LIMIT 1 fmt.Printf("subquery 2 got item: %+v", user)
users, err = ud.Distinct(u.Name).Find() // SELECT DISTINCT `users`.`name` FROM `users` WHERE `users`.`deleted_at` IS NULL fmt.Printf("select distinct got: %d", len(users))
/*--------------Diy query-------------*/ user, err = ud.FindByNameAndAge("tom", 29) // SELECT * FROM `users` WHERE name='tom' and age=29 AND `users`.`deleted_at` IS NULL fmt.Printf("FindByNameAndAge: %+v", user)}

总结


今天我们通过定义接口,生成实现代码这个场景作为切入点,了解了 gorm-gen 的最核心功能。其实生成的代码还是非常简洁,且功能强大的。并且支持从 table 直接生成业务结构。建议大家仔细看看我们的 demo 以及官方文档[1],相信对于 gorm-gen 熟练会帮助业务开发提效,安全。


参考资料


[1]gen: https://github.com/go-gorm/gen
[2]db-demo: https://github.com/ag9920/db-demo


程序员技术交流群


扫码进群记得备注:城市、昵称和技术方向


  1. 「Go工具箱」强烈推荐:一个能让http请求回放的工具

  2. 【GoLand教程】详解 GoLand 用户界面

  3. Golang 基于 flag 库实现一个命令行工具

92120GORM 强大的代码生成工具 —— gorm/gen

这个人很懒,什么都没留下

文章评论