gorm 框架使用教程
0 前言
從本期開始,我們正式步入 gorm 框架的領域.
gorm 是 golang 中最流行的 orm 框架,爲 go 語言使用者提供了簡便且豐富的數據庫操作 api.
有關 gorm 的分享話題會分爲實操篇和原理篇,本篇是其中的實操篇,旨在向大家詳細介紹 gorm 框架的使用方法.
gorm 本身也支持多種數據庫類型,在本文中,統一以 mysql 作爲操作的數據庫類型.
有關 gorm 的更多資訊:
-
開源地址:https://github.com/go-gorm/gorm
-
中文教程:https://gorm.io/zh_CN/docs/
1 數據庫
1.1 數據庫
本章中,我們重點向大家介紹如何通過 gorm 創建 mysql db 實例以及完成 db 配置:
-
設置好連接 mysql 的 dsn(data source name)
-
通過 gorm.Config 完成 db 有關的自定義配置
-
通過 gorm.Open 方法完成 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"`
}
幾類常用的標籤及對應的用途展示如下表:
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
}
- 使用 sql.Nullxx 類型:
我們將 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 單筆創建
執行單筆記錄創建操作:
-
創建 po 實例
-
po 實例的 age 字段通過 *int 方式規避零值問題
-
po 模型已聲明瞭 TableName 方法,用於關聯數據表
-
鏈式操作 DB,Create 方法傳入 po 指針,完成創建
-
通過 DB.Error 接收返回的錯誤
-
通過 DB.RowsAffected 獲取影響的行數
-
由於傳入的 po 爲指針,創建完成後,po 實例會更新主鍵信息
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 的策略設定:
- 策略 I:倘若衝突,則忽略
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)
}
- 策略 II:倘若衝突,則更新指定字段
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 模型的主鍵:
-
創建 po 模型,設置主鍵值
-
執行 Delete 方法,傳入 po 實例指針
-
由於 po 模型存在 deletedAt 字段,所以採取的是軟刪除操作
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 時,只會在原數據記錄的基礎上,增量更新用戶顯式聲明部分的字段:
-
在 po 模型中,通過指針的方式,標識字段 age 和 name 被顯式賦予了零值
-
調用 updates 方法,更新 age 和 name 列
-
本次 updates 操作會失敗,因爲沒有通過 where 限定條件,最終拋出 gorm.ErrMissingWhereClause 的錯誤
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 會限制這種行爲. 倘若用戶希望這種操作能夠得到允許,則可以採取如下兩種方式:
-
方式 I:在 gorm.Config 中將 AllowGlobalUpdate 參數設爲 true
-
方式 II:開啓一個 session 會話,臨時將 AllowGlobalUpdate 參數設爲 true(比較推薦,更能顯式突出這次操作的特殊性)
方式 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;最後一個方法的語義更軟一些,即便沒有查到指定記錄,也不會返回錯誤.
下面針對這四種方法逐一進行案例展示:
- First:
返回滿足條件的第一條數據記錄,指的是主鍵最小的記錄
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)
}
- Last
返回滿足條件的最後一條數據記錄,指的是主鍵最大的記錄
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)
}
- Take
從滿足條件的數據記錄中隨機返回一條:
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)
}
- Find
從滿足條件的數據記錄中隨機返回一條,即便沒有找到記錄,也不會拋出錯誤
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 列的使用也是可以支持的:
- 案例 I:json 列存在指定 kv 對
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)
}
}
- 案例 II:json 列存在指定 key
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 事務
使用事務的流程:
-
調用 db.Transaction 方法開啓事務
-
在 Transaction 中可以通過閉包函數執行事務邏輯,其中所有事務操作都需要圍繞着 tx *gorm.DB 實例展開
-
在閉包函數中,一旦返回 error 或者發生 panic,gorm 會自動執行回滾操作;倘若返回的 error 爲 nil,gorm 會自動執行提交操作
-
使用方也可以根據自己的需要,調用 tx.Rollback 和 tx.Commit 方法提前執行回滾或提交操作
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