You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

MyBatis动态更新语句中如何传递正确的数据类型

不用额外库实现MyBatis动态更新(保留类型信息)

我明白你遇到的问题:想用动态SQL生成只更新非null字段的语句,但不管是转Map还是用SQL Builder都遇到了类型丢失的问题——尤其是LocalDateTime这类特殊类型被当成字符串处理。下面给你几种不依赖mybatis-dynamic-sql的解决方案,按推荐优先级排序:

方案1:直接用MyBatis动态SQL标签(最推荐,无反射、类型安全)

这是最直接也最稳定的方式,虽然需要手动写每个字段的判断,但完全符合你“避免反射”的需求,而且MyBatis会自动处理参数类型转换。

把你的Mapper接口改成这样:

@Update({
    "<script>",
    "UPDATE POST",
    "<set>",
    // 逐个判断实体字段是否为null,非null则加入更新语句
    "<if test='p.title != null'>title = #{p.title},</if>",
    "<if test='p.body != null'>body = #{p.body},</if>",
    "<if test='p.createdAt != null'>createdAt = #{p.createdAt},</if>",
    "<if test='p.createdBy != null'>createdBy = #{p.createdBy},</if>",
    "<if test='p.updatedAt != null'>updatedAt = #{p.updatedAt},</if>",
    "<if test='p.updatedBy != null'>updatedBy = #{p.updatedBy},</if>",
    "</set>",
    "WHERE id = #{id}",
    "</script>"
})
public boolean update(@Param("id") Integer id, @Param("p") Post post);
  • 优点:不需要反射,参数类型完全由MyBatis管理(LocalDateTime这类类型会被正确识别并转换为数据库兼容的格式),逻辑清晰,容易调试。
  • 缺点:如果实体字段很多,需要手动写每个<if>标签,但对于大多数业务实体来说,这个工作量完全可接受。

方案2:改进Map转换方式(保留原类型)

你之前用ObjectMapper转Map时,LocalDateTime被序列化成了字符串,导致类型丢失。可以换一种方式生成Map,让值保留原类型:

方式A:手动复制非null字段(无反射)
@PatchMapping(path = "/{id}")
public Post patchById(@PathVariable Integer id, @RequestBody Post post) {
    Map<String, Object> updateMap = new HashMap<>();
    // 手动判断每个字段,非null则加入Map
    if (post.getTitle() != null) updateMap.put("title", post.getTitle());
    if (post.getBody() != null) updateMap.put("body", post.getBody());
    if (post.getCreatedAt() != null) updateMap.put("createdAt", post.getCreatedAt());
    if (post.getCreatedBy() != null) updateMap.put("createdBy", post.getCreatedBy());
    if (post.getUpdatedAt() != null) updateMap.put("updatedAt", post.getUpdatedAt());
    if (post.getUpdatedBy() != null) updateMap.put("updatedBy", post.getUpdatedBy());
    
    return this.postService.patchById(id, updateMap);
}

然后Mapper接口和你之前的一样:

@Update({
    "<script>",
    "UPDATE POST",
    "<set>",
    "<foreach item='item' index='index' collection='p.entrySet()'>",
    "${index} = #{item},",
    "</foreach>",
    "</set>",
    "WHERE id = #{id}",
    "</script>"
})
public boolean update(@Param("id") Integer id, @Param("p") Map<String, Object> post);

这里#{item}会自动识别Map中值的原类型,LocalDateTime不会被当成字符串处理。

方式B:用BeanUtils转Map(轻微反射,减少手动代码)

如果你觉得手动复制太繁琐,可以用Spring的BeanWrapper(或Apache BeanUtils)来生成保留原类型的Map,内部会用反射但不需要你自己写反射逻辑:

@PatchMapping(path = "/{id}")
public Post patchById(@PathVariable Integer id, @RequestBody Post post) {
    Map<String, Object> updateMap = new HashMap<>();
    BeanWrapper wrapper = new BeanWrapperImpl(post);
    
    for (PropertyDescriptor pd : wrapper.getPropertyDescriptors()) {
        String propName = pd.getName();
        // 跳过class属性和id字段(因为id是where条件,不需要更新)
        if (!"class".equals(propName) && !"id".equals(propName)) {
            Object value = wrapper.getPropertyValue(propName);
            if (value != null) {
                updateMap.put(propName, value);
            }
        }
    }
    
    return this.postService.patchById(id, updateMap);
}

Mapper接口同样使用上面的动态SQL即可。

方案3:正确使用@UpdateProvider(支持动态生成,可选反射)

你之前的SQL Builder方案问题出在手动拼接值为字符串,导致类型丢失。正确的做法是让MyBatis来处理参数绑定,而不是自己拼接值:

方式A:无反射版本
@UpdateProvider(type = PostUpdateProvider.class, method = "generateUpdateSql")
public boolean update(@Param("id") Integer id, @Param("post") Post post);

// 自定义Provider类
public class PostUpdateProvider {
    public String generateUpdateSql(@Param("id") Integer id, @Param("post") Post post) {
        return new SQL(){{
            UPDATE("POST");
            
            // 逐个判断字段,非null则加入SET语句,用#{post.field}绑定参数
            if (post.getTitle() != null) SET("title = #{post.title}");
            if (post.getBody() != null) SET("body = #{post.body}");
            if (post.getCreatedAt() != null) SET("createdAt = #{post.createdAt}");
            if (post.getCreatedBy() != null) SET("createdBy = #{post.createdBy}");
            if (post.getUpdatedAt() != null) SET("updatedAt = #{post.updatedAt}");
            if (post.getUpdatedBy() != null) SET("updatedBy = #{post.updatedBy}");
            
            WHERE("id = #{id}");
        }}.toString();
    }
}
方式B:反射版本(减少手动代码)

如果实体字段很多,可以用反射遍历字段,但依然用#{post.field}绑定参数,保证类型不丢失:

public class PostUpdateProvider {
    public String generateUpdateSql(@Param("id") Integer id, @Param("post") Post post) throws IllegalAccessException {
        SQL sql = new SQL(){{
            UPDATE("POST");
            WHERE("id = #{id}");
        }};
        
        Field[] fields = post.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            Object value = field.get(post);
            // 跳过id字段和null值
            if (value != null && !"id".equals(field.getName())) {
                sql.SET(field.getName() + " = #{post." + field.getName() + "}");
            }
        }
        
        return sql.toString();
    }
}

这里的关键是不要用单引号包裹值,而是通过#{}让MyBatis处理参数,这样就能保留原类型信息。


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

火山引擎 最新活动