Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Data JPA aggregate functions on an empty resultset

I am working on a project involving Spring and JPA/Hibernate. The database driver used in my development environment is H2. My application has a page that displays statistics, one example of such a statistic is the average age of my users. However, when I try to fetch the average age using JPQL, I receive an exception

Result must not be null!

Assume for simplicity reasons that I store age as an integer on every User object (in my application this is of course not the case, but that's not important for my problem).

User model

@Entity
public class User implements Identifiable<Long> {
    private int age;
    // more fields and methods, irrelevant
}

User repository

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

I cannot seem to figure out why calling UserRepository#averageAge(); is throwing the exception. I have tried replacing the function AVG in the query by COUNT and this behaves as expected. I have also tried to use an SQL query and setting nativeQuery = true in the annotation, yet to no avail. I can ofcourse solve it by fetching all the users and calculate the average age in plain Java, but this wouldn't be very efficient.

Stacktrace:

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

Solved

The exception was caused by the fact that AVG() returns null when performed on an empty table. I fixed it by modifying the query (inspired by the answer to this question) as follows:

@Query("SELECT coalesce(AVG(u.age), 0) FROM #{#entityName} u")
long averageAge();
like image 644
Pieter De Clercq Avatar asked Sep 16 '18 20:09

Pieter De Clercq


People also ask

Does Spring JPA return empty list or null?

The normal behavior is indeed returning an empty list if no results are found. If a List<Object> is the return value of the method in the defined interface, the method should never return Null .

What is difference between JpaRepository and CrudRepository?

CrudRepository provides CRUD functions. PagingAndSortingRepository provides methods to do pagination and sort records. JpaRepository provides JPA related methods such as flushing the persistence context and delete records in a batch.

How do I stop Spring data JPA from doing a select before a save?

The solution is to use @javax. persistence. Version on a new versionNumber column in all the tables. If you have a parent and child table then use @Version column in all the entity classes.

What does findById return in JPA?

Its findById method retrieves an entity by its id. The return value is Optional<T> . Optional<T> is a container object which may or may not contain a non-null value. If a value is present, isPresent returns true and get returns the value.


2 Answers

If you use Spring Data, and if your method returns null when Hibernate can't find a match, make sure you add @org.springframework.lang.Nullable to your method signature:

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

This is because Spring Data checks the nullability of your method, and if the annotation is missing, it's going to enforce that you always need to return an object:

/* 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... */

I used Spring Boot version 2.1.1.RELEASE and Spring Data 2.1.4.RELEASE.

like image 168
Stefan van den Akker Avatar answered Sep 25 '22 11:09

Stefan van den Akker


It seems that the EmptyResultDataAccessException exception is thrown when a result from a query was expected to have at least one row (or element) but none was returned.

Related documentation about this can be found here.

I would suggest to run the same query this attempts to run in order to further validate this theory. Now the good question's what to do with this.

You have two options. Either catch the EmptyResultDataAccessException exception at your calling point and handle it directly in there or alternatively you can have an ExceptionHandler which will be tasked with handling such exceptions.

Both ways of handling this, should be OK and you may choose between each depending on your scenario.

like image 38
akortex Avatar answered Sep 25 '22 11:09

akortex