本文最后更新于:2023年8月8日 晚上
描述 作为一个 gorm 的重度使用者,在习惯了 gorm 带来的便利,也遇到了很多的问题,日前看到一些小伙伴仍然会因为对 gorm 特性的不了解,导致出现 panic 或者数据的错误更新的问题,所以这边提供一些小技巧和避坑指南,持续更新…
插入 gorm 解决并发插入报错 当你有并发插入的情况,就难免会出现数据冲突的问题,如果不做处理的话,会出现报错 Duplicate key
,gorm 提供了中间件来解决这个问题。 如果你期望 upsert:
1 2 doc := &model.Table{Title: "table" } err := db.Table("table" ).Clauses(clause.OnConflict{UpdateAll: true }).Create(&doc).Error
你也可以根据实际使用场景选择其他不同的冲突处理方案:
1 2 3 4 5 6 7 8 9 type OnConflict struct { Columns []Column Where Where TargetWhere Where OnConstraint string DoNothing bool DoUpdates Set UpdateAll bool }
Clauses on conflict do nothing 带来的问题 如果你和我一样习惯用结构体指针插入,那么如果用 Clauses OnConflict do nothing 解决冲突的同时可能会带来另外一个问题,那就是数据不冲突时,insert id 会回写,但是冲突之后不会进行回写。 部分源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 if !db.DryRun && db.Error == nil { result, err := db.Statement.ConnPool.ExecContext(db.Statement.Context, db.Statement.SQL.String(), db.Statement.Vars...) if err == nil { db.RowsAffected, _ = result.RowsAffected() if db.RowsAffected > 0 { if db.Statement.Schema != nil && db.Statement.Schema.PrioritizedPrimaryField != nil && db.Statement.Schema.PrioritizedPrimaryField.HasDefaultValue { if insertID, err := result.LastInsertId(); err == nil && insertID > 0 { switch db.Statement.ReflectValue.Kind() { case reflect.Slice, reflect.Array: if config.LastInsertIDReversed { for i := db.Statement.ReflectValue.Len() - 1 ; i >= 0 ; i-- { rv := db.Statement.ReflectValue.Index(i) if reflect.Indirect(rv).Kind() != reflect.Struct { break } _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv) if isZero { db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID) insertID -= db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement } } } else { for i := 0 ; i < db.Statement.ReflectValue.Len(); i++ { rv := db.Statement.ReflectValue.Index(i) if reflect.Indirect(rv).Kind() != reflect.Struct { break } if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(rv); isZero { db.Statement.Schema.PrioritizedPrimaryField.Set(rv, insertID) insertID += db.Statement.Schema.PrioritizedPrimaryField.AutoIncrementIncrement } } } case reflect.Struct: if _, isZero := db.Statement.Schema.PrioritizedPrimaryField.ValueOf(db.Statement.ReflectValue); isZero { db.Statement.Schema.PrioritizedPrimaryField.Set(db.Statement.ReflectValue, insertID) } } } else { db.AddError(err) } } } } else { db.AddError(err) } }
大概意思就是,如果影响行数 > 1 才会进行 insert id 赋值,也就是说,如果此时直接使用 doc 中的 id,那么这个 id 的值会是 0。
批量 upsert id 错误 1 2 doc := []*model.Table{{Title: "table" ,Content:"xx" },{Title:"table1" ,Content:"yy" }} err := db.Table("table" ).Clauses(clause.OnConflict{UpdateAll: true }).Create(&doc).Error
Title 为唯一主键,如果数据库里已经存在了 title 为 table 的数据,那么 content 可以成功更新,响应行数也 >1 ,但是 id 的回写会从 lastInsertID 向下自增,也就是说,两个结构体回写的 id 都是错误的,这种情况直接使用,风险很大,可以考虑判断影响行数是否符合预期,不符合预期重新查询来解决这个问题。