You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Data中Embeddable对象重复插入问题求助

问题分析与解决方案:Embeddable集合重复插入与Spring Session/Jackson序列化的关联

让我们一步步拆解你遇到的这个问题,以及对应的解决办法:

核心原因拆解

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模块来避免触发懒加载:

  1. 添加依赖:
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate5</artifactId>
</dependency>
  1. 配置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:00:55