来源:juejin.cn/post/7133150674400837668
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[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 在于:
-
字段类型校验,过滤参数错误,为数字、字符串、布尔类型、时间类型硬编码制定差异化类型安全的表达式方法,杜绝了 SQL 注入的风险,能跑就安全;
-
映射数据库表像,DB 里面有数据表就能生成对应的 Golang 结构体;
-
用注释的形式描述查询的逻辑后,一键即可生成对应的安全可靠查询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.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
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) {
*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 以及官方文档[1],相信对于 gorm-gen 熟练会帮助业务开发提效,安全。
参考资料
[2]db-demo: https://github.com/ag9920/db-demo
程序员技术交流群
扫码进群记得备注:城市、昵称和技术方向。
文章评论