Gorm 实践指南

作者: adm 分类: go 发布时间: 2022-04-08

默认关闭事务
GORM 默认的数据更新、创建都在事务中,如无必要,可以关闭默认的事务,获得更大的性能提升, 事务的全局性或者临时关闭,即使在关闭默认事务,仍然可以通过方法 Begin, Transactions 方法开启事务。

事务模板

// 开始事务
tx := db.Begin()

// 在事务中做一些数据库操作(从这一点使用'tx',而不是'db')
tx.Create(...)

// ...

// 发生错误时回滚事务
tx.Rollback()

// 或提交事务
tx.Commit()

具体例子

func CreateAnimals(db *gorm.DB) err {
tx := db.Begin()
// 注意,一旦你在一个事务中,使用tx作为数据库句柄

if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
tx.Rollback()
return err
}

if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
tx.Rollback()
return err
}

tx.Commit()
return nil
}

Prepared Statement 加速
Prepared Statement 加速 可以大幅度提升所有的 SQL 执行性能, GORM 支持自动的 Prepared Statement 缓冲,启用后,由 Gorm 生成的 SQL 或者 RAW SQL 都会进行预处理并缓存,Prepare Statement 可与数据库事务协同工作。

临时性开启

// 临时性开启,后续该 tx 的 SQL 执行都会使用 Prepared Statement 模式
tx := db.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)

具体例子

// 会将 SELECT * FROM `users` 缓存,建立 Prepared Statement
db.Find(&user)

tx1 := dbProxy.Session(&Session{PreparedStmt: true})
// 会将 SELECT * FROM `users` WHERE id = ? 缓存,生成 Prepared Statement
tx1.First(&user, 1)
// 会使用前面已经缓存的 SELECT * FROM `users`
tx1.Find(&users)
// 会建立 UPDATE users SET age = ? 的 Prepared Statement
tx1.Model(&user).Update("Age", 18)
全局模式
// 全局模式,所有的 DB 操作都会进行 Prepared Statement 缓存
dbProxy, err := gorm.POpenWithConfig("mysql", "XXXX_DSN", gorm.Config{
PrepareStmt: true,
})

具体例子
db, err := gorm.Open(..., gorm.Config{
PrepareStmt: true,
})

// 会将 SELECT * FROM `users` 缓存,建立 Prepared Statement
db.Find(&user)
嵌套事务问题
GORM 提供了嵌套事务的支持,通过 save point, rollback saved point 实现,例如:

DB.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)

tx.Transaction(func(tx2 *gorm.DB) error {
tx.Create(&user2)
return errors.New("rollback user2") // rollback user2
})

tx.Transaction(func(tx2 *gorm.DB) error {
tx.Create(&user3)
return nil
})

return nil // commit user1 and user3
})
如果最外侧的事务 rollback 后,所有事务将会被rollback

SELECT ... FOR UPDATE
select ...for update 支持

DB.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE
多个字段 in 查询
DB.Where("(body, subject) IN ?", [][]interface{}{
{"a", 1}, {"b", 2}, {"c", 3},
}).Find(&contents)

产生 SQL:

select * from contents where (body,subject) in (('a', 1), ('b',2), ('c',3));

字段多重权限问题 (只读/写/更新/创建/忽略)
GORM v2 版本中,加入了对字段的支持, 用来避免对一些数据进行误操作,权限级别一共分为:忽略, 只读,只更新,只创建 等:

type User struct {
Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写(创建和更新)
  Name string `gorm:"<-:false"`  // 允许读,禁止写
  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)
Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
  Name string `gorm:"-"`  // 读写操作均会忽略该字段
}

Timeout 参数
timeout Timeout for establishing connections, aka dia timeout
readTime TCP/IO read timeout
writeTime TCP IO write timeout
如果要 SQL 执行超时关闭, 可以使用 Context.WithTimeOut
查询到数据映射到 map[string]interface{}
gorm v2 当查询数据到 map 时, 需要指定 Model 方法,或者Table 方法以指定查询的表, map 类型只支持map[string]interface{}, map 类型也支持 slice, 例如 []map[string]interface{}{}

var results map[string]interface{}{}
DB.Table("users").Find(&results)

DB.Model(&User{}).Find(&results)

var results []map[string]interface{}{}
DB.Model("users").Find(&results)

批量查询处理数据
Gorm v2 可以使用 FIndInBatch 对大量数据进行批量查询批量处理, 但是要注意的是,查询不是一个事务,如果要做成食物,需要在外面写事务。

// 设定批量数量为 100,每次查询 100 条数据,处理完毕后处理下 100 条数据
result := DB.Where("processed = ?", false).FindInBatches(&results, 100,
 func(tx *gorm.DB, batch int) error {
    for _, result := range results {
      // 批量处理数据
    }

    // 批量更新数据,在使用 Save 处理批量数据时,会使用 Insert OnConflict DoNothing 模式
    tx.Save(&results)

    // 本批次包含数据量,如果本批次只有50条数据返回,则为50
    tx.RowsAffected

    batch // 这是第几批次的数据

    // 如果返回 error ,后续查询处理操作将停止
    return nil
 },
)

result.Error // 返回处理完所有批量数据时有无错误发生
result.RowsAffected // 返回所有批次被处理的数据总量
更新多条记录
// 根据 struct 更新
db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;

// 根据 map 更新
db.Table("users").Where("id IN ?", []int{10, 11}).Updates(map[string]interface{}{"name": "hello", "age": 18})
// UPDATE users SET name='hello', age=18 WHERE id IN (10, 11);
更新选定字段
如果您想要在更新时选定、忽略某些字段,您可以使用 Select、Omit

// 使用 Map 进行 Select
// User's ID is `111`:
db.Model(&user).Select("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello' WHERE id=111;

db.Model(&user).Omit("name").Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
// 使用 Struct 进行 Select(会 select 零值的字段)
db.Model(&user).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})
// UPDATE users SET name='new_name', age=0 WHERE id=111;

// Select 所有字段(查询包括零值字段的所有字段)
db.Model(&user).Select("*").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

// Select 除 Role 外的所有字段(包括零值字段的所有字段)
db.Model(&user).Select("*").Omit("Role").Update(User{Name: "jinzhu", Role: "admin", Age: 0})

更新 Hook
对于更新操作,GORM 支持 BeforeSave、BeforeUpdate、AfterSave、AfterUpdate 钩子,这些方法将在更新记录时被调用,详情请参阅 钩子

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}

更新记录数
获取受更新影响的行数

// 通过 `RowsAffected` 得到更新的记录数
result := db.Model(User{}).Where("role = ?", "admin").Updates(User{Name: "hello", Age: 18})
// UPDATE users SET name='hello', age=18 WHERE role = 'admin;

result.RowsAffected // 更新的记录数
result.Error        // 更新的错误

检查字段是否有变更
GORM 提供了 Changed 方法,它可以被用在 Before Update Hook 里,它会返回字段是否有变更的布尔值 Changed 方法只能与 Update、Updates 方法一起使用,并且它只是检查 Model 对象字段的值与 Update、Updates 的值是否相等,如果值有变更,且字段没有被忽略,则返回 true

func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
  // 如果 Role 字段有变更
    if tx.Statement.Changed("Role") {
    return errors.New("role not allowed to change")
    }

  if tx.Statement.Changed("Name", "Admin") { // 如果 Name 或 Role 字段有变更
    tx.Statement.SetColumn("Age", 18)
  }

  // 如果任意字段有变更
    if tx.Statement.Changed() {
        tx.Statement.SetColumn("RefreshedAt", time.Now())
    }
    return nil
}

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(map[string]interface{"name": "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(map[string]interface{
"name": "jinzhu2", "admin": false,
})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新

db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu2"})
// Changed("Name") => true
db.Model(&User{ID: 1, Name: "jinzhu"}).Updates(User{Name: "jinzhu"})
// Changed("Name") => false, 因为 `Name` 没有变更
db.Model(&User{ID: 1, Name: "jinzhu"}).Select("Admin").Updates(User{Name: "jinzhu2"})
// Changed("Name") => false, 因为 `Name` 没有被 Select 选中并更新

在更新时修改
这个场景常用于数据加密,解密 若要在 Before 钩子中改变要更新的值,如果它是一个完整的更新,可以使用 Save;否则,应该使用 SetColumn ,例如:

func (user *User) BeforeSave(tx *gorm.DB) (err error) {
if pw, err := bcrypt.GenerateFromPassword(user.Password, 0); err == nil {
tx.Statement.SetColumn("EncryptedPassword", pw)
}

if tx.Statement.Changed("Code") {
s.Age += 20
tx.Statement.SetColumn("Age", s.Age+20)
}
}

db.Model(&user).Update("Name", "jinzhu")

更新数据时多零值问题
在更新数据时,如果使用了 struct 来更新数据,默认只会更新非零值字段,如果使用map更新数据,则会更新全部字段,在使用 struct 更新时,也可以使用 Select 方法来选择想要更新的字段,在这种情况下,零值/非零值字段都会更新,例如

// UPDATE users SET name='new_name', age=0 WHERE id=111;
DB.Model(&result).Select("Name", "Age").Updates(User{Name: "new_name", Age: 0})

// UPDATE users SET name='hello', updated_at = '2013-11-17 21:34' WHERE id = 1
db.Model(&user).Updates(User{Name: "hello", Age: 0, Active: false})

Smart Select 功能
如果使用一个较小的 struct 查询时,将会自动添加较小 struct 的字段到查询的 Select 当中,来减少需查询的字段数量,因此对于 API 来说,可以定义一个较小对象来来减少不必要的字段查询,例如:

type User struct {
ID     uint
Name   string
Age    int
Gender string
// hundreds of fields
}

type APIUser struct {
ID   uint
Name string
}

// 在查询时自动选择 id, name 字段,并忽略其它的字段
db.Model(&User{}).Limit(10).Find(&APIUser{})
// SELECT `id`, `name` FROM `users` LIMIT 10
JSON 特殊字段支持
GORM对一些特殊字段进行了封装支持,可以参考data_type

type UserWithJSON struct {
gorm.Model
Name       string
Attributes datatypes.JSON
}

DB.Create(&User{
Name:       "json-1",
Attributes: datatypes.JSON([]byte(`{"name": "jinzhu", "age": 18, "tags": ["tag1", "tag2"], "orgs": {"orga": "orga"}}`)),
}

db.Find(&user, datatypes.JSONQuery("attributes").HasKey("role"))
db.Find(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!