【GoLang】【框架学习】【GORM】4. 使用 BeforeUpdate hook 操作时,出现反射报错

文章目录

  • [0. 问题背景](#0. 问题背景)
  • [1. 问题环境](#1. 问题环境)
  • [2. 问题排查](#2. 问题排查)
  • [3. 总结](#3. 总结)

0. 问题背景

对于大多的业务操作的增删查改来说,都会记录一个 创建者、更新者。我们的业务上是通过 GORM hook 的方式来进行处理的。在 create 的时候用的挺顺的,但在 update 的时候出现反射的报错,最终分析代码后,可能是我们使用方式有误,或者 GORM 本身不支持该操作导致的。

在 Github 上找到了同样遇见此问题的 issue,但没有解决,我在下面也做了相应的评论:

1. 问题环境

对业务做了简单抽象,后 ORM 结构定义:

go 复制代码
type User struct {
    BaseModelOper
    Name  string
    Age   int
    Email string
}

func (u *User) TableName() string {
    return "user"
}

type BaseModelOper struct {
    ID        int64          `gorm:"primary_key;auto_increment:true;type:bigint" json:"id"`
    CreatedAt time.Time      `json:"created_at" gorm:"type:timestamp;autoCreateTime:milli"`
    UpdatedAt time.Time      `json:"updated_at" gorm:"type:timestamp;autoUpdateTime:milli"`
    DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"type:timestamp"`
    CreatedBy int64          `json:"created_by" gorm:"type:bigint"`
    UpdatedBy int64          `json:"updated_by" gorm:"type:bigint"`
}

func (*BaseModelOper) BeforeCreate(db *gorm.DB) error {
    userId := GetUserIdFromCtx(db.Statement.Context)
    if userId != 0 {
        db.Statement.SetColumn("created_by", userId)
    }
    return nil
}

func (*BaseModelOper) BeforeUpdate(db *gorm.DB) error {
    userId := GetUserIdFromCtx(db.Statement.Context)
    if userId != 0 {
        db.Statement.SetColumn("updated_by", userId)
    }
    return nil
}

update 操作:

go 复制代码
// 查询条件
type UserCond struct {
    Id *int64 `json:"id" gorm:"type:bigint"`
}
// 更新结构
type UserUpdate struct {
    Name *string
}

func (u *User) UpdateByCond(ctx context.Context, cond UserCond, updateV *UserUpdate) error {
    query, values := util.MakeStructQuery(cond)
    err = dbcon.CtxDB(ctx).Model(&User{}).Where(query, values...).Updates(updateV).Error
    if err != nil {
        logger.Errorf(ctx, "UpdateByCond has err. %+v", err)
        return err
    }
    return nil
}

建表、数据插入、数据更新操作:

go 复制代码
func TestUpdateBy() {
    if err := dbcon.GetDB().AutoMigrate(&User{}); err != nil {
        panic(err)
    }
    ctx := context.Background()
    ctx = SetUserIdToCtx(1)
    u := User{
        Age:   21,
        Name:  "AAA",
        Email: "xrc@xrc.com111",
    }
    if err := dbcon.CtxDB(ctx).Create(&u).Error; err != nil {
        panic(err)
    }
    
    if err := u.UpdateByCond(
        ctx,
        UserCond{Id: proto.Int64(1)},
        &UserUpdate{Name: proto.String("BBB")}); err != nil {
        panic(err)
    }
}

出现了 panic 错误!

2. 问题排查

  • 实际上就是设置单列的更新的操作。
  • 有几个关键信息点需要先了解:
    • stmt.Dest 就是我们传递进来的精简结构体 UserUpdate。其中不包含 UpdatedBy 这些基础字段,仅包含业务字段。
    • stmt.Schema 是 ORM 结构体,也就是对数据表映射的结构体。包含全部字段,即 基础字段+业务字段。

流程如下:

  • 注意上面的 field.Set 传入的是 destValue,这里的 destValue 字段是 ORM 结构的精简版,并不包含全体字段。

  • 这里的 field 是 ORM 结构体的结构,是 0,5

    • 0 指的是 ORM 结构体中第一个匿名结构体
    • 5 是这个匿名结构体中的第 5 个字段
    • 也就是说,需要通过反射,找到结构体里面的 0, 5 字段,给他赋值。

  • 但是这里的 v 是 dest 反射值的解引用,实际上是一个不完整的结构,能看到里面只有一个字段。

  • 所以当 i=5 的时候,在 dest 中就找不到对应的字段,那么自然就会报错。

3. 总结

正如我在上面 issue 的评论一样:

这块没明白为何要这样要求?在 SetColumn 的操作里面,dest 如果是结构体类型的话,就需要使用表对应的结构体类型才行。针对 create 类的操作确实没啥问题。而 update 操作既然已经支持了 struct/map 两种更新方式,为何在这又不支持自定义 struct 更新了呢?

涉及到一些代码逻辑的历史问题,简单的将自定义结构体通过序列化转换成了 map 结构才得以暂时解决...

既然 update 操作支持通过 struct 的方式去做更新,为何不支持的更彻底一点???先如今用 map 的方式去转换处理,代码属实冗余。

相关推荐
夏幻灵2 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
程序猿编码3 小时前
榨干 RTX5090 算力!Qwen3 专用单卡推理引擎,手写C++/CUDA 算子实现 MTP 推测解码
开发语言·c++·大模型推理·qwen3·多模态推理
敬往事一杯酒哈4 小时前
海康 AGV 导航读码器学习
学习
bkspiderx4 小时前
Qt 插件机制:动态扩展应用功能的核心框架
开发语言·qt·元数据·qt 插件·qpluginloader
程序员老陆4 小时前
深入理解 C++ thread_local:线程私有存储的正确打开方式
开发语言·c++·程序设计
沐晓时光4 小时前
C语言入门,深入理解指针(3)
c语言·开发语言
码云数智-园园5 小时前
unique_ptr 还是 shared_ptr?C++ 智能指针选型与内存泄漏实战分析
java·开发语言
qq_284274056 小时前
机械原理笔记:平面机构自由度计算(复合铰链、局部自由度、虚约束)与四杆机构入门(含考点)
笔记·学习·平面·自动化·制造
禾小西6 小时前
07丨Redis 哨兵机制:主库故障后,如何恢复服务?
java·开发语言·redis
我不是阵雨6 小时前
JDK 21虚拟线程Pinning陷阱:一文拔钉解困
java·开发语言