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




