DTO转实体后保存Hibernate抛TransientPropertyValueException求助
首先,我一眼就看出问题出在ModelMapper处理循环引用时的对象实例复用问题上,手动创建实体和DTO转换后的实体,在双向关联的对象引用上有本质区别:
问题根源
你手动创建实体时,所有双向关联的对象都是同一个实例:
car.setCarDetails(carDetails)和carDetails.setCar(car)用的是同一个car和carDetails对象carDetails.setBumper(bumper)和bumper.setCarDetails(carDetails)用的也是同一个carDetails和bumper对象
这种情况下,Hibernate的级联保存链条是完整的:保存car时会级联保存carDetails,保存carDetails时又会级联保存bumper,而且bumper引用的carDetails就是那个正在被级联保存的实例,Hibernate能识别它的状态。
但ModelMapper默认处理循环引用的DTO时,会为每个嵌套的DTO创建全新的实体实例:
- 转换
CarDto到Car时,会创建一个Car实例和对应的CarDetails实例 - 转换
CarDetailsDto里的BumperDto时,会创建一个Bumper实例,然后转换BumperDto里的CarDetailsDto时,又会创建一个新的CarDetails实例给Bumper.carDetails
这个新的CarDetails实例不在级联保存的链条里,是个未被管理的瞬态对象,所以Hibernate抛出了TransientPropertyValueException。
解决方案
这里提供三种可行的解决办法,你可以根据自己的需求选择:
方案1:配置ModelMapper处理循环引用,复用已转换对象
修改你的ModelMapper Bean配置,开启循环引用处理,让它遇到重复的DTO引用时,复用已经转换过的实体实例:
@Bean public ModelMapper modelMapper() { ModelMapper modelMapper = new ModelMapper(); Configuration config = modelMapper.getConfiguration(); config.setFieldMatchingEnabled(true) .setFieldAccessLevel(AccessLevel.PRIVATE) .setAmbiguityIgnored(true) // 处理循环引用:遇到循环时返回父级转换的目标对象 .setCircularReferenceHandler(context -> context.getParent().getDestination()); return modelMapper; }
这样配置后,ModelMapper在转换BumperDto里的CarDetailsDto时,会直接使用之前转换好的CarDetails实例,而不是新建一个,关联关系就和手动创建的实体一致了。
方案2:转换后手动修复双向关联
如果不想修改全局的ModelMapper配置,可以在转换完成后,手动修正所有双向关联的引用,确保它们指向同一个实例:
@Test public void saveCarByDto() { CarDto carDto = new CarDto(); CarDetailsDto carDetails = new CarDetailsDto(); BumperDto bumper = new BumperDto(); carDto.setCarDetails(carDetails); carDetails.setCar(carDto); carDetails.setBumper(bumper); bumper.setCarDetails(carDetails); Car car = modelMapper.map(carDto, Car.class); // 手动修复双向关联 CarDetails convertedCarDetails = car.getCarDetails(); convertedCarDetails.setCar(car); // 确保CarDetails关联当前的Car实例 Bumper convertedBumper = convertedCarDetails.getBumper(); convertedBumper.setCarDetails(convertedCarDetails); // 确保Bumper关联当前的CarDetails实例 Car savedEntity = carRepository.save(car); }
这种方式比较直接,适合临时处理特定场景的转换问题。
方案3:优化DTO结构,移除不必要的循环引用
从业务角度看,创建汽车的场景下,DTO其实不需要反向关联(比如CarDetailsDto里不需要CarDto,BumperDto里不需要CarDetailsDto),因为前端传递数据时只需要单向的嵌套结构。修改DTO如下:
@Data public class CarDto { private Long id; private CarDetailsDto carDetails; } @Data public class CarDetailsDto { private Long id; private BumperDto bumper; // 移除private CarDto car; } @Data public class BumperDto { private Long id; // 移除private CarDetailsDto carDetails; }
然后转换后再手动设置反向关联,和方案2类似,但DTO结构更简洁,也避免了ModelMapper处理循环引用的麻烦。
验证
不管用哪种方案,修改后重新运行saveCarByDto测试,Hibernate都能正确识别所有关联对象的状态,级联保存链条完整,就能顺利通过测试了。
内容的提问来源于stack exchange,提问作者user3562027

