0 前言
近期我们在分享有关于 golang-sql 系列专题,前两期分享内容前瞻:
从本期开始,我们正式步入 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"`
}
几类常用的标签及对应的用途展示如下表:
标签 | 作用 |
primarykey | 主键 |
unique_index | 唯一键 |
index | 键 |
auto_increment | 自增列 |
column | 列名 |
type | 列类型 |
default | 默认值 |
not null | 非空 |
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 框架的源码,解析其底层的技术实现原理.