spring-data-jpa: `NullPointerException` in version 3.0.2 when using modifying native queries or `SELECT` queries that Spring Data cannot parse

Commit 64b5a22a24b42e8660f441897b001e4b594fff1c introduced a null pointer exception in line 620 of QueryUtils. If a native query is not a select, but, for example, a delete, “variable” will be null, leading to said exception.

Example code in repository:

	@Modifying
	@Query(value = "delete from some_table where id in :ids", nativeQuery = true)
	void deleteStuffFromSomeTable(@Param("ids") Collection<UUID> ids);

Relevant part of the trace:

Caused by: java.lang.NullPointerException: Cannot invoke "String.contains(java.lang.CharSequence)" because "variable" is null
	at org.springframework.data.jpa.repository.query.QueryUtils.createCountQueryFor(QueryUtils.java:620)
	at org.springframework.data.jpa.repository.query.DefaultQueryEnhancer.createCountQueryFor(DefaultQueryEnhancer.java:49)
	at org.springframework.data.jpa.repository.query.StringQuery.deriveCountQuery(StringQuery.java:111)
	at org.springframework.data.jpa.repository.query.AbstractStringBasedJpaQuery.<init>(AbstractStringBasedJpaQuery.java:82)
	at org.springframework.data.jpa.repository.query.NativeJpaQuery.<init>(NativeJpaQuery.java:58)
	at org.springframework.data.jpa.repository.query.JpaQueryFactory.fromMethodWithQueryString(JpaQueryFactory.java:53)
	at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$DeclaredQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:170)
	at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$CreateIfNotFoundQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:252)
	at org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy$AbstractQueryLookupStrategy.resolveQuery(JpaQueryLookupStrategy.java:95)
	at org.springframework.data.repository.core.support.QueryExecutorMethodInterceptor.lookupQuery(QueryExecutorMethodInterceptor.java:111)
	... 79 more

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 21
  • Comments: 32 (4 by maintainers)

Commits related to this issue

Most upvoted comments

The next round of releases is scheduled for in 4 weeks, see https://calendar.spring.io/.

As suggested above, we recommend downgrading to Spring Data JPA 3.0.1 as the 3.0.2 release shipped with a few changes only.

Same with an UPDATE query in 2.7.9:

  @Modifying
  @Query(nativeQuery = true, value = "UPDATE seq_mgmt SET seq_no = :value WHERE seq_id = :key")
  int setNextValue(@Param("key") String key, @Param("value") Long value);

@otavioprado You don’t even need that full import, this should be enough:

<properties>
  <spring-data-bom.version>2021.2.9</spring-data-bom.version><!-- Temporary override until Spring Boot 2.7.10 -->
</properties>

If you’re on Spring Boot 3, you can upgrade to Spring Boot 3.0.4, which was just released.

If you use Spring Boot 2.7.9: Spring Data BOM 2021.2.9 has been released, with Spring Data 2.7.9. This will be included in Spring Boot 2.7.10 (scheduled for March 23), but you can temporarily override spring-data-bom.version to 2021.2.9 for now.

I am surprised there was no test that could have caught this.

In my case, the NPE occurs querying a sequence value.

@Query(value = "SELECT nexval('public.my_sequence')", nativeQuery = true)
Integer getNextvalMySequence();

@ChristianCiach please dont take it wrong we love spring and community behind it. This just caught us off guard which never affected us before in this manner. When i meant i was surprised it just expressed the feeling nothing more than that.

@SabareeshGC I usually don’t find these kinds of comments very productive. But I agree that it is fair to be a bit surprised that a project of this size did not contain a single unit test that just happens to execute a mutating native query. But, well, now there is one 😃

This also once again shows the importance of static code analysis. Eclipse (with my rather strict compiler settings) immediate marked the buggy line as a potential NPE, and tools like NullAway would surely do the same. Seeing that there are nullable annotations available in org.springframework.lang, maybe accidents like these could be prevented in the future by utilizing static code analysis.

The next round of releases is scheduled for in 4 weeks, see https://calendar.spring.io/. As suggested above, we recommend downgrading to Spring Data JPA 3.0.1 as the 3.0.2 release shipped with a few changes only.

Unfortunately this is easier said than done when using a bom that wraps around the spring-boot one because there is no way to declare the downgrade in the bom itself. Instead every consumer would have to explicitly downgrade using a custom gradle resolution strategy which is error prone and not practical when you have a large number of consumers.

We just skip this release for both Spring Boot 2.7.9 and 3.0.3 - the work it takes “fixing” the NPE for us is not worth it.

Workaround for people using Gradle if you don’t want to downgrade Spring Boot altogether:

configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        if (details.requested.name == 'spring-data-jpa' && details.requested.version == '3.0.2') {
            details.useVersion '3.0.1'
            details.because 'https://github.com/spring-projects/spring-data-jpa/issues/2812'
        }
    }
}

Same here, with non CRUD native query (spring-jpa 2.7.8)

@Query(value = """
select count(1) as totalItems,
sum(case when myField is null then 0 else 1 end) as notNullItems
from myClass
""", nativeQuery = true)

Found a workaround by adding a fictive countQuery

@Query(value = """
select count(1) as totalItems,
sum(case when myField is null then 0 else 1 end) as notNullItems
from myClass
""", countQuery = "select 1", nativeQuery = true)

Also affects 2.7.8

A simple test for QueryEnhancerTckTests that would reproduce issue:

    @Test
    void nativeUpdateQueryThrowsException() {
        DeclaredQuery declaredQuery = DeclaredQuery.of("delete from some_table where id in :ids", true);
        QueryEnhancer enhancer = createQueryEnhancer(declaredQuery);
        assertThatNoException().isThrownBy(() -> enhancer.createCountQueryFor(null));
    }

“Offending” query itself can be also:

  • UPDATE table_name SET column_name = a_value"
  • BEGIN ... WHEN MATCHED THEN ... END;

Works on 2.7.10. Thank you all!

This has been patched in main (3.1.0-SNAPSHOT) and backported to 3.0.x and 2.7.x).