-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Description
GORM Playground Link
Description
Since v1.30.0, the *gorm.DB instance passed to Preload callbacks has clone=0, which means calling chain methods on it does not create a new instance. This causes unsafe state sharing when the
db variable is reused multiple times within the callback.
Expected Behavior (v1.26.1 and earlier)
The db passed to a Preload callback should have clone=2, so each chain method call returns a new *gorm.DB instance. This makes the following pattern safe:
db.Preload("Items", func(db *gorm.DB) *gorm.DB {
q1 := db.Where("type = ?", "A") // returns new instance
q2 := db.Where("type = ?", "B") // returns new instance
return db.Where(q1.Or(q2)) // safe
})Actual Behavior (v1.30.0+)
The db passed to a Preload callback has clone=0, so chain methods return the same instance with accumulated state:
db.Preload("Items", func(db *gorm.DB) *gorm.DB {
q1 := db.Where("type = ?", "A") // modifies db in place
q2 := db.Where("type = ?", "B") // further modifies the same db
return db.Where(q1.Or(q2)) // produces unexpected query with accumulated conditions
})Root Cause
This change appears to have been introduced in PR #7424 (Generics API), which moved tx.Model().Where() to execute before the callback invocation:
// callbacks/preload.go:278 (v1.30.0+)
tx = tx.Model(reflectResults.Addr().Interface()).Where(clause.IN{Column: column, Values: values})
for _, cond := range conds {
if fc, ok := cond.(func(*gorm.DB) *gorm.DB); ok {
tx = fc(tx) // tx now has clone=0 due to getInstance()
}
}Model() internally calls getInstance(), which creates a new *gorm.DB with clone=0 (the default value).
In v1.26.1 and earlier, the callback received tx directly from Session() with clone=2:
// callbacks/preload.go (v1.26.1)
for _, cond := range conds {
if fc, ok := cond.(func(*gorm.DB) *gorm.DB); ok {
tx = fc(tx) // tx had clone=2 from Session()
}
}
if err := tx.Where(clause.IN{...}).Find(...).Error; err != nil {