Skip to content

Preload callback receives *gorm.DB with clone=0 since v1.30.0, causing unsafe state sharing #7662

@marimelon

Description

@marimelon

GORM Playground Link

go-gorm/playground#838

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 {

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions