Simple Reproduction
Full source: https://github.com/superhawk610/gorm-updates-bug/blob/main/main.go
Here's the important bits:
// this works fine...
jsonUpdates := &map[string]interface{}{"name": "Another List"}
if err := db.Model(&list).Updates(jsonUpdates).Error; err != nil {
panic(err)
}
// ...but this will panic with "reflect: call of reflect.Value.Field on string Value"
updates := &UpdateList{Name: "A Third List"}
if err := db.Model(&list).Updates(updates).Error; err != nil {
panic(err)
}
Description
In the docs, it states that updates can be performed with a custom struct or a map[string]interface{}. However, when using a struct, I'm seeing the following error:
panic: reflect: call of reflect.Value.Field on string Value
goroutine 1 [running]:
reflect.Value.Field(0x4346a40, 0xc00009a4a0, 0x198, 0x0, 0x4346a40, 0xc00009a4a0, 0x198)
/usr/local/go/src/reflect/value.go:850 +0x125
gorm.io/gorm/schema.(*Field).setupValuerAndSetter.func2(0x435d920, 0xc00009a4a0, 0x199, 0x2, 0x45496a0, 0xc00009a500)
/Users/superhawk610/go/pkg/mod/gorm.io/gorm@v1.20.11/schema/field.go:399 +0xd1
gorm.io/gorm/callbacks.ConvertToAssignments(0xc0000bad00, 0x43d9180, 0xc000098a00, 0xc000098a00)
/Users/superhawk610/go/pkg/mod/gorm.io/gorm@v1.20.11/callbacks/update.go:230 +0x1ff0
gorm.io/gorm/callbacks.Update(0xc000092bd0)
/Users/superhawk610/go/pkg/mod/gorm.io/gorm@v1.20.11/callbacks/update.go:64 +0x1ba
gorm.io/gorm.(*processor).Execute(0xc00006f580, 0xc000092bd0)
/Users/superhawk610/go/pkg/mod/gorm.io/gorm@v1.20.11/callbacks.go:105 +0x23d
gorm.io/gorm.(*DB).Updates(0xc000092bd0, 0x433e420, 0xc00009a4a0, 0xc0000925a0)
/Users/superhawk610/go/pkg/mod/gorm.io/gorm@v1.20.11/finisher_api.go:323 +0xa7
main.main()
/Users/superhawk610/code/gorm-updates-bug/main.go:41 +0x414
exit status2
When performing the same change with a map[string]interface{}, the panic doesn't occur. I've found a couple instances where this same panic was observed, but it doesn't look like they were ever resolved: link 1, link 2.
Comment From: doppl-neal
@superhawk610 when you are using a map, GORM only checks to make sure those fields exist in the struct you are updating.
However, when you are using => db.Model(&list).Updates(updates), you have 2 different types of structs list and updatelist. As far as I understand, you cannot do that.
Your 2 structs must be the same type.
Comment From: superhawk610
@doppl-neal I encountered this while following a guide from earlier last year, but checking around it seems that multiple recent guides recommend using a separate struct to handle updates (see here and here).
If using a different struct for updates isn't allowed, is there a recommended pattern for allowing updates only to certain fields? IMO any struct should work, as long as it can be serialized to the corresponding map[string]interface{} equivalent.
If nothing else, a more helpful error message would be nice.
Comment From: doppl-neal
@superhawk610 interesting. In the documents for GORM, for updating - https://gorm.io/docs/update.html
// Update attributes with struct, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
which is the same struct type.
However, as long as your struct you want to update has the primary key set, you can just provice one struct... repo..Debug().Updates(updateDataStruct)
Comment From: meddario
@doppl-neal @superhawk610 sorry for the (maybe) silly question, but what's the solution here?
I have a model with six fields and a struct with just three of those six fields (and this is by design, the smaller struct is to receive data from an HTTP request, which is not allowed to set all the fields). I want to use the smaller struct to update only those three fields (as described here https://gorm.io/docs/update.html#Updates-multiple-columns).
Calling models.DB.Model(&instance).Updates(input) , where instance is populated by models.DB.Where("id = ?", c.Param("id")).First(&instance) and input is an instance of the 3-field structure results in the error:
reflect: call of reflect.Value.Field on string Value
/usr/local/go/src/reflect/value.go:850 (0x50b824)
Value.Field: panic(&ValueError{"reflect.Value.Field", v.kind()})
/go/pkg/mod/gorm.io/gorm@v1.21.10/schema/field.go:413 (0x6ab793)
(*Field).setupValuerAndSetter.func2: fieldValue := reflect.Indirect(value).Field(field.StructField.Index[0]).Field(field.StructField.Index[1])
/go/pkg/mod/gorm.io/gorm@v1.21.10/callbacks/update.go:230 (0x78782d)
ConvertToAssignments: value, isZero := field.ValueOf(updatingValue)
/go/pkg/mod/gorm.io/gorm@v1.21.10/callbacks/update.go:64 (0x784747)
Update: if set := ConvertToAssignments(db.Statement); len(set) != 0 {
/go/pkg/mod/gorm.io/gorm@v1.21.10/callbacks.go:130 (0x6e69b5)
(*processor).Execute: f(db)
/go/pkg/mod/gorm.io/gorm@v1.21.10/finisher_api.go:338 (0x6f1947)
(*DB).Updates: return tx.callbacks.Update().Execute(tx)
/workspaces/healthchecks-clone/controllers/projects.go:72 (0xb851f8)
UpdateProject: models.DB.Model(&project).Updates(input)
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0xb61a2e)
(*Context).Next: c.handlers[c.index](c)
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/recovery.go:99 (0xb80504)
CustomRecoveryWithWriter.func1: c.Next()
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0xb61a2e)
(*Context).Next: c.handlers[c.index](c)
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/logger.go:241 (0xb7ee84)
LoggerWithConfig.func1: c.Next()
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/context.go:165 (0xb61a2e)
(*Context).Next: c.handlers[c.index](c)
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/gin.go:489 (0xb702b3)
(*Engine).handleHTTPRequest: c.Next()
/go/pkg/mod/github.com/gin-gonic/gin@v1.7.2/gin.go:445 (0xb6fd9e)
(*Engine).ServeHTTP: engine.handleHTTPRequest(c)
/usr/local/go/src/net/http/server.go:2887 (0x9214aa)
serverHandler.ServeHTTP: handler.ServeHTTP(rw, req)
/usr/local/go/src/net/http/server.go:1952 (0x91b6a4)
(*conn).serve: serverHandler{c.server}.ServeHTTP(w, w.req)
/usr/local/go/src/runtime/asm_amd64.s:1371 (0x47ff20)
goexit: BYTE $0x90 // NOP
Comment From: superhawk610
As @doppl-neal said
However, when you are using => db.Model(&list).Updates(updates), you have 2 different types of structs list and updatelist. As far as I understand, you cannot do that.
Your 2 structs must be the same type.
You'll have to manually convert input to the same type of struct as instance.
Comment From: meddario
Ok, thank you very much!
I was able to fix the problem using the https://github.com/jinzhu/copier package. I didn't find a better way to initialize the full structure used as a model (Book in the example) from the "smaller" structure (input), is there any?
Working example with copier:
var updates models.Book
copier.Copy(&input, &updates)
models.DB.Model(&book).Updates(updates)
Comment From: lord-tx
I faced this issue recently, although this working example seems to suggest that copier.Copy() takes a "From" value as its first parameter, swapping them and providing my updates model as the "To" value would be more of a correct approach.
var updates models.Book
copier.Copy(&updates, &input)
models.DB.Model(&book).Updates(updates)