Spring Data JPA在空结果集上的聚合函数

10
我正在处理一个涉及SpringJPA/Hibernate的项目。在我的开发环境中使用的数据库驱动程序是H2。我的应用程序有一个显示统计信息的页面,其中一个例子是用户的平均年龄。然而,当我尝试使用JPQL获取平均年龄时,我收到了一个异常。
Result must not be null!

假设为了简化,我在每个用户对象上将年龄存储为整数(在我的应用程序中当然不是这种情况,但这对我的问题并不重要)。
用户模型
@Entity
public class User implements Identifiable<Long> {
    private int age;
    // more fields and methods, irrelevant
}

用户存储库

@Repository
public interface UserRepository extends CrudRepository<User, Long> {
    @Query("SELECT AVG(u.age) FROM #{#entityName} u")
    long averageAge();
}

我似乎找不出为什么调用UserRepository#averageAge();会抛出异常。 我尝试将查询中的函数AVG替换为COUNT,并且这行为符合预期。 我还尝试使用SQL查询并在注释中设置nativeQuery = true,但无济于事。 当然,我可以通过获取所有用户并在普通Java中计算平均年龄来解决此问题,但这并不是很有效。 堆栈跟踪:
Caused by: org.springframework.dao.EmptyResultDataAccessException: Result must not be null!
    at org.springframework.data.repository.core.support.MethodInvocationValidator.invoke(MethodInvocationValidator.java:102)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy150.averageAge(Unknown Source)
    at my.test.application.StatisticsRunner.run(StatisticsRunner.java:72)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:809)
    ... 30 more

已解决

异常是由于在空表上执行AVG()会返回null引起的。我通过修改查询语句(灵感来源于这个问题的答案)来解决这个问题:

@Query("SELECT coalesce(AVG(u.age), 0) FROM #{#entityName} u")
long averageAge();

你能贴出堆栈跟踪吗? - akortex
@Aris_Kortex添加了堆栈跟踪(缺失部分是“无法加载ApplicationContext”,我想这与错误无关)。 - Pieter De Clercq
3个回答

6

如果你使用Spring Data,当Hibernate无法找到匹配项时,如果你的方法返回null,请确保在方法签名中添加@org.springframework.lang.Nullable

public interface SomeRepositoryCustom {
    @org.springframework.lang.Nullable
    public Thing findOneThingByAttr(Attribute attr) {
        /* ...your logic here... */
    }
}

这是因为Spring Data会检查你的方法是否可为空,如果缺少注释,它将强制要求你始终返回一个对象:
/* org.springframework.data.repository.core.support.MethodInvocationValidator */

@Nullable
@Override
public Object invoke(@SuppressWarnings("null") MethodInvocation invocation) throws Throwable {
    /* ...snip... */

    if (result == null && !nullability.isNullableReturn()) {
        throw new EmptyResultDataAccessException("Result must not be null!", 1);
    }

    /* ...snip... */

我使用的是Spring Boot版本2.1.1.RELEASE和Spring Data 2.1.4.RELEASE


2
似乎当查询结果至少应该返回一行(或一个元素),但没有返回任何内容时,会抛出EmptyResultDataAccessException异常。
有关此问题的相关文档可以在此处找到。
建议运行与此尝试运行的相同查询,以进一步验证这个理论。现在的好问题是要怎么做。
您有两个选择。要么在调用点捕获EmptyResultDataAccessException异常并直接在其中处理,要么可以有一个ExceptionHandler来处理这种异常。
处理这个问题的两种方式都可以,并且您可以根据您的情况选择其中之一。

我选择了第一种选项,因为它不算是异常情况;当没有数值时,它应该返回0,编写一个万能的异常处理程序并不总是适用。谢谢! - Pieter De Clercq
不错。是的,对我来说,使用异常来表示查询结果为空的方式非常愚蠢。理想情况下,我会将签名更改为 Long 而不是原始类型,并在未找到结果时返回 null。我认为 null 是空结果集的更好的逻辑表示,而不是返回 0。例如,如果您有记录其平均年龄为 0 的记录会发生什么?因此,对象和 null 检查应该是更好的选择。只需将此操作委托给服务,让调用者执行 null 检查即可。 - akortex
使用 Long 替代 long 会产生相同的异常。 - Pieter De Clercq
1
一个更好的解决方案可能是使用类似 SELECT ISNULL(SUM(u.age), 0) 的结构,但我不确定 H2 是否支持这个。 - Pieter De Clercq
1
COALESCE也可以是一个选项 http://www.h2database.com/html/functions.html#coalesce - akortex
显示剩余4条评论

0

我不完全确定,但我认为问题是因为返回类型是long导致的,也许你应该使用Long包装类,long不允许为空,因为它是一个原始数据类型,尝试更改为

@Query("SELECT AVG(u.age) FROM #{#entityName} u")
Long averageAge();

请阅读有关异常本身的文档,您就会明白为什么会出现这种情况。 - akortex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接