GORM 強大的代碼生成工具 — gorm-gen
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
#!/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 以及官方文檔 [1],相信對於 gorm-gen 熟練會幫助業務開發提效,安全。
參考資料
[1]
gen: https://github.com/go-gorm/gen
[2]
db-demo: https://github.com/ag9920/db-demo
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/z9O-E_5jCOx50FLWZbFTLw