Skip to content

Commit 6358eed

Browse files
committed
spring-projects#282 - Add support of case insensitive conditions and 'NOT CONTAINING' condition
1 parent 91b4660 commit 6358eed

File tree

3 files changed

+162
-43
lines changed

3 files changed

+162
-43
lines changed

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

+125-37
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2222
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
2323
import org.springframework.data.relational.core.sql.*;
24+
import org.springframework.data.relational.core.sql.render.RenderNamingStrategy;
2425
import org.springframework.data.repository.query.parser.Part;
2526
import org.springframework.util.Assert;
2627

@@ -31,19 +32,25 @@
3132
*/
3233
class ConditionFactory {
3334
private final MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
35+
private final RenderNamingStrategy namingStrategy;
3436
private final ParameterMetadataProvider parameterMetadataProvider;
3537

3638
/**
3739
* Creates new instance of this class with the given {@link MappingContext} and {@link ParameterMetadataProvider}.
3840
*
3941
* @param mappingContext mapping context (must not be {@literal null})
42+
* @param namingStrategy naming strategy for SQL rendering (must not be {@literal null})
4043
* @param parameterMetadataProvider parameter metadata provider (must not be {@literal null})
4144
*/
4245
ConditionFactory(MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext,
46+
RenderNamingStrategy namingStrategy,
4347
ParameterMetadataProvider parameterMetadataProvider) {
4448
Assert.notNull(mappingContext, "Mapping context must not be null");
49+
Assert.notNull(namingStrategy, "Render naming strategy must not be null");
4550
Assert.notNull(parameterMetadataProvider, "Parameter metadata provider must not be null");
51+
4652
this.mappingContext = mappingContext;
53+
this.namingStrategy = namingStrategy;
4754
this.parameterMetadataProvider = parameterMetadataProvider;
4855
}
4956

@@ -87,60 +94,64 @@ public Condition createCondition(Part part) {
8794
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
8895
return Conditions.isLessOrEqualTo(pathExpression, bindMarker);
8996
}
90-
case IS_NULL: {
91-
return Conditions.isNull(createPropertyPathExpression(part.getProperty()));
92-
}
97+
case IS_NULL:
9398
case IS_NOT_NULL: {
94-
return Conditions.isNull(createPropertyPathExpression(part.getProperty())).not();
95-
}
96-
case IN: {
97-
Expression pathExpression = createPropertyPathExpression(part.getProperty());
98-
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
99-
return Conditions.in(pathExpression, bindMarker);
99+
IsNull isNullCondition = Conditions.isNull(createPropertyPathExpression(part.getProperty()));
100+
return part.getType() == Part.Type.IS_NULL ? isNullCondition : isNullCondition.not();
100101
}
102+
case IN:
101103
case NOT_IN: {
102-
Expression pathExpression = createPropertyPathExpression(part.getProperty());
104+
Expression pathExpression = upperIfIgnoreCase(part, createPropertyPathExpression(part.getProperty()));
103105
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
104-
return Conditions.in(pathExpression, bindMarker).not();
106+
In inCondition = Conditions.in(pathExpression, bindMarker);
107+
return part.getType() == Part.Type.IN ? inCondition : inCondition.not();
105108
}
106109
case STARTING_WITH:
107110
case ENDING_WITH:
108111
case CONTAINING:
109-
case LIKE: {
110-
Expression pathExpression = createPropertyPathExpression(part.getProperty());
111-
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
112-
return Conditions.like(pathExpression, bindMarker);
113-
}
112+
case NOT_CONTAINING:
113+
case LIKE:
114114
case NOT_LIKE: {
115115
Expression pathExpression = createPropertyPathExpression(part.getProperty());
116-
BindMarker bindMarker = createBindMarker(parameterMetadataProvider.next(part));
117-
return NotLike.create(pathExpression, bindMarker);
118-
}
119-
case TRUE: {
120-
Expression pathExpression = createPropertyPathExpression(part.getProperty());
121-
// TODO: include factory method for '= TRUE' condition into spring-data-relational
122-
return Conditions.isEqual(pathExpression, SQL.literalOf((Object) "TRUE"));
116+
ParameterMetadata parameterMetadata = parameterMetadataProvider.next(part);
117+
BindMarker bindMarker = createBindMarker(parameterMetadata);
118+
Expression lhs = upperIfIgnoreCase(part, pathExpression);
119+
Expression rhs = upperIfIgnoreCase(part, bindMarker, parameterMetadata.getType());
120+
return part.getType() == Part.Type.NOT_LIKE || part.getType() == Part.Type.NOT_CONTAINING
121+
? NotLike.create(lhs, rhs)
122+
: Conditions.like(lhs, rhs);
123123
}
124+
case TRUE:
124125
case FALSE: {
125126
Expression pathExpression = createPropertyPathExpression(part.getProperty());
126-
// TODO: include factory method for '= FALSE' condition into spring-data-relational
127-
return Conditions.isEqual(pathExpression, SQL.literalOf((Object) "FALSE"));
127+
// TODO: include factory methods for '= TRUE/FALSE' conditions into spring-data-relational
128+
return Conditions.isEqual(pathExpression,
129+
SQL.literalOf((Object) (part.getType() == Part.Type.TRUE ? "TRUE" : "FALSE")));
128130
}
129131
case SIMPLE_PROPERTY: {
130132
Expression pathExpression = createPropertyPathExpression(part.getProperty());
131133
ParameterMetadata parameterMetadata = parameterMetadataProvider.next(part);
132134
if (parameterMetadata.isIsNullParameter()) {
133135
return Conditions.isNull(pathExpression);
134136
}
135-
return Conditions.isEqual(pathExpression, createBindMarker(parameterMetadata));
137+
138+
BindMarker bindMarker = createBindMarker(parameterMetadata);
139+
Expression lhs = upperIfIgnoreCase(part, pathExpression);
140+
Expression rhs = upperIfIgnoreCase(part, bindMarker, parameterMetadata.getType());
141+
return Conditions.isEqual(lhs, rhs);
136142
}
137143
case NEGATING_SIMPLE_PROPERTY: {
138144
Expression pathExpression = createPropertyPathExpression(part.getProperty());
139145
ParameterMetadata parameterMetadata = parameterMetadataProvider.next(part);
140-
return Conditions.isEqual(pathExpression, createBindMarker(parameterMetadata)).not();
146+
BindMarker bindMarker = createBindMarker(parameterMetadata);
147+
Expression lhs = upperIfIgnoreCase(part, pathExpression);
148+
Expression rhs = upperIfIgnoreCase(part, bindMarker, parameterMetadata.getType());
149+
return Conditions.isEqual(lhs, rhs).not();
141150
}
151+
default:
152+
throw new UnsupportedOperationException("Creating conditions for type " + type + " is unsupported");
142153
}
143-
throw new UnsupportedOperationException("Creating conditions for type " + type + " is unsupported");
154+
144155
}
145156

146157
@NotNull
@@ -160,6 +171,91 @@ private BindMarker createBindMarker(ParameterMetadata parameterMetadata) {
160171
return SQL.bindMarker();
161172
}
162173

174+
/**
175+
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
176+
* requires ignoring case.
177+
*
178+
* @param part method name part (must not be {@literal null})
179+
* @param expression expression to be uppercased (must not be {@literal null})
180+
* @return uppercased expression or original expression if ignoring case is not strictly required
181+
*/
182+
private Expression upperIfIgnoreCase(Part part, Expression expression) {
183+
return upperIfIgnoreCase(part, expression, part.getProperty().getType());
184+
}
185+
186+
/**
187+
* Applies an {@code UPPERCASE} conversion to the given {@link Expression} in case the underlying {@link Part}
188+
* requires ignoring case.
189+
*
190+
* @param part method name part (must not be {@literal null})
191+
* @param expression expression to be uppercased (must not be {@literal null})
192+
* @param expressionType type of the given expression (must not be {@literal null})
193+
* @return uppercased expression or original expression if ignoring case is not strictly required
194+
*/
195+
private Expression upperIfIgnoreCase(Part part, Expression expression, Class<?> expressionType) {
196+
switch (part.shouldIgnoreCase()) {
197+
case ALWAYS:
198+
Assert.state(canUpperCase(expressionType), "Unable to ignore case of " + expressionType.getName()
199+
+ " type, the property '" + part.getProperty().getSegment() + "' must reference a string");
200+
return new Upper(expression);
201+
case WHEN_POSSIBLE:
202+
if (canUpperCase(expressionType)) {
203+
return new Upper(expression);
204+
}
205+
case NEVER:
206+
default:
207+
return expression;
208+
}
209+
}
210+
211+
private boolean canUpperCase(Class<?> expressionType) {
212+
return expressionType == String.class;
213+
}
214+
215+
// TODO: include support of functions in WHERE conditions into spring-data-relational
216+
/**
217+
* Models the ANSI SQL {@code UPPER} function.
218+
*/
219+
private class Upper implements Expression {
220+
private Literal<Object> delegate;
221+
222+
/**
223+
* Creates new instance of this class with the given expression. Only expressions of type {@link Column} and
224+
* {@link BindMarker} are supported.
225+
*
226+
* @param expression expression to be uppercased (must not be {@literal null})
227+
*/
228+
private Upper(Expression expression) {
229+
Assert.notNull(expression, "Expression must not be null!");
230+
String functionArgument;
231+
if (expression instanceof BindMarker) {
232+
functionArgument = expression instanceof Named ? ((Named) expression).getName() : expression.toString();
233+
} else if (expression instanceof Column) {
234+
functionArgument = "";
235+
Table table = ((Column) expression).getTable();
236+
if (table != null) {
237+
functionArgument = namingStrategy.getReferenceName(table) + ".";
238+
}
239+
functionArgument += namingStrategy.getReferenceName((Column) expression);
240+
} else {
241+
throw new IllegalArgumentException("Unable to ignore case expression of type "
242+
+ expression.getClass().getName() + ". Only " + Column.class.getName() + " and "
243+
+ BindMarker.class.getName() + " types are supported");
244+
}
245+
this.delegate = SQL.literalOf((Object) ("UPPER(" + functionArgument + ")"));
246+
}
247+
248+
@Override
249+
public void visit(Visitor visitor) {
250+
delegate.visit(visitor);
251+
}
252+
253+
@Override
254+
public String toString() {
255+
return delegate.toString();
256+
}
257+
}
258+
163259
// TODO: include support of NOT LIKE operator into spring-data-relational
164260
/**
165261
* Negated LIKE {@link Condition} comparing two {@link Expression}s.
@@ -192,17 +288,9 @@ public void visit(Visitor visitor) {
192288
delegate.visit(visitor);
193289
}
194290

195-
public Expression getLeft() {
196-
return delegate.getLeft();
197-
}
198-
199-
public Expression getRight() {
200-
return delegate.getRight();
201-
}
202-
203291
@Override
204292
public String toString() {
205-
return getLeft().toString() + " NOT LIKE " + getRight();
293+
return delegate.toString();
206294
}
207295
}
208296
}

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
package org.springframework.data.r2dbc.repository.query;
1717

1818
import org.springframework.data.domain.Sort;
19-
import org.springframework.data.r2dbc.convert.R2dbcConverter;
2019
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
2120
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
2221
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
2322
import org.springframework.data.relational.core.sql.*;
2423
import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndJoin;
24+
import org.springframework.data.relational.core.sql.render.NamingStrategies;
2525
import org.springframework.data.relational.core.sql.render.RenderContext;
26+
import org.springframework.data.relational.core.sql.render.RenderNamingStrategy;
2627
import org.springframework.data.relational.core.sql.render.SqlRenderer;
2728
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
2829
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -68,8 +69,12 @@ public R2dbcQueryCreator(PartTree tree,
6869
this.dataAccessStrategy = dataAccessStrategy;
6970
this.entityMetadata = entityMetadata;
7071

71-
R2dbcConverter converter = dataAccessStrategy.getConverter();
72-
this.conditionFactory = new ConditionFactory(converter.getMappingContext(), parameterMetadataProvider);
72+
RenderContext renderContext = dataAccessStrategy.getStatementMapper().getRenderContext();
73+
RenderNamingStrategy namingStrategy = renderContext == null
74+
? NamingStrategies.asIs()
75+
: renderContext.getNamingStrategy();
76+
this.conditionFactory = new ConditionFactory(dataAccessStrategy.getConverter().getMappingContext(),
77+
namingStrategy, parameterMetadataProvider);
7378
}
7479

7580
@Override

Diff for: src/test/java/org/springframework/data/r2dbc/repository/query/PartTreeR2dbcQueryIntegrationTests.java

+29-3
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ public void createsQueryToFindAllEntitiesByStringAttributeEndingWith() throws Ex
299299

300300
@SuppressWarnings({"rawtypes", "unchecked"})
301301
@Test
302-
public void prependsLikeOperatorParameterWithPercentSymbolForStartingWithQuery() throws Exception {
302+
public void prependsLikeOperatorParameterWithPercentSymbolForEndingWithQuery() throws Exception {
303303
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameEndingWith", String.class);
304304
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
305305
dataAccessStrategy);
@@ -327,11 +327,35 @@ public void wrapsLikeOperatorParameterWithPercentSymbolsForContainingQuery() thr
327327
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameContaining", String.class);
328328
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
329329
dataAccessStrategy);
330-
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[]{"hn"});
330+
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[]{"oh"});
331+
BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor);
332+
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
333+
bindableQuery.bind(bindSpecMock);
334+
verify(bindSpecMock, times(1)).bind(0, "%oh%");
335+
}
336+
337+
@Test
338+
public void createsQueryToFindAllEntitiesByStringAttributeNotContaining() throws Exception {
339+
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class);
340+
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
341+
dataAccessStrategy);
342+
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[]{"oh"});
343+
BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor);
344+
String expectedSql = "SELECT " + ALL_FIELDS + " FROM " + TABLE + " WHERE " + TABLE + ".first_name NOT LIKE ?";
345+
assertThat(bindableQuery.get()).isEqualTo(expectedSql);
346+
}
347+
348+
@SuppressWarnings({"rawtypes", "unchecked"})
349+
@Test
350+
public void wrapsLikeOperatorParameterWithPercentSymbolsForNotContainingQuery() throws Exception {
351+
R2dbcQueryMethod queryMethod = getQueryMethod("findAllByFirstNameNotContaining", String.class);
352+
PartTreeR2dbcQuery r2dbcQuery = new PartTreeR2dbcQuery(queryMethod, databaseClient, r2dbcConverter,
353+
dataAccessStrategy);
354+
RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[]{"oh"});
331355
BindableQuery bindableQuery = r2dbcQuery.createQuery(accessor);
332356
DatabaseClient.BindSpec bindSpecMock = mock(DatabaseClient.BindSpec.class);
333357
bindableQuery.bind(bindSpecMock);
334-
verify(bindSpecMock, times(1)).bind(0, "%hn%");
358+
verify(bindSpecMock, times(1)).bind(0, "%oh%");
335359
}
336360

337361
@Test
@@ -462,6 +486,8 @@ private interface UserRepository extends Repository<User, Long> {
462486

463487
Flux<User> findAllByFirstNameContaining(String containing);
464488

489+
Flux<User> findAllByFirstNameNotContaining(String notContaining);
490+
465491
Flux<User> findAllByAgeOrderByLastNameDesc(Integer age);
466492

467493
Flux<User> findAllByLastNameNot(String lastName);

0 commit comments

Comments
 (0)