JpaQueryLookupStrategy shouldn't use exceptions for flow control.

By using exceptions for flow control, other critical exceptions are getting masked. The lack of a resolvable query should instead leverage some sort of null value object.

See #2018.
This commit is contained in:
Greg L. Turnquist
2022-05-18 09:01:07 -05:00
parent 6127d376e9
commit 2d3633e2fa
3 changed files with 65 additions and 17 deletions

View File

@@ -21,13 +21,13 @@ import javax.persistence.EntityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.core.NamedQueries;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.query.QueryLookupStrategy;
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
import org.springframework.data.repository.query.QueryMethod;
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
import org.springframework.data.repository.query.RepositoryQuery;
import org.springframework.lang.Nullable;
@@ -46,6 +46,12 @@ public final class JpaQueryLookupStrategy {
private static final Logger LOG = LoggerFactory.getLogger(JpaQueryLookupStrategy.class);
/**
* A null-value instance used to signal if no declared query could be found. It checks many different formats before
* falling through to this value object.
*/
private static final RepositoryQuery NO_QUERY = new NoQuery();
/**
* Private constructor to prevent instantiation.
*/
@@ -161,24 +167,20 @@ public final class JpaQueryLookupStrategy {
}
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, method.getRequiredAnnotatedQuery(),
getCountQuery(method, namedQueries, em),
evaluationContextProvider);
getCountQuery(method, namedQueries, em), evaluationContextProvider);
}
String name = method.getNamedQueryName();
if (namedQueries.hasQuery(name)) {
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name), getCountQuery(method, namedQueries, em),
evaluationContextProvider);
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(method, em, namedQueries.getQuery(name),
getCountQuery(method, namedQueries, em), evaluationContextProvider);
}
RepositoryQuery query = NamedQuery.lookupFrom(method, em);
if (null != query) {
return query;
}
throw new IllegalStateException(
String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method));
return query != null //
? query //
: NO_QUERY;
}
@Nullable
@@ -248,11 +250,13 @@ public final class JpaQueryLookupStrategy {
@Override
protected RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries) {
try {
return lookupStrategy.resolveQuery(method, em, namedQueries);
} catch (IllegalStateException e) {
return createStrategy.resolveQuery(method, em, namedQueries);
RepositoryQuery lookupQuery = lookupStrategy.resolveQuery(method, em, namedQueries);
if (lookupQuery != NO_QUERY) {
return lookupQuery;
}
return createStrategy.resolveQuery(method, em, namedQueries);
}
}
@@ -286,4 +290,20 @@ public final class JpaQueryLookupStrategy {
throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key));
}
}
/**
* A null value type that represents the lack of a defined query.
*/
static class NoQuery implements RepositoryQuery {
@Override
public Object execute(Object[] parameters) {
throw new IllegalStateException("NoQuery should not be executed!");
}
@Override
public QueryMethod getQueryMethod() {
throw new IllegalStateException("NoQuery does not have a QueryMethod!");
}
}
}

View File

@@ -32,6 +32,7 @@ import javax.persistence.*;
* @author Jens Schauder
* @author Jeff Sheets
* @author JyotirmoyVS
* @author Greg Turnquist
*/
@Entity
@NamedEntityGraphs({ @NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }),
@@ -91,7 +92,8 @@ import javax.persistence.*;
@Table(name = "SD_User")
public class User {
@Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id;
@Id
@GeneratedValue(strategy = GenerationType.AUTO) private Integer id;
private String firstname;
private String lastname;
private int age;

View File

@@ -33,7 +33,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
@@ -167,6 +166,30 @@ public class JpaQueryLookupStrategyUnitTests {
assertThat(repositoryQuery).isInstanceOf(AbstractStringBasedJpaQuery.class);
}
@Test // GH-2018
void namedQueryWithSortShouldThrowIllegalStateException() throws NoSuchMethodException {
QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, queryMethodFactory, Key.CREATE_IF_NOT_FOUND,
EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT);
Method method = UserRepository.class.getMethod("customNamedQuery", String.class, Sort.class);
RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);
assertThatIllegalStateException()
.isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries))
.withMessageContaining(
"is backed by a NamedQuery and must not contain a sort parameter as we cannot modify the query! Use @Query instead!");
}
@Test // GH-2018
void noQueryShouldNotBeInvoked() {
RepositoryQuery query = new JpaQueryLookupStrategy.NoQuery();
assertThatIllegalStateException().isThrownBy(() -> query.execute(new Object[] {}));
assertThatIllegalStateException().isThrownBy(() -> query.getQueryMethod());
}
interface UserRepository extends Repository<User, Integer> {
@Query("something absurd")
@@ -183,5 +206,8 @@ public class JpaQueryLookupStrategyUnitTests {
@Query(value = "something absurd", name = "my-query-name")
User annotatedQueryWithQueryAndQueryName();
// This is a named query with Sort parameter, which isn't supported
List<User> customNamedQuery(String firstname, Sort sort);
}
}