You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

GORM回调异常:日志插入时参数数量不符问题排查

问题

我在Go应用中尝试跟踪所有针对MySQL数据库的查询,于是在初始化函数中注册了Query阶段全局后置回调full_logger,用于将查询信息写入query_logs表。但遇到了参数注入错误,具体情况如下:

相关代码

初始化函数

func InitDatabase() error {
    err = DBEnv.Db.Callback().Query().After("*").Register("full_logger", setQueryLogger)
    if err != nil {
        return err
    }
    return nil
}

// Querylogger
func setQueryLogger(db *gorm.DB) {
    sql, vars := db.Statement.SQL.String(), db.Statement.Vars
    rows := db.Statement.RowsAffected
    varsJson, err := json.Marshal(vars)
    if err != nil {
        panic(err)
    }

    db.Exec("INSERT INTO `query_logs` (`sql`,`parameters`, `rows_affected`, `executed_at`, `executed_by`) VALUES (?, ?, ?, ?, ?)", sql, varsJson, rows, time.Now(), 1)
}

User结构体

type User struct {
    ID                    uint64         `gorm:"primarykey" json:"id"`
    CreatedAt             time.Time      `json:"createdAt"`
    CreatedBy             uint64         `json:"createdBy" validate:"required,gt=0"`
    UpdatedAt             time.Time      `json:"updatedAt"`
    UpdatedBy             uint64         `json:"updatedBy" validate:"required,gt=0"`
    DeletedAt             gorm.DeletedAt `json:"deletedAt"`
    DeletedBy             sql.NullInt64  `json:"deletedBy" validate:"required_with=DeletedAt"`
    ImportedBy            sql.NullInt64  `json:"importedBy" validate:"required_with=ImportedAt"`
    ImportedAt            sql.NullTime   `json:"importedAt" validate:"required_with=ImportedBy"`
    Email                 string         `json:"email" validate:"required"`
    Password              string         `json:"password" validate:"required"`
    FirstName             string         `json:"firstName" validate:"required"`
    LastName              string         `json:"lastName" validate:"required"`
    Active                bool           `json:"active" validate:"required"`
    LastPasswordChangedAt time.Time      `json:"lastPasswordChangedAt" validate:"required"`
    ActivatedBy           sql.NullInt64  `json:"activatedBy" validate:"required_with=ActivatedAt,gt=0"`
    ActivatedAt           sql.NullTime   `json:"activatedAt" validate:"required_with=ActivatedBy"`
    DeactivatedBy         sql.NullInt64  `json:"deactivatedBy" validate:"required_with=DeactivatedAt,gt=0"`
    DeactivatedAt         sql.NullTime   `json:"deactivatedAt" validate:"required_with=DeactivatedBy"`
    RoleId                uint64         `json:"roleId" validate:"required,gt=0"`
    InstituteId           sql.NullInt64  `json:"instituteId"`
}

查询方法

func (u *User) FindFirst() *User {
    db.DBEnv.Db.First(u)
    return u
}

调用代码

user3 := (*user.User).FindFirst(&user.User{ID: 3})

问题现象

GORM执行的查询语句为:

SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ? ORDER BY `users`.`id`  LIMIT 1

但回调中执行的日志插入语句却变成:

INSERT INTO `query_logs` (`sql`,`parameters`, `rows_affected`, `executed_at`, `executed_by`) VALUES (3, 'SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL AND `users`.`id` = ? ORDER BY `users`.`id` LIMIT 1', '[3]', 1, '2024-01-29 19:46:35.978')

导致报错:

sql: expected 5 arguments, got 6

疑问

为何GORM会将查询的ID作为额外参数注入到db.Exec中?是遗漏了配置还是Bug?

附query_logs表结构:

create table query_logs(
    id bigint unsigned not null primary key auto_increment,
    `sql` text not null,
    `parameters` json null,
    rows_affected bigint unsigned not null default 0,
    executed_at datetime        not null default current_timestamp,
    executed_by bigint unsigned not null,
    foreign key (executed_by) references users (id) on delete restrict on update restrict
) ENGINE = Innodb;
解答

这不是Bug,是因为你在回调中使用的db *gorm.DB实例依然保留着当前查询的上下文(包括原查询的参数),调用db.Exec时,GORM会自动将原Statement.Vars追加到你传入的参数列表中,导致参数数量超出预期。

解决方法

你需要创建一个新的DB会话,脱离当前查询的上下文,避免参数被自动追加。修改setQueryLogger函数如下:

func setQueryLogger(db *gorm.DB) {
    sql, vars := db.Statement.SQL.String(), db.Statement.Vars
    rows := db.Statement.RowsAffected
    varsJson, err := json.Marshal(vars)
    if err != nil {
        panic(err)
    }

    // 使用Session创建新的会话,清空当前Statement的上下文
    db.Session(&gorm.Session{}).Exec(
        "INSERT INTO `query_logs` (`sql`,`parameters`, `rows_affected`, `executed_at`, `executed_by`) VALUES (?, ?, ?, ?, ?)",
        sql, varsJson, rows, time.Now(), 1,
    )
}

原理说明

GORM的DB实例会携带当前操作的Statement信息,包括查询参数。当你在回调中直接调用db.Exec时,GORM会将原Statement.Vars(也就是查询用户时的[3])合并到你传入的参数里,最终参数变成[sql, varsJson, rows, time.Now(), 1, 3],总共6个参数,而INSERT语句只需要5个,因此报错。

通过db.Session(&gorm.Session{})创建新的会话后,新的DB实例不再携带原查询的上下文参数,此时执行Exec只会使用你传入的5个参数,就能正常插入日志了。


内容的提问来源于stack exchange,提问作者Anton Grünberg

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 09:07:33