Spring Boot 3中修改JPA实体List元素后无法持久化的问题排查
Spring Boot 3升级后JPA实体集合属性更新不生效问题
问题描述
我有一个名为User的JPA实体,代码如下:
import jakarta.persistence.* import java.util.List; @Entity(name = "USERS") public class User { private String username; private List<String> roles; @Id @Column(name = "USERNAME") public String getUsername() { return username; } public void setUsername(final String username) { this.username = username; } @Convert(converter = StringListConverter.class) @Column(name = "GROUPS") public List<String> getRoles() { return roles; } public void setRoles(final List<String> roles) { this.roles = roles; } }
在Spring Boot 2.7.9版本中,我可以通过以下代码更新该实体的角色集合,数据库能正常同步修改:
public User updateUser(String username, User updatedUser) { User user = this.userRepository.findById(username).orElseThrow(); user.getRoles().removeAll(List.of("user", "admin")); user.getRoles().addAll(updatedUser.getRoles()); return userRepository.save(user); }
但升级到Spring Boot 3后,上述代码不再生效:调用save()方法会返回包含更新后角色的User对象,过程无异常,但查看Hibernate日志发现没有执行任何更新查询,数据库中的角色数据也未修改。只有创建新的ArrayList并调用setRoles(),才能让修改持久化,有效代码如下:
public User updateUser(String username, User updatedUser) { User user = this.userRepository.findById(username).orElseThrow(); List<String> newRoles = new ArrayList<>(user.getRoles()); newRoles.removeAll(List.of("user", "admin")); newRoles.addAll(updatedUser.getRoles()); user.setRoles(newRoles); return userRepository.save(user); }
补充代码
StringListConverter转换器代码
import jakarta.persistence.AttributeConverter; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; public class StringListConverter implements AttributeConverter<List<String>, String> { @Override public String convertToDatabaseColumn(final List<String> strings) { return String.join(",", strings); } @Override public List<String> convertToEntityAttribute(final String s) { return Arrays.asList(s.split(",")).stream() .map(String::trim) .collect(Collectors.toList()); } }
UserRepository代码
@Repository interface UserRepository extends JpaRepository<User, String> { }
原因分析
核心原因是Spring Boot 3对应的Hibernate 6版本,对@Convert修饰的集合类型属性的脏检查逻辑发生了变化:
- 在Hibernate 5(Spring Boot 2.7.x默认版本)中,对于通过
@Convert转换的集合属性,框架会特殊处理,能够检测到集合内部元素的增删改操作,从而触发数据库更新。 - 升级到Hibernate 6后,这类被转换器处理的集合属性会被视为简单值类型(而非集合关联),Hibernate只会检测属性的引用是否发生变化(比如是否通过
setRoles()传入了新的List对象),不会追踪集合内部元素的修改。即使你修改了集合的内容,只要集合对象的引用没变,Hibernate就认为属性没有被修改,不会生成更新语句。
另外需要注意:你的StringListConverter中,convertToEntityAttribute方法返回的列表如果是不可变列表(比如Java 16+中Collectors.toList()默认返回不可变列表),调用removeAll/addAll会直接抛出异常,但你未遇到该问题,说明当前使用的Java版本返回的是可变列表,但这并不影响Hibernate的脏检查逻辑。
解决方案
- 保持当前有效写法:每次修改集合内容时,创建新的List对象,通过
setRoles()更新属性引用,触发Hibernate的脏检查。这是最稳妥、符合Hibernate 6规范的写法。 - 修改转换器返回可变列表:将
StringListConverter的convertToEntityAttribute方法改为返回ArrayList,确保列表可变,但这仍需要配合setRoles()修改引用才能触发更新,示例如下:
@Override public List<String> convertToEntityAttribute(final String s) { return new ArrayList<>(Arrays.asList(s.split(",")).stream() .map(String::trim) .collect(Collectors.toList())); }
- 手动触发脏检查:在修改集合内容后,通过
EntityManager手动标记实体为脏状态,示例如下:
@Autowired private EntityManager entityManager; public User updateUser(String username, User updatedUser) { User user = this.userRepository.findById(username).orElseThrow(); user.getRoles().removeAll(List.of("user", "admin")); user.getRoles().addAll(updatedUser.getRoles()); entityManager.detach(user); entityManager.merge(user); return userRepository.save(user); }
不过这种写法不如第一种简洁,推荐优先使用第一种方案。
内容的提问来源于stack exchange,提问作者Bram
相关产品推荐
相关产品推荐

