GORM Gen 使用指南

Gen 介紹

Gen 是由字節跳動無恆實驗室與 GORM 作者聯合研發的一個基於 GORM 的安全 ORM 框架,主要通過代碼生成方式實現 GORM 代碼封裝。

Gen 框架在 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 支持提供了一些約定語法,分爲三個方面:

返回結果

rOfLWs

示例

// 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)
}
模板佔位符

WpUGrQ

示例

// 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 提供了強大的表達式支持,目前支持以下表達式:

示例

// 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