diff --git a/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java index bee8bbc0..f32e7cd1 100644 --- a/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java @@ -34,19 +34,23 @@ import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.MutableBindings; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.query.CriteriaDefinition; import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator; +import org.springframework.data.relational.core.query.ValueFunction; import org.springframework.data.relational.core.sql.*; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.Pair; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** - * Maps {@link Criteria} and {@link Sort} objects considering mapping metadata and dialect-specific conversion. + * Maps {@link CriteriaDefinition} and {@link Sort} objects considering mapping metadata and dialect-specific + * conversion. * * @author Mark Paluch * @author Roman Chigvintsev @@ -344,7 +348,13 @@ public class QueryMapper { mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint()); typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue); + } else if (criteria.getValue() instanceof ValueFunction) { + ValueFunction valueFunction = (ValueFunction) criteria.getValue(); + Object value = valueFunction.apply(getEscaper(criteria.getComparator())); + + mappedValue = convertValue(value, propertyField.getTypeHint()); + typeHint = actualType.getType(); } else { mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint()); @@ -354,6 +364,15 @@ public class QueryMapper { return createCondition(column, mappedValue, typeHint, bindings, criteria.getComparator(), criteria.isIgnoreCase()); } + private Escaper getEscaper(Comparator comparator) { + + if (comparator == Comparator.LIKE || comparator == Comparator.NOT_LIKE) { + return dialect.getLikeEscaper(); + } + + return Escaper.DEFAULT; + } + /** * Potentially convert the {@link SettableValue}. * @@ -376,6 +395,21 @@ public class QueryMapper { return null; } + if (value instanceof Pair) { + + Pair pair = (Pair) value; + + Object first = convertValue(pair.getFirst(), + typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() + : ClassTypeInformation.OBJECT); + + Object second = convertValue(pair.getSecond(), + typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() + : ClassTypeInformation.OBJECT); + + return Pair.of(first, second); + } + if (value instanceof Iterable) { List mapped = new ArrayList<>(); @@ -456,6 +490,19 @@ public class QueryMapper { return condition; } + if (comparator == Comparator.BETWEEN || comparator == Comparator.NOT_BETWEEN) { + + Pair pair = (Pair) mappedValue; + + Expression begin = bind(pair.getFirst(), valueType, bindings, + bindings.nextMarker(column.getName().getReference()), ignoreCase); + Expression end = bind(mappedValue, valueType, bindings, bindings.nextMarker(column.getName().getReference()), + ignoreCase); + + return comparator == Comparator.BETWEEN ? Conditions.between(columnExpression, begin, end) + : Conditions.notBetween(columnExpression, begin, end); + } + BindMarker bindMarker = bindings.nextMarker(column.getName().getReference()); switch (comparator) { @@ -505,6 +552,10 @@ public class QueryMapper { return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext); } + Class getTypeHint(@Nullable Object mappedValue, Class propertyType) { + return propertyType; + } + Class getTypeHint(@Nullable Object mappedValue, Class propertyType, SettableValue settableValue) { if (mappedValue == null || propertyType.equals(Object.class)) { diff --git a/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java b/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java index 9c126d30..1504ec40 100644 --- a/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java @@ -26,7 +26,9 @@ import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.MutableBindings; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.relational.core.dialect.Escaper; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.query.ValueFunction; import org.springframework.data.relational.core.sql.AssignValue; import org.springframework.data.relational.core.sql.Assignment; import org.springframework.data.relational.core.sql.Assignments; @@ -134,6 +136,17 @@ public class UpdateMapper extends QueryMapper { mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint()); typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue); + } else if (value instanceof ValueFunction) { + + ValueFunction valueFunction = (ValueFunction) value; + + mappedValue = convertValue(valueFunction.apply(Escaper.DEFAULT), propertyField.getTypeHint()); + + if (mappedValue == null) { + return Assignments.value(column, SQL.nullLiteral()); + } + + typeHint = actualType.getType(); } else { mappedValue = convertValue(value, propertyField.getTypeHint()); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/CriteriaFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/query/CriteriaFactory.java deleted file mode 100644 index 79420ede..00000000 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/CriteriaFactory.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.r2dbc.repository.query; - -import org.springframework.data.r2dbc.query.Criteria; -import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.util.Assert; - -/** - * Simple factory to contain logic to create {@link Criteria}s from {@link Part}s. - * - * @author Roman Chigvintsev - */ -class CriteriaFactory { - private final ParameterMetadataProvider parameterMetadataProvider; - - /** - * Creates new instance of this class with the given {@link ParameterMetadataProvider}. - * - * @param parameterMetadataProvider parameter metadata provider (must not be {@literal null}) - */ - CriteriaFactory(ParameterMetadataProvider parameterMetadataProvider) { - Assert.notNull(parameterMetadataProvider, "Parameter metadata provider must not be null!"); - this.parameterMetadataProvider = parameterMetadataProvider; - } - - /** - * Creates {@link Criteria} for the given {@link Part}. - * - * @param part method name part (must not be {@literal null}) - * @return {@link Criteria} instance - * @throws IllegalArgumentException if part type is not supported - */ - public Criteria createCriteria(Part part) { - Part.Type type = part.getType(); - - String propertyName = part.getProperty().getSegment(); - Class propertyType = part.getProperty().getType(); - - Criteria.CriteriaStep criteriaStep = Criteria.where(propertyName); - - if (type == Part.Type.IS_NULL || type == Part.Type.IS_NOT_NULL) { - return part.getType() == Part.Type.IS_NULL ? criteriaStep.isNull() : criteriaStep.isNotNull(); - } - - if (type == Part.Type.TRUE || type == Part.Type.FALSE) { - return part.getType() == Part.Type.TRUE ? criteriaStep.isTrue() : criteriaStep.isFalse(); - } - - switch (type) { - case BETWEEN: { - ParameterMetadata geParamMetadata = parameterMetadataProvider.next(part); - ParameterMetadata leParamMetadata = parameterMetadataProvider.next(part); - return criteriaStep.greaterThanOrEquals(geParamMetadata.getValue()) - .and(propertyName).lessThanOrEquals(leParamMetadata.getValue()); - } - case AFTER: - case GREATER_THAN: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - return criteriaStep.greaterThan(paramMetadata.getValue()); - } - case GREATER_THAN_EQUAL: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - return criteriaStep.greaterThanOrEquals(paramMetadata.getValue()); - } - case BEFORE: - case LESS_THAN: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - return criteriaStep.lessThan(paramMetadata.getValue()); - } - case LESS_THAN_EQUAL: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - return criteriaStep.lessThanOrEquals(paramMetadata.getValue()); - } - case IN: - case NOT_IN: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - Criteria criteria = part.getType() == Part.Type.IN - ? criteriaStep.in(paramMetadata.getValue()) - : criteriaStep.notIn(paramMetadata.getValue()); - return criteria.ignoreCase(shouldIgnoreCase(part) - && checkCanUpperCase(part, part.getProperty().getType())); - } - case STARTING_WITH: - case ENDING_WITH: - case CONTAINING: - case NOT_CONTAINING: - case LIKE: - case NOT_LIKE: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - Criteria criteria = part.getType() == Part.Type.NOT_LIKE || part.getType() == Part.Type.NOT_CONTAINING - ? criteriaStep.notLike(paramMetadata.getValue()) - : criteriaStep.like(paramMetadata.getValue()); - return criteria.ignoreCase(shouldIgnoreCase(part) - && checkCanUpperCase(part, propertyType, paramMetadata.getType())); - } - case SIMPLE_PROPERTY: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - if (paramMetadata.getValue() == null) { - return criteriaStep.isNull(); - } - return criteriaStep.is(paramMetadata.getValue()).ignoreCase(shouldIgnoreCase(part) - && checkCanUpperCase(part, propertyType, paramMetadata.getType())); - } - case NEGATING_SIMPLE_PROPERTY: { - ParameterMetadata paramMetadata = parameterMetadataProvider.next(part); - return criteriaStep.not(paramMetadata.getValue()).ignoreCase(shouldIgnoreCase(part) - && checkCanUpperCase(part, propertyType, paramMetadata.getType())); - } - default: - throw new IllegalArgumentException("Unsupported keyword " + type); - } - } - - /** - * Checks whether comparison should be done in case-insensitive way. - * - * @param part method name part (must not be {@literal null}) - * @return {@literal true} if comparison should be done in case-insensitive way - */ - private boolean shouldIgnoreCase(Part part) { - return part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS - || part.shouldIgnoreCase() == Part.IgnoreCaseType.WHEN_POSSIBLE; - } - - /** - * Checks whether "upper-case" conversion can be applied to the given {@link Expression}s in case the underlying - * {@link Part} requires ignoring case. - * - * @param part method name part (must not be {@literal null}) - * @param expressionTypes types of the given expressions (must not be {@literal null} or empty) - * @throws IllegalStateException if {@link Part} requires ignoring case but "upper-case" conversion cannot be - * applied to at least one of the given {@link Expression}s - */ - private boolean checkCanUpperCase(Part part, Class... expressionTypes) { - Assert.notEmpty(expressionTypes, "Expression types must not be null or empty"); - boolean strict = part.shouldIgnoreCase() == Part.IgnoreCaseType.ALWAYS; - for (Class expressionType : expressionTypes) { - if (!canUpperCase(expressionType)) { - if (strict) { - throw new IllegalStateException("Unable to ignore case of " + expressionType.getName() - + " type, the property '" + part.getProperty().getSegment() + "' must reference a string"); - } - return false; - } - } - return true; - } - - private boolean canUpperCase(Class expressionType) { - return expressionType == String.class; - } -} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/LikeEscaper.java b/src/main/java/org/springframework/data/r2dbc/repository/query/LikeEscaper.java deleted file mode 100644 index 54a7674f..00000000 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/LikeEscaper.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.r2dbc.repository.query; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.lang.Nullable; - -/** - * Helper class encapsulating an escape character for LIKE queries and the actually usage of it in escaping - * {@link String}s. - *

- * This class is an adapted version of {@code org.springframework.data.jpa.repository.query.EscapeCharacter} from - * Spring Data JPA project. - * - * @author Roman Chigvintsev - */ -public class LikeEscaper { - public static final LikeEscaper DEFAULT = LikeEscaper.of('\\'); - - private final char escapeCharacter; - private final List toReplace; - - private LikeEscaper(char escapeCharacter) { - if (escapeCharacter == '_' || escapeCharacter == '%') { - throw new IllegalArgumentException("'_' and '%' are special characters and cannot be used as " - + "escape character"); - } - this.escapeCharacter = escapeCharacter; - this.toReplace = Arrays.asList(String.valueOf(escapeCharacter), "_", "%"); - } - - /** - * Creates new instance of this class with the given escape character. - * - * @param escapeCharacter escape character - * @return new instance of {@link LikeEscaper} - * @throws IllegalArgumentException if escape character is one of special characters ('_' and '%') - */ - public static LikeEscaper of(char escapeCharacter) { - return new LikeEscaper(escapeCharacter); - } - - /** - * Escapes all special like characters ({@code _}, {@code %}) using the configured escape character. - * - * @param value value to be escaped - * @return escaped value - */ - @Nullable - public String escape(@Nullable String value) { - if (value == null) { - return null; - } - return toReplace.stream().reduce(value, (it, character) -> it.replace(character, escapeCharacter + character)); - } -} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadata.java b/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadata.java deleted file mode 100644 index 659c2be1..00000000 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadata.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.r2dbc.repository.query; - -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Helper class for holding information about query parameter. - */ -class ParameterMetadata { - private final String name; - @Nullable private final Object value; - private final Class type; - - public ParameterMetadata(String name, @Nullable Object value, Class type) { - Assert.notNull(type, "Parameter type must not be null"); - this.name = name; - this.value = value; - this.type = type; - } - - public String getName() { - return name; - } - - @Nullable - public Object getValue() { - return value; - } - - public Class getType() { - return type; - } -} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadataProvider.java b/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadataProvider.java deleted file mode 100644 index 9c4baaf0..00000000 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadataProvider.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.r2dbc.repository.query; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - -import org.jetbrains.annotations.NotNull; -import org.springframework.data.relational.repository.query.RelationalParameterAccessor; -import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.parser.Part; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Helper class to allow easy creation of {@link ParameterMetadata}s. - *

- * This class is an adapted version of {@code org.springframework.data.jpa.repository.query.ParameterMetadataProvider} - * from Spring Data JPA project. - * - * @author Roman Chigvintsev - */ -class ParameterMetadataProvider implements Iterable { - private static final Object VALUE_PLACEHOLDER = new Object(); - - private final Iterator bindableParameterIterator; - @Nullable private final Iterator bindableParameterValueIterator; - private final List parameterMetadata = new ArrayList<>(); - private final LikeEscaper likeEscaper; - - /** - * Creates new instance of this class with the given {@link RelationalParameterAccessor} and {@link LikeEscaper}. - * - * @param accessor relational parameter accessor (must not be {@literal null}). - * @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null}) - */ - ParameterMetadataProvider(RelationalParameterAccessor accessor, LikeEscaper likeEscaper) { - this(accessor.getBindableParameters(), accessor.iterator(), likeEscaper); - } - - /** - * Creates new instance of this class with the given {@link Parameters} and {@link LikeEscaper}. - * - * @param parameters method parameters (must not be {@literal null}) - * @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null}) - */ - ParameterMetadataProvider(Parameters parameters, LikeEscaper likeEscaper) { - this(parameters, null, likeEscaper); - } - - /** - * Creates new instance of this class with the given {@link Parameters}, {@link Iterator} over all bindable - * parameter values and {@link LikeEscaper}. - * - * @param bindableParameterValueIterator iterator over bindable parameter values - * @param parameters method parameters (must not be {@literal null}) - * @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null}) - */ - private ParameterMetadataProvider(Parameters parameters, - @Nullable Iterator bindableParameterValueIterator, LikeEscaper likeEscaper) { - Assert.notNull(parameters, "Parameters must not be null!"); - Assert.notNull(likeEscaper, "Like escaper must not be null!"); - - this.bindableParameterIterator = parameters.getBindableParameters().iterator(); - this.bindableParameterValueIterator = bindableParameterValueIterator; - this.likeEscaper = likeEscaper; - } - - @NotNull - @Override - public Iterator iterator() { - return parameterMetadata.iterator(); - } - - /** - * Creates new instance of {@link ParameterMetadata} for the given {@link Part} and next {@link Parameter}. - */ - public ParameterMetadata next(Part part) { - Assert.isTrue(bindableParameterIterator.hasNext(), - () -> String.format("No parameter available for part %s.", part)); - Parameter parameter = bindableParameterIterator.next(); - String parameterName = getParameterName(parameter, part.getProperty().getSegment()); - Object parameterValue = getParameterValue(); - Part.Type partType = part.getType(); - - checkNullIsAllowed(parameterName, parameterValue, partType); - Class parameterType = parameter.getType(); - Object preparedParameterValue = prepareParameterValue(parameterValue, parameterType, partType); - - ParameterMetadata metadata = new ParameterMetadata(parameterName, preparedParameterValue, parameterType); - parameterMetadata.add(metadata); - return metadata; - } - - private String getParameterName(Parameter parameter, String defaultName) { - if (parameter.isExplicitlyNamed()) { - return parameter.getName().orElseThrow(() -> new IllegalArgumentException("Parameter needs to be named")); - } - return defaultName; - } - - @Nullable - private Object getParameterValue() { - return bindableParameterValueIterator == null ? VALUE_PLACEHOLDER : bindableParameterValueIterator.next(); - } - - /** - * Checks whether {@literal null} is allowed as parameter value. - * - * @param parameterName parameter name - * @param parameterValue parameter value - * @param partType method name part type (must not be {@literal null}) - * @throws IllegalArgumentException if {@literal null} is not allowed as parameter value - */ - private void checkNullIsAllowed(String parameterName, @Nullable Object parameterValue, Part.Type partType) { - if (parameterValue == null && !Part.Type.SIMPLE_PROPERTY.equals(partType)) { - String message = String.format("Value of parameter with name %s must not be null!", parameterName); - throw new IllegalArgumentException(message); - } - } - - /** - * Prepares parameter value before it's actually bound to the query. - * - * @param value must not be {@literal null} - * @return prepared query parameter value - */ - @Nullable - protected Object prepareParameterValue(@Nullable Object value, Class valueType, Part.Type partType) { - if (value != null && String.class == valueType) { - switch (partType) { - case STARTING_WITH: - return likeEscaper.escape(value.toString()) + "%"; - case ENDING_WITH: - return "%" + likeEscaper.escape(value.toString()); - case CONTAINING: - case NOT_CONTAINING: - return "%" + likeEscaper.escape(value.toString()) + "%"; - } - } - return value; - } -} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java index 8bc5647f..7b150d74 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java @@ -15,8 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import java.util.Collection; - import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.core.DatabaseClient; @@ -25,33 +23,28 @@ import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameters; -import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.data.util.Streamable; /** * An {@link AbstractR2dbcQuery} implementation based on a {@link PartTree}. - *

- * This class is an adapted version of {@code org.springframework.data.jpa.repository.query.PartTreeJpaQuery} from - * Spring Data JPA project. * * @author Roman Chigvintsev + * @since 1.1 */ public class PartTreeR2dbcQuery extends AbstractR2dbcQuery { + private final ReactiveDataAccessStrategy dataAccessStrategy; private final RelationalParameters parameters; private final PartTree tree; - private LikeEscaper likeEscaper = LikeEscaper.DEFAULT; - /** * Creates new instance of this class with the given {@link R2dbcQueryMethod}, {@link DatabaseClient}, * {@link R2dbcConverter} and {@link ReactiveDataAccessStrategy}. * - * @param method query method (must not be {@literal null}) - * @param databaseClient database client (must not be {@literal null}) - * @param converter converter (must not be {@literal null}) - * @param dataAccessStrategy data access strategy (must not be {@literal null}) + * @param method query method, must not be {@literal null}. + * @param databaseClient database client, must not be {@literal null}. + * @param converter converter, must not be {@literal null}. + * @param dataAccessStrategy data access strategy, must not be {@literal null}. */ public PartTreeR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, R2dbcConverter converter, ReactiveDataAccessStrategy dataAccessStrategy) { @@ -61,86 +54,24 @@ public class PartTreeR2dbcQuery extends AbstractR2dbcQuery { try { this.tree = new PartTree(method.getName(), method.getEntityInformation().getJavaType()); - validate(this.tree, this.parameters, method.getName()); - } catch (Exception e) { - String message = String.format("Failed to create query for method %s! %s", method, e.getMessage()); - throw new IllegalArgumentException(message, e); + R2dbcQueryCreator.validate(this.tree, this.parameters); + } catch (RuntimeException e) { + throw new IllegalArgumentException( + String.format("Failed to create query for method %s! %s", method, e.getMessage()), e); } } - public void setLikeEscaper(LikeEscaper likeEscaper) { - this.likeEscaper = likeEscaper; - } - - /** - * Creates new {@link BindableQuery} for the given {@link RelationalParameterAccessor}. - * - * @param accessor query parameter accessor (must not be {@literal null}) - * @return new instance of {@link BindableQuery} - */ @Override protected BindableQuery createQuery(RelationalParameterAccessor accessor) { + RelationalEntityMetadata entityMetadata = getQueryMethod().getEntityInformation(); - ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(accessor, likeEscaper); - R2dbcQueryCreator queryCreator = new R2dbcQueryCreator(tree, dataAccessStrategy, entityMetadata, - parameterMetadataProvider); + R2dbcQueryCreator queryCreator = new R2dbcQueryCreator(tree, dataAccessStrategy, entityMetadata, accessor); PreparedOperation preparedQuery = queryCreator.createQuery(getDynamicSort(accessor)); + return new PreparedOperationBindableQuery(preparedQuery); } private Sort getDynamicSort(RelationalParameterAccessor accessor) { return parameters.potentiallySortsDynamically() ? accessor.getSort() : Sort.unsorted(); } - - private static void validate(PartTree tree, RelationalParameters parameters, String methodName) { - int argCount = 0; - Iterable parts = () -> tree.stream().flatMap(Streamable::stream).iterator(); - for (Part part : parts) { - int numberOfArguments = part.getNumberOfArguments(); - for (int i = 0; i < numberOfArguments; i++) { - throwExceptionOnArgumentMismatch(methodName, part, parameters, argCount); - argCount++; - } - } - } - - private static void throwExceptionOnArgumentMismatch(String methodName, Part part, RelationalParameters parameters, - int index) { - Part.Type type = part.getType(); - String property = part.getProperty().toDotPath(); - - if (!parameters.getBindableParameters().hasParameterAt(index)) { - String msgTemplate = "Method %s expects at least %d arguments but only found %d. " - + "This leaves an operator of type %s for property %s unbound."; - String formattedMsg = String.format(msgTemplate, methodName, index + 1, index, type.name(), property); - throw new IllegalStateException(formattedMsg); - } - - RelationalParameters.RelationalParameter parameter = parameters.getBindableParameter(index); - if (expectsCollection(type) && !parameterIsCollectionLike(parameter)) { - String message = wrongParameterTypeMessage(methodName, property, type, "Collection", parameter); - throw new IllegalStateException(message); - } else if (!expectsCollection(type) && !parameterIsScalarLike(parameter)) { - String message = wrongParameterTypeMessage(methodName, property, type, "scalar", parameter); - throw new IllegalStateException(message); - } - } - - private static boolean expectsCollection(Part.Type type) { - return type == Part.Type.IN || type == Part.Type.NOT_IN; - } - - private static boolean parameterIsCollectionLike(RelationalParameters.RelationalParameter parameter) { - return parameter.getType().isArray() || Collection.class.isAssignableFrom(parameter.getType()); - } - - private static boolean parameterIsScalarLike(RelationalParameters.RelationalParameter parameter) { - return !Collection.class.isAssignableFrom(parameter.getType()); - } - - private static String wrongParameterTypeMessage(String methodName, String property, Part.Type operatorType, - String expectedArgumentType, RelationalParameters.RelationalParameter parameter) { - return String.format("Operator %s on %s requires a %s argument, found %s in method %s.", operatorType.name(), - property, expectedArgumentType, parameter.getType(), methodName); - } } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQuery.java index 293fdb57..2a4c9072 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQuery.java @@ -26,15 +26,18 @@ import org.springframework.util.Assert; * @author Roman Chigvintsev */ class PreparedOperationBindableQuery implements BindableQuery { + private final PreparedOperation preparedQuery; /** * Creates new instance of this class with the given {@link PreparedOperation}. * - * @param preparedQuery prepared SQL query (must not be {@literal null}) + * @param preparedQuery prepared SQL query, must not be {@literal null}. */ PreparedOperationBindableQuery(PreparedOperation preparedQuery) { + Assert.notNull(preparedQuery, "Prepared query must not be null!"); + this.preparedQuery = preparedQuery; } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java index 2154f1a5..291df57d 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryCreator.java @@ -15,7 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; @@ -23,13 +22,14 @@ import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.core.PreparedOperation; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.core.StatementMapper; -import org.springframework.data.r2dbc.query.Criteria; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.query.Criteria; import org.springframework.data.relational.core.sql.SqlIdentifier; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; +import org.springframework.data.relational.repository.query.RelationalParameterAccessor; +import org.springframework.data.relational.repository.query.RelationalQueryCreator; import org.springframework.data.repository.query.parser.AbstractQueryCreator; -import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; @@ -37,82 +37,46 @@ import org.springframework.util.Assert; * Implementation of {@link AbstractQueryCreator} that creates {@link PreparedOperation} from a {@link PartTree}. * * @author Roman Chigvintsev + * @author Mark Paluch + * @since 1.1 */ -public class R2dbcQueryCreator extends AbstractQueryCreator, Criteria> { +public class R2dbcQueryCreator extends RelationalQueryCreator> { + private final PartTree tree; private final ReactiveDataAccessStrategy dataAccessStrategy; private final RelationalEntityMetadata entityMetadata; - private final CriteriaFactory criteriaFactory; /** * Creates new instance of this class with the given {@link PartTree}, {@link ReactiveDataAccessStrategy}, - * {@link RelationalEntityMetadata} and {@link ParameterMetadataProvider}. + * {@link RelationalEntityMetadata} and {@link RelationalParameterAccessor}. * - * @param tree part tree (must not be {@literal null}) - * @param dataAccessStrategy data access strategy (must not be {@literal null}) - * @param entityMetadata relational entity metadata (must not be {@literal null}) - * @param parameterMetadataProvider parameter metadata provider (must not be {@literal null}) + * @param tree part tree, must not be {@literal null}. + * @param dataAccessStrategy data access strategy, must not be {@literal null}. + * @param entityMetadata relational entity metadata, must not be {@literal null}. + * @param accessor parameter metadata provider, must not be {@literal null}. */ public R2dbcQueryCreator(PartTree tree, ReactiveDataAccessStrategy dataAccessStrategy, - RelationalEntityMetadata entityMetadata, ParameterMetadataProvider parameterMetadataProvider) { - super(tree); + RelationalEntityMetadata entityMetadata, RelationalParameterAccessor accessor) { + super(tree, accessor); this.tree = tree; Assert.notNull(dataAccessStrategy, "Data access strategy must not be null"); Assert.notNull(entityMetadata, "Relational entity metadata must not be null"); - Assert.notNull(parameterMetadataProvider, "Parameter metadata provider must not be null"); this.dataAccessStrategy = dataAccessStrategy; this.entityMetadata = entityMetadata; - this.criteriaFactory = new CriteriaFactory(parameterMetadataProvider); - } - - /** - * Creates {@link Criteria} for the given method name part. - * - * @param part method name part (must not be {@literal null}) - * @param iterator iterator over query parameter values - * @return new instance of {@link Criteria} - */ - @Override - protected Criteria create(Part part, Iterator iterator) { - return criteriaFactory.createCriteria(part); - } - - /** - * Combines the given {@link Criteria} with the new one created for the given method name part using {@code AND}. - * - * @param part method name part (must not be {@literal null}) - * @param base {@link Criteria} to be combined (must not be {@literal null}) - * @param iterator iterator over query parameter values - * @return {@link Criteria} combination - */ - @Override - protected Criteria and(Part part, Criteria base, Iterator iterator) { - return base.and(criteriaFactory.createCriteria(part)); - } - - /** - * Combines two {@link Criteria}s using {@code OR}. - * - * @param base {@link Criteria} to be combined (must not be {@literal null}) - * @param criteria another {@link Criteria} to be combined (must not be {@literal null}) - * @return {@link Criteria} combination - */ - @Override - protected Criteria or(Criteria base, Criteria criteria) { - return base.or(criteria); } /** * Creates {@link PreparedOperation} applying the given {@link Criteria} and {@link Sort} definition. * * @param criteria {@link Criteria} to be applied to query - * @param sort sort option to be applied to query (must not be {@literal null}) + * @param sort sort option to be applied to query, must not be {@literal null}. * @return instance of {@link PreparedOperation} */ @Override protected PreparedOperation complete(Criteria criteria, Sort sort) { + StatementMapper statementMapper = dataAccessStrategy.getStatementMapper().forType(entityMetadata.getJavaType()); StatementMapper.SelectSpec selectSpec = statementMapper.createSelect(entityMetadata.getTableName()) .withProjection(getSelectProjection()); @@ -135,26 +99,27 @@ public class R2dbcQueryCreator extends AbstractQueryCreator } private SqlIdentifier[] getSelectProjection() { + List columnNames; + if (tree.isExistsProjection()) { columnNames = dataAccessStrategy.getIdentifierColumns(entityMetadata.getJavaType()); } else { columnNames = dataAccessStrategy.getAllColumns(entityMetadata.getJavaType()); } + return columnNames.toArray(new SqlIdentifier[0]); } private Sort getSort(Sort sort) { + RelationalPersistentEntity tableEntity = entityMetadata.getTableEntity(); + List orders = sort.get().map(order -> { RelationalPersistentProperty property = tableEntity.getRequiredPersistentProperty(order.getProperty()); - String columnName = dataAccessStrategy.toSql(property.getColumnName()); - String orderProperty = entityMetadata.getTableName() + "." + columnName; - // TODO: org.springframework.data.relational.core.sql.render.OrderByClauseVisitor from - // spring-data-relational does not prepend column name with table name. It makes sense to render - // column names uniformly. - return order.isAscending() ? Sort.Order.asc(orderProperty) : Sort.Order.desc(orderProperty); + return order.isAscending() ? Sort.Order.asc(property.getName()) : Sort.Order.desc(property.getName()); }).collect(Collectors.toList()); + return Sort.by(orders); } } diff --git a/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java b/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java index ba8b4372..48108d3c 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java @@ -43,6 +43,7 @@ import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; + import org.springframework.beans.factory.annotation.Value; import org.springframework.data.annotation.Id; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; diff --git a/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java b/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java index 2ea379d2..0fa448ed 100644 --- a/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java @@ -22,7 +22,7 @@ import java.util.Arrays; import org.assertj.core.api.SoftAssertions; import org.junit.Test; -import org.springframework.data.r2dbc.query.Criteria.*; + import org.springframework.data.relational.core.sql.SqlIdentifier; /** @@ -72,7 +72,6 @@ public class CriteriaUnitTests { assertThat(Criteria.from(empty, notEmpty).isEmpty()).isFalse(); assertThat(Criteria.from(notEmpty, empty).isEmpty()).isFalse(); - }); } @@ -272,16 +271,18 @@ public class CriteriaUnitTests { assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); } - @Test + @Test // gh-282 public void shouldBuildIsTrueCriteria() { + Criteria criteria = where("foo").isTrue(); assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_TRUE); } - @Test + @Test // gh-282 public void shouldBuildIsFalseCriteria() { + Criteria criteria = where("foo").isFalse(); assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo")); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index d7aeba1e..a8af367f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -130,7 +130,7 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg shouldInsertNewItems(); - repository.findByNameContains("%F%") // + repository.findByNameContains("F") // .map(LegoSet::getName) // .collectList() // .as(StepVerifier::create) // diff --git a/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java index e410d1b4..f8c989a7 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java @@ -114,10 +114,6 @@ public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIn interface H2LegoSetRepository extends LegoSetRepository { - @Override - @Query("SELECT * FROM legoset WHERE name like $1") - Flux findByNameContains(String name); - @Override @Query("SELECT name FROM legoset") Flux findAsProjection(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/JasyncMySqlR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/JasyncMySqlR2dbcRepositoryIntegrationTests.java index ce444dcb..af29662c 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/JasyncMySqlR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/JasyncMySqlR2dbcRepositoryIntegrationTests.java @@ -82,10 +82,6 @@ public class JasyncMySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRep interface MySqlLegoSetRepository extends LegoSetRepository { - @Override - @Query("SELECT * FROM legoset WHERE name like ?") - Flux findByNameContains(String name); - @Override @Query("SELECT name FROM legoset") Flux findAsProjection(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java index 0bff5c5c..0b97933a 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/MariaDbR2dbcRepositoryIntegrationTests.java @@ -81,10 +81,6 @@ public class MariaDbR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposit interface MySqlLegoSetRepository extends LegoSetRepository { - @Override - @Query("SELECT * FROM legoset WHERE name like ?") - Flux findByNameContains(String name); - @Override @Query("SELECT name FROM legoset") Flux findAsProjection(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java index 376d3f50..0a03c4cd 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java @@ -81,10 +81,6 @@ public class MySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositor interface MySqlLegoSetRepository extends LegoSetRepository { - @Override - @Query("SELECT * FROM legoset WHERE name like ?") - Flux findByNameContains(String name); - @Override @Query("SELECT name FROM legoset") Flux findAsProjection(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index c7fba7c8..56df078d 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -81,10 +81,6 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi interface PostgresLegoSetRepository extends LegoSetRepository { - @Override - @Query("SELECT * FROM legoset WHERE name like $1") - Flux findByNameContains(String name); - @Override @Query("SELECT name FROM legoset") Flux findAsProjection(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/LikeEscaperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/LikeEscaperUnitTests.java deleted file mode 100644 index f86a8e82..00000000 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/LikeEscaperUnitTests.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * 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.r2dbc.repository.query; - -import static org.assertj.core.api.Assertions.*; -import static org.junit.jupiter.api.Assertions.*; - -import org.junit.Test; - -/** - * @author Roman Chigvintsev - */ -public class LikeEscaperUnitTests { - @Test - public void ignoresNulls() { - assertNull(LikeEscaper.DEFAULT.escape(null)); - } - - @Test - public void ignoresEmptyString() { - assertThat(LikeEscaper.DEFAULT.escape("")).isEqualTo(""); - } - - @Test - public void ignoresBlankString() { - assertThat(LikeEscaper.DEFAULT.escape(" ")).isEqualTo(" "); - } - - @Test(expected = IllegalArgumentException.class) - public void throwsExceptionWhenEscapeCharacterIsUnderscore() { - LikeEscaper.of('_'); - } - - @Test(expected = IllegalArgumentException.class) - public void throwsExceptionWhenEscapeCharacterIsPercent() { - LikeEscaper.of('%'); - } - - @Test - public void escapesUnderscoresUsingDefaultEscapeCharacter() { - assertThat(LikeEscaper.DEFAULT.escape("_test_")).isEqualTo("\\_test\\_"); - } - - @Test - public void escapesPercentsUsingDefaultEscapeCharacter() { - assertThat(LikeEscaper.DEFAULT.escape("%test%")).isEqualTo("\\%test\\%"); - } - - @Test - public void escapesSpecialCharactersUsingCustomEscapeCharacter() { - assertThat(LikeEscaper.of('$').escape("_%")).isEqualTo("$_$%"); - } - - @Test - public void doublesEscapeCharacter() { - assertThat(LikeEscaper.DEFAULT.escape("\\")).isEqualTo("\\\\"); - } -} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java similarity index 74% rename from src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java index f1fa1707..8d8e2aa7 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryUnitTests.java @@ -20,6 +20,7 @@ import static org.mockito.Mockito.*; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; +import lombok.Data; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,12 +30,11 @@ import java.util.Collections; import java.util.Date; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.annotation.Id; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.convert.R2dbcConverter; @@ -51,29 +51,28 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; /** + * Unit tests for {@link PartTreeR2dbcQuery}. + * * @author Roman Chigvintsev + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) -public class PartTreeR2dbcQueryIntegrationTests { +public class PartTreeR2dbcQueryUnitTests { + private static final String TABLE = "users"; - private static final String ALL_FIELDS = TABLE + ".id, " - + TABLE + ".first_name, " - + TABLE + ".last_name, " - + TABLE + ".date_of_birth, " - + TABLE + ".age, " - + TABLE + ".active"; + private static final String ALL_FIELDS = TABLE + ".id, " + TABLE + ".first_name, " + TABLE + ".last_name, " + TABLE + + ".date_of_birth, " + TABLE + ".age, " + TABLE + ".active"; - @Mock private ConnectionFactory connectionFactory; - @Mock private R2dbcConverter r2dbcConverter; + @Mock ConnectionFactory connectionFactory; + @Mock R2dbcConverter r2dbcConverter; - @Rule public ExpectedException thrown = ExpectedException.none(); - - private RelationalMappingContext mappingContext; - private ReactiveDataAccessStrategy dataAccessStrategy; - private DatabaseClient databaseClient; + RelationalMappingContext mappingContext; + ReactiveDataAccessStrategy dataAccessStrategy; + DatabaseClient databaseClient; @Before public void setUp() { + ConnectionFactoryMetadata metadataMock = mock(ConnectionFactoryMetadata.class); when(metadataMock.getName()).thenReturn("PostgreSQL"); when(connectionFactory.getMetadata()).thenReturn(metadataMock); @@ -90,195 +89,225 @@ public class PartTreeR2dbcQueryIntegrationTests { .dataAccessStrategy(dataAccessStrategy).build(); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttribute() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); BindableQuery bindableQuery = r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "John" })); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1"); } - @Test + @Test // gh-282 public void createsQueryWithIsNullCondition() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); BindableQuery bindableQuery = r2dbcQuery.createQuery((getAccessor(queryMethod, new Object[] { null }))); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name IS NULL"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name IS NULL"); } - @Test + @Test // gh-282 public void createsQueryWithLimitForExistsProjection() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("existsByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); BindableQuery query = r2dbcQuery.createQuery((getAccessor(queryMethod, new Object[] { "John" }))); - String expectedSql = "SELECT " + TABLE + ".id FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1 LIMIT 1"; - assertThat(query.get()).isEqualTo(expectedSql); + + assertThat(query.get()) + .isEqualTo("SELECT " + TABLE + ".id FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1 LIMIT 1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByTwoStringAttributes() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameAndFirstName", String.class, String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); BindableQuery bindableQuery = r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" })); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".last_name = $1 AND (" + TABLE + ".first_name = $2)"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()).isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + + ".last_name = $1 AND (" + TABLE + ".first_name = $2)"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByOneOfTwoStringAttributes() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameOrFirstName", String.class, String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); BindableQuery bindableQuery = r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { "Doe", "John" })); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".last_name = $1 OR (" + TABLE + ".first_name = $2)"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()).isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + + ".last_name = $1 OR (" + TABLE + ".first_name = $2)"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBetween", Date.class, Date.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); - RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, - new Object[] { new Date(), new Date() }); + RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date(), new Date() }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".date_of_birth >= $1 AND " + TABLE + ".date_of_birth <= $2"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".date_of_birth BETWEEN $1 AND $2"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeLessThan() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThan", Integer.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age < $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age < $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeLessThanEqual() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeLessThanEqual", Integer.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age <= $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age <= $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThan() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThan", Integer.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age > $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age > $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeGreaterThanEqual() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeGreaterThanEqual", Integer.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { 30 }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age >= $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age >= $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByDateAttributeAfter() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthAfter", Date.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".date_of_birth > $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".date_of_birth > $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByDateAttributeBefore() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBefore", Date.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".date_of_birth < $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".date_of_birth < $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeIsNull() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNull"); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IS NULL"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IS NULL"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeIsNotNull() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIsNotNull"); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IS NOT NULL"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IS NOT NULL"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeLike() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameLike", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeNotLike() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotLike", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "%John%" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name NOT LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name NOT LIKE $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeStartingWith() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Jo" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"); } @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test + @Test // gh-282 public void appendsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameStartingWith", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); @@ -286,23 +315,27 @@ public class PartTreeR2dbcQueryIntegrationTests { BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class); bindableQuery.bind(bindSpecMock); + verify(bindSpecMock, times(1)).bind(0, "Jo%"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeEndingWith() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "hn" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"); } @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test + @Test // gh-282 public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); @@ -310,23 +343,27 @@ public class PartTreeR2dbcQueryIntegrationTests { BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class); bindableQuery.bind(bindSpecMock); + verify(bindSpecMock, times(1)).bind(0, "%hn"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeContaining() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name LIKE $1"); } @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test + @Test // gh-282 public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); @@ -334,24 +371,27 @@ public class PartTreeR2dbcQueryIntegrationTests { BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class); bindableQuery.bind(bindSpecMock); + verify(bindSpecMock, times(1)).bind(0, "%oh%"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeNotContaining() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".first_name NOT LIKE $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name NOT LIKE $1"); } @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test + @Test // gh-282 public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); @@ -359,10 +399,11 @@ public class PartTreeR2dbcQueryIntegrationTests { BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class); bindableQuery.bind(bindSpecMock); + verify(bindSpecMock, times(1)).bind(0, "%oh%"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeWithDescendingOrderingByStringAttribute() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameDesc", Integer.class); @@ -370,48 +411,50 @@ public class PartTreeR2dbcQueryIntegrationTests { dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".age = $1 ORDER BY users.last_name DESC"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age = $1 ORDER BY last_name DESC"); } - @Test - public void createsQueryToFindAllEntitiesByIntegerAttributeWithAscendingOrderingByStringAttribute() - throws Exception { + @Test // gh-282 + public void createsQueryToFindAllEntitiesByIntegerAttributeWithAscendingOrderingByStringAttribute() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameAsc", Integer.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "oh" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".age = $1 ORDER BY users.last_name ASC"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age = $1 ORDER BY last_name ASC"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeNot() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findAllByLastNameNot", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "Doe" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".last_name != $1"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".last_name != $1"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeIn() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeIn", Collection.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { Collections.singleton(25) }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IN ($1)"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age IN ($1)"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByIntegerAttributeNotIn() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeNotIn", Collection.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, @@ -419,118 +462,120 @@ public class PartTreeR2dbcQueryIntegrationTests { RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { Collections.singleton(25) }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age NOT IN ($1)"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".age NOT IN ($1)"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByBooleanAttributeTrue() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByActiveTrue"); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".active = TRUE"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".active = TRUE"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByBooleanAttributeFalse() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByActiveFalse"); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[0]); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".active = FALSE"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".active = FALSE"); } - @Test + @Test // gh-282 public void createsQueryToFindAllEntitiesByStringAttributeIgnoringCase() throws Exception { + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameIgnoreCase", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE UPPER(" + TABLE + ".first_name) = UPPER($1)"; - assertThat(bindableQuery.get()).isEqualTo(expectedSql); + + assertThat(bindableQuery.get()) + .isEqualTo("SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE UPPER(" + TABLE + ".first_name) = UPPER($1)"); } - @Test + @Test // gh-282 public void throwsExceptionWhenIgnoringCaseIsImpossible() throws Exception { - thrown.expect(IllegalStateException.class); - thrown.expectMessage("Unable to ignore case of java.lang.Long type, " - + "the property 'id' must reference a string"); + R2dbcQueryMethod queryMethod = getQueryMethod("findByIdIgnoringCase", Long.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); - r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L })); + + assertThatIllegalStateException() + .isThrownBy(() -> r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L }))); } - @Test + @Test // gh-282 public void throwsExceptionWhenInPredicateHasNonIterableParameter() throws Exception { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Operator IN on id requires a Collection argument, " - + "found class java.lang.Long in method findAllByIdIn."); + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByIdIn", Long.class); - PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, - dataAccessStrategy); - r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { 1L })); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy)); } - @Test + @Test // gh-282 public void throwsExceptionWhenSimplePropertyPredicateHasIterableParameter() throws Exception { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Operator SIMPLE_PROPERTY on id requires a scalar argument, " - + "found interface java.util.Collection in method findAllById."); + R2dbcQueryMethod queryMethod = getQueryMethod("findAllById", Collection.class); - PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, - dataAccessStrategy); - r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[] { Collections.singleton(1L) })); + + assertThatIllegalArgumentException() + .isThrownBy(() -> new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy)); } - @Test + @Test // gh-282 public void throwsExceptionWhenConditionKeywordIsUnsupported() throws Exception { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Unsupported keyword IS_EMPTY"); + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByIdIsEmpty"); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); - r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[0])); + + assertThatIllegalArgumentException() + .isThrownBy(() -> r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[0]))); } - @Test + @Test // gh-282 public void throwsExceptionWhenInvalidNumberOfParameterIsGiven() throws Exception { - thrown.expect(IllegalArgumentException.class); - thrown.expectMessage("Invalid number of parameters given!"); + R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); - r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[0])); + + assertThatIllegalArgumentException() + .isThrownBy(() -> r2dbcQuery.createQuery(getAccessor(queryMethod, new Object[0]))); } - @Test + @Test // gh-282 public void createsQueryWithLimitToFindEntitiesByStringAttribute() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findTop3ByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".first_name = $1 LIMIT 3"; + String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1 LIMIT 3"; assertThat(bindableQuery.get()).isEqualTo(expectedSql); } - @Test + @Test // gh-282 public void createsQueryToFindFirstEntityByStringAttribute() throws Exception { R2dbcQueryMethod queryMethod = getQueryMethod("findFirstByFirstName", String.class); PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter, dataAccessStrategy); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { "John" }); BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor); - String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE - + " WHERE " + TABLE + ".first_name = $1 LIMIT 1"; + String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name = $1 LIMIT 1"; assertThat(bindableQuery.get()).isEqualTo(expectedSql); } @@ -544,7 +589,8 @@ public class PartTreeR2dbcQueryIntegrationTests { return new RelationalParametersParameterAccessor(queryMethod, values); } - private interface UserRepository extends Repository { + interface UserRepository extends Repository { + Flux findAllByFirstName(String firstName); Flux findAllByLastNameAndFirstName(String lastName, String firstName); @@ -613,60 +659,14 @@ public class PartTreeR2dbcQueryIntegrationTests { } @Table("users") + @Data private static class User { - @Id private Long id; + + private @Id Long id; private String firstName; private String lastName; private Date dateOfBirth; private Integer age; private Boolean active; - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getFirstName() { - return firstName; - } - - public void setFirstName(String firstName) { - this.firstName = firstName; - } - - public String getLastName() { - return lastName; - } - - public void setLastName(String lastName) { - this.lastName = lastName; - } - - public Date getDateOfBirth() { - return dateOfBirth; - } - - public void setDateOfBirth(Date dateOfBirth) { - this.dateOfBirth = dateOfBirth; - } - - public Integer getAge() { - return age; - } - - public void setAge(Integer age) { - this.age = age; - } - - public Boolean getActive() { - return active; - } - - public void setActive(Boolean active) { - this.active = active; - } } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQueryUnitTests.java index 6690286c..3ad79dbb 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/PreparedOperationBindableQueryUnitTests.java @@ -32,16 +32,13 @@ import org.springframework.data.r2dbc.core.PreparedOperation; @RunWith(MockitoJUnitRunner.class) @Ignore public class PreparedOperationBindableQueryUnitTests { - @Mock private PreparedOperation preparedOperation; - @Test(expected = IllegalArgumentException.class) - public void throwsExceptionWhenPreparedOperationIsNull() { - new PreparedOperationBindableQuery(null); - } + @Mock PreparedOperation preparedOperation; @SuppressWarnings({ "rawtypes", "unchecked" }) - @Test + @Test // gh-282 public void bindsQueryParameterValues() { + DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class); PreparedOperationBindableQuery query = new PreparedOperationBindableQuery(preparedOperation); @@ -49,12 +46,12 @@ public class PreparedOperationBindableQueryUnitTests { verify(preparedOperation, times(1)).bindTo(any()); } - @Test + @Test // gh-282 public void returnsSqlQuery() { - String sql = "SELECT * FROM test"; - when(preparedOperation.get()).thenReturn(sql); + + when(preparedOperation.get()).thenReturn("SELECT * FROM test"); PreparedOperationBindableQuery query = new PreparedOperationBindableQuery(preparedOperation); - assertThat(query.get()).isEqualTo(sql); + assertThat(query.get()).isEqualTo("SELECT * FROM test"); } }