前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Gorm 实践指南

Gorm 实践指南

作者头像
王小明_HIT
发布2021-07-05 16:38:04
2K0
发布2021-07-05 16:38:04
举报
文章被收录于专栏:程序员奇点程序员奇点

默认关闭事务

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

事务模板
代码语言:javascript
复制
// 开始事务
tx := db.Begin()

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

// ...

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

// 或提交事务
tx.Commit()
具体例子
代码语言:javascript
复制
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 可与数据库事务协同工作。

临时性开启
代码语言:javascript
复制
// 临时性开启,后续该 tx 的 SQL 执行都会使用 Prepared Statement 模式 
tx := db.Session(&Session{PrepareStmt: true})
tx.First(&user, 1)
tx.Find(&users)
tx.Model(&user).Update("Age", 18)
具体例子
代码语言:javascript
复制
// 会将 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)
全局模式
代码语言:javascript
复制
// 全局模式,所有的 DB 操作都会进行 Prepared Statement 缓存
dbProxy, err := gorm.POpenWithConfig("mysql", "XXXX_DSN", gorm.Config{
  PrepareStmt: true,
})
具体例子
代码语言:javascript
复制
db, err := gorm.Open(..., gorm.Config{
  PrepareStmt: true,
})

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

嵌套事务问题

GORM 提供了嵌套事务的支持,通过 save point, rollback saved point 实现,例如:

代码语言:javascript
复制
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 支持

代码语言:javascript
复制
DB.Clauses(clause.Locking{Strength: "UPDATE"}).Find(&users)
// SELECT * FROM `users` FOR UPDATE

多个字段 in 查询

代码语言:javascript
复制
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 版本中,加入了对字段的支持, 用来避免对一些数据进行误操作,权限级别一共分为:忽略, 只读,只更新,只创建 等:

代码语言:javascript
复制
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{}{}

代码语言:javascript
复制
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 对大量数据进行批量查询批量处理, 但是要注意的是,查询不是一个事务,如果要做成食物,需要在外面写事务。

代码语言:javascript
复制
// 设定批量数量为 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 // 返回所有批次被处理的数据总量

更新多条记录

代码语言:javascript
复制
// 根据 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

代码语言:javascript
复制
// 使用 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 钩子,这些方法将在更新记录时被调用,详情请参阅 钩子

代码语言:javascript
复制
func (u *User) BeforeUpdate(tx *gorm.DB) (err error) {
    if u.Role == "admin" {
        return errors.New("admin user not allowed to update")
    }
    return
}

更新记录数

获取受更新影响的行数

代码语言:javascript
复制
// 通过 `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

代码语言:javascript
复制
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 ,例如:

代码语言:javascript
复制
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 方法来选择想要更新的字段,在这种情况下,零值/非零值字段都会更新,例如

代码语言:javascript
复制
// 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 来说,可以定义一个较小对象来来减少不必要的字段查询,例如:

代码语言:javascript
复制
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

代码语言:javascript
复制
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"))
欢迎关注公众号:程序员开发者社区

微信号:程序员开发者社区

博客:CSDN 王小明

关注我们,了解更多

关注后:回复 “AI” 或者 “内推”, 有惊喜

参考资料

  • https://jasperxu.com/gorm-zh/crud.html
  • https://gorm.io/zh_CN/docs/update.html
  • https://gorm.io/zh_CN/docs/hooks.html
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-06-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员奇点 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 默认关闭事务
    • 事务模板
      • 具体例子
      • Prepared Statement 加速
        • 临时性开启
          • 具体例子
            • 全局模式
              • 具体例子
          • 嵌套事务问题
          • SELECT ... FOR UPDATE
          • 多个字段 in 查询
            • 字段多重权限问题 (只读/写/更新/创建/忽略)
            • Timeout 参数
            • 查询到数据映射到 map[string]interface{}
            • 批量查询处理数据
            • 更新多条记录
            • 更新选定字段
            • 更新 Hook
            • 更新记录数
            • 检查字段是否有变更
            • 在更新时修改
            • 更新数据时多零值问题
            • Smart Select 功能
            • JSON 特殊字段支持
              • 欢迎关注公众号:程序员开发者社区
              • 参考资料
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档