使用Stream.map()替换列表对象时抛出非唯一查询结果异常排查
现有一个元素列表,业务需求如下:
- 若根据列表元素的
name字段可在数据库中查询到对应记录,则使用查询到的记录构造新对象替换列表中原有元素 - 若未查询到匹配记录,则保留列表中原元素不做修改
实现逻辑使用Java Stream编写,示例代码如下:
result = list.stream().map(element -> { Optional<Object> dbRecord = repository.findByName(element.getName()); dbRecord.ifPresent( x -> { x.setSmth(element.getSmth()); x.setSmth1(element.getSmth1()); // 其余属性赋值逻辑 ... } ); return dbRecord.orElse(element); }).collect(Collectors.toList());
调试时观察到所有步骤中repository.findByName()方法均返回Optional.empty(),但执行到return语句返回原element元素时,捕获到如下异常:
org.springframework.dao.IncorrectResultSizeDataAccessException: query did not return a unique result: 2; nested exception is javax.persistence.NonUniqueResultException: query did not return a unique result: 2 at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:385) ~[spring-orm-5.3.14.jar:5.3.14] at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:235) ~[spring-orm-5.3.14.jar:5.3.14] at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:551) ~[spring-orm-5.3.14.jar:5.3.14] at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61) ~[spring-tx-5.3.14.jar:5.3.14] at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:242) ~[spring-tx-5.3.14.jar:5.3.14] at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152) ~[spring-tx-5.3.14.jar:5.3.14] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14] at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:145) ~[spring-data-jpa-2.6.0.jar:2.6.0] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14] at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.14.jar:5.3.14] at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.14.jar:5.3.14] at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215) ~[spring-aop-5.3.14.jar:5.3.14] at com.sun.proxy.$Proxy186.findByName(Unknown Source) ~[?:?] at ru.app.MyService.lambda$processRequest$4(MyService.java:166) ~[my-application-2.0.0.jar:?]
1. 异常抛出点判断错误
异常根本不是在return dbRecord.orElse(element)这行抛出的,看异常栈最上层业务代码调用点:at ru.app.MyService.lambda$processRequest$4(MyService.java:166),这行就是调用repository.findByName(element.getName())的位置。
Spring Data JPA的规则是:返回单个实体/Optional类型的查询方法,只要匹配到的记录数不等于1,就会按如下逻辑处理:匹配0条时正常返回Optional.empty();匹配数大于1时直接抛出IncorrectResultSizeDataAccessException,根本不会构造返回值。
调试时看到所有findByName都返回Optional.empty()是调试器的干扰:IDE调试时默认会自动对断点行上的方法做求值预览,这个预览执行的查询和流实际执行的查询是隔离的,看到的empty是预览求值的结果,不是流实际运行时的结果。实际流执行到某个name对应2条数据库记录的元素时,findByName方法内部直接抛异常,根本走不到后续的ifPresent和return逻辑。
2. Stream.map运行机制理解偏差
Stream.map()是中间操作,调用它的时候不会执行任何传入的lambda逻辑,只会构造一个新的流节点挂在流管道上。只有当最终操作(这里是collect(Collectors.toList()))触发时,才会逐个遍历流中的元素,按顺序应用map里的lambda逻辑:
- 只要某一个元素的lambda执行抛出异常,整个流操作就会立即终止,异常向上抛出
- 不存在“所有findByName都执行完返回empty,再走到return逻辑”的执行顺序,元素是逐个处理的,处理完一个才会处理下一个
之前误以为所有findByName都执行完了才到return步骤,是对Stream懒加载特性不熟悉导致的判断偏差。
- 首先排查数据库中重复的name数据,确认
findByName的查询逻辑是否应该允许返回多条记录,如果允许,把Repository方法返回值改成List<Object>,自行取第一条处理即可 - 如果业务上要求name唯一,先清理脏数据,给name字段加唯一索引避免后续出现重复值
内容的提问来源于stack exchange,提问作者Anton Chaparin

