Spring Data中Embeddable对象重复插入问题求助
让我们一步步拆解你遇到的这个问题,以及对应的解决办法:
核心原因拆解
1. Spring Session的会话持久化触发意外操作
你用了Spring Session,它会在请求结束时序列化会话里的所有属性并存入存储(比如Redis或数据库)。如果你的实体A被放到了HttpSession中,Jackson序列化A的过程会触发两个关键问题:
- 你的
Set<B> bs是@ElementCollection,默认是懒加载的。Jackson遍历集合序列化时会强制初始化它,而此时原事务已经结束,Hibernate Session本应关闭,但Spring Session的序列化过程可能在新的上下文里重新关联实体,触发脏检查。 - 游离状态的实体(事务结束后脱离Hibernate Session的对象)被重新关联后,Hibernate会误以为集合元素有变更,从而执行额外插入。
2. Embeddable类B的equals/hashCode缺失
这是最致命的问题!你的B类没重写equals和hashCode:
- 虽然你代码里用
bs.add(b)的返回值判断重复,但因为没有正确的equals逻辑,Set其实无法识别相同业务属性的B对象,后续Hibernate管理集合时也会混乱。 - Hibernate处理
@ElementCollection时,完全依赖Embeddable的equals/hashCode来跟踪元素变化,没有正确实现的话,它会把同一个业务逻辑的B当成新对象重复插入。
3. 事务结束后的实体状态风险
你的@Transactional方法执行完后,A从持久化状态变成游离状态。如果此时它还在Session里,Jackson序列化时访问懒加载集合,可能触发Hibernate代理初始化,甚至重新打开Session进行脏检查,最终导致重复插入。
具体解决方案
第一步:给B类重写equals和hashCode
这是必须先做的核心修复,用业务唯一字段(这里是name,因为你通过name查找B,且它是非空约束字段)来实现:
@Embeddable public class B { @Column(nullable = false) private String name; @Temporal(TemporalType.TIMESTAMP) @Column(nullable = false) private Date creationTimestamp; // 构造方法、getter/setter省略 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; B b = (B) o; return Objects.equals(name, b.name); } @Override public int hashCode() { return Objects.hash(name); } }
提示:如果
name不是业务唯一的,要换成真正唯一的字段组合。
第二步:不要在Session里存持久化实体
持久化实体(哪怕是游离状态)不适合放在HttpSession中,因为它们带Hibernate代理,序列化时容易触发意外数据库操作。替代方案:
- 把需要存Session的数据转成DTO(只保留必要字段,比如id、name等),序列化DTO而非整个实体。
- 如果必须存实体,要在事务内提前初始化所有懒加载集合,然后调用
EntityManager.detach(a)让它彻底脱离Hibernate管理。
第三步:检查Open Session In View(OSIV)配置
很多Spring Boot项目默认开启OSIV,它会让Hibernate Session在整个请求周期保持打开,包括Spring Session序列化阶段,这会加剧游离实体的重新关联问题。可以尝试关闭它(确认不影响其他功能的前提下):
spring.jpa.open-in-view=false
关闭后,事务结束Session就会关闭,Jackson序列化未初始化集合会抛异常,这能帮你定位到哪里在访问懒加载数据。此时需要在事务内提前初始化集合:
@Transactional public void yourBusinessMethod(Long aId) { Optional<A> aOpt = aRepository.findById(aId); A a = aOpt.orElseThrow(() -> new RuntimeException("A not found")); // 提前初始化集合,避免后续序列化时触发懒加载 Hibernate.initialize(a.getBs()); // 你的业务逻辑 B b = a.getB(...); if(b == null) { b = new B(...); a.addB(b); } aRepository.save(a); }
第四步:配置Jackson处理Hibernate懒加载对象
如果必须在Session里存实体,可以用Jackson的Hibernate模块来避免触发懒加载:
- 添加依赖:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-hibernate5</artifactId> </dependency>
- 配置ObjectMapper:
@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper(Hibernate5Module hibernate5Module) { return new ObjectMapper() .registerModule(hibernate5Module); } @Bean public Hibernate5Module hibernate5Module() { Hibernate5Module module = new Hibernate5Module(); // 禁用懒加载对象的序列化,避免触发初始化 module.disable(Hibernate5Module.Feature.USE_TRANSIENT_ANNOTATION); module.enable(Hibernate5Module.Feature.SERIALIZE_IDENTIFIER_FOR_LAZY_NOT_LOADED_OBJECTS); return module; } }
这样Jackson会跳过未初始化的懒加载集合,不会触发Hibernate的额外操作。
总结
问题的根源是:B类缺少正确的equals/hashCode导致Hibernate集合管理混乱,加上Spring Session序列化持久化实体时触发了意外的Hibernate操作。按照上面的步骤逐一修复,就能解决重复插入的问题。
内容的提问来源于stack exchange,提问作者manash

