Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPE in JpaQueryCreator when query result is projection-based interface #2408

Closed
cigaly opened this issue Jan 15, 2022 · 11 comments
Closed

NPE in JpaQueryCreator when query result is projection-based interface #2408

cigaly opened this issue Jan 15, 2022 · 11 comments
Assignees
Labels
type: regression A regression from a previous release

Comments

@cigaly
Copy link
Contributor

cigaly commented Jan 15, 2022

Bug has been introduced in Spaing Data JPA 2.6.1 which causes NPE to be thrown in org.springframework.data.jpa.repository.query.JpaQueryCreator when query result is projection-based interface or generic class with projection-based interface as type parameter.

Attached is simple diff file that changes org.springframework.data.jpa.repository.sample.UserRepository to demonstrate such behaviour.

While testing, if noticed that NPE is not thrown when line 181 in org.springframework.data.jpa.repository.query.JpaQueryCreator is changed from

Class<?> typeToRead = returnedType.getTypeToRead();

to

Class<?> typeToRead = returnedType.getReturnedType();

but I do not know is this can be proper fix or not.

bug.diff.txt

[Additional "feature" is that this bug is causing test to throw NPE's in infinte loop]

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Jan 15, 2022
@christophstrobl christophstrobl added type: regression A regression from a previous release and removed status: waiting-for-triage An issue we've not yet triaged labels Jan 17, 2022
@lukaseckert
Copy link

Confirmed after updating spring-boot from 2.6.2 to 2.6.3 with Repository Code like

public interface MyProjection {
    String getField1();
    String getField2();
    // ... more simple string getters
}

public interface MyJpaRepository {
    Optional<MyProjection> findFirstByField1Equals(String field1);
}

Causing a NPE since typeToRead is null in query = typeToRead.isInterface() in JpaQueryCreator.java:181

@everflux
Copy link

A possible workaround is to use an abstract class instead of an interface type.

@cigaly
Copy link
Contributor Author

cigaly commented Jan 22, 2022

Method ReturnedType.getReturnedType() must not be used with interface. In Spring Data Commons exists test org.springframework.data.repository.query.ReturnedTypeUnitTests.detectsProjection which is explicitely requesting that getReturnedType is returning null on interfaces:

void detectsProjection() throws Exception {

	ReturnedType type = getReturnedType("findAllProjection");

	assertThat(type.getTypeToRead()).isNull();
	assertThat(type.getInputProperties()).contains("lastname");
}

It seems that (proper?) solution of this problem will be to replace line (which will throw NPE whenever type to read is interface)

query = typeToRead.isInterface()

with line

query = returnedType.needsCustomConstruction()
JpaQueryCreator.java.diff.txt

@achimgrimm
Copy link

Hi,
I was able to work around this using the @Value annotation.

The example above would look like this:

public interface MyProjection {
    @org.springframework.beans.factory.annotation.Value("#{target.field1}")
    String getField1();
    @org.springframework.beans.factory.annotation.Value("#{target.field1}")
    String getField2();
    // ... more simple string getters
}

@jonilsonds9
Copy link

Hi guys, the exact same thing happened to me too.

Repositories with closed projections break with the same error:

Caused by: java.lang.NullPointerException: Cannot invoke "java.lang.Class.isInterface()" because "typeToRead" is null
	at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:181) ~[spring-data-jpa-2.6.1.jar:2.6.1]
	at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:152) ~[spring-data-jpa-2.6.1.jar:2.6.1]
	at org.springframework.data.jpa.repository.query.JpaQueryCreator.complete(JpaQueryCreator.java:59) ~[spring-data-jpa-2.6.1.jar:2.6.1]
	at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:95) ~[spring-data-commons-2.6.1.jar:2.6.1]
	at org.springframework.data.repository.query.parser.AbstractQueryCreator.createQuery(AbstractQueryCreator.java:81) ~[spring-data-commons-2.6.1.jar:2.6.1]
	at org.springframework.data.jpa.repository.query.PartTreeJpaQuery$QueryPreparer.<init>(PartTreeJpaQuery.java:217) ~[spring-data-jpa-2.6.1.jar:2.6.1]
	at org.springframework.data.jpa.repository.query.PartTreeJpaQuery.<init>(PartTreeJpaQuery.java:92) ~[spring-data-jpa-2.6.1.jar:2.6.1]
	... 61 common frames omitted

I'm trying to upgrade from spring-boot 2.6.2 to 2.6.3 with Java 17 but I can't because of this error.

The temporary solution in my case is the same as reported by @achimgrimm,

Or annotating the repository method with @Query and passing a manual query with the correct alias:

@Query("select t.id as id, t.name as name from Table t t.id = :id")
    List<ProjectionName> findAllByProjection_Id(@Param("id") Long id);

And the projection with the following structure:

public interface ProjectionName {
    Long getId();
    Long getName();
}

But it's not as interesting as before.

odrotbohm added a commit that referenced this issue Jan 25, 2022
With the commit for GH-2363, we introduced an unguarded call to ReturnedType.getTypeToRead(), which could return null under certain conditions. Unfortunately Spring Data JPA dod not contain a test case that triggered that scenario.

This commit switches to ReturnedType.getReturnedType() which is non-nullable and lets us inspect the calls results for interfaces, which we need to create the JPA query properly.

Fixes GH-2408
odrotbohm added a commit that referenced this issue Jan 25, 2022
With the commit for GH-2363, we introduced an unguarded call to ReturnedType.getTypeToRead(), which could return null under certain conditions. Unfortunately Spring Data JPA dod not contain a test case that triggered that scenario.

This commit switches to ReturnedType.getReturnedType() which is non-nullable and lets us inspect the calls results for interfaces, which we need to create the JPA query properly.

Fixes GH-2408
@odrotbohm
Copy link
Member

This should be fixed now in 2.6, 2.7 and 3.0 snapshots.

@akuma8
Copy link

akuma8 commented Feb 6, 2022

This should be fixed now in 2.6, 2.7 and 3.0 snapshots.

Hi,
Sorry to reopen this but I still have the issue in the 2021.2.0-M2 milestone.
I migrated from Spring Boot 2.6.3 to 2.7.0-M1, Spring Data Jpa version is 2.7.0-M2 and the issue is still there.
Should I downgraded the Spring Data's BOM version until the released of Spring Boot 2.7.0?

@odrotbohm
Copy link
Member

The fix was applied post 2.7 M2. It’s gonna be shipped with the upcoming M3.

@jase52476
Copy link

I'm getting this after upgrading spring-boot-starter-parent 2.7.8 -> 2.7.9, it's pulling spring-data-jpa 2.7.8 from 2.7.7 and my repository that was working previously now is having a NPE :

Caused by: java.lang.NullPointerException: null at org.springframework.data.jpa.repository.query.QueryUtils.createCountQueryFor(QueryUtils.java:607) at org.springframework.data.jpa.repository.query.DefaultQueryEnhancer.createCountQueryFor(DefaultQueryEnhancer.java:49) at org.springframework.data.jpa.repository.query.StringQuery.deriveCountQuery(StringQuery.java:119)

on line 594, this string is created and it defaults to null if there's no match:

String variable = matcher.matches() ? matcher.group(VARIABLE_NAME_GROUP_INDEX) : null;

But the null is not handled on line 607:

if (nativeQuery && (variable.contains(",") || "*".equals(variable))) {
				replacement = "1";	

Should I make a new issue? Sorry, I've never had to open a ticket with Spring. I searched the open issues and there was one other but didn't seem to be the same issue.

If I switch back to spring-boot 2.7.8 and do a mvn clean install it starts up just fine with no modification of my repository.

@cigaly
Copy link
Contributor Author

cigaly commented Mar 1, 2023

No, this is something completely diferent. I've met problem you are talking about in version 3.0.something. Most likely your (native) query which is causing NPE is not select, but something that is updating data. For some reason, Spring Data Jpa is trying to create count query for that (modifying) query (why?), but regular expression that is used can not match query string (no select). I can only guess that authors of that part of code never expected such situation (why should modifying query have count query?) and did not checked for null.

It seems that this problem has already been reported #2824 and #2812 ... and #2822 ...

@jase52476
Copy link

jase52476 commented Mar 1, 2023

No, this is something completely diferent. I've met problem you are talking about in version 3.0.something. Most likely your (native) query which is causing NPE is not select, but something that is updating data. For some reason, Spring Data Jpa is trying to create count query for that (modifying) query (why?), but regular expression that is used can not match query string (no select). I can only guess that authors of that part of code never expected such situation (why should modifying query have count query?) and did not checked for null.

Thanks for your help. It's actually a select going into a projection-based interface. There is a join on another table and a group by, but I've verified all the fields I'm selecting, and matched them to the type, but it seems there's an unhandled null in the new code.

Here's my select statement with the nativeQuery = true. I've modified the field and table names to make them generic.

SELECT u.field1, u.field2, u.field3, u.field4 as myCustomField4, u.field5 as myCustomField5, u.field6 as myCustomField6 
FROM "table1" u 
LEFT JOIN table2 l ON l.field1 = u.field1 
WHERE u.field1 = :myVariable1 AND l.field2 = :myVariable2 
GROUP BY u.field1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: regression A regression from a previous release
Projects
None yet
Development

No branches or pull requests