gorm 框架使用教程

0 前言

從本期開始,我們正式步入 gorm 框架的領域.

gorm 是 golang 中最流行的 orm 框架,爲 go 語言使用者提供了簡便且豐富的數據庫操作 api.

有關 gorm 的分享話題會分爲實操篇和原理篇,本篇是其中的實操篇,旨在向大家詳細介紹 gorm 框架的使用方法.

gorm 本身也支持多種數據庫類型,在本文中,統一以 mysql 作爲操作的數據庫類型.

有關 gorm 的更多資訊:

1 數據庫

1.1 數據庫

本章中,我們重點向大家介紹如何通過 gorm 創建 mysql db 實例以及完成 db 配置:

對應流程示例如下:

package mysql


import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)


var (
    // 全局 db 模式
    db *gorm.DB
    // 單例工具
    dbOnce sync.Once
    // 連接 mysql 的 dsn
    dsn = "username:password@(ip:port)/database?timeout=5000ms&readTimeout=5000ms&writeTimeout=5000ms&charset=utf8mb4&parseTime=true&loc=Local"
)


func getDB()(*gorm.DB ,error){
    var err error
    dbOnce.Do(func(){
       // 創建 db 實例
       db, err = gorm.Open(mysql.Open(dsn),&gorm.Config{})
    })  
    return db,err
}

與 database/sql 中原生的 sql.DB 實例不同,在創建 gorm.DB 實例時,默認情況下會向數據庫服務端發起一次連接,以保證 dsn 的正確性.

另外想提的一個點是,在 gorm 體系之下,這個 DB 對象是絕對的核心,基本所有操作都是圍繞着這個 DB 實例展開的,後續大家也會看到大量通過使用 DB 進行鏈式調用的代碼風格,形如:

    db.Where(...).Order(...).WithContext(...).Find(...)

1.2 配置

在創建 gorm.DB 實例時,可以通過 gorm.Config 進行自定義配置,其中各配置項含義如下:

type Config struct {
    // gorm 中,針對單筆增、刪、改操作默認會啓用事務. 可以通過將該參數設置爲 true,禁用此機制
    SkipDefaultTransaction bool
    // 表、列的命名策略
    NamingStrategy schema.Namer
    // 自定義日誌模塊
    Logger logger.Interface
    // 自定義獲取當前時間的方法
    NowFunc func() time.Time
    // 是否啓用 prepare sql 模板緩存模式
    PrepareStmt bool
    // 在 gorm 創建 db 實例時,會創建 conn 並通過 ping 方法確認 dsn 的正確性. 倘若設置此參數,則會禁用 db 初始化時的 ping 操作
    DisableAutomaticPing bool
    // 不啓用遷移過程中的外聯鍵限制
    DisableForeignKeyConstraintWhenMigrating bool
    // 是否禁用嵌套事務
    DisableNestedTransaction bool
    // 是否允許全局更新操作. 即未使用 where 條件的情況下,對整張表的字段進行更新
    AllowGlobalUpdate bool
    // 執行 sql 查詢時使用全量字段
    QueryFields bool
    // 批量創建時,每個批次的數據量大小
    CreateBatchSize int
    // 條件創建器
    ClauseBuilders map[string]clause.ClauseBuilder
    // 數據庫連接池
    ConnPool ConnPool
    // 數據庫連接器
    Dialector
    // 插件集合
    Plugins map[string]Plugin
    // 回調鉤子
    callbacks  *callbacks
    // 全局緩存數據,如 stmt、schema 等內容
    cacheStore *sync.Map
}

2 模型

2.1 gorm.Model

在定義持久化模型 PO(persist object) 時,推薦組合使用 gorm.Model 中預定義的幾個通用字段,包括主鍵、增刪改時間等:

type PO struct {
    gorm.Model
}
package gorm
type Model struct {
    // 主鍵 id
    ID        uint `gorm:"primarykey"`
    // 創建時間
    CreatedAt time.Time
    // 更新時間
    UpdatedAt time.Time
    // 刪除時間
    DeletedAt DeletedAt `gorm:"index"`
}

值得一提的是,在 gorm 體系中,一個 po 模型只要啓用了 deletedAt 字段,則默認會開啓軟刪除機制:在執行刪除操作時,不會立刻物理刪除數據,而是僅僅將 po 的 deletedAt 字段置爲非空.

這裏暫且點到爲止,軟刪除的細節本文第 4 章中再作詳細展開.

2.2 標籤

下面我們介紹一下 po 模型中的常用標籤:

type PO struct{
   // 組合使用 gorm Model,引用 id、createdAt、updatedAt、deletedAt 等字段
   gorm.Model
  // 列名爲 name;列類型字符串;使用該列作爲唯一索引
   Name string `gorm:"column:name;type:varchar(15);unique_index"` 
   // 該列默認值爲 18
   Age int `gorm:"default:18"` 
   // 該列值不爲空
   Email string `gorm:"not null"` 
   // 該列的數值逐行遞增
   Num int `gorm:"auto_increment"` 
}

幾類常用的標籤及對應的用途展示如下表:

prZAGU

2.3 零值

在使用 po 模型時,可能會存在一個與零值有關的問題.

在 golang 中一些基礎類型都存在對應的零值,即便用戶未顯式給字段賦值,字段在初始化時也會首先賦上零值. 比如 bool 類型的零值爲 false;string 類型爲 "",int 類型爲 0.

這樣就會導致,在我們執行創建、更新等操作時,倘若 po 模型中存在零值字段,此時 gorm 無法區分到底是用戶顯式聲明的零值,還是未顯式聲明而被編譯器默認賦予的零值. 在無法區分的情況下,gorm 會統一按照後者,採取忽略處理的方式.

倘若此時我們想要明確是顯式將字段設置爲零值的,對應可以採取以下兩種處理方式:

我們將 age 字段類型設定爲 *int,只要指針非空,就代表使用方進行了顯式賦值.

type PO struct{
   gorm.Model
   Age *int `gorm:"column:age"` // 默認值爲 18
}

我們將 age 字段類型設定爲 sql.NullInt64,只要 Valid 標識爲 true,就代表使用方進行了顯式賦值.

type PO struct{
   gorm.Model
   Age sql.NullInt64 `gorm:"column:age"` // 默認值爲 18
}


type NullInt64 struct {
    Int64 int64
    Valid bool // Valid is true if Int64 is not NULL
}

2.4 時間 & 表情

在設置 dsn 時,建議添加上 parseTime=true 的設置,這樣能兼容支持將 mysql 中的時間解析到 golang 中的 time.Time 類型字段

在設定字符集時,建議使用 uft8mb4 替代 utf8,這樣能支持更廣泛的字符集,包括表情包等特殊字符的存儲

2.5 表名指定

在定義 PO 模型時,可以通過聲明 TableName 方法來指定其對應的表名:

func (p PO) TableName() string {
    return "po"
}

此外,也可以在操作 gorm.DB 實例時通過 Table 方法顯式指定表名:

    db = db.Table("po")

接下來我們按照 CRUD 的順序,分別介紹 gorm 體系下的四種操作類型:

3 創建

3.1 單筆創建

執行單筆記錄創建操作:

type PO struct{
   gorm.Model
   Age *int `gorm:"column:age"` // 默認值爲 18
}


func Test_db_create(t *testing.T) {
    // ...
    // 構造 po 實例,通過指針方式,實現將 age 零值存入數據庫(age 存在默認值爲 18)
    age := 0
    po := PO{
        Age: &age,
    }
    
    // 執行創建操作
    // INSERT INTO `po` (`age`) VALUES (0);
    resDB := db.WithContext(ctx).Create(&po)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 -> 1
    t.Logf("rows affected: %d", resDB.RowsAffected)
    // 結果輸出
    t.Logf("po: %+v", po)
}

3.2 批量創建

Create 方法同樣支持完成 po 的批量創建操作,示例如下:

func Test_db_batchCreate(t *testing.T) {
    // ...
    // 構造 po 列表 
    age1 := 20
    age2 := 21
    pos := []PO{
        {Age: &age1},
        {Age: &age2},
    }


    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()    
    
    // 批量創建
    // 批量創建時會根據 gorm.Config 中的 CreateBatchSize 進行分批創建操作
    resDB := db.WithContext(ctx).Table("po").Create(&pos)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 輸出影響行數 -> 2
    t.Logf("rows affected: %d", resDB.RowsAffected)


    // 打印各 po,輸出其主鍵
    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

另一種批量創建的方式是使用 CreateInBatches 方法,可以通過在入參中顯式指定單個批次創建的數據量上限:

func Test_db_batchCreate(t *testing.T) {
    // ...
    // 構造 po
    age1 := 20
    // ...
    age1000 := 21
    pos := []PO{
        {Age: &age1},
        // ...
        {Age: &age1000},
    }
    
    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()


    // 批量創建,在 createInBatch 方法中顯式指定了單個批次的數據上限 正好爲 pos 切片的長度
    resDB := db.WithContext(ctx).Table("po").CreateInBatches(&pos, len(pos))
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 -> len(p)
    t.Logf("rows affected: %d", resDB.RowsAffected)
    // 打印各 po,輸出其主鍵
    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

3.3 upsert

所謂 upsert,指的是數據如果不存在則創建,倘若存在,則按照預定義的策略執行更新操作.

可以通過 DB 的 Clauses 方法完成 upsert 的策略設定:

func Test_db_upsert(t *testing.T) {
    // ...
    pos := []PO{
        //...
    }
    
    // 批量插入,倘若發生衝突(id主鍵),則直接忽略執行該條記錄
    // INSERT INTO `po` ... ON DUPLICATE KEY UPDATE `id` = `id`
    resDB := db.WithContext(ctx).Clauses(
        clause.OnConflict{
            Columns:   []clause.Column{{Name: "id"}},
            DoNothing: true,
        },
    ).Create(&pos)
}
func Test_db_upsert(t *testing.T) {
    // ...
    pos := []PO{
        //...
    }


    // 批量插入,倘若發生衝突(id主鍵),則將 age 更新爲新值
    // INSERT INTO `po` ... ON DUPLICATE KEY UPDATE `age` = VALUES(age)
    resDB := db.WithContext(ctx).Clauses(
        clause.OnConflict{
            Columns:   []clause.Column{{Name: "id"}},
            DoUpdates: clause.AssignmentColumns([]string{"age"}),
        },
    ).Create(&pos)
}

4 刪除

4.1 單條刪除

刪除是一類比較敏感的操作,需要確保設置合適的限制條件,在沒有指定 where 條件時,需要確保顯式指定了 po 模型的主鍵:

func Test_db_delete(t *testing.T) {
    // ...
    // 構造 po
    po := PO{
        Model: gorm.Model{
            // 指定主鍵
            ID: 1,
        },
    }


    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()


    // 軟刪除
    // UPDATE `po` SET deleted_at = /* current unix second */ WHERE id = 1
    resDB := db.WithContext(ctx).Delete(&po)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> 1
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

4.2 批量刪除

通過設定 where 條件,可以執行批量刪除操作,代碼示例如下:

func Test_db_delete(t *testing.T) {
    // ...
    // 構造 po,未顯式指定 id
    po := PO{}


    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()


    // 批量軟刪除所有 age > 10 的記錄
    // UPDATE `po` SET deleted_at = /* current unix second */ WHERE age > 10
    resDB := db.WithContext(ctx).Where("age > ?", 10).Delete(&po)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> x
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

4.3 軟刪除

在 po 模型中,倘若使用 gorm.Model 啓用了 DeletedAt 字段的話,會啓用軟刪除機制.

type PO struct {
    gorm.Model
    Age *int `gorm:"column:age"` // 默認值爲 18
}


type Model struct {
    ID        uint `gorm:"primarykey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    // 刪除鍵,啓用軟刪除機制
    DeletedAt DeletedAt `gorm:"index"`
}

軟刪除模式下,Delete 方法只會把 DeletedAt 字段置爲非空,設爲刪除時的時間戳.

func Test_db_delete(t *testing.T) {
    // ...
    // 軟刪除
    // UPDATE `po` SET deleted_at = /* current unix second */ WHERE ...
    db.Delete(&po)
    // ...
}

後續在查詢和更新操作時,默認都會帶上【 WHERE deleted_at IS NULL】的條件,保證這部分軟刪除的數據是不可見的.

func Test_db_query(t *testing.T) {
    // ...
    // 正常查詢無法獲取到軟刪除的數據
    // SELECT * FROM `po` WHERE id = 1 AND deleted_at IS NULL 
    db.WHERE("id = ?",1).Find(&po)       
}

倘若想要獲取到這部分軟刪除狀態的數據,可以在查詢時帶上 Unscope 標識

func Test_db_unscopeQuery(t *testing.T) {   
    // 允許查詢到軟刪除的數據
    // SELECT * FROM `po` WHERE id = 1
    db.Unscope().WHERE("id = ?",1).Find(&po)
    // ...
}

4.4 物理刪除

在 po 模型中未啓用 deletedAt 字段時,執行的 Delete 操作都是物理刪除.

在啓用 deletedAt 字段時,可以通過帶上 unscope 標識,來強制執行物理刪除操作

func Test_db_unscopeDelete(t *testing.T) {
    // ...
    // 硬刪除
    // DELETE FROM `po` WHERE id = 1 
    db.Unscope().Delete(&po)
    // ...
}

5 更新

更新操作其實又分爲增量更新(PATCH)和全量保存(PUT)的語義,前者對應的是 DB 的 Updates 方法,後者對應的是 DB 的 Save 方法.

5.1 批量更新

在 updates 時,只會在原數據記錄的基礎上,增量更新用戶顯式聲明部分的字段:

func Test_db_update(t *testing.T) {
    // ...
    age := 0
    name := ""
    // 批量更新 po 中顯式聲明的字段,未顯式指定 where 條件,會報錯 gorm.ErrMissingWhereClause
    // UPDATE `po` SET age = 0, name = ""
    resDB := db.WithContext(ctx).Updates(&PO{
        Age:  &age,
        Name: &name,
    })
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> x
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

在沒有限定 where 條件的情況下,支持 updates 操作是非常危險的,這意味着會對整張表執行更新操作,因此默認情況下 gorm 會限制這種行爲. 倘若用戶希望這種操作能夠得到允許,則可以採取如下兩種方式:

方式 II 的示例代碼如下:

func Test_db_update(t *testing.T) {
    // ...
    // 開啓一個會話,將全局更新配置設爲 true
    dbSession := db.Session(&gorm.Session{
        AllowGlobalUpdate: true,
    })


    age := 0
    name := ""
    // 全局更新 age 和 name 字段
    // UPDATE `po` SET age = 0, name = ""
    resDB := dbSession.WithContext(ctx).Updates(&PO{
        Age:  &age,
        Name: &name,
    })
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> x
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

常規的更新操作是通過 where 進行條件限制:

func Test_db_update(t *testing.T) {
    // ...


    age := 0
    name := ""
    // 批量更新,po 中所有顯式聲明的字段
    // UPDATE `po` SET age = 0, name = "" WHERE age > 10
    resDB := db.WithContext(ctx).Where("age > ?", 10).Updates(&PO{
        Age:  &age,
        Name: &name,
    })
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> x
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

更新時支持通過 Select 或者 Omit 語句,來選定或者忽略指定的列:

    // 限定只更新 age 字段
    // UPDATE `po` SET age = 0 WHERE age > 10
    resDB := db.WithContext(ctx).Where("age > ?", 10).Select("age").Updates(&PO{
        Age:  &age,
        Name: &name,
    })
    // 限定更新時忽略 age 字段
    // UPDATE `po` SET name = "" WHERE age > 10
    resDB := db.WithContext(ctx).Where("age > ?", 10).Omit("age").Updates(&PO{
        Age:  &age,
        Name: &name,
    })

5.2 表達式更新

更新時,還可以通過表達式執行 sql 更新操作,比如把年齡放大兩倍再加一:

func Test_db_update(t *testing.T) {
    // ...


    // UPDATE `po` SET age = age * 2 + 1 WHERE id = 1 
    resDB := db.WithContext(ctx).Table("po").Where("id = ?", 1).UpdateColumn("age", gorm.Expr("age * ? + ?", 2, 1))
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    // 影響行數 —> 1
    t.Logf("rows affected: %d", resDB.RowsAffected)
}

5.3 json 列更新

在 mysql 中有一種特殊的列類型——json. 針對 json 類型的列執行更新操作時,可以使用 gorm.io/datatypes lib 包中封裝的相關方法:

import(
    "gorm.io/datatypes"
)

func Test_db_updateJSON(t *testing.T) {
    // 對 extra json 字段新增一組 kv 對
    // UPDATE `po` SET extra = json_insert(extra,"$.key","value") WHERE id = 1
    resDB := db.Where("id = ?", 1).UpdateColumn("extra", datatypes.JSONSet("extra").Set("key", "value"))
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }
}

5.4 批量保存

DB 中的 Save 方法對應的是全量保存的語義,指的是會對整個 po 模型的數據進行溢寫存儲,即便其中有些未顯式聲明的字段,也會被更新爲零值.

基於此,Save 方法需要慎用,通常是先通過 query 方法查到數據並進行少量字段更新後,再調用 Save 方法進行保存,以保證 po 實例是擁有完整數據的:

func Test_db_save(t *testing.T) {
    // ...
    // 首先查出對應的數據
    pos := []PO{
        {Model: gorm.Model{ID: 1}},
        {Model: gorm.Model{ID: 2}},
    }
    ctxDB := db.WithContext(ctx)
    if err := ctxDB.Scan(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    // 更新數據
    for _, po := range pos {
        *po.Age += 100
    }


    // 將更新後的數據存儲到數據庫
    if err := ctxDB.Save(&pos); err != nil {
        t.Error(err)
        return
    }
}

6 查詢

6.1 單筆查詢

gorm 中,First、Last、Take、Find 方法都可以用於查詢單條記錄. 前三個方法的特點是,倘若未查詢到指定記錄,則會報錯 gorm.ErrRecordNotFound;最後一個方法的語義更軟一些,即便沒有查到指定記錄,也不會返回錯誤.

下面針對這四種方法逐一進行案例展示:

返回滿足條件的第一條數據記錄,指的是主鍵最小的記錄

func Test_query(t *testing.T) {
    // ...
    // 查詢到第一條記錄返回. 由於 where 條件缺省,則會取主鍵最小的 記錄
    var po PO
    // SELECT * FROM `po` WHERE deleted_at IS NULL ORDER BY id ASC LIMIT 1
    if err := db.WithContext(ctx).First(&po).Error; err != nil {
        t.Error(err)
        return
    }


    t.Logf("po: %+v", po)
}

返回滿足條件的最後一條數據記錄,指的是主鍵最大的記錄

func Test_query(t *testing.T) {
    // ...


    // 取 age > 10 的記錄中主鍵最大的記錄
    var po PO
    // SELECT * FROM `po` WHERE age > 10 AND deleted_at IS NULL ORDER BY id DESC imit 1 
    if err := db.WithContext(ctx).Where("age > ?",10).Last(&po).Error; err != nil {
        t.Error(err)
        return
    }


    t.Logf("po: %+v", po)
}

從滿足條件的數據記錄中隨機返回一條:

func Test_query(t *testing.T) {
    // ...


    // 取 id < 10 的記錄中隨機一條記錄返回
    var po PO
    // SELECT * FROM `po` WHERE id < 10  AND deleted_at IS NULL LIMIT 1
    if err := db.WithContext(ctx).Where("id < ?",10).Take(&po).Error; err != nil {
        t.Error(err)
        return
    }


    t.Logf("po: %+v", po)
}

從滿足條件的數據記錄中隨機返回一條,即便沒有找到記錄,也不會拋出錯誤

func Test_query(t *testing.T) {
    // ...
    // 通過 find 檢索記錄,找不到滿足條件的記錄時,也不會返回錯誤
    var po PO
    // SELECT * FROM `po` WHERE id = 999 AND deleted_at IS NULL 
    if err := db.WithContext(ctx).Where("id = ?",999).Find(&po).Error; err != nil {
        t.Error(err)
        return
    }


    // po 裏的數據可能爲空
    t.Logf("po: %+v", po)
}

查詢時可以通過 Select 方法聲明只返回特定的列:

func Test_query(t *testing.T) {
    // ...
    // 只返回 age 列的數據
    var po PO
    // SELECT age FROM `po` WHERE id = 999 AND deleted_at IS NULL ORDER BY id ASC limit 1
    if err := db.WithContext(ctx).Select("age").Where("id = ?",999).First(&po).Error; err != nil {
        t.Error(err)
        return
    }


    // po 裏只有 age 字段有數據
    t.Logf("po: %+v", po)
}

6.2 批量查詢

Find 方法還可以應用於批量查詢:

func Test_batchQuery(t *testing.T) {
    // ...
    var pos []PO
    // SELECT * FROM `po` WHERE age > 1 AND deleted_at IS NULL 
    if err := db.WithContext(ctx).Where("age > ?", 1).Find(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

此外,還可以使用 Scan 方法執行批量查詢,Scan 與 Find 的區別在於,使用時必須顯式指定表名:

func Test_batchQuery(t *testing.T) {
    // ...
    var pos []PO
    // SELECT * FROM `po` WHERE age > 1 AND deleted_at IS NULL  
    if err := db.WithContext(ctx).Table("po").Where("age > ?", 1).Scan(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

此外,還可以通過 Pluck 方法實現批量查詢指定列的操作:

func Test_query(t *testing.T) {
    // ...


    var ages []int64
    // SELECT age from `po` WHERE age > 1 AND deleted_at IS NULL 
    if err := db.WithContext(ctx).Table("po").Where("age > ?", 1).Pluck("age", &ages).Error; err != nil {
        t.Error(err)
        return
    }


    t.Logf("ages: %+v", ages)
}

6.3 條件查詢

限定條件時,可以通過 Where 鏈式調用的方式實現 "AND" 的語義,也可以通過 Or 方法實現 "OR" 的語義:

   // WHERE age = 1 AND name = 'xu'
   db.Where("age = 1").Where("name = ?",xu)
   // WHERE age = 1 OR name = 'xu'
   db.Where("age = 1").Or("name = ?","xu")

嵌套的條件也是可以支持的:

   // WHERE (age = 1 AND name = 'xu') OR (age = 2 AND name  = 'x')
   db.Where(db.Where("age = 1").Where("name = ?","xu")).Or(db.Where("age = 2").Where("name = ?","x"))

在 where 條件中結合對 json 列的使用也是可以支持的:

func Test_jsonQuery(t *testing.T) {
    // ...
  
    var pos []PO
    // SELECT * FROM `po` WHERE json_extract("extra","$.key") = "value" AND deleted_at IS NULL 
    if err := db.WithContext(ctx).Table("po").Where(datatypes.JSONQuery("extra").Equals("value", "key")).Find(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}
func Test_jsonQuery(t *testing.T) {
    // ...


    var pos []PO
    // SELECT * FROM `po` WHERE json_extract("extra","$.key") IS NOT NULL AND deleted_at IS NULL 
    if err := db.WithContext(ctx).Table("po").Where(datatypes.JSONQuery("extra").HasKey("key")).Find(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

6.4 數量統計

可以通過 DB.Count 方法實現數量統計操作:

func Test_Count(t *testing.T) {
    // ...


    var cnt int64
    // SELECT COUNT(*) FROM `po` WHERE age > 10 AND deleted_at IS NULL
    if err := db.WithContext(ctx).Table("po").Where("age > ?", 10).Count(&cnt); err != nil {
        t.Error(err)
        return
    }
    
    t.Logf("cnt: %d", cnt)
}

6.5 分組求和

對應於 group 分組操作可以通過 DB.Group 方法實現,分組之後的 Sum、Max、Avg 等聚合函數都可以通過 Select 方法進行聲明. 此處給出對應於 Sum 函數的使用示例:

type UserRecord struct {
    UserID int64 `gorm:"int64"`
    Amount int64 `gorm:"amount"`
}


func Test_sumGroup(t *testing.T) {
    // ...
    var groups []UserRecord
    // SELECT user_id, sum(amount) AS amount FROM `user_record` WHERE id < 100 AND deleted_at IS NULL GROUP BY user_id
    resDB := db.WithContext(ctx).Table("user_record").Select("user_id", "sum(amount) AS amount").
        Where("id < ?", 100).Group("user_id").Scan(&groups)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }


    for _, group := range groups {
        t.Logf("group: %+v\n", group)
    }
}

6.6 子查詢

對應於子查詢操作的使用示例:

func Test_subQuery(t *testing.T) {
    db, _ := getDB()
    ctx := context.Background()


    // UPDATE `user_record` SET amount = (SELECT amount FROM `user_record` WHERE user_id = 1000 ORDER BY id DESC limit 1) WHERE user_id = 100 
    subQuery := db.Table("user_record").Select("amount").Where("user_id = ?", 1000)
    
    resDB := db.WithContext(ctx).Table("user_record").Where("user_id = ?", 100).UpdateColumn("amount", subQuery)
    if resDB.Error != nil {
        t.Error(resDB.Error)
        return
    }
}

6.7 排序偏移

在批量查詢的場景中,通常還會存在排序和偏移的需求:

func Test_orderLimit(t *testing.T) {
    db, _ := getDB()
    ctx := context.Background()


    var pos []PO
    // SELECT * FROM `po` WHERE id > 10 AND deleted_at is NULL ORDER BY age DESC LIMIT 2 OFFSET 10
    if err := db.WithContext(ctx).Table("po").Where("id > ?", 10).Order("age DESC").Limit(2).Offset(10).Scan(&pos).Error; err != nil {
        t.Error(err)
        return
    }


    for _, po := range pos {
        t.Logf("po: %+v\n", po)
    }
}

7 事務

本章介紹一下如何基於 gorm DB 實現事務和寫鎖操作:

7.1 事務

使用事務的流程:

func Test_tx(t *testing.T) {
    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
  
    // 需要包含在事務中執行的閉包函數
    do := func(tx *gorm.DB) error {
        // do something ...
        return nil
    }


    // 開啓事務
    // BEGIN
    // OPERATE...
    // COMMIT/ROLLBACK
    if err := db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // do some preprocess ...
        // do ...
        err := do()
        // do some postprocess ...
        return err
    }); err != nil {
        t.Error(err)
    }
}

7.2 寫鎖

在事務中,針對某條記錄可以通過 select for update 的方式進行加持寫鎖的操作:

func Test_tx(t *testing.T) {
    // 超時控制
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
  
    // 需要包含在事務中執行的閉包函數
    do := func(ctx context.Context, tx *gorm.DB, po *PO) error {
        // do something ...
        return nil
    }


    // BEGIN 
    // SELECT * FROM po WHERE id = 1 AND deleted_at IS NULL ORDER BY id ASC limit 1 FOR UPDATE
    // OPERATE ....
    // COMMIT/ROLLBACK
    // 開啓事務
    db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        // 針對一條 po 記錄加寫鎖
        var po PO
        if err := tx.Set("gorm: query option", "FOR UPDATE").Where("id = ?", 1).First(&po).Error; err != nil {
            return err
        }
        
        // 執行業務邏輯
        return do(ctx, tx, &po)
    })
}

8 回調

在定義 po 模型時,可以遵循 gorm 中預留的接口協議,聲明指定的回調方法,這樣能在特定操作執行前後執行用戶預期的回調邏輯:

在 gorm 中預定義好的各個回調接口協議如下:

// 創建操作前回調
type BeforeCreateInterface interface {
    BeforeCreate(*gorm.DB) error
}


// 創建操作後回調
type AfterCreateInterface interface {
    AfterCreate(*gorm.DB) error
}


// 更新操作前回調
type BeforeUpdateInterface interface {
    BeforeUpdate(*gorm.DB) error
}


// 更新操作後回調
type AfterUpdateInterface interface {
    AfterUpdate(*gorm.DB) error
}


// 保存操作前回調
type BeforeSaveInterface interface {
    BeforeSave(*gorm.DB) error
}


// 保存操作後回調
type AfterSaveInterface interface {
    AfterSave(*gorm.DB) error
}


// 刪除操作前回調
type BeforeDeleteInterface interface {
    BeforeDelete(*gorm.DB) error
}


// 刪除操作後回調
type AfterDeleteInterface interface {
    AfterDelete(*gorm.DB) error
}


// find 操作後回調
type AfterFindInterface interface {
    AfterFind(*gorm.DB) error
}

9 總結

本期和大家一起分享了 go 語言最常用 orm 框架——gorm 的使用教程,下期我們將和大家一起深入到 gorm 框架的源碼,解析其底層的技術實現原理.

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/plzG1mCK8yZwVQOSKZi2XQ