Skip to content

Commit cca24c8

Browse files
committed
spring-projects#282 - Add support of 'STARTING/ENDING WITH' conditions
1 parent 39c745a commit cca24c8

File tree

6 files changed

+237
-56
lines changed

6 files changed

+237
-56
lines changed

Diff for: src/main/java/org/springframework/data/r2dbc/repository/query/ConditionFactory.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import org.jetbrains.annotations.NotNull;
1919
import org.springframework.data.mapping.PropertyPath;
2020
import org.springframework.data.mapping.context.MappingContext;
21-
import org.springframework.data.r2dbc.repository.query.ParameterMetadataProvider.ParameterMetadata;
2221
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2322
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
2423
import org.springframework.data.relational.core.sql.*;
@@ -94,6 +93,8 @@ public Condition createCondition(Part part) {
9493
case IS_NOT_NULL: {
9594
return Conditions.isNull(createPropertyPathExpression(part.getProperty())).not();
9695
}
96+
case STARTING_WITH:
97+
case ENDING_WITH:
9798
case LIKE: {
9899
Expression pathExpression = createPropertyPathExpression(part.getProperty());
99100
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
@@ -136,6 +137,11 @@ private BindMarker createBindMarker(ParameterMetadata parameterMetadata) {
136137
}
137138

138139
// TODO: include support of NOT LIKE operator into spring-data-relational
140+
/**
141+
* Negated LIKE {@link Condition} comparing two {@link Expression}s.
142+
* <p/>
143+
* Results in a rendered condition: {@code <left> NOT LIKE <right>}.
144+
*/
139145
private static class NotLike implements Segment, Condition {
140146
private final Comparison delegate;
141147

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2018-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.repository.query;
17+
18+
import org.springframework.lang.Nullable;
19+
20+
import java.util.Arrays;
21+
import java.util.List;
22+
23+
/**
24+
* Helper class encapsulating an escape character for LIKE queries and the actually usage of it in escaping
25+
* {@link String}s.
26+
* <p>
27+
* This class is an adapted version of {@code org.springframework.data.jpa.repository.query.EscapeCharacter} from
28+
* Spring Data JPA project.
29+
*
30+
* @author Roman Chigvintsev
31+
*/
32+
public class LikeEscaper {
33+
public static final LikeEscaper DEFAULT = LikeEscaper.of('\\');
34+
35+
private final char escapeCharacter;
36+
private final List<String> toReplace;
37+
38+
private LikeEscaper(char escapeCharacter) {
39+
this.escapeCharacter = escapeCharacter;
40+
this.toReplace = Arrays.asList(String.valueOf(escapeCharacter), "_", "%");
41+
}
42+
43+
/**
44+
* Creates new instance of this class with the given escape character.
45+
*
46+
* @param escapeCharacter escape character
47+
* @return new instance of {@link LikeEscaper}
48+
*/
49+
public static LikeEscaper of(char escapeCharacter) {
50+
return new LikeEscaper(escapeCharacter);
51+
}
52+
53+
/**
54+
* Escapes all special like characters ({@code _}, {@code %}) using the configured escape character.
55+
*
56+
* @param value value to be escaped
57+
* @return escaped value
58+
*/
59+
@Nullable
60+
public String escape(@Nullable String value) {
61+
if (value == null) {
62+
return null;
63+
}
64+
return toReplace.stream().reduce(value, (it, character) -> it.replace(character, escapeCharacter + character));
65+
}
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2018-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.repository.query;
17+
18+
import lombok.AllArgsConstructor;
19+
import lombok.Builder;
20+
import lombok.NonNull;
21+
import org.springframework.data.repository.query.parser.Part;
22+
import org.springframework.lang.Nullable;
23+
import org.springframework.util.Assert;
24+
25+
/**
26+
* Helper class for holding information about query parameter and preparing query parameter value.
27+
*/
28+
@Builder
29+
@AllArgsConstructor
30+
class ParameterMetadata {
31+
@Nullable
32+
private final String name;
33+
@NonNull
34+
private final Class<?> type;
35+
@NonNull
36+
private final Part.Type partType;
37+
private final boolean isNullParameter;
38+
@NonNull
39+
private final LikeEscaper likeEscaper;
40+
41+
/**
42+
* Prepares parameter value before it's actually bound to the query.
43+
*
44+
* @param value must not be {@literal null}
45+
* @return prepared query parameter value
46+
*/
47+
public Object prepare(Object value) {
48+
Assert.notNull(value, "Value must not be null!");
49+
if (String.class.equals(type)) {
50+
switch (partType) {
51+
case STARTING_WITH:
52+
return String.format("%s%%", likeEscaper.escape(value.toString()));
53+
case ENDING_WITH:
54+
return String.format("%%%s", likeEscaper.escape(value.toString()));
55+
case CONTAINING:
56+
case NOT_CONTAINING:
57+
return String.format("%%%s%%", likeEscaper.escape(value.toString()));
58+
default:
59+
return value;
60+
}
61+
}
62+
return value;
63+
}
64+
65+
@Nullable
66+
public String getName() {
67+
return name;
68+
}
69+
70+
public Class<?> getType() {
71+
return type;
72+
}
73+
74+
/**
75+
* Determines whether parameter value should be translated to {@literal IS NULL} condition.
76+
*
77+
* @return {@literal true} if parameter value should be translated to {@literal IS NULL} condition
78+
*/
79+
public boolean isIsNullParameter() {
80+
return isNullParameter;
81+
}
82+
}

Diff for: src/main/java/org/springframework/data/r2dbc/repository/query/ParameterMetadataProvider.java

+23-51
Original file line numberDiff line numberDiff line change
@@ -41,23 +41,26 @@ class ParameterMetadataProvider {
4141
@Nullable
4242
private final Iterator<Object> bindableParameterValueIterator;
4343
private final List<ParameterMetadata> parameterMetadata = new ArrayList<>();
44+
private final LikeEscaper likeEscaper;
4445

4546
/**
4647
* Creates new instance of this class with the given {@link RelationalParameterAccessor}.
4748
*
4849
* @param accessor relational parameter accessor (must not be {@literal null}).
50+
* @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null})
4951
*/
50-
ParameterMetadataProvider(RelationalParameterAccessor accessor) {
51-
this(accessor.getBindableParameters(), accessor.iterator());
52+
ParameterMetadataProvider(RelationalParameterAccessor accessor, LikeEscaper likeEscaper) {
53+
this(accessor.getBindableParameters(), accessor.iterator(), likeEscaper);
5254
}
5355

5456
/**
5557
* Creates new instance of this class with the given {@link Parameters}.
5658
*
57-
* @param parameters method parameters (must not be {@literal null})
59+
* @param parameters method parameters (must not be {@literal null})
60+
* @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null})
5861
*/
59-
ParameterMetadataProvider(Parameters<?, ?> parameters) {
60-
this(parameters, null);
62+
ParameterMetadataProvider(Parameters<?, ?> parameters, LikeEscaper likeEscaper) {
63+
this(parameters, null, likeEscaper);
6164
}
6265

6366
/**
@@ -66,25 +69,33 @@ class ParameterMetadataProvider {
6669
*
6770
* @param bindableParameterValueIterator iterator over bindable parameter values
6871
* @param parameters method parameters (must not be {@literal null})
72+
* @param likeEscaper escaper for LIKE operator parameters (must not be {@literal null})
6973
*/
7074
private ParameterMetadataProvider(Parameters<?, ?> parameters,
71-
@Nullable Iterator<Object> bindableParameterValueIterator) {
75+
@Nullable Iterator<Object> bindableParameterValueIterator,
76+
LikeEscaper likeEscaper) {
7277
Assert.notNull(parameters, "Parameters must not be null!");
78+
Assert.notNull(likeEscaper, "Like escaper must not be null!");
79+
7380
this.bindableParameterIterator = parameters.getBindableParameters().iterator();
7481
this.bindableParameterValueIterator = bindableParameterValueIterator;
82+
this.likeEscaper = likeEscaper;
7583
}
7684

7785
/**
7886
* Creates new instance of {@link ParameterMetadata} for the given {@link Part} and next {@link Parameter}.
7987
*/
8088
public ParameterMetadata next(Part part) {
81-
Assert.isTrue(bindableParameterIterator.hasNext(), () -> String.format("No parameter available for part %s.", part));
89+
Assert.isTrue(bindableParameterIterator.hasNext(),
90+
() -> String.format("No parameter available for part %s.", part));
8291
Parameter parameter = bindableParameterIterator.next();
83-
String name = getParameterName(parameter);
84-
Class<?> type = parameter.getType();
85-
Object value = getParameterValue();
86-
boolean isNullProperty = value == null && Part.Type.SIMPLE_PROPERTY.equals(part.getType());
87-
ParameterMetadata metadata = new ParameterMetadata(name, type, isNullProperty);
92+
ParameterMetadata metadata = ParameterMetadata.builder()
93+
.type(parameter.getType())
94+
.partType(part.getType())
95+
.name(getParameterName(parameter))
96+
.isNullParameter(getParameterValue() == null && Part.Type.SIMPLE_PROPERTY.equals(part.getType()))
97+
.likeEscaper(likeEscaper)
98+
.build();
8899
parameterMetadata.add(metadata);
89100
return metadata;
90101
}
@@ -105,43 +116,4 @@ private String getParameterName(Parameter parameter) {
105116
private Object getParameterValue() {
106117
return bindableParameterValueIterator == null ? VALUE_PLACEHOLDER : bindableParameterValueIterator.next();
107118
}
108-
109-
/**
110-
* Helper class to hold information about query parameter.
111-
*/
112-
static class ParameterMetadata {
113-
@Nullable
114-
private final String name;
115-
private final Class<?> type;
116-
private final boolean isNullParameter;
117-
118-
/**
119-
* Creates new instance of this class with the given method name part, parameter name, parameter type and
120-
* parameter value.
121-
*
122-
* @param name parameter name
123-
* @param type parameter type (must not be {@literal null})
124-
* @param isNullParameter whether parameter value should be translated to {@literal IS NULL} condition
125-
*/
126-
public ParameterMetadata(@Nullable String name, Class<?> type, boolean isNullParameter) {
127-
Assert.notNull(type, "Type must not be null");
128-
129-
this.name = name;
130-
this.type = type;
131-
this.isNullParameter = isNullParameter;
132-
}
133-
134-
@Nullable
135-
public String getName() {
136-
return name;
137-
}
138-
139-
public Class<?> getType() {
140-
return type;
141-
}
142-
143-
public boolean isIsNullParameter() {
144-
return isNullParameter;
145-
}
146-
}
147119
}

Diff for: src/main/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQuery.java

+7-4
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
*/
1616
package org.springframework.data.r2dbc.repository.query;
1717

18+
import lombok.Setter;
1819
import org.springframework.data.domain.Sort;
1920
import org.springframework.data.r2dbc.convert.R2dbcConverter;
2021
import org.springframework.data.r2dbc.core.DatabaseClient;
2122
import org.springframework.data.r2dbc.core.DatabaseClient.BindSpec;
2223
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
23-
import org.springframework.data.r2dbc.repository.query.ParameterMetadataProvider.ParameterMetadata;
2424
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
2525
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
2626
import org.springframework.data.relational.repository.query.RelationalParameters;
@@ -41,6 +41,9 @@ public class PartTreeR2dbcQuery extends AbstractR2dbcQuery {
4141
private final RelationalParameters parameters;
4242
private final PartTree tree;
4343

44+
@Setter
45+
private LikeEscaper likeEscaper = LikeEscaper.DEFAULT;
46+
4447
/**
4548
* Creates new instance of this class with the given {@link R2dbcQueryMethod} and {@link DatabaseClient}.
4649
*
@@ -69,7 +72,7 @@ public PartTreeR2dbcQuery(R2dbcQueryMethod method,
6972
@Override
7073
protected BindableQuery createQuery(RelationalParameterAccessor accessor) {
7174
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
72-
ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(accessor);
75+
ParameterMetadataProvider parameterMetadataProvider = new ParameterMetadataProvider(accessor, likeEscaper);
7376
R2dbcQueryCreator queryCreator = new R2dbcQueryCreator(tree, dataAccessStrategy, entityMetadata,
7477
parameterMetadataProvider);
7578
String sql = queryCreator.createQuery(getDynamicSort(accessor));
@@ -92,15 +95,15 @@ public <T extends BindSpec<T>> T bind(T bindSpec) {
9295
parameterName);
9396
bindSpecToUse = bindSpecToUse.bindNull(parameterName, parameterType);
9497
} else {
95-
bindSpecToUse = bindSpecToUse.bind(parameterName, value);
98+
bindSpecToUse = bindSpecToUse.bind(parameterName, metadata.prepare(value));
9699
}
97100
} else {
98101
if (value == null) {
99102
checkNullIsAllowed(metadata, "Value of parameter with index %d must not be null!",
100103
bindingIndex);
101104
bindSpecToUse = bindSpecToUse.bindNull(bindingIndex++, parameterType);
102105
} else {
103-
bindSpecToUse = bindSpecToUse.bind(bindingIndex++, value);
106+
bindSpecToUse = bindSpecToUse.bind(bindingIndex++, metadata.prepare(value));
104107
}
105108
}
106109
}

0 commit comments

Comments
 (0)