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

16,266 阅读15分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 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 在于:

  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 感兴趣的同学可以看一下 gendemo 目录下的代码。

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

go get -u gorm.io/gen

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

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

image.png

  • 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连接关键字ANDOR,会将它们去除。
  • 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 文件夹

image.png

其中,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,对外提供查询,更新能力。所以这里我们有两种方案:

  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 以及官方文档,相信对于 gorm-gen 熟练会帮助业务开发提效,安全。

感谢阅读!欢迎在评论区交流!