GORM 中同一个 *DB 对象污染问题的分析与解决

当然可以!以下是重新排版后的代码和解释,适合粘贴在 CSDN 上:


GORM 查询污染问题及解决方案:如何防止查询条件污染

问题背景

在使用 GORM 进行数据库操作时,链式调用可能会导致查询条件污染。例如,当你在查询时添加了分页条件(LIMITOFFSET),这些分页条件可能会影响到统计查询(COUNT)。下面是一个示例,展示了这个问题。

示例代码:
type Account struct {
    UserId   string
    Username string
    Email    string
    Phone    string
}

func main() {
    // getDB 实现省略
    db, err := getDB("root", "secret", "localhost", 15432, "testdb")
    if err != nil {
        log.Println(err)
        return
    }

    var cnt int64
    var items []Account

    // 假设此处只做了一个简单的 where 条件
    db = db.Table("account").Where("username='admin'")

    // 希望在这里保留上面 db 的 where 条件,用于统计总数
    // 但又不想让后续的操作影响到这里,所以创建了 cntDb
    cntDb := db

    // 下面这行代码对 db 做了分页限制
    // 期望 cntDb 不被影响,然而执行后会发现 cntDb 被污染了
    db = db.Limit(1).Offset(2)

    // 执行 .Count 时却带上了分页限制
    if err := cntDb.Debug().Count(&cnt).Error; err != nil {
        log.Println(err)
        return
    }

    // 查询实际数据
    if err := db.Debug().Find(&items).Error; err != nil {
        log.Println(err)
        return
    }

    log.Printf("items: %#v, cnt: %d", items, cnt)
    time.Sleep(3 * time.Second)
}
输出的 SQL 日志:
[2.208ms] [rows:0] SELECT count(*) FROM "account" WHERE username='admin' LIMIT 1 OFFSET 2
[0.979ms] [rows:0] SELECT * FROM "account" WHERE username='admin' LIMIT 1 OFFSET 2
items: []main.Account{}, cnt: 0

可以看到,原本只想给查询数据(Find)加上 LIMIT 1 OFFSET 2 的语句,却意外影响到了统计总数(Count)的 SQL。显然,cntDb 并没有独立于 db,导致最终统计结果也带上了分页限制。

初步尝试:使用 Session 函数

为了解决这个问题,我尝试使用 Session 函数来创建一个新的数据库会话:

cntDb := db.Session(&gorm.Session{})

根据 GORM 的注释,Session 应该创建一个新的数据库会话。可惜实际测试中,cntDb 依然会被后续 db.Limit(...) 的操作所影响。

再试:将 NewDB 设为 true

GORM 的 Session 配置结构体中有一个 NewDB 选项。如果将其设置为 true,会在新的 Session 中将 clone 值置为 1,这应该会导致创建一个全新的 Statement。然而,这样虽然可以得到一个干净的 db,但原本希望保留的 Where 条件也被清空了,仍旧无法满足需求。

深入研究:为什么会“污染”?

要理解为什么会出现“污染”问题,我们需要了解 GORM 内部如何管理查询条件的。GORM 中的大部分链式调用都会将查询条件保存在一个名为 Statement 的结构体里。每当我们调用 Where()Limit()Offset() 等查询方法时,都会修改同一个 Statement 的内容。

gorm.Session 内部,有一个与 clone 相关的逻辑,核心代码大致如下:

func (db *DB) getInstance() *DB {
    if db.clone > 0 {
        tx := &DB{Config: db.Config, Error: db.Error}

        if db.clone == 1 {
            // 当 clone == 1 时,创建一个全新的 Statement
            tx.Statement = &Statement{...} // 干净的 Statement
        } else {
            // 当 clone == 2 时,进行深拷贝
            tx.Statement = db.Statement.clone()
            tx.Statement.DB = tx
        }

        return tx
    }
    return db
}
  • clone == 1 时,Statement 会被全新创建,原先的查询条件不再保留。
  • clone == 2 时,Statement 会深拷贝到新的 tx,继承已设置的查询条件,但与原对象分离,不再互相影响。

解决方案:在 Session 时触发 Statement 克隆

为了保留原先的查询条件,并确保后续查询不受影响,关键是让 GORM 在调用 Session 时,立即进行 Statement 的克隆。

我们可以通过为 Session 的配置赋值(例如 Context),来触发 Statement 的克隆。这样可以避免后续操作影响到 cntDb

cntDb := db.Session(&gorm.Session{Context: context.Background()})

这段代码通过为 Session 提供一个 Context,触发了 Statement 的克隆,使得 cntDbdb 完全分离,保留原有的查询条件,又不会受到后续分页条件的影响。

总结

  • 问题本质:GORM 通过共享一个 Statement 对象来构建查询条件,多个链式调用会修改同一个 Statement,导致查询条件相互污染。
  • Session 关键点:仅调用 Session(&gorm.Session{}) 不足以触发深拷贝;需要给 Context 或其他配置项赋值,才能立刻克隆 Statement
  • 正确用法:如果你希望保留当前查询条件,并避免后续修改影响到原查询,可以使用以下方式:
cntDb := db.Session(&gorm.Session{Context: context.Background()})

或者:

cntDb := db.Session(&gorm.Session{PrepareStmt: true})

或者:

cntDb := db.Session(&gorm.Session{SkipHooks: true})

只要能够触发 Statement 克隆的操作,cntDb 就会和 db 完全分离,避免相互影响。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值