< 返回版块

lijiulong-dev 发表于 2026-07-28 18:23

Tags:局部更新接口,orm。

背景

在分布式与微服务架构中,API 接口的数据更新(Update)是高频场景。如何优雅地实现“部分字段更新”,准确区分“不修改”、“置空”与“设为默认值”,是衡量工程规范性的关键细节,也直接考验着不同编程语言的类型系统与表达力。

java实现

Java 的实现思路非常清晰:利用包装类(Integer、Boolean)的 null 语义来表达“未传”,然后借助 MyBatis 等框架的动态 SQL 能力,只拼接需要更新的字段。

<update id="updateUserSelectiveWithVersion">
    UPDATE user
    <set>
        <if test="name != null"> name = #{name}, </if>
        <if test="age != null"> age = #{age}, </if>
        version = version + 1,
    </set>
    WHERE id = #{id} AND version = #{version}
</update>

优点:

  1. 基于java生态,无需重复造轮子。
  2. 与数据库的null兼容性好。

缺点:

  1. 运行时风险: xml的错误无法在编译阶段发现。
  2. 动态sql维护风险。
  3. 不修改和置空边界模糊,比如数据库字段设置成"null", 就无法很好区分。
  4. java框架臃肿,反射开销大。
  5. 基础类型 int=0, 无法区分是不修改和置空。

go实现

Go 没有 Java 那样的注解和动态代理机制,它崇尚 "显式优于隐式" 。因此,Go 的实现思路非常直接:用指针或 map[string]interface{} 来表达"字段是否被传入",然后手动构建更新语句。

func (s *UserService) UpdateUser(req *UpdateUserReq) error {
    // 构建需要更新的字段 map
    updates := make(map[string]interface{})
    
    if req.Name != nil {
        updates["name"] = *req.Name
    }
    if req.Age != nil {
        updates["age"] = *req.Age
    }
    if req.Email != nil {
        updates["email"] = *req.Email
    }
    if req.Active != nil {
        updates["active"] = *req.Active
    }
    
    // 必须有字段需要更新
    if len(updates) == 0 {
        return errors.New("no fields to update")
    }
    
    // 使用 GORM 的 Updates 方法(传入 map)
    result := s.db.Model(&User{}).
        Where("id = ? AND version = ?", req.ID, req.Version).
        Updates(updates)
    
    if result.RowsAffected == 0 {
        return errors.New("update failed: version mismatch or record not found")
    }
    return result.Error
}

优点:

  1. 编译期就能确定指针结构体的结构正确,不会出现拼写错误。
  2. 通过nil能清晰区分 不修改 和 置空,
  3. 无反射,性能较快。

缺点:

  1. 代码重复较多,大量if err != nil.
  2. 类型系统表达力有限:Go 没有代数数据类型(ADT),无法像 Rust 的 Option<Option> 那样在类型层面区分"未传"和"传 null"。
  3. 会有json类型歧义,不太好区分 {"age": null} 和 {}
  4. Map 方案失去类型安全:使用 map[string]interface{} 虽然灵活,但字段名错误无法在编译期发现,且需要手动类型断言,代码变得脆弱。

rust实现

Rust 没有 Java 的反射和动态代理,也没有 Go 的 nil 指针。它解决这个问题的思路是在类型层面把三种语义明确区分开(Option<Option>),让编译器帮你检查所有情况。

use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub struct UpdateUserReq {
    pub id: i64,
    
    // 关键:Option<Option<String>> 
    // 外层 Option 表示"字段是否在 JSON 中出现"
    // 内层 Option 表示"值是否为 null"
    pub name: Option<Option<String>>,
    pub age: Option<Option<i32>>,
    pub email: Option<Option<String>>,
    pub active: Option<Option<bool>>,
}

优点:

  1. 编译期安全区分三种类型语意,不修改,修改,置空。
  2. 性能快,零运行时开销。
  3. 没有歧义: None、Some(None)、Some(Some(T)) 三种状态完全独立。

缺点:

  1. 类型复杂度增加,对于新人来说,理解"双层 Option"的语义需要一定的学习成本。
  2. 仍然需要考虑动态sql拼接,代码相对也更冗余,字段多的时候,代码会很长。
  3. sea-orm还在继续完善中。

一句话总结

Java:生态成熟,框架替你搞定动态SQL,开发效率高。但 null 语义需团队严格约定,否则"不修改"和"置空"易混淆。XML拼写错误和反射开销是运行时才暴露的隐患。 Go:指针加显式判断,控制流清晰透明,性能轻量。但每个字段需手写 if,代码略显啰嗦。更要警惕 encoding/json 无法区分 {} 和 {"age": null} 这个经典陷阱。 Rust:用 Option<Option> 在类型层面彻底区分三种语义,编译器强迫你穷尽所有分支。代码更冗长,双层 Option 有学习成本。但换来的是零开销和编译期绝对安全,Bug无处藏身。

rust一些常见updator优化点

  1. 使用别名或者枚举来优化代码
pub type FieldUpdate<T> = Option<Option<T>>;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]  // 允许 JSON 灵活匹配
pub enum UpdateField<T> {
    Ignore,      // 不修改(对应 JSON 字段缺失)
    SetNull,     // 置空(对应 JSON 传 null)
    Set(T),      // 设为具体值(对应 JSON 传具体值)
}
  1. 完善updator的orm专用框架解决 结构体映射到sql的正确性和可维护性。
/// 设置方法抽象
pub trait SetMethods<T> {
    /// 设置普通字段(直接赋值,不区分 NULL)
    fn set(&mut self, column: &str, value: T) -> &mut Self;
    
    /// 设置可选字段(支持三种语义)
    fn set_optional(&mut self, column: &str, value: Option<Option<T>>) -> &mut Self;
    
    /// 设置为 NULL
    fn set_null(&mut self, column: &str) -> &mut Self;
}

评论区

写评论

还没有评论

1 共 0 条评论, 1 页