From bc209b1114fb763eb5f368cd4c9b9a266ef446e2 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 9 Jan 2018 15:47:04 +0100 Subject: [PATCH] DATAJDBC-164 - Add support for basic @Query annotation. --- README.adoc | 22 +++- .../jdbc/core/DefaultDataAccessStrategy.java | 8 +- .../data/jdbc/core/EntityRowMapper.java | 2 +- .../mapping/model/JdbcMappingContext.java | 14 ++- .../repository/config/JdbcConfiguration.java | 6 +- .../data/jdbc/repository/query/Query.java | 40 ++++++ .../support/JdbcQueryLookupStrategy.java | 59 +++++++++ .../repository/support/JdbcQueryMethod.java | 50 ++++++++ .../support/JdbcRepositoryFactory.java | 19 +++ .../support/JdbcRepositoryQuery.java | 64 ++++++++++ .../DefaultDataAccessStrategyUnitTests.java | 12 +- .../core/DefaultJdbcInterpreterUnitTests.java | 10 +- .../jdbc/core/EntityRowMapperUnitTests.java | 3 +- ...orContextBasedNamingStrategyUnitTests.java | 4 +- ...GeneratorFixedNamingStrategyUnitTests.java | 4 +- .../data/jdbc/core/SqlGeneratorUnitTests.java | 9 +- .../JdbcEntityDeleteWriterUnitTests.java | 5 +- .../conversion/JdbcEntityWriterUnitTests.java | 4 +- ...cPersistentEntityInformationUnitTests.java | 6 +- .../BasicJdbcPersistentPropertyUnitTests.java | 4 +- .../SimpleJdbcRepositoryEventsUnitTests.java | 2 +- .../QueryAnnotationHsqlIntegrationTests.java | 117 ++++++++++++++++++ .../data/jdbc/testing/TestConfiguration.java | 30 ++--- ...eryAnnotationHsqlIntegrationTests-hsql.sql | 1 + 24 files changed, 442 insertions(+), 53 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/Query.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java create mode 100644 src/test/resources/org.springframework.data.jdbc.repository.query/QueryAnnotationHsqlIntegrationTests-hsql.sql diff --git a/README.adoc b/README.adoc index 5c440d6a..42ae7cf7 100644 --- a/README.adoc +++ b/README.adoc @@ -111,6 +111,19 @@ But the default mapping itself will stay limited. If you want highly customizable mappings which support almost everything one can imagine you will probably be much happier with (Spring Data) JPA. Which is a very powerful and mature technology. +=== Query annotation + +You can annotate a query method with `@Query` to specify a SQL statement to be used for that method. +You can bind method arguments using named parameters in the SQL statement like in the following example: + +[source,java] +---- +@Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower") +List findByNameRange(@Param("lower") String lower, @Param("upper") String upper); +---- + +If you compile your sources with the `-parameters` compiler flag you can omit the `@Param` annotations. + === Id generation Spring Data JDBC uses the id to identify entities but also to determine if an entity is new or already existing in the database. @@ -254,9 +267,12 @@ Note that the type used for prefixing the statement name is the name of the aggr == Features planned for the not to far future -=== Query annotation +=== Advance query annotation support -Just annotate a method with a SQL query to use this whenever the method gets called. +* customizable `RowMapper` +* projections +* modifying queries +* SpEL expressions === MyBatis per method support @@ -274,7 +290,7 @@ Currently you will need to build it locally. == Getting Help Right now the best source of information is the source code in this repository. -Especially the integration tests (type `t` and then `IntegrationTests.java`) +Especially the integration tests (When you are reading this on github type `t` and then `IntegrationTests.java`) We are keeping an eye on the (soon to be created) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow]. diff --git a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index fec32161..82b690fa 100644 --- a/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -24,7 +24,11 @@ import java.util.stream.StreamSupport; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.NonTransientDataAccessException; -import org.springframework.data.jdbc.mapping.model.*; +import org.springframework.data.jdbc.mapping.model.BasicJdbcPersistentEntityInformation; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntityInformation; +import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.PropertyPath; @@ -278,7 +282,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { } } - private EntityRowMapper getEntityRowMapper(Class domainType) { + public EntityRowMapper getEntityRowMapper(Class domainType) { return new EntityRowMapper<>(getRequiredPersistentEntity(domainType), context.getConversions(), context, accessStrategy); } diff --git a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java index ea862993..24f60df4 100644 --- a/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/jdbc/core/EntityRowMapper.java @@ -43,7 +43,7 @@ import org.springframework.jdbc.core.RowMapper; * @author Oliver Gierke * @since 2.0 */ -class EntityRowMapper implements RowMapper { +public class EntityRowMapper implements RowMapper { private final JdbcPersistentEntity entity; private final EntityInstantiator instantiator = new ClassGeneratingEntityInstantiator(); diff --git a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java index 381f9ab6..570d4a8f 100644 --- a/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java +++ b/src/main/java/org/springframework/data/jdbc/mapping/model/JdbcMappingContext.java @@ -37,6 +37,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * {@link MappingContext} implementation for JDBC. @@ -53,20 +54,22 @@ public class JdbcMappingContext extends AbstractMappingContext {}); + public JdbcMappingContext(NamedParameterJdbcOperations template) { + this(new DefaultNamingStrategy(), template, __ -> {}); } public List referencedEntities(Class rootType, PropertyPath path) { @@ -126,5 +129,4 @@ public class JdbcMappingContext extends AbstractMappingContext namingStrategy, + JdbcMappingContext jdbcMappingContext(NamedParameterJdbcTemplate template, Optional namingStrategy, Optional conversionCustomizer) { return new JdbcMappingContext( - namingStrategy.orElse(new DefaultNamingStrategy()), - conversionCustomizer.orElse(conversionService -> {})); + namingStrategy.orElse(new DefaultNamingStrategy()), template, conversionCustomizer.orElse(conversionService -> {})); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/Query.java b/src/main/java/org/springframework/data/jdbc/repository/query/Query.java new file mode 100644 index 00000000..bf7037c8 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/Query.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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 + * + * http://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.query; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; + +/** + * Annotation to provide SQL statements that will get used for executing the method. + * + * The SQL statement may contain named parameters as supported by {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. + * Those parameters will get bound to the arguments of the annotated method. + * + * @author Jens Schauder + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@QueryAnnotation +@Documented +public @interface Query { + String value(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java new file mode 100644 index 00000000..ec097d6a --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 + * + * http://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.support; + +import java.lang.reflect.Method; + +import org.springframework.data.jdbc.core.DataAccessStrategy; +import org.springframework.data.jdbc.core.EntityRowMapper; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +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.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.jdbc.core.RowMapper; + +/** + * {@link QueryLookupStrategy} for JDBC repositories. Currently only supports annotated queries. + * + * @author Jens Schauder + */ +class JdbcQueryLookupStrategy implements QueryLookupStrategy { + + private final JdbcMappingContext context; + private final DataAccessStrategy accessStrategy; + + JdbcQueryLookupStrategy(EvaluationContextProvider evaluationContextProvider, JdbcMappingContext context, + DataAccessStrategy accessStrategy) { + + this.context = context; + this.accessStrategy = accessStrategy; + } + + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory, NamedQueries namedQueries) { + + JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory); + Class domainType = queryMethod.getReturnedObjectType(); + RowMapper rowMapper = new EntityRowMapper<>(context.getRequiredPersistentEntity(domainType), + context.getConversions(), context, accessStrategy); + + return new JdbcRepositoryQuery(queryMethod, context, rowMapper); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java new file mode 100644 index 00000000..f1552190 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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 + * + * http://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.support; + +import java.lang.reflect.Method; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; + +/** + * {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on + * that method. + * + * Binds method arguments to named parameters in the SQL statement. + * + * @author Jens Schauder + */ +public class JdbcQueryMethod extends QueryMethod { + + private final Method method; + + public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); + + this.method = method; + } + + public String getAnnotatedQuery() { + + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + + return queryAnnotation == null ? null : queryAnnotation.value(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java index fc37dfcb..49281dc7 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryFactory.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jdbc.repository.support; +import java.util.Optional; + import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.JdbcEntityTemplate; @@ -25,8 +27,12 @@ import org.springframework.data.repository.core.EntityInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryFactorySupport; +import org.springframework.data.repository.query.EvaluationContextProvider; +import org.springframework.data.repository.query.QueryLookupStrategy; /** + * Creates repository implementation based on JDBC. + * * @author Jens Schauder * @author Greg Turnquist * @since 2.0 @@ -67,4 +73,17 @@ public class JdbcRepositoryFactory extends RepositoryFactorySupport { return SimpleJdbcRepository.class; } + @Override + protected Optional getQueryLookupStrategy(QueryLookupStrategy.Key key, + EvaluationContextProvider evaluationContextProvider) { + + if (key != null // + && key != QueryLookupStrategy.Key.USE_DECLARED_QUERY // + && key != QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND // + ) { + throw new IllegalArgumentException(String.format("Unsupported query lookup strategy %s!", key)); + } + + return Optional.of(new JdbcQueryLookupStrategy(evaluationContextProvider, context, accessStrategy)); + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java new file mode 100644 index 00000000..07b46e26 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 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 + * + * http://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.support; + +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; + +/** + * A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the + * method. + * + * @author Jens Schauder + */ +class JdbcRepositoryQuery implements RepositoryQuery { + + private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters. Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters."; + + private final JdbcQueryMethod queryMethod; + private final JdbcMappingContext context; + private final RowMapper rowMapper; + + JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper rowMapper) { + + this.queryMethod = queryMethod; + this.context = context; + + this.rowMapper = rowMapper; + } + + @Override + public Object execute(Object[] objects) { + + String query = queryMethod.getAnnotatedQuery(); + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + queryMethod.getParameters().getBindableParameters().forEach(p -> { + + String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); + parameters.addValue(parameterName, objects[p.getIndex()]); + }); + + return context.getTemplate().query(query, parameters, rowMapper); + } + + @Override + public JdbcQueryMethod getQueryMethod() { + return queryMethod; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java index d22eda11..6ffbe140 100644 --- a/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategyUnitTests.java @@ -15,12 +15,15 @@ */ package org.springframework.data.jdbc.core; -import java.util.HashMap; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import lombok.RequiredArgsConstructor; + +import java.util.HashMap; + import org.junit.Test; import org.mockito.ArgumentCaptor; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; @@ -28,9 +31,6 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.KeyHolder; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - /** * @author Jens Schauder */ @@ -39,8 +39,8 @@ public class DefaultDataAccessStrategyUnitTests { public static final long ID_FROM_ADDITIONAL_VALUES = 23L; public static final long ORIGINAL_ID = 4711L; - JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy(), __ -> {}); NamedParameterJdbcOperations jdbcOperations = mock(NamedParameterJdbcOperations.class); + JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy(), jdbcOperations, __ -> {}); HashMap additionalParameters = new HashMap<>(); ArgumentCaptor captor = ArgumentCaptor.forClass(SqlParameterSource.class); diff --git a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index b16635e8..134c826c 100644 --- a/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -15,12 +15,14 @@ */ package org.springframework.data.jdbc.core; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + import java.util.AbstractMap.SimpleEntry; import java.util.Map; import org.junit.Test; import org.mockito.ArgumentCaptor; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.conversion.DbAction; import org.springframework.data.jdbc.core.conversion.DbAction.Insert; @@ -28,9 +30,7 @@ import org.springframework.data.jdbc.core.conversion.JdbcPropertyPath; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for {@link DefaultJdbcInterpreter} @@ -47,7 +47,7 @@ public class DefaultJdbcInterpreterUnitTests { public String getReverseColumnName(JdbcPersistentProperty property) { return BACK_REFERENCE; } - }, __ -> {}); + }, mock(NamedParameterJdbcOperations.class), __ -> {}); DataAccessStrategy dataAccessStrategy = mock(DataAccessStrategy.class); DefaultJdbcInterpreter interpreter = new DefaultJdbcInterpreter(context, dataAccessStrategy); diff --git a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java index b4c3645a..9c55fdc5 100644 --- a/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/EntityRowMapperUnitTests.java @@ -43,6 +43,7 @@ import org.springframework.data.convert.Jsr310Converters; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.Assert; /** @@ -117,7 +118,7 @@ public class EntityRowMapperUnitTests { private EntityRowMapper createRowMapper(Class type) { - JdbcMappingContext context = new JdbcMappingContext(); + JdbcMappingContext context = new JdbcMappingContext(mock(NamedParameterJdbcOperations.class)); DataAccessStrategy accessStrategy = mock(DataAccessStrategy.class); // the ID of the entity is used to determin what kind of resultset is needed for subsequent selects. diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java index 1305727e..88c48f15 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorContextBasedNamingStrategyUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -29,6 +30,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.mapping.PropertyPath; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests to verify a contextual {@link NamingStrategy} implementation that customizes using a user-centric {@link ThreadLocal}. @@ -185,7 +187,7 @@ public class SqlGeneratorContextBasedNamingStrategyUnitTests { */ private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { - JdbcMappingContext context = new JdbcMappingContext(namingStrategy, __ -> {}); + JdbcMappingContext context = new JdbcMappingContext(namingStrategy, mock(NamedParameterJdbcOperations.class), __ -> {}); JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java index 495f7ba3..0fe50121 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorFixedNamingStrategyUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import org.assertj.core.api.SoftAssertions; import org.junit.Test; @@ -26,6 +27,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.mapping.PropertyPath; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests the {@link SqlGenerator} with a fixed {@link NamingStrategy} implementation containing a hard wired @@ -180,7 +182,7 @@ public class SqlGeneratorFixedNamingStrategyUnitTests { */ private SqlGenerator configureSqlGenerator(NamingStrategy namingStrategy) { - JdbcMappingContext context = new JdbcMappingContext(namingStrategy, __ -> {}); + JdbcMappingContext context = new JdbcMappingContext(namingStrategy, mock(NamedParameterJdbcOperations.class), __ -> {}); JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); return new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); } diff --git a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java index b32583a7..41db7987 100644 --- a/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/SqlGeneratorUnitTests.java @@ -15,13 +15,15 @@ */ package org.springframework.data.jdbc.core; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + import java.util.Map; import java.util.Set; import org.assertj.core.api.SoftAssertions; import org.junit.Before; import org.junit.Test; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; @@ -29,8 +31,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcPersistentEntity; import org.springframework.data.jdbc.mapping.model.JdbcPersistentProperty; import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.mapping.PropertyPath; - -import static org.assertj.core.api.Assertions.*; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for the {@link SqlGenerator}. @@ -46,7 +47,7 @@ public class SqlGeneratorUnitTests { public void setUp() { NamingStrategy namingStrategy = new PrefixingNamingStrategy(); - JdbcMappingContext context = new JdbcMappingContext(namingStrategy, __ -> {}); + JdbcMappingContext context = new JdbcMappingContext(namingStrategy, mock(NamedParameterJdbcOperations.class), __ -> {}); JdbcPersistentEntity persistentEntity = context.getRequiredPersistentEntity(DummyEntity.class); this.sqlGenerator = new SqlGenerator(context, persistentEntity, new SqlGeneratorSource(context)); } diff --git a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java index 3a95f9f9..d26b40b1 100644 --- a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityDeleteWriterUnitTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jdbc.core.conversion; +import static org.mockito.Mockito.*; + import lombok.Data; import org.assertj.core.api.Assertions; @@ -26,6 +28,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.conversion.AggregateChange.Kind; import org.springframework.data.jdbc.core.conversion.DbAction.Delete; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for the {@link JdbcEntityDeleteWriter} @@ -35,7 +38,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; @RunWith(MockitoJUnitRunner.class) public class JdbcEntityDeleteWriterUnitTests { - JdbcEntityDeleteWriter converter = new JdbcEntityDeleteWriter(new JdbcMappingContext()); + JdbcEntityDeleteWriter converter = new JdbcEntityDeleteWriter(new JdbcMappingContext(mock(NamedParameterJdbcOperations.class))); @Test public void deleteDeletesTheEntityAndReferencedEntities() { diff --git a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java index 4d7babd0..0b45c71d 100644 --- a/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/conversion/JdbcEntityWriterUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.conversion; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import lombok.RequiredArgsConstructor; @@ -33,6 +34,7 @@ import org.springframework.data.jdbc.core.conversion.DbAction.Delete; import org.springframework.data.jdbc.core.conversion.DbAction.Insert; import org.springframework.data.jdbc.core.conversion.DbAction.Update; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for the {@link JdbcEntityWriter} @@ -43,7 +45,7 @@ import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; public class JdbcEntityWriterUnitTests { public static final long SOME_ENTITY_ID = 23L; - JdbcEntityWriter converter = new JdbcEntityWriter(new JdbcMappingContext()); + JdbcEntityWriter converter = new JdbcEntityWriter(new JdbcMappingContext(mock(NamedParameterJdbcOperations.class))); @Test // DATAJDBC-112 public void newEntityGetsConvertedToOneInsert() { diff --git a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformationUnitTests.java b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformationUnitTests.java index 1f0082e0..13f3ae9d 100644 --- a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformationUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentEntityInformationUnitTests.java @@ -15,11 +15,13 @@ */ package org.springframework.data.jdbc.mapping.model; -import static org.assertj.core.api.Java6Assertions.assertThat; +import static org.assertj.core.api.Java6Assertions.*; +import static org.mockito.Mockito.*; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Persistable; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.lang.Nullable; /** @@ -27,7 +29,7 @@ import org.springframework.lang.Nullable; */ public class BasicJdbcPersistentEntityInformationUnitTests { - JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy(), cs -> {}); + JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy(), mock(NamedParameterJdbcOperations.class), cs -> {}); private DummyEntity dummyEntity = new DummyEntity(); private PersistableDummyEntity persistableDummyEntity = new PersistableDummyEntity(); diff --git a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java index aae27ddd..98f80951 100644 --- a/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/mapping/model/BasicJdbcPersistentPropertyUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.mapping.model; import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.mockito.Mockito.*; import lombok.Data; @@ -26,6 +27,7 @@ import java.util.Date; import org.assertj.core.api.Assertions; import org.junit.Test; import org.springframework.data.mapping.PropertyHandler; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** * Unit tests for the {@link BasicJdbcPersistentProperty}. @@ -37,7 +39,7 @@ public class BasicJdbcPersistentPropertyUnitTests { @Test // DATAJDBC-104 public void enumGetsStoredAsString() { - JdbcPersistentEntity persistentEntity = new JdbcMappingContext() + JdbcPersistentEntity persistentEntity = new JdbcMappingContext(mock(NamedParameterJdbcOperations.class)) .getRequiredPersistentEntity(DummyEntity.class); persistentEntity.doWithProperties((PropertyHandler) p -> { diff --git a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java index ef6b0879..a5b8c5a7 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/SimpleJdbcRepositoryEventsUnitTests.java @@ -46,7 +46,7 @@ public class SimpleJdbcRepositoryEventsUnitTests { @Before public void before() { - final JdbcMappingContext context = new JdbcMappingContext(); + final JdbcMappingContext context = new JdbcMappingContext(mock(NamedParameterJdbcOperations.class)); JdbcRepositoryFactory factory = new JdbcRepositoryFactory( // publisher, // context, // diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java new file mode 100644 index 00000000..cde1c740 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2018 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 + * + * http://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.query; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; +import org.springframework.transaction.annotation.Transactional; + +/** + * Tests the execution of queries from {@link Query} annotations on repository methods. + * + * @author Jens Schauder + */ +@ContextConfiguration +@Transactional +public class QueryAnnotationHsqlIntegrationTests { + + @Autowired DummyEntityRepository repository; + + @ClassRule public static final SpringClassRule classRule = new SpringClassRule(); + @Rule public SpringMethodRule methodRule = new SpringMethodRule(); + + @Test // DATAJDBC-164 + public void executeCustomQueryWithoutParameter() { + + repository.save(dummyEntity("Example")); + repository.save(dummyEntity("example")); + repository.save(dummyEntity("EXAMPLE")); + + List entities = repository.findByNameContainingCapitalLetter(); + + assertThat(entities) // + .extracting(e -> e.name) // + .containsExactlyInAnyOrder("Example", "EXAMPLE"); + + } + + @Test // DATAJDBC-164 + public void executeCustomQueryWithNamedParameters() { + + repository.save(dummyEntity("a")); + repository.save(dummyEntity("b")); + repository.save(dummyEntity("c")); + + List entities = repository.findByNamedRangeWithNamedParameter("a", "c"); + + assertThat(entities) // + .extracting(e -> e.name) // + .containsExactlyInAnyOrder("b"); + + } + + private DummyEntity dummyEntity(String name) { + + DummyEntity entity = new DummyEntity(); + entity.name = name; + return entity; + } + + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true) + static class Config { + + @Bean + Class testClass() { + return QueryAnnotationHsqlIntegrationTests.class; + } + } + + private static class DummyEntity { + + @Id Long id; + + String name; + } + + private interface DummyEntityRepository extends CrudRepository { + + @Query("SELECT * FROM DUMMYENTITY WHERE lower(name) <> name") + List findByNameContainingCapitalLetter(); + + + @Query("SELECT * FROM DUMMYENTITY WHERE name < :upper and name > :lower") + List findByNamedRangeWithNamedParameter(@Param("lower") String lower, @Param("upper") String upper); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index 4bb1a3a3..50889396 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -30,7 +30,10 @@ import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; import org.springframework.data.jdbc.core.SqlGeneratorSource; -import org.springframework.data.jdbc.mapping.model.*; +import org.springframework.data.jdbc.mapping.model.ConversionCustomizer; +import org.springframework.data.jdbc.mapping.model.DefaultNamingStrategy; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.jdbc.mapping.model.NamingStrategy; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -54,19 +57,16 @@ public class TestConfiguration { @Bean JdbcRepositoryFactory jdbcRepositoryFactory() { - final JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy() { - @Override - public String getColumnName(JdbcPersistentProperty property) { - return super.getColumnName(property); - } - }, __ -> {}); + NamedParameterJdbcTemplate jdbcTemplate = namedParameterJdbcTemplate(); + + final JdbcMappingContext context = new JdbcMappingContext(new DefaultNamingStrategy(), jdbcTemplate, __ -> {}); return new JdbcRepositoryFactory( // publisher, // context, // new DefaultDataAccessStrategy( // new SqlGeneratorSource(context), // - namedParameterJdbcTemplate(), // + jdbcTemplate, // context) // ); } @@ -83,7 +83,7 @@ public class TestConfiguration { @Bean DataAccessStrategy defaultDataAccessStrategy(JdbcMappingContext context, - @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { + @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations operations) { DelegatingDataAccessStrategy accessStrategy = new DelegatingDataAccessStrategy(); @@ -98,11 +98,13 @@ public class TestConfiguration { } @Bean - JdbcMappingContext jdbcMappingContext(Optional namingStrategy, - Optional conversionCustomizer) { + JdbcMappingContext jdbcMappingContext(NamedParameterJdbcOperations template, Optional namingStrategy, + Optional conversionCustomizer) { - return new JdbcMappingContext( - namingStrategy.orElse(new DefaultNamingStrategy()), - conversionCustomizer.orElse(conversionService -> {})); + return new JdbcMappingContext( // + namingStrategy.orElse(new DefaultNamingStrategy()), // + template, // + conversionCustomizer.orElse(conversionService -> {}) // + ); } } diff --git a/src/test/resources/org.springframework.data.jdbc.repository.query/QueryAnnotationHsqlIntegrationTests-hsql.sql b/src/test/resources/org.springframework.data.jdbc.repository.query/QueryAnnotationHsqlIntegrationTests-hsql.sql new file mode 100644 index 00000000..23bf117b --- /dev/null +++ b/src/test/resources/org.springframework.data.jdbc.repository.query/QueryAnnotationHsqlIntegrationTests-hsql.sql @@ -0,0 +1 @@ +CREATE TABLE dummyentity ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100))