#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:
committed by
Mark Paluch
parent
6dcd8787c1
commit
dbe935c45a
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("\\\\");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user