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

使用Bun ORM实现PostgreSQL部分字段更新及Null值设置

问题描述

我正在开发一个REST API,需要创建PATCH端点更新用户信息(用户名、生日等),但在和PostgreSQL数据库交互时卡壳了好几天。

我用Bun ORM操作数据库,问题出在这段代码:

/* 数据库模型 */
type User struct {
    Id             uuid.UUID `json:"id,omitempty" bun:"column:pk,type:uuid,default:gen_random_uuid()"`
    Email          string    `json:"email,omitempty"`
    Name           string    `json:"name,omitempty"`
    Username       string    `json:"username,omitempty"`
    Password       string    `json:"-" dbKey:"password"`
    Verified       bool      `json:"verified,omitempty"`
    EmailVerified  bool      `json:"emailVerified,omitempty"`
    Avatar         *string   `json:"avatar,omitempty"`
    GraduationYear int       `json:"graduationYear,omitempty"`
    Degree         *string   `json:"degree,omitempty"`
    Major          *string   `json:"major,omitempty"`
    Intrests       int       `json:"intrests,omitempty"`
    Clubs          int       `json:"clubs,omitempty"`
    Sports         int       `json:"sports,omitempty"`
    NotificationId string    `json:"notificationId,omitempty"`
    CreatedAt      time.Time `json:"createdAt,omitempty" bun:"type:timestamp,default:now()"`
    UpdatedAt      time.Time `json:"updatedAt,omitempty" bun:"type:timestamp,default:now()"`
}

func (r *userRepositoryImpl) UpdateUser(userId string, data models.User) (*models.User, error) {
    user := models.User(data)

    _, err := r.db.NewUpdate().Model(&user).Where("id = ?", userId).Returning("*").Exec(r.context)
    if err != nil {
        return nil, err
    }

    return &user, nil
}

这个函数本该根据传入的User结构体更新信息,但当我只想更新单个字段时,数据库里其他所有列都会被设为默认值或零值。比如:

/* 这段代码会把其他所有列重置为默认值/零值,只更新avatar列 */
updatedUser, err := s.userRepository.UpdateUser(userId, models.User{
    Avatar: &avatarUrl,
})

字段被设为零值

后来我知道Bun ORM的Omit Zero Values方法可以只更新指定字段、忽略结构体里的空字段,但这样又没法把avatar列重置为空字符串——空值会被直接忽略。

我试了好几种方法,甚至用了反射:

  • 把非空字段转成map[string]interface{}传给Model方法,但ORM没法转换成User结构体定义的类型。
  • 合并旧结构体和待更新数据的结构体,虽然有效但实现太繁琐,不是理想方案。
  • 用*string这类指针类型表示null值,但结果和最初的问题一样。

编辑1
补充一下端点接收的请求结构体(因为是PATCH请求,某些字段可能不会传):

/* PATCH端点接收的请求结构体,部分字段可能缺失 */
type UpdateDataRequest struct {
    Email          string `json:"email,omitempty" validate:"omitempty,email"`
    Name           string `json:"name,omitempty" validate:"omitempty,min=3,max=32"`
    Username       string `json:"username,omitempty" validate:"omitempty,min=3,max=24"`
    GraduationYear int    `json:"graduationYear,omitempty" validate:"omitempty,number"`
    Degree         string `json:"degree,omitempty" validate:"omitempty,min=3,max=32"`
    Major          string `json:"major,omitempty" validate:"omitempty,min=3,max=32"`
    Intrests       int    `json:"intrests,omitempty" validate:"omitempty,number"`
    Clubs          int    `json:"clubs,omitempty" validate:"omitempty,number"`
    Sports         int    `json:"sports,omitempty" validate:"omitempty,number"`
}

/* 端点里的更新逻辑 */
updatedUser, err := s.userRepository.UpdateUser(userId, models.User{
    Email:          updateUserData.Email,
    Name:           updateUserData.Name,
    Username:       updateUserData.Username,
    GraduationYear: updateUserData.GraduationYear,
    Degree:         &updateUserData.Degree,
    Major:          &updateUserData.Major,
    Intrests:       updateUserData.Intrests,
    Clubs:          updateUserData.Clubs,
    Sports:         updateUserData.Sports,
    UpdatedAt:      time.Now(),
})
解决方案

针对Bun ORM处理PATCH更新的场景,核心是区分「不更新字段」和「主动设置为null/零值」,这里有几种靠谱的实现方式:

1. 用OmitZero()+指针类型区分状态

你的User模型已经用了*string这类指针类型,结合OmitZero()就能解决问题——指针的零值是nil,主动设置的空字符串是&"",两者会被Bun区别对待:

修改更新函数:

func (r *userRepositoryImpl) UpdateUser(userId string, data models.User) (*models.User, error) {
    user := models.User(data)
    user.UpdatedAt = time.Now() // 确保每次更新刷新时间

    _, err := r.db.NewUpdate().
        Model(&user).
        OmitZero(). // 忽略nil指针的字段,保留&""这类主动设置的空值
        Where("id = ?", userId).
        Returning("*").
        Exec(r.context)
    if err != nil {
        return nil, err
    }

    return &user, nil
}

处理请求时,要根据UpdateDataRequest的字段是否存在来决定传指针nil还是空字符串指针:

// 以Degree字段为例
var degree *string
if updateUserData.Degree != "" || reflect.ValueOf(updateUserData.Degree).IsZero() {
    // 请求明确传了Degree(哪怕是空字符串),就生成对应指针
    val := updateUserData.Degree
    degree = &val
}
// 其他字段同理处理

这样,请求没传的字段会是nil,被OmitZero()忽略;主动传空字符串时,会被设置到数据库。

2. 动态指定更新字段

如果不想处理指针逻辑,可以用Bun的Column()方法逐个添加要更新的字段,完全控制更新范围:

func (r *userRepositoryImpl) UpdateUser(userId string, req UpdateDataRequest) (*models.User, error) {
    // 先查询旧用户信息(可选,用于合并字段)
    var oldUser models.User
    if err := r.db.NewSelect().Model(&oldUser).Where("id = ?", userId).Scan(r.context); err != nil {
        return nil, err
    }

    updateQuery := r.db.NewUpdate().Model(&oldUser).Where("id = ?", userId).Returning("*")

    // 逐个判断请求字段,添加到更新列
    if req.Email != "" {
        oldUser.Email = req.Email
        updateQuery.Column("email")
    }
    if req.Name != "" {
        oldUser.Name = req.Name
        updateQuery.Column("name")
    }
    // 处理可设为空的字段:只要请求里有传(包括空字符串)就更新
    if reflect.ValueOf(req.Degree).IsValid() {
        val := req.Degree
        oldUser.Degree = &val
        updateQuery.Column("degree")
    }
    // 其他字段同理...
    oldUser.UpdatedAt = time.Now()
    updateQuery.Column("updated_at")

    _, err := updateQuery.Exec(r.context)
    if err != nil {
        return nil, err
    }

    return &oldUser, nil
}

这种方式直观可控,适合字段数量不多的场景。

3. 用SetMap()批量设置

如果请求字段和模型字段对应,也可以用SetMap()结合动态map来更新,无需提前查询旧用户:

func (r *userRepositoryImpl) UpdateUser(userId string, req UpdateDataRequest) (*models.User, error) {
    updateMap := make(map[string]interface{})

    if req.Email != "" {
        updateMap["email"] = req.Email
    }
    if req.Name != "" {
        updateMap["name"] = req.Name
    }
    // 处理空字符串场景
    if req.Degree != "" || reflect.ValueOf(req.Degree).IsZero() {
        updateMap["degree"] = req.Degree
    }
    updateMap["updated_at"] = time.Now()

    var updatedUser models.User
    _, err := r.db.NewUpdate().
        Model(&updatedUser).
        SetMap(updateMap).
        Where("id = ?", userId).
        Returning("*").
        Exec(r.context)
    if err != nil {
        return nil, err
    }

    return &updatedUser, nil
}

Bun会自动处理map到模型的类型转换,适合快速实现批量更新。

内容的提问来源于stack exchange,提问作者TheLawlessDev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 13:35:01