使用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

