GORM 强大的代码生成工具 —— gorm/gen
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详情
GORM 的那些困扰
GORM 进入到 2.0 时代之后解决了很多 jinzhu/gorm 时代的问题,整体的扩展性以及功能也更强大。但总有一些绕不开的问题困扰着我们。
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 对自己的定位就是通过代码生成,让 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 在于:
-
字段类型校验,过滤参数错误,为数字、字符串、布尔类型、时间类型硬编码制定差异化类型安全的表达式方法,杜绝了 SQL 注入的风险,能跑就安全;
-
映射数据库表像,DB 里面有数据表就能生成对应的 Golang 结构体;
-
用注释的形式描述查询的逻辑后,一键即可生成对应的安全可靠查询API。
此外还有一个好处是,我们用 GORM 来 Find 数据时,总还是要先声明结果,然后把指针传入 API,由 GORM 进行填充,而有了 Gen 之后,直接返回对应的数据结构,免于提前实例化数据后在注入API的繁琐。
复杂 SQL 怎么解
通过 interface 指明我们希望查询的语义,自动生成查询代码,这个可以说是 gorm-gen 最香的能力了。原因很简单:
- 根据表结构倒回来生成结构体,这件事情非常低频,大多数情况下我们是先有一个 Persistent Object,再去创建表;
- 类型安全,很重要,但对业务本身的能力上没有加成,也很难量化怎样算做的好,大家感触不深。
所以,大家最关心的能力还是,能不能我定义个接口,说清楚我需要什么数据(或者 sql 提供出来),你自己来生成查询代码,gorm 的封装,类型安全,数据转换等等,一切都由工具搞定,作为业务开发者,我只管调用从你生成的方法就行。能不能做到?
能!这就是 gorm-gen 带来的能力。这一节我们直接来实战演练一下。
本节实例源码在 db-demo 感兴趣的同学可以看一下 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.DB
var 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 User
type 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 bool, id int, col, name 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 Generating
2022/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.go
2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/users.gen.go
2022/08/18 17:12:01 generate query file: /Users/ag9920/go/src/github.com/ag9920/db-demo/gendemo/dal/query/gen.go
2022/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=@id
func (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,对外提供查询,更新能力。所以这里我们有两种方案:
- 调用 SetDefault 之后,直接引用两个对象对应的分别的 DAO:Passport 或 User。
- 通过 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 以及官方文档,相信对于 gorm-gen 熟练会帮助业务开发提效,安全。
感谢阅读!欢迎在评论区交流!
转载自:https://juejin.cn/post/7133150674400837668