为何此模式下保存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自动维护关联关系:
- 修改
Animal类的@OneToMany注解:
@OneToMany(mappedBy = "animal", cascade = {CascadeType.REMOVE, CascadeType.PERSIST}) @Builder.Default private List<Toy> toys = new ArrayList<>();
- 修改
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; }
- 简化
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
相关产品推荐
相关产品推荐

