#282 - Add support for query derivation.

We now support query derivation for R2DBC repositories:

interface ReactivePersonRepository extends ReactiveSortingRepository<Person, String> {

  Flux<Person> findByFirstname(String firstname);

  Flux<Person> findByFirstname(Publisher<String> firstname);

  Mono<Person> findByFirstnameAndLastname(String firstname, String lastname);

  Flux<Person> findFirstByLastnameLike(String pattern);
}

Original pull request: #295.
This commit is contained in:
Roman Chigvintsev
2020-01-23 01:59:39 +03:00
committed by Mark Paluch
parent 6dcd8787c1
commit dbe935c45a
16 changed files with 1933 additions and 22 deletions

View File

@@ -226,6 +226,15 @@ class DefaultStatementMapper implements StatementMapper {
return getMappedObject(deleteSpec, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getRenderContext()
*/
@Override
public RenderContext getRenderContext() {
return renderContext;
}
private PreparedOperation<Delete> getMappedObject(DeleteSpec deleteSpec,
@Nullable RelationalPersistentEntity<?> entity) {
@@ -375,5 +384,14 @@ class DefaultStatementMapper implements StatementMapper {
public PreparedOperation<?> getMappedObject(DeleteSpec deleteSpec) {
return DefaultStatementMapper.this.getMappedObject(deleteSpec, this.entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getRenderContext()
*/
@Override
public RenderContext getRenderContext() {
return DefaultStatementMapper.this.getRenderContext();
}
}
}

View File

@@ -34,6 +34,7 @@ import org.springframework.data.r2dbc.query.Update;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.SqlIdentifier;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.render.RenderContext;
import org.springframework.lang.Nullable;
/**
@@ -177,6 +178,16 @@ public interface StatementMapper {
return DeleteSpec.create(table);
}
/**
* Returns {@link RenderContext}.
*
* @return {@link RenderContext} instance or {@literal null} if {@link RenderContext} is not available
*/
@Nullable
default RenderContext getRenderContext() {
return null;
}
/**
* {@code SELECT} specification.
*/

View File

@@ -57,13 +57,20 @@ public class Criteria {
private final @Nullable SqlIdentifier column;
private final @Nullable Comparator comparator;
private final @Nullable Object value;
private final boolean ignoreCase;
private Criteria(SqlIdentifier column, Comparator comparator, @Nullable Object value) {
this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value);
this(null, Combinator.INITIAL, Collections.emptyList(), column, comparator, value, false);
}
private Criteria(@Nullable Criteria previous, Combinator combinator, List<Criteria> group,
@Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value) {
this(previous, combinator, group, column, comparator, value, false);
}
private Criteria(@Nullable Criteria previous, Combinator combinator, List<Criteria> group,
@Nullable SqlIdentifier column, @Nullable Comparator comparator, @Nullable Object value,
boolean ignoreCase) {
this.previous = previous;
this.combinator = previous != null && previous.isEmpty() ? Combinator.INITIAL : combinator;
@@ -71,6 +78,7 @@ public class Criteria {
this.column = column;
this.comparator = comparator;
this.value = value;
this.ignoreCase = ignoreCase;
}
private Criteria(@Nullable Criteria previous, Combinator combinator, List<Criteria> group) {
@@ -81,6 +89,7 @@ public class Criteria {
this.column = null;
this.comparator = null;
this.value = null;
this.ignoreCase = false;
}
/**
@@ -236,6 +245,19 @@ public class Criteria {
return new Criteria(Criteria.this, Combinator.OR, criteria);
}
/**
* Creates a new {@link Criteria} with the given "ignore case" flag.
*
* @param ignoreCase {@literal true} if comparison should be done in case-insensitive way
* @return a new {@link Criteria} object
*/
public Criteria ignoreCase(boolean ignoreCase) {
if (this.ignoreCase != ignoreCase) {
return new Criteria(previous, combinator, group, column, comparator, value, ignoreCase);
}
return this;
}
/**
* @return the previous {@link Criteria} object. Can be {@literal null} if there is no previous {@link Criteria}.
* @see #hasPrevious()
@@ -338,8 +360,17 @@ public class Criteria {
return value;
}
/**
* Checks whether comparison should be done in case-insensitive way.
*
* @return {@literal true} if comparison should be done in case-insensitive way
*/
boolean isIgnoreCase() {
return ignoreCase;
}
enum Comparator {
INITIAL, EQ, NEQ, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_IN, IN,
INITIAL, EQ, NEQ, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_LIKE, NOT_IN, IN, IS_TRUE, IS_FALSE
}
enum Combinator {
@@ -428,6 +459,14 @@ public class Criteria {
*/
Criteria like(Object value);
/**
* Creates a {@link Criteria} using {@code NOT LIKE}.
*
* @param value must not be {@literal null}
* @return a new {@link Criteria} object
*/
Criteria notLike(Object value);
/**
* Creates a {@link Criteria} using {@code IS NULL}.
*/
@@ -437,6 +476,20 @@ public class Criteria {
* Creates a {@link Criteria} using {@code IS NOT NULL}.
*/
Criteria isNotNull();
/**
* Creates a {@link Criteria} using {@code IS TRUE}.
*
* @return a new {@link Criteria} object
*/
Criteria isTrue();
/**
* Creates a {@link Criteria} using {@code IS FALSE}.
*
* @return a new {@link Criteria} object
*/
Criteria isFalse();
}
/**
@@ -596,6 +649,16 @@ public class Criteria {
return createCriteria(Comparator.LIKE, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notLike(java.lang.Object)
*/
@Override
public Criteria notLike(Object value) {
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.NOT_LIKE, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNull()
@@ -614,6 +677,24 @@ public class Criteria {
return createCriteria(Comparator.IS_NOT_NULL, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isTrue()
*/
@Override
public Criteria isTrue() {
return createCriteria(Comparator.IS_TRUE, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isFalse()
*/
@Override
public Criteria isFalse() {
return createCriteria(Comparator.IS_FALSE, null);
}
protected Criteria createCriteria(Comparator comparator, Object value) {
return new Criteria(this.property, comparator, value);
}

View File

@@ -320,7 +320,8 @@ public class QueryMapper {
typeHint = actualType.getType();
}
return createCondition(column, mappedValue, typeHint, bindings, criteria.getComparator());
return createCondition(column, mappedValue, typeHint, bindings, criteria.getComparator(),
criteria.isIgnoreCase());
}
/**
@@ -370,7 +371,7 @@ public class QueryMapper {
}
private Condition createCondition(Column column, @Nullable Object mappedValue, Class<?> valueType,
MutableBindings bindings, Comparator comparator) {
MutableBindings bindings, Comparator comparator, boolean ignoreCase) {
if (comparator.equals(Comparator.IS_NULL)) {
return column.isNull();
@@ -380,6 +381,19 @@ public class QueryMapper {
return column.isNotNull();
}
if (comparator == Comparator.IS_TRUE) {
return column.isEqualTo(SQL.literalOf((Object) ("TRUE")));
}
if (comparator == Comparator.IS_FALSE) {
return column.isEqualTo(SQL.literalOf((Object) ("FALSE")));
}
Expression columnExpression = column;
if (ignoreCase && String.class == valueType) {
columnExpression = new Upper(column);
}
if (comparator == Comparator.NOT_IN || comparator == Comparator.IN) {
Condition condition;
@@ -395,14 +409,14 @@ public class QueryMapper {
expressions.add(bind(o, valueType, bindings, bindMarker));
}
condition = column.in(expressions.toArray(new Expression[0]));
condition = Conditions.in(columnExpression, expressions.toArray(new Expression[0]));
} else {
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
condition = column.in(expression);
condition = Conditions.in(columnExpression, expression);
}
if (comparator == Comparator.NOT_IN) {
@@ -413,23 +427,40 @@ public class QueryMapper {
}
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
switch (comparator) {
case EQ:
return column.isEqualTo(expression);
case NEQ:
return column.isNotEqualTo(expression);
case LT:
case EQ: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
return Conditions.isEqual(columnExpression, expression);
}
case NEQ: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
return Conditions.isEqual(columnExpression, expression).not();
}
case LT: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
return column.isLess(expression);
case LTE:
}
case LTE: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
return column.isLessOrEqualTo(expression);
case GT:
}
case GT: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
return column.isGreater(expression);
case GTE:
}
case GTE: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
return column.isGreaterOrEqualTo(expression);
case LIKE:
return column.like(expression);
}
case LIKE: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
return Conditions.like(columnExpression, expression);
}
case NOT_LIKE: {
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
return NotLike.create(columnExpression, expression);
}
default:
throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
}
@@ -459,6 +490,11 @@ public class QueryMapper {
private Expression bind(@Nullable Object mappedValue, Class<?> valueType, MutableBindings bindings,
BindMarker bindMarker) {
return bind(mappedValue, valueType, bindings, bindMarker, false);
}
private Expression bind(@Nullable Object mappedValue, Class<?> valueType, MutableBindings bindings,
BindMarker bindMarker, boolean ignoreCase) {
if (mappedValue != null) {
bindings.bind(bindMarker, mappedValue);
@@ -466,7 +502,8 @@ public class QueryMapper {
bindings.bindNull(bindMarker, valueType);
}
return SQL.bindMarker(bindMarker.getPlaceholder());
return ignoreCase ? new Upper(SQL.bindMarker(bindMarker.getPlaceholder()))
: SQL.bindMarker(bindMarker.getPlaceholder());
}
/**
@@ -665,4 +702,89 @@ public class QueryMapper {
return toSql(IdentifierProcessing.ANSI);
}
}
// TODO: include support of NOT LIKE operator into spring-data-relational
/**
* Negated LIKE {@link Condition} comparing two {@link Expression}s.
* <p/>
* Results in a rendered condition: {@code <left> NOT LIKE <right>}.
*/
private static class NotLike implements Segment, Condition {
private final Comparison delegate;
private NotLike(Expression leftColumnOrExpression, Expression rightColumnOrExpression) {
this.delegate = Comparison.create(leftColumnOrExpression, "NOT LIKE", rightColumnOrExpression);
}
/**
* Creates new instance of this class with the given {@link Expression}s.
*
* @param leftColumnOrExpression the left {@link Expression}
* @param rightColumnOrExpression the right {@link Expression}
* @return {@link NotLike} condition
*/
public static NotLike create(Expression leftColumnOrExpression, Expression rightColumnOrExpression) {
Assert.notNull(leftColumnOrExpression, "Left expression must not be null!");
Assert.notNull(rightColumnOrExpression, "Right expression must not be null!");
return new NotLike(leftColumnOrExpression, rightColumnOrExpression);
}
@Override
public void visit(Visitor visitor) {
Assert.notNull(visitor, "Visitor must not be null!");
delegate.visit(visitor);
}
@Override
public String toString() {
return delegate.toString();
}
}
// TODO: include support of functions in WHERE conditions into spring-data-relational
/**
* Models the ANSI SQL {@code UPPER} function.
* <p>
* Results in a rendered function: {@code UPPER(<expression>)}.
*/
private class Upper implements Expression {
private Literal<Object> delegate;
/**
* Creates new instance of this class with the given expression. Only expressions of type {@link Column} and
* {@link org.springframework.data.relational.core.sql.BindMarker} are supported.
*
* @param expression expression to be uppercased (must not be {@literal null})
*/
private Upper(Expression expression) {
Assert.notNull(expression, "Expression must not be null!");
String functionArgument;
if (expression instanceof org.springframework.data.relational.core.sql.BindMarker) {
functionArgument = expression instanceof Named ? ((Named) expression).getName().getReference()
: expression.toString();
} else if (expression instanceof Column) {
functionArgument = "";
Table table = ((Column) expression).getTable();
if (table != null) {
functionArgument = toSql(table.getName()) + ".";
}
functionArgument += toSql(((Column) expression).getName());
} else {
throw new IllegalArgumentException("Unable to ignore case expression of type " + expression.getClass().getName()
+ ". Only " + Column.class.getName() + " and "
+ org.springframework.data.relational.core.sql.BindMarker.class.getName() + " types are supported");
}
this.delegate = SQL.literalOf((Object) ("UPPER(" + functionArgument + ")"));
}
@Override
public void visit(Visitor visitor) {
delegate.visit(visitor);
}
@Override
public String toString() {
return delegate.toString();
}
}
}

View File

@@ -0,0 +1,167 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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.
* <p>
* 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<String> 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));
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,158 @@
/*
* 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.
* <p>
* 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<ParameterMetadata> {
private static final Object VALUE_PLACEHOLDER = new Object();
private final Iterator<? extends Parameter> bindableParameterIterator;
@Nullable private final Iterator<Object> bindableParameterValueIterator;
private final List<ParameterMetadata> 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<Object> 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<ParameterMetadata> 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;
}
}

View File

@@ -0,0 +1,146 @@
/*
* 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.Collection;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.PreparedOperation;
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}.
* <p>
* This class is an adapted version of {@code org.springframework.data.jpa.repository.query.PartTreeJpaQuery} from
* Spring Data JPA project.
*
* @author Roman Chigvintsev
*/
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})
*/
public PartTreeR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, R2dbcConverter converter,
ReactiveDataAccessStrategy dataAccessStrategy) {
super(method, databaseClient, converter);
this.dataAccessStrategy = dataAccessStrategy;
this.parameters = method.getParameters();
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);
}
}
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);
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<Part> 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);
}
}

View File

@@ -0,0 +1,85 @@
/*
* 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.core.DatabaseClient;
import org.springframework.data.r2dbc.core.PreparedOperation;
import org.springframework.data.r2dbc.dialect.BindTarget;
import org.springframework.util.Assert;
/**
* A {@link BindableQuery} implementation based on a {@link PreparedOperation}.
*
* @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})
*/
PreparedOperationBindableQuery(PreparedOperation<?> preparedQuery) {
Assert.notNull(preparedQuery, "Prepared query must not be null!");
this.preparedQuery = preparedQuery;
}
@SuppressWarnings("unchecked")
@Override
public <T extends DatabaseClient.BindSpec<T>> T bind(T bindSpec) {
BindSpecBindTargetAdapter<T> bindTargetAdapter = new BindSpecBindTargetAdapter<>(bindSpec);
preparedQuery.bindTo(bindTargetAdapter);
return (T) bindTargetAdapter.bindSpec;
}
@Override
public String get() {
return preparedQuery.get();
}
/**
* This class adapts {@link org.springframework.data.r2dbc.core.DatabaseClient.BindSpec} to {@link BindTarget}
* allowing easy binding of query parameters using {@link PreparedOperation}.
*/
private static class BindSpecBindTargetAdapter<T extends DatabaseClient.BindSpec<T>> implements BindTarget {
DatabaseClient.BindSpec<T> bindSpec;
private BindSpecBindTargetAdapter(DatabaseClient.BindSpec<T> bindSpec) {
this.bindSpec = bindSpec;
}
@Override
public void bind(String identifier, Object value) {
this.bindSpec = this.bindSpec.bind(identifier, value);
}
@Override
public void bind(int index, Object value) {
this.bindSpec = this.bindSpec.bind(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
this.bindSpec = this.bindSpec.bindNull(identifier, type);
}
@Override
public void bindNull(int index, Class<?> type) {
this.bindSpec = this.bindSpec.bind(index, type);
}
}
}

View File

@@ -0,0 +1,160 @@
/*
* 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.Iterator;
import java.util.List;
import java.util.stream.Collectors;
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.sql.SqlIdentifier;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
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;
/**
* Implementation of {@link AbstractQueryCreator} that creates {@link PreparedOperation} from a {@link PartTree}.
*
* @author Roman Chigvintsev
*/
public class R2dbcQueryCreator extends AbstractQueryCreator<PreparedOperation<?>, Criteria> {
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}.
*
* @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})
*/
public R2dbcQueryCreator(PartTree tree, ReactiveDataAccessStrategy dataAccessStrategy,
RelationalEntityMetadata<?> entityMetadata, ParameterMetadataProvider parameterMetadataProvider) {
super(tree);
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<Object> 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<Object> 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})
* @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());
if (tree.isExistsProjection()) {
selectSpec = selectSpec.limit(1);
} else if (tree.isLimiting()) {
selectSpec = selectSpec.limit(tree.getMaxResults());
}
if (criteria != null) {
selectSpec = selectSpec.withCriteria(criteria);
}
if (sort.isSorted()) {
selectSpec = selectSpec.withSort(getSort(sort));
}
return statementMapper.getMappedObject(selectSpec);
}
private SqlIdentifier[] getSelectProjection() {
List<SqlIdentifier> 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<Sort.Order> 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);
}).collect(Collectors.toList());
return Sort.by(orders);
}
}

View File

@@ -25,6 +25,7 @@ import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.query.PartTreeR2dbcQuery;
import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
@@ -104,7 +105,8 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
@Override
protected Optional<QueryLookupStrategy> getQueryLookupStrategy(@Nullable Key key,
QueryMethodEvaluationContextProvider evaluationContextProvider) {
return Optional.of(new R2dbcQueryLookupStrategy(this.databaseClient, evaluationContextProvider, this.converter));
return Optional.of(new R2dbcQueryLookupStrategy(this.databaseClient, evaluationContextProvider, this.converter,
this.dataAccessStrategy));
}
/*
@@ -134,12 +136,15 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private final DatabaseClient databaseClient;
private final QueryMethodEvaluationContextProvider evaluationContextProvider;
private final R2dbcConverter converter;
private final ReactiveDataAccessStrategy dataAccessStrategy;
R2dbcQueryLookupStrategy(DatabaseClient databaseClient,
QueryMethodEvaluationContextProvider evaluationContextProvider, R2dbcConverter converter) {
QueryMethodEvaluationContextProvider evaluationContextProvider, R2dbcConverter converter,
ReactiveDataAccessStrategy dataAccessStrategy) {
this.databaseClient = databaseClient;
this.evaluationContextProvider = evaluationContextProvider;
this.converter = converter;
this.dataAccessStrategy = dataAccessStrategy;
}
/*
@@ -161,9 +166,10 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
} else if (queryMethod.hasAnnotatedQuery()) {
return new StringBasedR2dbcQuery(queryMethod, this.databaseClient, this.converter, EXPRESSION_PARSER,
this.evaluationContextProvider);
} else {
return new PartTreeR2dbcQuery(queryMethod, this.databaseClient, this.converter,
this.dataAccessStrategy);
}
throw new UnsupportedOperationException("Query derivation not yet supported!");
}
}
}

View File

@@ -155,6 +155,16 @@ public class CriteriaUnitTests {
assertThat(criteria.getValue()).isEqualTo("bar");
}
@Test
public void shouldBuildEqualsIgnoreCaseCriteria() {
Criteria criteria = where("foo").is("bar").ignoreCase(true);
assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo"));
assertThat(criteria.getComparator()).isEqualTo(Comparator.EQ);
assertThat(criteria.getValue()).isEqualTo("bar");
assertThat(criteria.isIgnoreCase()).isTrue();
}
@Test // gh-64
public void shouldBuildNotEqualsCriteria() {
@@ -235,6 +245,15 @@ public class CriteriaUnitTests {
assertThat(criteria.getValue()).isEqualTo("hello%");
}
@Test
public void shouldBuildNotLikeCriteria() {
Criteria criteria = where("foo").notLike("hello%");
assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo"));
assertThat(criteria.getComparator()).isEqualTo(Comparator.NOT_LIKE);
assertThat(criteria.getValue()).isEqualTo("hello%");
}
@Test // gh-64
public void shouldBuildIsNullCriteria() {
@@ -252,4 +271,20 @@ public class CriteriaUnitTests {
assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo"));
assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL);
}
@Test
public void shouldBuildIsTrueCriteria() {
Criteria criteria = where("foo").isTrue();
assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo"));
assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_TRUE);
}
@Test
public void shouldBuildIsFalseCriteria() {
Criteria criteria = where("foo").isFalse();
assertThat(criteria.getColumn()).isEqualTo(SqlIdentifier.unquoted("foo"));
assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_FALSE);
}
}

View File

@@ -0,0 +1,71 @@
/*
* 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("\\\\");
}
}

View File

@@ -0,0 +1,672 @@
/*
* 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.mockito.Mockito.*;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryMetadata;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Method;
import java.util.Collection;
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;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.dialect.DialectResolver;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.R2dbcMappingContext;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.Table;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
/**
* @author Roman Chigvintsev
*/
@RunWith(MockitoJUnitRunner.class)
public class PartTreeR2dbcQueryIntegrationTests {
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";
@Mock private ConnectionFactory connectionFactory;
@Mock private R2dbcConverter r2dbcConverter;
@Rule public ExpectedException thrown = ExpectedException.none();
private RelationalMappingContext mappingContext;
private ReactiveDataAccessStrategy dataAccessStrategy;
private DatabaseClient databaseClient;
@Before
public void setUp() {
ConnectionFactoryMetadata metadataMock = mock(ConnectionFactoryMetadata.class);
when(metadataMock.getName()).thenReturn("PostgreSQL");
when(connectionFactory.getMetadata()).thenReturn(metadataMock);
when(r2dbcConverter.writeValue(any(), any())).thenAnswer(invocation -> invocation.getArgument(0));
mappingContext = new R2dbcMappingContext();
doReturn(mappingContext).when(r2dbcConverter).getMappingContext();
R2dbcDialect dialect = DialectResolver.getDialect(connectionFactory);
dataAccessStrategy = new DefaultReactiveDataAccessStrategy(dialect, r2dbcConverter);
databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory)
.dataAccessStrategy(dataAccessStrategy).build();
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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() });
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void appendsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() 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);
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
bindableQuery.bind(bindSpecMock);
verify(bindSpecMock, times(1)).bind(0, "Jo%");
}
@Test
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);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() 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);
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
bindableQuery.bind(bindSpecMock);
verify(bindSpecMock, times(1)).bind(0, "%hn");
}
@Test
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);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() 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);
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
bindableQuery.bind(bindSpecMock);
verify(bindSpecMock, times(1)).bind(0, "%oh%");
}
@Test
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);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() 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);
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
bindableQuery.bind(bindSpecMock);
verify(bindSpecMock, times(1)).bind(0, "%oh%");
}
@Test
public void createsQueryToFindAllEntitiesByIntegerAttributeWithDescendingOrderingByStringAttribute()
throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeOrderByLastNameDesc", 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 DESC";
assertThat(bindableQuery.get()).isEqualTo(expectedSql);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
public void createsQueryToFindAllEntitiesByIntegerAttributeNotIn() throws Exception {
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByAgeNotIn", 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 NOT IN ($1)";
assertThat(bindableQuery.get()).isEqualTo(expectedSql);
}
@Test
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);
}
@Test
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);
}
@Test
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);
}
@Test
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 }));
}
@Test
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 }));
}
@Test
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) }));
}
@Test
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]));
}
@Test
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]));
}
@Test
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";
assertThat(bindableQuery.get()).isEqualTo(expectedSql);
}
@Test
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";
assertThat(bindableQuery.get()).isEqualTo(expectedSql);
}
private R2dbcQueryMethod getQueryMethod(String methodName, Class<?>... parameterTypes) throws Exception {
Method method = UserRepository.class.getMethod(methodName, parameterTypes);
return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class),
new SpelAwareProxyProjectionFactory(), mappingContext);
}
private RelationalParametersParameterAccessor getAccessor(R2dbcQueryMethod queryMethod, Object[] values) {
return new RelationalParametersParameterAccessor(queryMethod, values);
}
private interface UserRepository extends Repository<User, Long> {
Flux<User> findAllByFirstName(String firstName);
Flux<User> findAllByLastNameAndFirstName(String lastName, String firstName);
Flux<User> findAllByLastNameOrFirstName(String lastName, String firstName);
Mono<Boolean> existsByFirstName(String firstName);
Flux<User> findAllByDateOfBirthBetween(Date from, Date to);
Flux<User> findAllByAgeLessThan(Integer age);
Flux<User> findAllByAgeLessThanEqual(Integer age);
Flux<User> findAllByAgeGreaterThan(Integer age);
Flux<User> findAllByAgeGreaterThanEqual(Integer age);
Flux<User> findAllByDateOfBirthAfter(Date date);
Flux<User> findAllByDateOfBirthBefore(Date date);
Flux<User> findAllByAgeIsNull();
Flux<User> findAllByAgeIsNotNull();
Flux<User> findAllByFirstNameLike(String like);
Flux<User> findAllByFirstNameNotLike(String like);
Flux<User> findAllByFirstNameStartingWith(String starting);
Flux<User> findAllByFirstNameEndingWith(String ending);
Flux<User> findAllByFirstNameContaining(String containing);
Flux<User> findAllByFirstNameNotContaining(String notContaining);
Flux<User> findAllByAgeOrderByLastNameAsc(Integer age);
Flux<User> findAllByAgeOrderByLastNameDesc(Integer age);
Flux<User> findAllByLastNameNot(String lastName);
Flux<User> findAllByAgeIn(Collection<Integer> ages);
Flux<User> findAllByAgeNotIn(Collection<Integer> ages);
Flux<User> findAllByActiveTrue();
Flux<User> findAllByActiveFalse();
Flux<User> findAllByFirstNameIgnoreCase(String firstName);
Mono<User> findByIdIgnoringCase(Long id);
Flux<User> findAllByIdIn(Long id);
Flux<User> findAllById(Collection<Long> ids);
Flux<User> findAllByIdIsEmpty();
Flux<User> findTop3ByFirstName(String firstName);
Mono<User> findFirstByFirstName(String firstName);
}
@Table("users")
private static class User {
@Id private 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;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* 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.mockito.Mockito.*;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.data.r2dbc.core.PreparedOperation;
/**
* @author Roman Chigvintsev
*/
@RunWith(MockitoJUnitRunner.class)
@Ignore
public class PreparedOperationBindableQueryUnitTests {
@Mock private PreparedOperation<?> preparedOperation;
@Test(expected = IllegalArgumentException.class)
public void throwsExceptionWhenPreparedOperationIsNull() {
new PreparedOperationBindableQuery(null);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void bindsQueryParameterValues() {
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
PreparedOperationBindableQuery query = new PreparedOperationBindableQuery(preparedOperation);
query.bind(bindSpecMock);
verify(preparedOperation, times(1)).bindTo(any());
}
@Test
public void returnsSqlQuery() {
String sql = "SELECT * FROM test";
when(preparedOperation.get()).thenReturn(sql);
PreparedOperationBindableQuery query = new PreparedOperationBindableQuery(preparedOperation);
assertThat(query.get()).isEqualTo(sql);
}
}