diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java index cac47ce50..c15b52b99 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -27,8 +27,8 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the - * method. + * Base class for queries based on a repository method. It holds the infrastructure for executing a query and knows how + * to execute a query based on the return type of the method. How to construct the query is left to subclasses. * * @author Jens Schauder * @author Kazuki Shimizu diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 7695885aa..342b6bf0c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -65,7 +65,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { * @param entityMetadata relational entity metadata, must not be {@literal null}. * @param accessor parameter metadata provider, must not be {@literal null}. */ - public JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect, + JdbcQueryCreator(PartTree tree, JdbcConverter converter, Dialect dialect, RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor) { super(tree, accessor); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java index 227f96083..afaffd192 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryExecution.java @@ -19,7 +19,8 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.lang.Nullable; /** - * Interface specifying a result execution strategy. + * Interface specifying a query execution strategy. Implementations encapsulate information how to actually execute the + * query and how to process the result in order to get the desired return type. * * @author Mark Paluch * @since 2.0 @@ -28,11 +29,11 @@ import org.springframework.lang.Nullable; interface JdbcQueryExecution { /** - * Execute the given {@code query}. + * Execute the given {@code query} and {@code parameter} and transforms the result into a {@code T}. * - * @param query - * @param parameter - * @return + * @param query the query to be executed. Must not be {@literal null}. + * @param parameter the parameters to be bound to the query. Must not be {@literal null}. + * @return the result of the query. Might be {@literal null}. */ @Nullable T execute(String query, SqlParameterSource parameter); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java index b72d807e9..02a2b3af1 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/ParametrizedQuery.java @@ -18,7 +18,7 @@ package org.springframework.data.jdbc.repository.query; import org.springframework.jdbc.core.namedparam.SqlParameterSource; /** - * Value object encapsulating a parametrized query containing named parameters and {@link SqlParameterSource}. + * Value object encapsulating a query containing named parameters and a{@link SqlParameterSource} to bind the parameters. * * @author Mark Paluch * @since 2.0 @@ -28,16 +28,17 @@ class ParametrizedQuery { private final String query; private final SqlParameterSource parameterSource; - public ParametrizedQuery(String query, SqlParameterSource parameterSource) { + ParametrizedQuery(String query, SqlParameterSource parameterSource) { + this.query = query; this.parameterSource = parameterSource; } - public String getQuery() { + String getQuery() { return query; } - public SqlParameterSource getParameterSource() { + SqlParameterSource getParameterSource() { return parameterSource; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index 3ad0c8621..775080391 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -64,9 +64,11 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery { this.converter = converter; try { + this.tree = new PartTree(queryMethod.getName(), queryMethod.getEntityInformation().getJavaType()); JdbcQueryCreator.validate(this.tree, this.parameters); } catch (RuntimeException e) { + throw new IllegalArgumentException( String.format("Failed to create query for method %s! %s", queryMethod, e.getMessage()), e); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java index 65eb0925f..b56b3bb1f 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -69,7 +69,7 @@ class QueryMapper { * @param converter must not be {@literal null}. */ @SuppressWarnings({ "unchecked", "rawtypes" }) - public QueryMapper(Dialect dialect, JdbcConverter converter) { + QueryMapper(Dialect dialect, JdbcConverter converter) { Assert.notNull(dialect, "Dialect must not be null!"); Assert.notNull(converter, "JdbcConverter must not be null!"); @@ -80,13 +80,13 @@ class QueryMapper { } /** - * Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}. + * Map the {@link Sort} object to apply field name mapping using {@link RelationalPersistentEntity the type to read}. * * @param sort must not be {@literal null}. * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. * @return */ - public List getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { + List getMappedSort(Table table, Sort sort, @Nullable RelationalPersistentEntity entity) { List mappedOrder = new ArrayList<>(); @@ -102,13 +102,14 @@ class QueryMapper { } /** - * Map the {@link Expression} object to apply field name mapping using {@link Class the type to read}. + * Map the {@link Expression} object to apply field name mapping using {@link RelationalPersistentEntity the type to + * read}. * * @param expression must not be {@literal null}. * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. - * @return the mapped {@link Expression}. + * @return the mapped {@link Expression}. Guaranteed to be not {@literal null}. */ - public Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity entity) { + Expression getMappedObject(Expression expression, @Nullable RelationalPersistentEntity entity) { if (entity == null || expression instanceof AsteriskFromTable) { return expression; @@ -120,6 +121,8 @@ class QueryMapper { Field field = createPropertyField(entity, column.getName()); Table table = column.getTable(); + Assert.state(table != null, String.format("The column %s must have a table set.", column)); + Column columnFromTable = table.column(field.getMappedColumnName()); return column instanceof Aliased ? columnFromTable.as(((Aliased) column).getAlias()) : columnFromTable; } @@ -144,7 +147,7 @@ class QueryMapper { } /** - * Map a {@link CriteriaDefinition} object into {@link Condition} and consider value/{@code NULL} {@link Bindings}. + * Map a {@link CriteriaDefinition} object into {@link Condition} and consider value/{@code NULL} bindings. * * @param parameterSource bind parameterSource object, must not be {@literal null}. * @param criteria criteria definition to map, must not be {@literal null}. @@ -152,7 +155,7 @@ class QueryMapper { * @param entity related {@link RelationalPersistentEntity}, can be {@literal null}. * @return the mapped {@link Condition}. */ - public Condition getMappedObject(MapSqlParameterSource parameterSource, CriteriaDefinition criteria, Table table, + Condition getMappedObject(MapSqlParameterSource parameterSource, CriteriaDefinition criteria, Table table, @Nullable RelationalPersistentEntity entity) { Assert.notNull(parameterSource, "MapSqlParameterSource must not be null!"); @@ -349,13 +352,13 @@ class QueryMapper { Pair pair = (Pair) value; - Object first = convertValue(pair.getFirst(), - typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() - : ClassTypeInformation.OBJECT); + Object first = convertValue(pair.getFirst(), typeInformation.getActualType() != null // + ? typeInformation.getRequiredActualType() + : ClassTypeInformation.OBJECT); - Object second = convertValue(pair.getSecond(), - typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() - : ClassTypeInformation.OBJECT); + Object second = convertValue(pair.getSecond(), typeInformation.getActualType() != null // + ? typeInformation.getRequiredActualType() + : ClassTypeInformation.OBJECT); return Pair.of(first, second); } @@ -365,6 +368,7 @@ class QueryMapper { List mapped = new ArrayList<>(); for (Object o : (Iterable) value) { + mapped.add(convertValue(o, typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() : ClassTypeInformation.OBJECT)); } @@ -498,10 +502,6 @@ class QueryMapper { return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext, converter); } - Class getTypeHint(@Nullable Object mappedValue, Class propertyType) { - return propertyType; - } - int getTypeHint(@Nullable Object mappedValue, Class propertyType, JdbcValue settableValue) { if (mappedValue == null || propertyType.equals(Object.class)) { @@ -560,7 +560,7 @@ class QueryMapper { * * @param name must not be {@literal null} or empty. */ - public Field(SqlIdentifier name) { + Field(SqlIdentifier name) { Assert.notNull(name, "Name must not be null!"); this.name = name; 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 f48dd174b..2df4b4225 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 @@ -94,11 +94,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { JdbcQueryMethod queryMethod = new JdbcQueryMethod(method, repositoryMetadata, projectionFactory, namedQueries, context); - if (namedQueries.hasQuery(queryMethod.getNamedQueryName())) { - - RowMapper mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod); - return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter); - } else if (queryMethod.hasAnnotatedQuery()) { + if (namedQueries.hasQuery(queryMethod.getNamedQueryName()) || queryMethod.hasAnnotatedQuery()) { RowMapper mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod); return new StringBasedJdbcQuery(queryMethod, operations, mapper, converter); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 90b7330dc..862d45d7f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; -import lombok.Data; import java.lang.reflect.Method; import java.util.Collection; @@ -31,7 +30,6 @@ import java.util.Properties; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; @@ -63,6 +61,26 @@ public class PartTreeJdbcQueryUnitTests { JdbcMappingContext mappingContext = new JdbcMappingContext(); JdbcConverter converter = new BasicJdbcConverter(mappingContext, mock(RelationResolver.class)); + @Test // DATAJDBC-318 + public void selectContainsColumnsForOneToOneReference() throws Exception { + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" })); + + assertThat(query.getQuery()).contains("hated.\"name\" AS \"hated_name\""); + } + + @Test // DATAJDBC-318 + public void doesNotContainsColumnsForOneToManyReference() throws Exception{ + + JdbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" })); + + assertThat(query.getQuery().toLowerCase()).doesNotContain("hobbies"); + } + @Test // DATAJDBC-318 public void createsQueryToFindAllEntitiesByStringAttribute() throws Exception { @@ -622,7 +640,6 @@ public class PartTreeJdbcQueryUnitTests { } @Table("users") - @Data static class User { @Id Long id; @@ -633,12 +650,18 @@ public class PartTreeJdbcQueryUnitTests { Boolean active; @Embedded(prefix = "user_", onEmpty = Embedded.OnEmpty.USE_NULL) Address address; + + List hobbies; + Hobby hated; } - @Data @AllArgsConstructor static class Address { String street; String city; } + + static class Hobby { + String name; + } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java index 4db4410d7..283d37c0c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalQueryCreator.java @@ -103,8 +103,10 @@ public abstract class RelationalQueryCreator extends AbstractQueryCreator parts = () -> tree.stream().flatMap(Streamable::stream).iterator(); for (Part part : parts) { + int numberOfArguments = part.getNumberOfArguments(); for (int i = 0; i < numberOfArguments; i++) { + throwExceptionOnArgumentMismatch(part, parameters, argCount); argCount++; } @@ -117,6 +119,7 @@ public abstract class RelationalQueryCreator extends AbstractQueryCreator extends AbstractQueryCreator Use `Pageable` to pass offset and sorting parameters to the database. <3> Find a single entity for the given criteria. It completes with `IncorrectResultSizeDataAccessException` on non-unique results. -<4> Unless <3>, the first entity is always emitted even if the query yields more result documents. +<4> In contrast to <3>, the first entity is always emitted even if the query yields more result documents. <5> The `findByLastname` method shows a query for all people with the given last name. ==== @@ -541,7 +541,7 @@ The following table shows the keywords that are supported for query methods: | `active IS FALSE` |=== -NOTE: Query derivation is limited to properties that can be used in a `WHERE` clause without involving joins. +NOTE: Query derivation is limited to properties that can be used in a `WHERE` clause without using joins. [[jdbc.query-methods.strategies]] === Query Lookup Strategies @@ -622,7 +622,7 @@ Iterating happens in the order of registration, so make sure to register more ge If applicable, wrapper types such as collections or `Optional` are unwrapped. Thus, a return type of `Optional` uses the `Person` type in the preceding process. -NOTE: Using a custom `RowMapper` through `QueryMappingConfiguration`, `@Query(rowMapperClass=…)`, or a custom `ResultSetExtractor` disables Entity Callbacks and Lifecycle Events as these components are under full control of the result mapping and can issue their own events/callbacks if needed. +NOTE: Using a custom `RowMapper` through `QueryMappingConfiguration`, `@Query(rowMapperClass=…)`, or a custom `ResultSetExtractor` disables Entity Callbacks and Lifecycle Events as the result mapping can issue its own events/callbacks if needed. [[jdbc.query-methods.at-query.modifying]] ==== Modifying Query