GORM Gen 使用指南
Gen 介紹
Gen 是由字節跳動無恆實驗室與 GORM 作者聯合研發的一個基於 GORM 的安全 ORM 框架,主要通過代碼生成方式實現 GORM 代碼封裝。
Gen 框架在 GORM 框架的基礎上提供了以下能力:
-
基於原始 SQL 語句生成可重用的 CRUD API
-
生成不使用
interface{}
的 100% 安全的 DAO API -
依據數據庫生成遵循 GORM 約定的結構體 Model
-
支持 GORM 的所有特性
簡單來說,使用 Gen 框架後我們無需手動定義結構體 Model,同時 Gen 框架也能幫我們生成類型安全的 CRUD 代碼。
更多詳細介紹請查看 Gen 官方文檔。
此外,Facebook 開源的 ent 也是社區中常用的類似框架,大家可按需選擇使用。
如何使用 Gen
Gen 框架的使用非常簡單,如果你熟悉 GORM 框架,那麼你可以通過以下教程快速上手。
安裝依賴
go get -u gorm.io/gen
快速指南
想要在項目中使用 Gen 框架,通常只需三步。本節將通過一個簡單示例快速帶大家熟悉 Gen 框架的使用。
首先,我們假設數據庫中已經有一張book
表,建表語句如下。
CREATE TABLE book
(
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`title` varchar(128) NOT NULL COMMENT '書籍名稱',
`author` varchar(128) NOT NULL COMMENT '作者',
`price` int NOT NULL DEFAULT '0' COMMENT '價格',
`publish_date` datetime COMMENT '出版日期',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='書籍表';
本教程演示的爲先有數據表的業務場景,通常這也是比較主流的工程實現流程。
定義 Gen 配置
配置即代碼。我們通常會在項目的cmd
目錄下定義好 Gen 框架生成代碼的配置。例如,我們的項目名稱爲gen_demo
,那麼我們就在gen_demo/cmd/gen/generate.go
文件。
package main
// gorm gen configure
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gen"
)
const MySQLDSN = "root:root1234@tcp(127.0.0.1:13306)/db2?charset=utf8mb4&parseTime=True"
func connectDB(dsn string) *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic(fmt.Errorf("connect db fail: %w", err))
}
return db
}
func main() {
// 指定生成代碼的具體相對目錄(相對當前文件),默認爲:./query
// 默認生成需要使用WithContext之後纔可以查詢的代碼,但可以通過設置gen.WithoutContext禁用該模式
g := gen.NewGenerator(gen.Config{
// 默認會在 OutPath 目錄生成CRUD代碼,並且同目錄下生成 model 包
// 所以OutPath最終package不能設置爲model,在有數據庫表同步的情況下會產生衝突
// 若一定要使用可以通過ModelPkgPath單獨指定model package的名稱
OutPath: "../../dal/query",
/* ModelPkgPath: "dal/model"*/
// gen.WithoutContext:禁用WithContext模式
// gen.WithDefaultQuery:生成一個全局Query對象Q
// gen.WithQueryInterface:生成Query接口
Mode: gen.WithDefaultQuery | gen.WithQueryInterface,
})
// 通常複用項目中已有的SQL連接配置db(*gorm.DB)
// 非必需,但如果需要複用連接時的gorm.Config或需要連接數據庫同步表信息則必須設置
g.UseDB(connectDB(MySQLDSN))
// 從連接的數據庫爲所有表生成Model結構體和CRUD代碼
// 也可以手動指定需要生成代碼的數據表
g.ApplyBasic(g.GenerateAllTable()...)
// 執行並生成代碼
g.Execute()
}
爲什麼要放到
cmd
目錄下?👉 Go 官方模塊佈局說明
生成代碼
進入項目下的cmd/gen
目錄下,執行以下命令。
go run generate.go
上述命令會在項目目錄下生成dal
目錄,其中dal/query
中是 CRUD 代碼,dal/model
下則是生成 Model 結構體。
├── cmd
│ └── gen
│ └── generate.go
├── dal
│ ├── model
│ │ └── book.gen.go
│ └── query
│ ├── book.gen.go
│ └── gen.go
├── go.mod
├── go.sum
└── main.go
我們可以在 dal 下新建db.go
文件,保存如下初始化數據庫連接的代碼。
package dal
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDB(dsn string) *gorm.DB {
db, err := gorm.Open(mysql.Open(dsn))
if err != nil {
panic(fmt.Errorf("connect db fail: %w", err))
}
return db
}
注意:通常不建議直接修改 Gen 框架生成的代碼。
使用生成的代碼
Gen 會生成基礎的查詢方法,並且綁定到結構體上,我們可以在項目中使用了它們。
package main
import (
"context"
"fmt"
"gen_demo/dal"
"gen_demo/dal/model"
"gen_demo/dal/query"
"time"
)
// gen demo
// MySQLDSN MySQL data source name
const MySQLDSN = "root:root1234@tcp(127.0.0.1:13306)/db2?charset=utf8mb4&parseTime=True"
func init() {
dal.DB = dal.ConnectDB(MySQLDSN).Debug()
}
func main() {
// 設置默認DB對象
query.SetDefault(dal.DB)
// 創建
b1 := model.Book{
Title: "《七米的Go語言之路》",
Author: "七米",
PublishDate: time.Date(2023, 11, 15, 0, 0, 0, 0, time.UTC),
Price: 100,
}
err := query.Book.WithContext(context.Background()).Create(&b1)
if err != nil {
fmt.Printf("create book fail, err:%v\n", err)
return
}
// 更新
ret, err := query.Book.WithContext(context.Background()).
Where(query.Book.ID.Eq(1)).
Update(query.Book.Price, 200)
if err != nil {
fmt.Printf("update book fail, err:%v\n", err)
return
}
fmt.Printf("RowsAffected:%v\n", ret.RowsAffected)
// 查詢
book, err := query.Book.WithContext(context.Background()).First()
// 也可以使用全局Q對象查詢
//book, err := query.Q.Book.WithContext(context.Background()).First()
if err != nil {
fmt.Printf("query book fail, err:%v\n", err)
return
}
fmt.Printf("book:%v\n", book)
// 刪除
ret, err = query.Book.WithContext(context.Background()).Where(query.Book.ID.Eq(1)).Delete()
if err != nil {
fmt.Printf("delete book fail, err:%v\n", err)
return
}
fmt.Printf("RowsAffected:%v\n", ret.RowsAffected)
}
通過上述教程,基本即可掌握 Gen 框架的基本使用,大家可點擊查看 Gen 官方最佳實踐示例代碼。
自定義 SQL 查詢
Gen 框架使用模板註釋的方法支持自定義 SQL 查詢,我們只需要按對應規則將 SQL 語句註釋到 interface 的方法上即可。Gen 將對其進行解析,併爲應用的結構生成查詢 API。
通常建議將自定義查詢方法添加到model
模塊下。
註釋語法
Gen 爲動態條件 SQL 支持提供了一些約定語法,分爲三個方面:
-
返回結果
-
模板佔位符
-
模板表達式
返回結果
示例
// dal/model/querier.go
package model
import "gorm.io/gen"
// 通過添加註釋生成自定義方法
type Querier interface {
// SELECT * FROM @@table WHERE id=@id
GetByID(id int) (gen.T, error) // 返回結構體和error
// GetByIDReturnMap 根據ID查詢返回map
//
// SELECT * FROM @@table WHERE id=@id
GetByIDReturnMap(id int) (gen.M, error) // 返回 map 和 error
// SELECT * FROM @@table WHERE author=@author
GetBooksByAuthor(author string) ([]*gen.T, error) // 返回數據切片和 error
}
在 Gen 配置處(cmd/gen/generate.go
)添加自定義方法綁定關係。
// 通過ApplyInterface添加爲book表添加自定義方法
g.ApplyInterface(func(model.Querier) {}, g.GenerateModel("book"))
重新生成代碼後,即可使用自定義方法。
// 使用自定義的GetBooksByAuthor方法
rets, err := query.Book.WithContext(context.Background()).GetBooksByAuthor("七米")
if err != nil {
fmt.Printf("GetBooksByAuthor fail, err:%v\n", err)
return
}
for i, b := range rets {
fmt.Printf("%d:%v\n", i, b)
}
模板佔位符
示例
// Filter 自定義Filter接口
type Filter interface {
// SELECT * FROM @@table WHERE @@column=@value
FilterWithColumn(column string, value string) (gen.T, error)
}
// 爲`Book`添加 `Filter`接口
g.ApplyInterface(func(model.Filter) {}, g.GenerateModel("book"))
模板表達式
Gen 爲動態條件 SQL 提供了強大的表達式支持,目前支持以下表達式:
-
if/else
-
where
-
set
-
for
示例
// Searcher 自定義接口
type Searcher interface {
// Search 根據指定條件查詢書籍
//
// SELECT * FROM book
// WHERE publish_date is not null
// {{if book != nil}}
// {{if book.ID > 0}}
// AND id = @book.ID
// {{else if book.Author != ""}}
// AND author=@book.Author
// {{end}}
// {{end}}
Search(book *gen.T) ([]*gen.T, error)
}
// 通過ApplyInterface添加爲book表添加Searcher接口
g.ApplyInterface(func(model.Searcher) {}, g.GenerateModel("book"))
重新生成代碼後,即可直接使用自定義的Search
方法進行查詢。
b := &model.Book{Author: "Q1mi"}
rets, err = query.Book.WithContext(context.Background()).Search(b)
if err != nil {
fmt.Printf("Search fail, err:%v\n", err)
return
}
for i, b := range rets {
fmt.Printf("%d:%v\n", i, b)
}
數據庫到結構體
Gen 支持根據 GORM 約定依據數據庫生成結構體,在之前的示例中我們已經使用過類似的代碼。
// 根據`users`表生成對應結構體`User`
g.GenerateModel("users")
// 基於`users`表生成名爲`Employee`的結構體
g.GenerateModelAs("users", "Employee")
// 在生成結構體時還可指定額外的生成選項
// gen.FieldIgnore("address"):忽略 address 字段
// gen.FieldType("id", "int64"):id字段使用 int64 類型
g.GenerateModel("users", gen.FieldIgnore("address"), gen.FieldType("id", "int64"))
// 爲連接的數據庫中的所有表生成對應結構體
g.GenerateAllTable()
方法模板
當從數據庫生成結構體時,還可以爲它們生成事先配置的模板方法,例如:
type CommonMethod struct {
ID int32
Name *string
}
func (m *CommonMethod) IsEmpty() bool {
if m == nil {
return true
}
return m.ID == 0
}
func (m *CommonMethod) GetName() string {
if m == nil || m.Name == nil {
return ""
}
return *m.Name
}
// 當生成 `People` 結構體時添加 IsEmpty 方法
g.GenerateModel("people", gen.WithMethod(CommonMethod{}.IsEmpty))
// 生成`User`結構體時添加 `CommonMethod` 的所有方法
g.GenerateModel("user", gen.WithMethod(CommonMethod{}))
最終將生成類下面的代碼。
// Generated Person struct
type Person struct {
// ...
}
func (m *Person) IsEmpty() bool {
if m == nil {
return true
}
return m.ID == 0
}
// Generated User struct
type User struct {
// ...
}
func (m *User) IsEmpty() bool {
if m == nil {
return true
}
return m.ID == 0
}
func (m *User) GetName() string {
if m == nil || m.Name == nil {
return ""
}
return *m.Name
}
數據映射
可以自行指定字段類型和數據庫列類型之間的數據類型映射。
在某些業務場景下,這個功能非常有用,例如,我們希望將數據庫中數字列在生成結構體時都定義爲int64
類型。
var dataMap = map[string]func(gorm.ColumnType) (dataType string){
// int mapping
"int": func(columnType gorm.ColumnType) (dataType string) {
if n, ok := columnType.Nullable(); ok && n {
return "*int32"
}
return "int32"
},
// bool mapping
"tinyint": func(columnType gorm.ColumnType) (dataType string) {
ct, _ := columnType.ColumnType()
if strings.HasPrefix(ct, "tinyint(1)") {
return "bool"
}
return "byte"
},
}
g.WithDataTypeMap(dataMap)
從 SQL 語句生成結構體
Gen 支持遵循 GORM 約定從 sql 生成結構體,具體用法如下。
package main
import (
"gorm.io/gen"
"gorm.io/gorm"
"gorm.io/rawsql"
)
func main() {
g := gen.NewGenerator(gen.Config{
OutPath: "../query",
Mode: gen.WithoutContext | gen.WithDefaultQuery | gen.WithQueryInterface, // generate mode
})
// https://github.com/go-gorm/rawsql/blob/master/tests/gen_test.go
gormdb, _ := gorm.Open(rawsql.New(rawsql.Config{
//SQL: rawsql, // create table sql
FilePath: []string{
//"./sql/user.sql", // create table sql file
"./test_sql", // create table sql file directory
},
}))
g.UseDB(gormdb) // reuse your gorm db
// Generate basic type-safe DAO API for struct `model.User` following conventions
g.ApplyBasic(
// Generate struct `User` based on table `users`
g.GenerateModel("users"),
// Generate struct `Employee` based on table `users`
g.GenerateModelAs("users", "Employee"),
)
g.ApplyBasic(
// Generate structs from all tables of current database
g.GenerateAllTable()...,
)
// Generate the code
g.Execute()
}
關於 Gen 框架的更多技巧,推薦查看官方文檔。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6RVvO8J7CqC3HtPejE8u7Q