Browse Source
We now support Conditions.between, notBetween, and notLike as additional criteria conditions and support case-insensitive comparisons. For LIKE escaping we pick up the Escaper configured at Dialect level. The newly introduced ValueFunction allows string transformation before computing a value by applying the Escaper to the raw value. Escaping is required for StartingWith, Contains and EndsWith PartTree operations. Original pull request: spring-projects/spring-data-r2dbc#295.pull/197/head
26 changed files with 819 additions and 83 deletions
@ -0,0 +1,59 @@
@@ -0,0 +1,59 @@
|
||||
/* |
||||
* 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.relational.core.query; |
||||
|
||||
import java.util.function.Function; |
||||
import java.util.function.Supplier; |
||||
|
||||
import org.springframework.data.relational.core.dialect.Escaper; |
||||
import org.springframework.lang.Nullable; |
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* Represents a value function to return arbitrary values that can be escaped before returning the actual value. Can be |
||||
* used with the criteria API for deferred value retrieval. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.0 |
||||
* @see Escaper |
||||
* @see Supplier |
||||
*/ |
||||
@FunctionalInterface |
||||
public interface ValueFunction<T> extends Function<Escaper, T> { |
||||
|
||||
/** |
||||
* Produces a value by considering the given {@link Escaper}. |
||||
* |
||||
* @param escaper the escaper to use. |
||||
* @return the return value, may be {@literal null}. |
||||
*/ |
||||
@Nullable |
||||
@Override |
||||
T apply(Escaper escaper); |
||||
|
||||
/** |
||||
* Adapts this value factory into a {@link Supplier} by using the given {@link Escaper}. |
||||
* |
||||
* @param escaper the escaper to use. |
||||
* @return the value factory |
||||
*/ |
||||
default Supplier<T> toSupplier(Escaper escaper) { |
||||
|
||||
Assert.notNull(escaper, "Escaper must not be null"); |
||||
|
||||
return () -> apply(escaper); |
||||
} |
||||
} |
||||
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* Copyright 2019-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.relational.core.sql; |
||||
|
||||
import org.springframework.util.Assert; |
||||
|
||||
/** |
||||
* BETWEEN {@link Condition} comparing between {@link Expression}s. |
||||
* <p/> |
||||
* Results in a rendered condition: {@code <left> BETWEEN <begin> AND <end>}. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.2 |
||||
*/ |
||||
public class Between extends AbstractSegment implements Condition { |
||||
|
||||
private final Expression column; |
||||
private final Expression begin; |
||||
private final Expression end; |
||||
private final boolean negated; |
||||
|
||||
private Between(Expression column, Expression begin, Expression end, boolean negated) { |
||||
|
||||
super(column, begin, end); |
||||
|
||||
this.column = column; |
||||
this.begin = begin; |
||||
this.end = end; |
||||
this.negated = negated; |
||||
} |
||||
|
||||
/** |
||||
* Creates a new {@link Between} {@link Condition} given two {@link Expression}s. |
||||
* |
||||
* @param columnOrExpression left side of the comparison. |
||||
* @param begin begin value of the comparison. |
||||
* @param end end value of the comparison. |
||||
* @return the {@link Between} condition. |
||||
*/ |
||||
public static Between create(Expression columnOrExpression, Expression begin, Expression end) { |
||||
|
||||
Assert.notNull(columnOrExpression, "Column or expression must not be null!"); |
||||
Assert.notNull(begin, "Begin value must not be null!"); |
||||
Assert.notNull(end, "end value must not be null!"); |
||||
|
||||
return new Between(columnOrExpression, begin, end, false); |
||||
} |
||||
|
||||
/** |
||||
* @return the column {@link Expression}. |
||||
*/ |
||||
public Expression getColumn() { |
||||
return column; |
||||
} |
||||
|
||||
/** |
||||
* @return the begin {@link Expression}. |
||||
*/ |
||||
public Expression getBegin() { |
||||
return begin; |
||||
} |
||||
|
||||
/** |
||||
* @return the end {@link Expression}. |
||||
*/ |
||||
public Expression getEnd() { |
||||
return end; |
||||
} |
||||
|
||||
public boolean isNegated() { |
||||
return negated; |
||||
} |
||||
|
||||
@Override |
||||
public Between not() { |
||||
return new Between(this.column, this.begin, this.end, !negated); |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return column.toString() + " BETWEEN " + begin.toString() + " AND " + end.toString(); |
||||
} |
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright 2019-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.relational.core.sql; |
||||
|
||||
/** |
||||
* Represents a {@link Boolean} literal. |
||||
* |
||||
* @author Mark Paluch |
||||
* @since 2.0 |
||||
*/ |
||||
public class BooleanLiteral extends Literal<Boolean> { |
||||
|
||||
BooleanLiteral(boolean content) { |
||||
super(content); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.Literal#getContent() |
||||
*/ |
||||
@Override |
||||
public Boolean getContent() { |
||||
return super.getContent(); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.Literal#toString() |
||||
*/ |
||||
@Override |
||||
public String toString() { |
||||
return getContent() ? "TRUE" : "FALSE"; |
||||
} |
||||
} |
||||
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2019-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.relational.core.sql.render; |
||||
|
||||
import org.springframework.data.relational.core.sql.Between; |
||||
import org.springframework.data.relational.core.sql.Condition; |
||||
import org.springframework.data.relational.core.sql.Expression; |
||||
import org.springframework.data.relational.core.sql.Visitable; |
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* {@link org.springframework.data.relational.core.sql.Visitor} rendering comparison {@link Condition}. Uses a |
||||
* {@link RenderTarget} to call back for render results. |
||||
* |
||||
* @author Mark Paluch |
||||
* @see Between |
||||
* @since 2.0 |
||||
*/ |
||||
class BetweenVisitor extends FilteredSubtreeVisitor { |
||||
|
||||
private final Between between; |
||||
private final RenderContext context; |
||||
private final RenderTarget target; |
||||
private final StringBuilder part = new StringBuilder(); |
||||
private boolean renderedTestExpression = false; |
||||
private boolean renderedPreamble = false; |
||||
private boolean done = false; |
||||
private @Nullable PartRenderer current; |
||||
|
||||
BetweenVisitor(Between condition, RenderContext context, RenderTarget target) { |
||||
super(it -> it == condition); |
||||
this.between = condition; |
||||
this.context = context; |
||||
this.target = target; |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#enterNested(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation enterNested(Visitable segment) { |
||||
|
||||
if (segment instanceof Expression) { |
||||
ExpressionVisitor visitor = new ExpressionVisitor(context); |
||||
current = visitor; |
||||
return Delegation.delegateTo(visitor); |
||||
} |
||||
|
||||
if (segment instanceof Condition) { |
||||
ConditionVisitor visitor = new ConditionVisitor(context); |
||||
current = visitor; |
||||
return Delegation.delegateTo(visitor); |
||||
} |
||||
|
||||
throw new IllegalStateException("Cannot provide visitor for " + segment); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation leaveNested(Visitable segment) { |
||||
|
||||
if (current != null && !done) { |
||||
|
||||
if (renderedPreamble) { |
||||
|
||||
part.append(" AND "); |
||||
part.append(current.getRenderedPart()); |
||||
done = true; |
||||
} |
||||
|
||||
if (renderedTestExpression && !renderedPreamble) { |
||||
|
||||
part.append(' '); |
||||
|
||||
if (between.isNegated()) { |
||||
part.append("NOT "); |
||||
} |
||||
|
||||
part.append("BETWEEN "); |
||||
renderedPreamble = true; |
||||
part.append(current.getRenderedPart()); |
||||
} |
||||
|
||||
if (!renderedTestExpression) { |
||||
part.append(current.getRenderedPart()); |
||||
renderedTestExpression = true; |
||||
} |
||||
|
||||
current = null; |
||||
} |
||||
|
||||
return super.leaveNested(segment); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.FilteredSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation leaveMatched(Visitable segment) { |
||||
|
||||
target.onRendered(part); |
||||
|
||||
return super.leaveMatched(segment); |
||||
} |
||||
} |
||||
@ -0,0 +1,93 @@
@@ -0,0 +1,93 @@
|
||||
/* |
||||
* Copyright 2019-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.relational.core.sql.render; |
||||
|
||||
import org.springframework.data.relational.core.sql.SimpleFunction; |
||||
import org.springframework.data.relational.core.sql.Visitable; |
||||
|
||||
/** |
||||
* Renderer for {@link org.springframework.data.relational.core.sql.SimpleFunction}. Uses a {@link RenderTarget} to call |
||||
* back for render results. |
||||
* |
||||
* @author Mark Paluch |
||||
* @author Jens Schauder |
||||
* @since 1.1 |
||||
*/ |
||||
class SimpleFunctionVisitor extends TypedSingleConditionRenderSupport<SimpleFunction> implements PartRenderer { |
||||
|
||||
private final StringBuilder part = new StringBuilder(); |
||||
private boolean needsComma = false; |
||||
private String functionName; |
||||
|
||||
SimpleFunctionVisitor(RenderContext context) { |
||||
super(context); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveNested(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation leaveNested(Visitable segment) { |
||||
|
||||
if (hasDelegatedRendering()) { |
||||
|
||||
if (needsComma) { |
||||
part.append(", "); |
||||
} |
||||
|
||||
if (part.length() == 0) { |
||||
part.append(functionName).append("("); |
||||
} |
||||
part.append(consumeRenderedPart()); |
||||
needsComma = true; |
||||
} |
||||
|
||||
return super.leaveNested(segment); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#enterMatched(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation enterMatched(SimpleFunction segment) { |
||||
|
||||
functionName = segment.getFunctionName(); |
||||
return super.enterMatched(segment); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.TypedSubtreeVisitor#leaveMatched(org.springframework.data.relational.core.sql.Visitable) |
||||
*/ |
||||
@Override |
||||
Delegation leaveMatched(SimpleFunction segment) { |
||||
|
||||
part.append(")"); |
||||
|
||||
return super.leaveMatched(segment); |
||||
} |
||||
|
||||
/* |
||||
* (non-Javadoc) |
||||
* @see org.springframework.data.relational.core.sql.render.PartRenderer#getRenderedPart() |
||||
*/ |
||||
@Override |
||||
public CharSequence getRenderedPart() { |
||||
return part; |
||||
} |
||||
} |
||||
@ -1,25 +0,0 @@
@@ -1,25 +0,0 @@
|
||||
/* |
||||
* Copyright 2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package org.springframework.data.relational.core.query; |
||||
|
||||
import static org.junit.Assert.*; |
||||
|
||||
/** |
||||
* @author Mark Paluch |
||||
*/ |
||||
public class CriteriaTests { |
||||
|
||||
} |
||||
@ -0,0 +1,101 @@
@@ -0,0 +1,101 @@
|
||||
/* |
||||
* 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.relational.repository.query; |
||||
|
||||
import static org.assertj.core.api.Assertions.*; |
||||
|
||||
import java.lang.reflect.Method; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.data.projection.ProjectionFactory; |
||||
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; |
||||
import org.springframework.data.relational.core.dialect.Escaper; |
||||
import org.springframework.data.relational.core.query.ValueFunction; |
||||
import org.springframework.data.repository.Repository; |
||||
import org.springframework.data.repository.core.RepositoryMetadata; |
||||
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; |
||||
import org.springframework.data.repository.query.QueryMethod; |
||||
import org.springframework.data.repository.query.parser.PartTree; |
||||
|
||||
/** |
||||
* Unit tests for {@link ParameterMetadataProvider}. |
||||
* |
||||
* @author Mark Paluch |
||||
*/ |
||||
public class ParameterMetadataProviderUnitTests { |
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void shouldCreateValueFunctionForContains() throws Exception { |
||||
|
||||
ParameterMetadata metadata = getParameterMetadata("findByNameContains", "hell%o"); |
||||
|
||||
assertThat(metadata.getValue()).isInstanceOf(ValueFunction.class); |
||||
ValueFunction<Object> function = (ValueFunction<Object>) metadata.getValue(); |
||||
assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("%hell\\%o%"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void shouldCreateValueFunctionForStartingWith() throws Exception { |
||||
|
||||
ParameterMetadata metadata = getParameterMetadata("findByNameStartingWith", "hell%o"); |
||||
|
||||
assertThat(metadata.getValue()).isInstanceOf(ValueFunction.class); |
||||
ValueFunction<Object> function = (ValueFunction<Object>) metadata.getValue(); |
||||
assertThat(function.apply(Escaper.DEFAULT)).isEqualTo("hell\\%o%"); |
||||
} |
||||
|
||||
@Test // DATAJDBC-514
|
||||
public void shouldCreateValue() throws Exception { |
||||
|
||||
ParameterMetadata metadata = getParameterMetadata("findByName", "hell%o"); |
||||
|
||||
assertThat(metadata.getValue()).isEqualTo("hell%o"); |
||||
} |
||||
|
||||
private ParameterMetadata getParameterMetadata(String methodName, Object value) throws Exception { |
||||
|
||||
Method method = UserRepository.class.getMethod(methodName, String.class); |
||||
ParameterMetadataProvider provider = new ParameterMetadataProvider(new RelationalParametersParameterAccessor( |
||||
new RelationalQueryMethod(method, new DefaultRepositoryMetadata(UserRepository.class), |
||||
new SpelAwareProxyProjectionFactory()), |
||||
new Object[] { value })); |
||||
|
||||
PartTree tree = new PartTree(methodName, User.class); |
||||
|
||||
return provider.next(tree.getParts().iterator().next()); |
||||
} |
||||
|
||||
static class RelationalQueryMethod extends QueryMethod { |
||||
|
||||
public RelationalQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { |
||||
super(method, metadata, factory); |
||||
} |
||||
} |
||||
|
||||
interface UserRepository extends Repository<User, String> { |
||||
|
||||
String findByNameStartingWith(String prefix); |
||||
|
||||
String findByNameContains(String substring); |
||||
|
||||
String findByName(String substring); |
||||
} |
||||
|
||||
static class User { |
||||
String name; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue