Gorm 实践指南
默认关闭事务
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"))