diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java index 32aee712a..bbe257008 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategy.java @@ -81,7 +81,7 @@ public final class JpaQueryLookupStrategy { @Override public final RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, NamedQueries namedQueries) { - return resolveQuery(queryMethodFactory.build(method, metadata, factory, provider), em, namedQueries); + return resolveQuery(queryMethodFactory.build(method, metadata, factory), em, namedQueries); } protected abstract RepositoryQuery resolveQuery(JpaQueryMethod method, EntityManager em, NamedQueries namedQueries); @@ -98,7 +98,8 @@ public final class JpaQueryLookupStrategy { private final PersistenceProvider persistenceProvider; private final EscapeCharacter escape; - public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escape, JpaQueryMethodFactory queryMethodFactory) { + public CreateQueryLookupStrategy(EntityManager em, QueryExtractor extractor, EscapeCharacter escape, + JpaQueryMethodFactory queryMethodFactory) { super(em, extractor, queryMethodFactory); this.persistenceProvider = PersistenceProvider.fromEntityManager(em); @@ -134,6 +135,7 @@ public final class JpaQueryLookupStrategy { JpaQueryMethodFactory queryMethodFactory, QueryMethodEvaluationContextProvider evaluationContextProvider) { super(em, extractor, queryMethodFactory); + this.evaluationContextProvider = evaluationContextProvider; } @@ -232,7 +234,8 @@ public final class JpaQueryLookupStrategy { * @return */ public static QueryLookupStrategy create(EntityManager em, @Nullable Key key, QueryExtractor extractor, - JpaQueryMethodFactory queryMethodFactory, QueryMethodEvaluationContextProvider evaluationContextProvider, EscapeCharacter escape) { + JpaQueryMethodFactory queryMethodFactory, QueryMethodEvaluationContextProvider evaluationContextProvider, + EscapeCharacter escape) { Assert.notNull(em, "EntityManager must not be null!"); Assert.notNull(extractor, "QueryExtractor must not be null!"); @@ -244,7 +247,7 @@ public final class JpaQueryLookupStrategy { case USE_DECLARED_QUERY: return new DeclaredQueryLookupStrategy(em, extractor, queryMethodFactory, evaluationContextProvider); case CREATE_IF_NOT_FOUND: - return new CreateIfNotFoundQueryLookupStrategy(em, extractor,queryMethodFactory, + return new CreateIfNotFoundQueryLookupStrategy(em, extractor, queryMethodFactory, new CreateQueryLookupStrategy(em, extractor, escape, queryMethodFactory), new DeclaredQueryLookupStrategy(em, extractor, queryMethodFactory, evaluationContextProvider)); default: diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index 4d962c1c6..aec4c1df8 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -90,6 +90,20 @@ public class JpaQueryMethod extends QueryMethod { private final Lazy isProcedureQuery; private final Lazy> entityMetadata; + /** + * Creates a {@link JpaQueryMethodFactory} which will create instances of this class. + * + * @param extractor must not be {@literal null}. + * @return a {@link JpaQueryMethodFactory} guaranteed to be not {@literal null}. + * @since 2.3 + */ + public static JpaQueryMethodFactory createMethodFactory(QueryExtractor extractor) { + + Assert.notNull(extractor, "QueryExtractor must not be null"); + + return (method, metadata, factory) -> new JpaQueryMethod(method, metadata, factory, extractor); + } + /** * Creates a {@link JpaQueryMethod}. * @@ -427,19 +441,4 @@ public class JpaQueryMethod extends QueryMethod { return storedProcedureAttributes; } - - public static class Factory implements JpaQueryMethodFactory { - - public static final Factory INSTANCE = new Factory(); - - private Factory() { - - } - - @Override - public JpaQueryMethod build(Method method, RepositoryMetadata metadata, ProjectionFactory factory, - QueryExtractor extractor) { - return new JpaQueryMethod(method, metadata, factory, extractor); - } - } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethodFactory.java b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethodFactory.java index a9618c8a6..049e1b1d4 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethodFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethodFactory.java @@ -22,7 +22,12 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; /** + * A factory interface for creating {@link JpaQueryMethodFactory} instances. + * + * This may be implemented by extensions to Spring Data JPA in order create instances of custom subclasses. + * * @author Réda Housni Alaoui + * @since 2.3 */ public interface JpaQueryMethodFactory { @@ -32,8 +37,7 @@ public interface JpaQueryMethodFactory { * @param method must not be {@literal null} * @param metadata must not be {@literal null} * @param factory must not be {@literal null} - * @param extractor must not be {@literal null} */ - JpaQueryMethod build(Method method, RepositoryMetadata metadata, ProjectionFactory factory, QueryExtractor extractor); + JpaQueryMethod build(Method method, RepositoryMetadata metadata, ProjectionFactory factory); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index a34d1893e..aeaaba19a 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -91,7 +91,7 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { this.extractor = PersistenceProvider.fromEntityManager(entityManager); this.crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); this.entityPathResolver = SimpleEntityPathResolver.INSTANCE; - this.queryMethodFactory = JpaQueryMethod.Factory.INSTANCE; + this.queryMethodFactory = JpaQueryMethod.createMethodFactory(extractor); addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); addRepositoryProxyPostProcessor((factory, repositoryInformation) -> { @@ -139,11 +139,12 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { } /** - * Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link JpaQueryMethod.Factory#INSTANCE}. + * Configures the {@link JpaQueryMethodFactory} to be used. Defaults to {@link JpaQueryMethod.DefaultJpaQueryMethodFactory#INSTANCE}. * * @param queryMethodFactory must not be {@literal null}. */ public void setQueryMethodFactory(JpaQueryMethodFactory queryMethodFactory) { + Assert.notNull(queryMethodFactory, "QueryMethodFactory must not be null!"); this.queryMethodFactory = queryMethodFactory; diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java index 5de800920..8c71b9768 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactoryBean.java @@ -20,6 +20,7 @@ import javax.persistence.PersistenceContext; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryMethod; import org.springframework.data.jpa.repository.query.JpaQueryMethodFactory; @@ -91,14 +92,14 @@ public class JpaRepositoryFactoryBean, S, ID> } /** - * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but fallback to - * {@link JpaQueryMethod.Factory#INSTANCE} in case none is available. + * Configures the {@link JpaQueryMethodFactory} to be used. Will expect a canonical bean to be present but will fallback to + * {@link JpaQueryMethod#createMethodFactory(QueryExtractor)} in case none is available. * * @param resolver must not be {@literal null}. */ @Autowired public void setQueryMethodFactory(ObjectProvider resolver) { - this.queryMethodFactory = resolver.getIfAvailable(() -> JpaQueryMethod.Factory.INSTANCE); + this.queryMethodFactory = resolver.getIfAvailable(() -> null); } /* @@ -121,7 +122,10 @@ public class JpaRepositoryFactoryBean, S, ID> JpaRepositoryFactory jpaRepositoryFactory = new JpaRepositoryFactory(entityManager); jpaRepositoryFactory.setEntityPathResolver(entityPathResolver); jpaRepositoryFactory.setEscapeCharacter(escapeCharacter); - jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory); + + if (queryMethodFactory != null) { + jpaRepositoryFactory.setQueryMethodFactory(queryMethodFactory); + } return jpaRepositoryFactory; } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersTests.java b/src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersIntegrationTests.java similarity index 81% rename from src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersTests.java rename to src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersIntegrationTests.java index 45636ab5f..f7b5928f6 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/CustomNonBindableJpaParametersIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2019 the original author or authors. + * Copyright 2019-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ package org.springframework.data.jpa.repository.query; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import java.util.Optional; @@ -30,6 +30,7 @@ import org.springframework.context.annotation.FilterType; import org.springframework.context.annotation.ImportResource; import org.springframework.core.MethodParameter; import org.springframework.data.jpa.domain.sample.Product; +import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; @@ -39,25 +40,28 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; /** + * Tests that the requirement of binding an argument to a query can get controlled by a module extending Spring Data + * JPA. + * * @author Réda Housni Alaoui */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration -public class CustomNonBindableJpaParametersTests { +public class CustomNonBindableJpaParametersIntegrationTests { @Autowired ProductRepository products; - @Test - public void test() { + @Test // DATAJPA-1497 + public void methodWithNonBindableParameterCanBeCalled() { + Product product = products.save(new Product()); + assertThat(products.findById(product.getId(), new NonBindable())).isNotEmpty(); } - private static class NonBindable { + private static class NonBindable {} - } - - public interface ProductRepository extends JpaRepository { + interface ProductRepository extends JpaRepository { Optional findById(long id, NonBindable nonBindable); } @@ -103,9 +107,14 @@ public class CustomNonBindableJpaParametersTests { private static class NonBindableAwareJpaQueryMethodFactory implements JpaQueryMethodFactory { + private final QueryExtractor extractor; + + private NonBindableAwareJpaQueryMethodFactory(QueryExtractor extractor) { + this.extractor = extractor; + } + @Override - public JpaQueryMethod build(Method method, RepositoryMetadata metadata, ProjectionFactory factory, - QueryExtractor extractor) { + public JpaQueryMethod build(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { return new NonBindableAwareJpaQueryMethod(method, metadata, factory, extractor); } } @@ -118,9 +127,7 @@ public class CustomNonBindableJpaParametersTests { @Bean JpaQueryMethodFactory jpaQueryMethodFactory() { - return new NonBindableAwareJpaQueryMethodFactory(); + return new NonBindableAwareJpaQueryMethodFactory(PersistenceProvider.HIBERNATE); } - } - } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index 883d6be6e..8f6f317ec 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -56,7 +56,6 @@ import org.springframework.data.repository.query.QueryMethodEvaluationContextPro public class JpaQueryLookupStrategyUnitTests { private static final QueryMethodEvaluationContextProvider EVALUATION_CONTEXT_PROVIDER = QueryMethodEvaluationContextProvider.DEFAULT; - private static final JpaQueryMethodFactory QUERY_METHOD_FACTORY = JpaQueryMethod.Factory.INSTANCE; @Mock EntityManager em; @Mock EntityManagerFactory emf; @@ -65,6 +64,8 @@ public class JpaQueryLookupStrategyUnitTests { @Mock Metamodel metamodel; @Mock ProjectionFactory projectionFactory; + JpaQueryMethodFactory queryMethodFactory; + @Before public void setUp() { @@ -72,13 +73,14 @@ public class JpaQueryLookupStrategyUnitTests { when(em.getEntityManagerFactory()).thenReturn(emf); when(emf.createEntityManager()).thenReturn(em); when(em.getDelegate()).thenReturn(em); + queryMethodFactory = JpaQueryMethod.createMethodFactory(extractor); } @Test // DATAJPA-226 public void invalidAnnotatedQueryCausesException() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor, - QUERY_METHOD_FACTORY, EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + queryMethodFactory, EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("findByFoo", String.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); @@ -94,7 +96,7 @@ public class JpaQueryLookupStrategyUnitTests { public void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries() throws Exception { QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor, - QUERY_METHOD_FACTORY, EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); + queryMethodFactory, EVALUATION_CONTEXT_PROVIDER, EscapeCharacter.DEFAULT); Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class);