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

为何此模式下保存OneToMany关联数据时DataJpaTest测试失败?

Hibernate JOINED继承下关联集合加载失败问题解决

问题背景

使用Hibernate的JOINED继承策略定义了Animal父类和Dog子类,Animal与Toy是一对多关联。在DogService中先保存Dog再保存关联的Toy,测试时断言Dog的toys集合大小为1失败;给测试添加@Transactional(propagation = Propagation.NEVER)后又出现懒加载异常。

实体与业务代码

Dog实体

@Entity
@Data
@SuperBuilder(toBuilder = true)
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public class Dog extends Animal {
    String barkType;
}

Animal实体(JOINED继承)

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Data
@SuperBuilder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
public class Animal {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Type(type = "uuid-char")
    private UUID id;

    @OneToMany(cascade = CascadeType.REMOVE)
    @JoinColumn(name = "animalId", referencedColumnName = "id", insertable = false, updatable = false)
    @Builder.Default
    private List<Toy> toys = new ArrayList<>();
}

Toy实体

@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Toy {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Type(type = "uuid-char")
    private UUID id;
    
    @Type(type = "uuid-char")
    private UUID animalId;
    
    private String shape;
}

DogService业务代码

@Service
@AllArgsConstructor
public class DogService {

    DogRepository repository;
    ToyRepository toyRepository;

    @Transactional
    public Dog saveDogDTO(DogDTO dogDTO) {
        Dog entity = Dog.builder()
                .barkType(dogDTO.getBarkType())
                .build();

        repository.save(entity);
        toyRepository.save(Toy.builder()
                .shape(dogDTO.getToyShape())
                .animalId(entity.getId())
                .build());

        return entity;
    }
}

测试用例及失败信息

测试代码:

@DataJpaTest
class DogServiceTests {

    private DogService dogService;

    @Autowired
    private DogRepository dogRepository;

    @Autowired
    private ToyRepository toyRepository;

    @Test
    void save_not_working_example() {
        dogService = new DogService(dogRepository, toyRepository);

        var dogDTO = DogDTO.builder()
                .barkType("big bark")
                .toyShape("some shape")
                .build();

        var savedDog = dogService.saveDogDTO(dogDTO);

        assertThat(dogRepository.count()).isEqualTo(1);
        assertThat(toyRepository.count()).isEqualTo(1);

        var findByIdResult = dogRepository.findById(savedDog.getId());
        assertThat(findByIdResult.get().getToys()).hasSize(1);
    }
}

第一次失败信息:

Expected size: 1 but was: 0 in:
[]
java.lang.AssertionError: 
Expected size: 1 but was: 0 in:
[]

添加@Transactional(propagation = Propagation.NEVER)后失败信息:

failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.example.datajpatest.demo.models.Animal.toys, could not initialize proxy - no Session

解决方案

方案1:修正双向关联映射(推荐)

原代码中Animal与Toy是单向关联,且未配置级联保存,导致保存Toy后,缓存中的Dog实例未更新关联集合。改为双向关联并配置级联,让JPA自动维护关联关系:

  1. 修改Animal类的@OneToMany注解:
@OneToMany(mappedBy = "animal", cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
@Builder.Default
private List<Toy> toys = new ArrayList<>();
  1. 修改Toy类,添加@ManyToOne关联:
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Toy {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Type(type = "uuid-char")
    private UUID id;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "animalId", referencedColumnName = "id")
    private Animal animal;
    
    private String shape;
}
  1. 简化DogService的保存逻辑,无需单独保存Toy:
@Transactional
public Dog saveDogDTO(DogDTO dogDTO) {
    Dog entity = Dog.builder()
            .barkType(dogDTO.getBarkType())
            .build();
    Toy toy = Toy.builder()
            .shape(dogDTO.getToyShape())
            .animal(entity)
            .build();
    entity.getToys().add(toy);
    // 保存Dog时会级联保存Toy
    repository.save(entity);
    return entity;
}

方案2:刷新持久化上下文并重新查询实体

如果不想修改关联映射,可在测试中手动刷新缓存,确保查询到最新的关联数据:

修改测试代码:

@Test
void save_not_working_example() {
    dogService = new DogService(dogRepository, toyRepository);

    var dogDTO = DogDTO.builder()
            .barkType("big bark")
            .toyShape("some shape")
            .build();

    var savedDog = dogService.saveDogDTO(dogDTO);

    assertThat(dogRepository.count()).isEqualTo(1);
    assertThat(toyRepository.count()).isEqualTo(1);

    // 刷新上下文并清除缓存,确保重新查询时获取最新数据
    dogRepository.flush();
    dogRepository.clear();

    var findByIdResult = dogRepository.findById(savedDog.getId());
    assertThat(findByIdResult.get().getToys()).hasSize(1);
}

解决懒加载异常

若使用Propagation.NEVER让测试脱离事务,可通过以下方式避免懒加载异常:

  • 方式A:使用JOIN FETCH查询
    在DogRepository中添加自定义查询方法,关联加载toys集合:
@Repository
public interface DogRepository extends JpaRepository<Dog, UUID> {
    @Query("SELECT d FROM Dog d JOIN FETCH d.toys WHERE d.id = :id")
    Optional<Dog> findByIdWithToys(@Param("id") UUID id);
}

测试中调用该方法:

var findByIdResult = dogRepository.findByIdWithToys(savedDog.getId());
  • 方式B:修改关联为立即加载(不推荐)
    将Animal类中的@OneToMany添加fetch = FetchType.EAGER,但会导致每次查询Animal都加载toys,影响性能。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 08:15:43