Recalculate fields in `Scope.Fields' when empty (nil) or number of fields is LTE 1.
Protect the `.fields' variable state from partial-initialization timing issues. As gorm warms state and makes `.GetStructFields' calls, they lead to a long deferred function in model_struct.go. Then the deferred function calls `Scope.Fields', leading to another `.GetStructFields' call.. cyclically. Once information is cached the cycling ends. This extra rule evicts incorrect/suspiciously short fields information. Symptom of incorrect internal state ocurring can manifest as invalid insert SQL statement errors generated during `DB.Save', e.g.: INSERT INTO "your_table" DEFAULT VALUES RETURNING "your_table"."id" This is why we refresh if nil or only a single field (usually is the ID field). Included is a unit-test which triggers the scenario and prove that it is fixed.
This commit is contained in:
parent
fd9e42655f
commit
c53f494ff6
@ -1210,6 +1210,7 @@ db.Where("email = ?", "x@example.org").Attrs(User{RegisteredIp: "111.111.111.111
|
|||||||
* Github Pages
|
* Github Pages
|
||||||
* AlertColumn, DropColumn
|
* AlertColumn, DropColumn
|
||||||
* R/W Splitting, Validation
|
* R/W Splitting, Validation
|
||||||
|
* AddForeignKey compatibility with all dialects
|
||||||
|
|
||||||
# Author
|
# Author
|
||||||
|
|
||||||
|
20
field.go
20
field.go
@ -54,7 +54,25 @@ func (field *Field) Set(value interface{}) error {
|
|||||||
|
|
||||||
// Fields get value's fields
|
// Fields get value's fields
|
||||||
func (scope *Scope) Fields() map[string]*Field {
|
func (scope *Scope) Fields() map[string]*Field {
|
||||||
if scope.fields == nil {
|
// Recalculate if fields is empty (nil) or number of fields is LTE 1.
|
||||||
|
//
|
||||||
|
// Protect the `.fields' variable state from partial-initialization timing
|
||||||
|
// issues.
|
||||||
|
//
|
||||||
|
// As gorm warms state and makes `.GetStructFields' calls, they lead to a long
|
||||||
|
// deferred function in model_struct.go. Then the deferred function calls
|
||||||
|
// `Scope.Fields', leading to another `.GetStructFields' call.. cyclically.
|
||||||
|
// Once information is cached the cycling ends.
|
||||||
|
//
|
||||||
|
// This extra rule evicts incorrect/suspiciously short fields information.
|
||||||
|
//
|
||||||
|
// Symptom of incorrect internal state ocurring can manifest as invalid
|
||||||
|
// insert SQL statement errors generated during `DB.Save', e.g.:
|
||||||
|
//
|
||||||
|
// INSERT INTO "your_table" DEFAULT VALUES RETURNING "your_table"."id"
|
||||||
|
//
|
||||||
|
// This is why we refresh if nil or only a single field (usually is the ID field).
|
||||||
|
if scope.fields == nil || len(scope.fields) <= 1 {
|
||||||
fields := map[string]*Field{}
|
fields := map[string]*Field{}
|
||||||
structFields := scope.GetStructFields()
|
structFields := scope.GetStructFields()
|
||||||
|
|
||||||
|
68
internal_state_test.go
Normal file
68
internal_state_test.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package gorm_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Organization struct {
|
||||||
|
Id int64
|
||||||
|
Name string `sql:"type:varchar(255);not null;"`
|
||||||
|
}
|
||||||
|
|
||||||
|
App struct {
|
||||||
|
Id int64
|
||||||
|
Organization Organization
|
||||||
|
OrganizationId int64 `sql:"not null;"`
|
||||||
|
Name string `sql:"type:varchar(255);not null;"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMultipleSingularTableInvocations runs `DB.SingularTable(true)' at the
|
||||||
|
// beginning and middle of operation. The `SingularTable' call clears out all
|
||||||
|
// of the `gorm.modelStructs' internal package state, triggering the conditions
|
||||||
|
// where gorm must recover, or this test will not pass.
|
||||||
|
//
|
||||||
|
// Also see the `Scope.Fields' function in scope.go for additional information
|
||||||
|
// and context.
|
||||||
|
func TestMultipleSingularTableInvocations(t *testing.T) {
|
||||||
|
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||||
|
DB.SingularTable(true) // Invocation #1.
|
||||||
|
defer DB.SingularTable(false) // Restore original plurality.
|
||||||
|
//DB.LogMode(true)
|
||||||
|
|
||||||
|
entities := []interface{}{
|
||||||
|
&Organization{},
|
||||||
|
&App{},
|
||||||
|
}
|
||||||
|
for i := len(entities) - 1; i >= 0; i-- {
|
||||||
|
if err := DB.DropTableIfExists(entities[i]).Error; err != nil {
|
||||||
|
t.Fatalf("Problem dropping table entity=%+v: %s", entities[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := DB.AutoMigrate(entities...).Error; err != nil {
|
||||||
|
t.Fatalf("Auto-migrate failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
createFixtures(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createFixtures(t *testing.T) {
|
||||||
|
DB.SingularTable(true) // Invocation #2. Clobber/reset internal gorm state.
|
||||||
|
|
||||||
|
org := &Organization{
|
||||||
|
Name: "Some Organization for Testing",
|
||||||
|
}
|
||||||
|
if err := DB.Save(org).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
app := &App{
|
||||||
|
OrganizationId: org.Id,
|
||||||
|
Name: "my-app-for-test-purposes",
|
||||||
|
}
|
||||||
|
if err := DB.Save(app).Error; err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user