From ca00d31d45d5bec2ad5cdc1798c798dd276389eb Mon Sep 17 00:00:00 2001 From: Diego Krupitza Date: Thu, 24 Feb 2022 12:46:28 +0100 Subject: [PATCH 1/2] Introduced `queryLookupStrategy` to `EnableJdbcRepositories`. Added the missing functionality that is found in the documentation of spring-data-jdbc, but was not present in the code as functionality. Users can not choose between various QueryLookupStrategies. Closes #1043 --- .../config/EnableJdbcRepositories.java | 13 +- .../support/JdbcQueryLookupStrategy.java | 213 ++++++++++++++++-- .../support/JdbcRepositoryFactory.java | 5 +- ...ositoryLookUpStrategyIntegrationTests.java | 91 ++++++++ ...otFoundLookUpStrategyIntegrationTests.java | 79 +++++++ ...yCreateLookUpStrategyIntegrationTests.java | 76 +++++++ ...eclaredLookUpStrategyIntegrationTests.java | 60 +++++ .../JdbcQueryLookupStrategyUnitTests.java | 59 ++++- ...oryLookUpStrategyIntegrationTests-hsql.sql | 5 + 9 files changed, 567 insertions(+), 34 deletions(-) create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateIfNotFoundLookUpStrategyIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateLookUpStrategyIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryDeclaredLookUpStrategyIntegrationTests.java create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-hsql.sql diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java index dd60dd7dd6..c5e1694508 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -27,8 +27,8 @@ import org.springframework.context.annotation.Import; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; import org.springframework.data.repository.config.DefaultRepositoryBaseClass; +import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.transaction.PlatformTransactionManager; /** * Annotation to enable JDBC repositories. Will scan the package of the annotated configuration class for Spring Data @@ -39,6 +39,7 @@ * @author Mark Paluch * @author Fei Dong * @author Antoine Sauray + * @author Diego Krupitza * @see AbstractJdbcConfiguration */ @Target(ElementType.TYPE) @@ -124,11 +125,17 @@ */ String dataAccessStrategyRef() default ""; - /** + /** * Configures the name of the {@link DataSourceTransactionManager} bean definition to be used to create repositories * discovered through this annotation. Defaults to {@code transactionManager}. + * * @since 2.1 */ String transactionManagerRef() default "transactionManager"; + /** + * Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to + * {@link QueryLookupStrategy.Key#CREATE_IF_NOT_FOUND}. + */ + QueryLookupStrategy.Key queryLookupStrategy() default QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index ab2fc67dd1..4fbdd69f86 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2022 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. @@ -21,7 +21,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.convert.EntityRowMapper; @@ -41,7 +40,6 @@ import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.query.QueryCreationException; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.RowMapper; @@ -51,7 +49,7 @@ import org.springframework.util.Assert; /** - * {@link QueryLookupStrategy} for JDBC repositories. + * Abstract {@link QueryLookupStrategy} for JDBC repositories. * * @author Jens Schauder * @author Kazuki Shimizu @@ -60,8 +58,9 @@ * @author Maciej Walkowiak * @author Moises Cisneros * @author Hebert Coelho + * @author Diego Krupitza */ -class JdbcQueryLookupStrategy implements QueryLookupStrategy { +abstract class JdbcQueryLookupStrategy implements QueryLookupStrategy { private static final Log LOG = LogFactory.getLog(JdbcQueryLookupStrategy.class); @@ -72,12 +71,12 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { private final Dialect dialect; private final QueryMappingConfiguration queryMappingConfiguration; private final NamedParameterJdbcOperations operations; - private final BeanFactory beanfactory; + @Nullable private final BeanFactory beanfactory; JdbcQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, - BeanFactory beanfactory) { + @Nullable BeanFactory beanfactory) { Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); @@ -96,18 +95,52 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { this.beanfactory = beanfactory; } - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) + /** + * {@link QueryLookupStrategy} to create a query from the method name. + * + * @author Diego Krupitza */ - @Override - public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, - ProjectionFactory projectionFactory, NamedQueries namedQueries) { + static class CreateQueryLookupStrategy extends JdbcQueryLookupStrategy { + + CreateQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, + RelationalMappingContext context, JdbcConverter converter, Dialect dialect, + QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, + @Nullable BeanFactory beanfactory) { + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, NamedQueries namedQueries) { + + JdbcQueryMethod queryMethod = getJdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries); + + return new PartTreeJdbcQuery(getContext(), queryMethod, getDialect(), getConverter(), getOperations(), + this::createMapper); + } + } + + /** + * {@link QueryLookupStrategy} that tries to detect a declared query declared via + * {@link org.springframework.data.jdbc.repository.query.Query} annotation followed by a JPA named query lookup. + * + * @author Diego Krupitza + */ + static class DeclaredQueryLookupStrategy extends JdbcQueryLookupStrategy { + + DeclaredQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, + RelationalMappingContext context, JdbcConverter converter, Dialect dialect, + QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, + @Nullable BeanFactory beanfactory) { + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, NamedQueries namedQueries) { - JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, - context); + JdbcQueryMethod queryMethod = getJdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries); - try { if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) { if (queryMethod.hasAnnotatedQuery() && queryMethod.hasAnnotatedQueryName()) { @@ -115,24 +148,156 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repository "Query method %s is annotated with both, a query and a query name. Using the declared query.", method)); } - StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, this::createMapper, converter); - query.setBeanFactory(beanfactory); + StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, getOperations(), this::createMapper, + getConverter()); + query.setBeanFactory(getBeanfactory()); return query; - } else { - return new PartTreeJdbcQuery(context, queryMethod, dialect, converter, operations, this::createMapper); } - } catch (Exception e) { - throw QueryCreationException.create(queryMethod, e); + + throw new IllegalStateException( + String.format("Did neither find a NamedQuery nor an annotated query for method %s!", method)); + } + } + + /** + * {@link QueryLookupStrategy} to try to detect a declared query first ( + * {@link org.springframework.data.jdbc.repository.query.Query}, JDBC named query). In case none is found we fall back + * on query creation. + *

+ * Modified based on original source: {@link org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy} + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Diego Krupitza + */ + static class CreateIfNotFoundQueryLookupStrategy extends JdbcQueryLookupStrategy { + + private final DeclaredQueryLookupStrategy lookupStrategy; + private final CreateQueryLookupStrategy createStrategy; + + /** + * Creates a new {@link CreateIfNotFoundQueryLookupStrategy}. + * + * @param createStrategy must not be {@literal null}. + * @param lookupStrategy must not be {@literal null}. + */ + public CreateIfNotFoundQueryLookupStrategy(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, + RelationalMappingContext context, JdbcConverter converter, Dialect dialect, + QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, + @Nullable BeanFactory beanfactory, CreateQueryLookupStrategy createStrategy, + DeclaredQueryLookupStrategy lookupStrategy) { + + super(publisher, callbacks, context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + + Assert.notNull(createStrategy, "CreateQueryLookupStrategy must not be null!"); + Assert.notNull(lookupStrategy, "DeclaredQueryLookupStrategy must not be null!"); + + this.createStrategy = createStrategy; + this.lookupStrategy = lookupStrategy; } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, NamedQueries namedQueries) { + + try { + return lookupStrategy.resolveQuery(method, repositoryMetadata, projectionFactory, namedQueries); + } catch (IllegalStateException e) { + return createStrategy.resolveQuery(method, repositoryMetadata, projectionFactory, namedQueries); + } + } + } + + /** + * Creates a {@link JdbcQueryMethod} based on the parameters + */ + JdbcQueryMethod getJdbcQueryMethod(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, NamedQueries namedQueries) { + return new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, context); + } + + /** + * Creates a {@link QueryLookupStrategy} based on the provided + * {@link org.springframework.data.repository.query.QueryLookupStrategy.Key}. + * + * @param key the key that decides what {@link QueryLookupStrategy} shozld be used. + * @param publisher must not be {@literal null} + * @param callbacks may be {@literal null} + * @param context must not be {@literal null} + * @param converter must not be {@literal null} + * @param dialect must not be {@literal null} + * @param queryMappingConfiguration must not be {@literal null} + * @param operations must not be {@literal null} + * @param beanfactory may be {@literal null} + */ + public static QueryLookupStrategy create(@Nullable Key key, ApplicationEventPublisher publisher, + @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcConverter converter, Dialect dialect, + QueryMappingConfiguration queryMappingConfiguration, NamedParameterJdbcOperations operations, + @Nullable BeanFactory beanfactory) { + + Assert.notNull(publisher, "ApplicationEventPublisher must not be null"); + Assert.notNull(context, "RelationalMappingContextPublisher must not be null"); + Assert.notNull(converter, "JdbcConverter must not be null"); + Assert.notNull(dialect, "Dialect must not be null"); + Assert.notNull(queryMappingConfiguration, "QueryMappingConfiguration must not be null"); + Assert.notNull(operations, "NamedParameterJdbcOperations must not be null"); + + CreateQueryLookupStrategy createQueryLookupStrategy = new CreateQueryLookupStrategy(publisher, callbacks, context, + converter, dialect, queryMappingConfiguration, operations, beanfactory); + + DeclaredQueryLookupStrategy declaredQueryLookupStrategy = new DeclaredQueryLookupStrategy(publisher, callbacks, + context, converter, dialect, queryMappingConfiguration, operations, beanfactory); + + Key cleanedKey = key != null ? key : Key.CREATE_IF_NOT_FOUND; + + LOG.debug(String.format("Using the queryLookupStrategy %s", cleanedKey)); + + switch (cleanedKey) { + case CREATE: + return createQueryLookupStrategy; + case USE_DECLARED_QUERY: + return declaredQueryLookupStrategy; + case CREATE_IF_NOT_FOUND: + return new CreateIfNotFoundQueryLookupStrategy(publisher, callbacks, context, converter, dialect, + queryMappingConfiguration, operations, beanfactory, createQueryLookupStrategy, declaredQueryLookupStrategy); + default: + throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); + } + } + + protected ApplicationEventPublisher getPublisher() { + return publisher; + } + + protected RelationalMappingContext getContext() { + return context; + } + + protected JdbcConverter getConverter() { + return converter; + } + + protected Dialect getDialect() { + return dialect; + } + + protected NamedParameterJdbcOperations getOperations() { + return operations; + } + + @Nullable + protected BeanFactory getBeanfactory() { + return beanfactory; } @SuppressWarnings("unchecked") - private RowMapper createMapper(Class returnedObjectType) { + RowMapper createMapper(Class returnedObjectType) { RelationalPersistentEntity persistentEntity = context.getPersistentEntity(returnedObjectType); if (persistentEntity == null) { - return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, converter.getConversionService()); + return (RowMapper) SingleColumnRowMapper.newInstance(returnedObjectType, + converter.getConversionService()); } return (RowMapper) determineDefaultMapper(returnedObjectType); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index 36558f64ba..f3d1f61fb3 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2021 the original author or authors. + * Copyright 2017-2022 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. @@ -46,6 +46,7 @@ * @author Christoph Strobl * @author Mark Paluch * @author Hebert Coelho + * @author Diego Krupitza */ public class JdbcRepositoryFactory extends RepositoryFactorySupport { @@ -145,7 +146,7 @@ protected Class getRepositoryBaseClass(RepositoryMetadata repositoryMetadata) protected Optional getQueryLookupStrategy(@Nullable QueryLookupStrategy.Key key, QueryMethodEvaluationContextProvider evaluationContextProvider) { - return Optional.of(new JdbcQueryLookupStrategy(publisher, entityCallbacks, context, converter, dialect, + return Optional.of(JdbcQueryLookupStrategy.create(key, publisher, entityCallbacks, context, converter, dialect, queryMappingConfiguration, operations, beanFactory)); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests.java new file mode 100644 index 0000000000..41e5646c98 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.repository.CrudRepository; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +/** + * Base class to test @EnableJdbcRepositories(queryLookupStrategy = ...) Tests based on logic from + * {@link org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy} + * + * @author Diego Krupitza + */ +abstract class AbstractJdbcRepositoryLookUpStrategyIntegrationTests { + + @Autowired protected OnesRepository onesRepository; + @Autowired NamedParameterJdbcTemplate template; + @Autowired RelationalMappingContext context; + + protected void insertTestInstances() { + AggregateOne firstAggregate = new AggregateOne("Diego"); + AggregateOne secondAggregate = new AggregateOne("Franz"); + AggregateOne thirdAggregate = new AggregateOne("Daniela"); + + onesRepository.saveAll(Arrays.asList(firstAggregate, secondAggregate, thirdAggregate)); + } + + protected void callDeclaredQuery(String name, int expectedSize, String... expectedNames) { + insertTestInstances(); + + List likeNameD = onesRepository.findAllByName(name); + + assertThat(likeNameD).hasSize(expectedSize); + + assertThat(likeNameD.stream().map(item -> item.name).collect(Collectors.toList())) // + .contains(expectedNames); + + } + + protected void callDerivedQuery() { + insertTestInstances(); + + AggregateOne diego = onesRepository.findByName("Diego"); + assertThat(diego).isNotNull(); + assertThat(diego.id).isNotNull(); + assertThat(diego.name).isEqualToIgnoringCase("Diego"); + } + + interface OnesRepository extends CrudRepository { + + // if derived is used it is just a basic findByName + // if declared is used it should be a like check + @Query("Select * from aggregate_one where NAME like concat('%', :name, '%') ") + List findAllByName(String name); + + AggregateOne findByName(String name); + } + + static class AggregateOne { + + @Id Long id; + String name; + + public AggregateOne(String name) { + this.name = name; + } + } +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateIfNotFoundLookUpStrategyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateIfNotFoundLookUpStrategyIntegrationTests.java new file mode 100644 index 0000000000..f435219790 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateIfNotFoundLookUpStrategyIntegrationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Test to verify that + * @EnableJdbcRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND) works as + * intended. Tests based on logic from + * {@link org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy.CreateIfNotFoundQueryLookupStrategy} + * + * @author Diego Krupitza + */ +@ContextConfiguration +@Transactional +@ActiveProfiles("hsql") +@ExtendWith(SpringExtension.class) +class JdbcRepositoryCreateIfNotFoundLookUpStrategyIntegrationTests + extends AbstractJdbcRepositoryLookUpStrategyIntegrationTests { + + @Test + void declaredQueryShouldWork() { + onesRepository.deleteAll(); + callDeclaredQuery("D", 2, "Diego", "Daniela"); + } + + @Test + void derivedQueryShouldWork() { + onesRepository.deleteAll(); + callDerivedQuery(); + } + + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true, + queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND, + includeFilters = @ComponentScan.Filter( + value = AbstractJdbcRepositoryLookUpStrategyIntegrationTests.OnesRepository.class, + type = FilterType.ASSIGNABLE_TYPE)) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return AbstractJdbcRepositoryLookUpStrategyIntegrationTests.class; + } + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateLookUpStrategyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateLookUpStrategyIntegrationTests.java new file mode 100644 index 0000000000..59371955ef --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCreateLookUpStrategyIntegrationTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Test to verify that @EnableJdbcRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE) works + * as intended. Tests based on logic from + * {@link org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy.CreateQueryLookupStrategy} + * + * @author Diego Krupitza + */ +@ContextConfiguration +@Transactional +@ActiveProfiles("hsql") +@ExtendWith(SpringExtension.class) +class JdbcRepositoryCreateLookUpStrategyIntegrationTests extends AbstractJdbcRepositoryLookUpStrategyIntegrationTests { + + @Test + void declaredQueryShouldWork() { + onesRepository.deleteAll(); + + // here the declared query will use the dervice query which does something totally different + callDeclaredQuery("D", 0); + } + + @Test + void derivedQueryShouldWork() { + onesRepository.deleteAll(); + callDerivedQuery(); + } + + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true, queryLookupStrategy = QueryLookupStrategy.Key.CREATE, + includeFilters = @ComponentScan.Filter(value = OnesRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return AbstractJdbcRepositoryLookUpStrategyIntegrationTests.class; + } + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryDeclaredLookUpStrategyIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryDeclaredLookUpStrategyIntegrationTests.java new file mode 100644 index 0000000000..40fd53eb69 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryDeclaredLookUpStrategyIntegrationTests.java @@ -0,0 +1,60 @@ +package org.springframework.data.jdbc.repository; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.annotation.Import; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.query.QueryLookupStrategy; + +/** + * Test to verify that + * @EnableJdbcRepositories(queryLookupStrategy = QueryLookupStrategy.Key.USE_DECLARED_QUERY) works as + * intended. Tests based on logic from + * {@link org.springframework.data.jdbc.repository.support.JdbcQueryLookupStrategy.DeclaredQueryLookupStrategy} + * + * @author Diego Krupitza + */ +class JdbcRepositoryDeclaredLookUpStrategyIntegrationTests + extends AbstractJdbcRepositoryLookUpStrategyIntegrationTests { + + @Test + void contextCannotByCreatedDueToFindByNameNotDeclaredQuery() { + + try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) { + + context.register(JdbcRepositoryDeclaredLookUpStrategyIntegrationTests.Config.class); + + assertThatThrownBy(() -> { + context.refresh(); + context.getBean(OnesRepository.class); + }).hasMessageContaining("findByName"); + } + } + + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true, + queryLookupStrategy = QueryLookupStrategy.Key.USE_DECLARED_QUERY, + includeFilters = @ComponentScan.Filter( + value = AbstractJdbcRepositoryLookUpStrategyIntegrationTests.OnesRepository.class, + type = FilterType.ASSIGNABLE_TYPE)) + static class Config { + + @Autowired JdbcRepositoryFactory factory; + + @Bean + Class testClass() { + return AbstractJdbcRepositoryLookUpStrategyIntegrationTests.class; + } + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java index c32c22ca11..4b2187c776 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategyUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2021 the original author or authors. + * Copyright 2018-2022 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,15 +15,20 @@ */ package org.springframework.data.jdbc.repository.support; +import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; import java.text.NumberFormat; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.repository.QueryMappingConfiguration; @@ -35,6 +40,7 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext; 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.RepositoryQuery; import org.springframework.data.util.ClassTypeInformation; import org.springframework.jdbc.core.RowMapper; @@ -52,6 +58,7 @@ * @author Evgeni Dimitrov * @author Mark Paluch * @author Hebert Coelho + * @author Diego Krupitza */ class JdbcQueryLookupStrategyUnitTests { @@ -81,7 +88,8 @@ void typeBasedRowMapperGetsUsedForQuery() { QueryMappingConfiguration mappingConfiguration = new DefaultQueryMappingConfiguration() .registerRowMapper(NumberFormat.class, numberFormatMapper); - RepositoryQuery repositoryQuery = getRepositoryQuery("returningNumberFormat", mappingConfiguration); + RepositoryQuery repositoryQuery = getRepositoryQuery(QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND, + "returningNumberFormat", mappingConfiguration); repositoryQuery.execute(new Object[] {}); @@ -95,16 +103,55 @@ void prefersDeclaredQuery() { QueryMappingConfiguration mappingConfiguration = new DefaultQueryMappingConfiguration() .registerRowMapper(NumberFormat.class, numberFormatMapper); - RepositoryQuery repositoryQuery = getRepositoryQuery("annotatedQueryWithQueryAndQueryName", mappingConfiguration); + RepositoryQuery repositoryQuery = getRepositoryQuery(QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND, + "annotatedQueryWithQueryAndQueryName", mappingConfiguration); repositoryQuery.execute(new Object[] {}); verify(operations).queryForObject(eq("some SQL"), any(SqlParameterSource.class), any(RowMapper.class)); } - private RepositoryQuery getRepositoryQuery(String name, QueryMappingConfiguration mappingConfiguration) { + @Test + void shouldFailOnMissingDeclaredQuery() { - JdbcQueryLookupStrategy queryLookupStrategy = new JdbcQueryLookupStrategy(publisher, callbacks, mappingContext, + RowMapper numberFormatMapper = mock(RowMapper.class); + QueryMappingConfiguration mappingConfiguration = new DefaultQueryMappingConfiguration() + .registerRowMapper(NumberFormat.class, numberFormatMapper); + + assertThatThrownBy( + () -> getRepositoryQuery(QueryLookupStrategy.Key.USE_DECLARED_QUERY, "findByName", mappingConfiguration)) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Did neither find a NamedQuery nor an annotated query for method") + .hasMessageContaining("findByName"); + } + + @ParameterizedTest + @MethodSource("correctLookUpStrategyForKeySource") + void correctLookUpStrategyForKey(QueryLookupStrategy.Key key, Class expectedClass) { + RowMapper numberFormatMapper = mock(RowMapper.class); + QueryMappingConfiguration mappingConfiguration = new DefaultQueryMappingConfiguration() + .registerRowMapper(NumberFormat.class, numberFormatMapper); + + QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, + converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null); + + assertThat(queryLookupStrategy).isInstanceOf(expectedClass); + } + + private static Stream correctLookUpStrategyForKeySource() { + return Stream.of( // + Arguments.of(QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND, + JdbcQueryLookupStrategy.CreateIfNotFoundQueryLookupStrategy.class), // + Arguments.of(QueryLookupStrategy.Key.CREATE, JdbcQueryLookupStrategy.CreateQueryLookupStrategy.class), // + Arguments.of(QueryLookupStrategy.Key.USE_DECLARED_QUERY, + JdbcQueryLookupStrategy.DeclaredQueryLookupStrategy.class) // + ); + } + + private RepositoryQuery getRepositoryQuery(QueryLookupStrategy.Key key, String name, + QueryMappingConfiguration mappingConfiguration) { + + QueryLookupStrategy queryLookupStrategy = JdbcQueryLookupStrategy.create(key, publisher, callbacks, mappingContext, converter, H2Dialect.INSTANCE, mappingConfiguration, operations, null); Method method = ReflectionUtils.findMethod(MyRepository.class, name); @@ -119,5 +166,7 @@ interface MyRepository { @Query(value = "some SQL", name = "query-name") void annotatedQueryWithQueryAndQueryName(); + + NumberFormat findByName(); } } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-hsql.sql new file mode 100644 index 0000000000..8e5b1318ac --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-hsql.sql @@ -0,0 +1,5 @@ +CREATE TABLE aggregate_one +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); From 3e9001aef9f04b384480813a1dee0788650f06d8 Mon Sep 17 00:00:00 2001 From: Diego Krupitza Date: Sun, 6 Mar 2022 18:59:27 +0100 Subject: [PATCH 2/2] Fixed failing test for all dbs. --- ...actJdbcRepositoryLookUpStrategyIntegrationTests-db2.sql | 7 +++++++ ...ractJdbcRepositoryLookUpStrategyIntegrationTests-h2.sql | 5 +++++ ...dbcRepositoryLookUpStrategyIntegrationTests-mariadb.sql | 5 +++++ ...tJdbcRepositoryLookUpStrategyIntegrationTests-mssql.sql | 7 +++++++ ...tJdbcRepositoryLookUpStrategyIntegrationTests-mysql.sql | 5 +++++ ...JdbcRepositoryLookUpStrategyIntegrationTests-oracle.sql | 7 +++++++ ...bcRepositoryLookUpStrategyIntegrationTests-postgres.sql | 6 ++++++ 7 files changed, 42 insertions(+) create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-db2.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-h2.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mariadb.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mssql.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mysql.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-oracle.sql create mode 100644 spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-postgres.sql diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-db2.sql new file mode 100644 index 0000000000..205dff68d4 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-db2.sql @@ -0,0 +1,7 @@ +DROP TABLE aggregate_one; + +CREATE TABLE aggregate_one +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-h2.sql new file mode 100644 index 0000000000..6ab837bf09 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-h2.sql @@ -0,0 +1,5 @@ +CREATE TABLE aggregate_one +( + id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mariadb.sql new file mode 100644 index 0000000000..1e08bbef5e --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mariadb.sql @@ -0,0 +1,5 @@ +CREATE TABLE aggregate_one +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mssql.sql new file mode 100644 index 0000000000..e6eee1fd7a --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mssql.sql @@ -0,0 +1,7 @@ +DROP TABLE IF EXISTS aggregate_one; + +CREATE TABLE aggregate_one +( + id BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mysql.sql new file mode 100644 index 0000000000..53c5933b59 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-mysql.sql @@ -0,0 +1,5 @@ +CREATE TABLE aggregate_one +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100) +); \ No newline at end of file diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-oracle.sql new file mode 100644 index 0000000000..d32bc37643 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-oracle.sql @@ -0,0 +1,7 @@ +DROP TABLE aggregate_one CASCADE CONSTRAINTS PURGE; + +CREATE TABLE aggregate_one +( + id NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + NAME VARCHAR2(100) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-postgres.sql new file mode 100644 index 0000000000..c551f53fa0 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/AbstractJdbcRepositoryLookUpStrategyIntegrationTests-postgres.sql @@ -0,0 +1,6 @@ +DROP TABLE aggregate_one; +CREATE TABLE aggregate_one +( + id SERIAL PRIMARY KEY, + NAME VARCHAR(100) +); \ No newline at end of file