Skip to content

Commit 3821fc4

Browse files
committed
Add support for Query Mapping.
1 parent 29c4d35 commit 3821fc4

File tree

17 files changed

+932
-206
lines changed

17 files changed

+932
-206
lines changed

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java

Lines changed: 86 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
3636
import org.springframework.data.relational.core.query.CriteriaDefinition;
3737
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
38+
import org.springframework.data.relational.core.query.QueryExpression;
3839
import org.springframework.data.relational.core.query.ValueFunction;
3940
import org.springframework.data.relational.core.sql.*;
4041
import org.springframework.data.relational.domain.SqlSort;
@@ -48,6 +49,7 @@
4849
import org.springframework.r2dbc.core.binding.MutableBindings;
4950
import org.springframework.util.Assert;
5051
import org.springframework.util.ClassUtils;
52+
import org.springframework.util.StringUtils;
5153

5254
/**
5355
* Maps {@link CriteriaDefinition} and {@link Sort} objects considering mapping metadata and dialect-specific
@@ -296,31 +298,58 @@ private Condition combine(CriteriaDefinition criteria, @Nullable Condition curre
296298
private Condition mapCondition(CriteriaDefinition criteria, MutableBindings bindings, Table table,
297299
@Nullable RelationalPersistentEntity<?> entity) {
298300

299-
Field propertyField = createPropertyField(entity, criteria.getColumn(), this.mappingContext);
300-
Column column = table.column(propertyField.getMappedColumnName());
301-
TypeInformation<?> actualType = propertyField.getTypeHint().getRequiredActualType();
302-
303301
Object mappedValue;
304-
Class<?> typeHint;
302+
TypeInformation<?> typeHint;
303+
Class<?> targetType;
304+
Expression expression;
305+
String nameHint;
306+
307+
if (criteria.hasExpression()) {
308+
309+
R2dbcEvaluationContext context = new R2dbcEvaluationContext(table, converter, entity, bindings);
310+
QueryExpression queryExpression = criteria.getExpression();
311+
typeHint = queryExpression.getType(context).getTargetType();
312+
313+
expression = queryExpression.evaluate(context);
314+
nameHint = queryExpression.getNameHint();
315+
316+
if (criteria.getComparator() == null) {
317+
return Conditions.from(expression);
318+
}
319+
320+
} else if (criteria.hasColumn()) {
321+
322+
Field propertyField = createPropertyField(entity, criteria.getColumn(), this.mappingContext);
323+
Column column = table.column(propertyField.getMappedColumnName());
324+
expression = column;
325+
nameHint = column.getName().getReference();
326+
typeHint = propertyField.getTypeHint();
327+
328+
} else {
329+
throw new IllegalStateException("Cannot map empty Criteria");
330+
}
331+
332+
TypeInformation<?> actualType = typeHint.getRequiredActualType();
305333

306334
Comparator comparator = criteria.getComparator();
335+
307336
if (criteria.getValue() instanceof Parameter parameter) {
308337

309-
mappedValue = convertValue(comparator, parameter.getValue(), propertyField.getTypeHint());
310-
typeHint = getTypeHint(mappedValue, actualType.getType(), parameter);
338+
mappedValue = convertValue(comparator, parameter.getValue(), typeHint);
339+
targetType = getTypeHint(mappedValue, actualType.getType(), parameter);
311340
} else if (criteria.getValue() instanceof ValueFunction<?> valueFunction) {
312341

313-
mappedValue = valueFunction.map(v -> convertValue(comparator, v, propertyField.getTypeHint()))
314-
.apply(getEscaper(comparator));
342+
mappedValue = valueFunction.map(v -> convertValue(comparator, v, typeHint)).apply(getEscaper(comparator));
315343

316-
typeHint = actualType.getType();
344+
targetType = actualType.getType();
317345
} else {
318346

319-
mappedValue = convertValue(comparator, criteria.getValue(), propertyField.getTypeHint());
320-
typeHint = actualType.getType();
347+
mappedValue = convertValue(comparator, criteria.getValue(), typeHint);
348+
targetType = actualType.getType();
321349
}
322350

323-
return createCondition(column, mappedValue, typeHint, bindings, comparator, criteria.isIgnoreCase());
351+
return createCondition(expression, nameHint, mappedValue, targetType, bindings, comparator,
352+
criteria.isIgnoreCase());
324353
}
325354

326355
private Escaper getEscaper(Comparator comparator) {
@@ -393,32 +422,32 @@ protected MappingContext<? extends RelationalPersistentEntity<?>, RelationalPers
393422
return this.mappingContext;
394423
}
395424

396-
private Condition createCondition(Column column, @Nullable Object mappedValue, Class<?> valueType,
397-
MutableBindings bindings, Comparator comparator, boolean ignoreCase) {
425+
private Condition createCondition(Expression source, String nameHint, @Nullable Object mappedValue,
426+
Class<?> valueType, MutableBindings bindings, Comparator comparator, boolean ignoreCase) {
398427

399428
if (comparator.equals(Comparator.IS_NULL)) {
400-
return column.isNull();
429+
return Conditions.isNull(source);
401430
}
402431

403432
if (comparator.equals(Comparator.IS_NOT_NULL)) {
404-
return column.isNotNull();
433+
return Conditions.isNull(source).not();
405434
}
406435

407436
if (comparator == Comparator.IS_TRUE) {
408-
Expression bind = booleanBind(column, mappedValue, valueType, bindings, ignoreCase);
409437

410-
return column.isEqualTo(bind);
438+
Expression bind = booleanBind(nameHint, mappedValue, valueType, bindings, ignoreCase);
439+
return Conditions.isEqual(source, bind);
411440
}
412441

413442
if (comparator == Comparator.IS_FALSE) {
414-
Expression bind = booleanBind(column, mappedValue, valueType, bindings, ignoreCase);
415443

416-
return column.isEqualTo(bind);
444+
Expression bind = booleanBind(nameHint, mappedValue, valueType, bindings, ignoreCase);
445+
return Conditions.isEqual(source, bind);
417446
}
418447

419-
Expression columnExpression = column;
448+
Expression columnExpression = source;
420449
if (ignoreCase) {
421-
columnExpression = Functions.upper(column);
450+
columnExpression = Functions.upper(source);
422451
}
423452

424453
if (comparator == Comparator.NOT_IN || comparator == Comparator.IN) {
@@ -432,15 +461,16 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, C
432461

433462
for (Object o : (Iterable<?>) mappedValue) {
434463

435-
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
464+
BindMarker bindMarker = bindMarker(nameHint, bindings);
436465
expressions.add(bind(o, valueType, bindings, bindMarker));
437466
}
438467

439468
condition = Conditions.in(columnExpression, expressions.toArray(new Expression[0]));
440469

441470
} else {
442471

443-
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
472+
BindMarker bindMarker = bindMarker(nameHint, bindings);
473+
;
444474
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
445475

446476
condition = Conditions.in(columnExpression, expression);
@@ -457,53 +487,51 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, C
457487

458488
Pair<Object, Object> pair = (Pair<Object, Object>) mappedValue;
459489

460-
Expression begin = bind(pair.getFirst(), valueType, bindings,
461-
bindings.nextMarker(column.getName().getReference()), ignoreCase);
462-
Expression end = bind(pair.getSecond(), valueType, bindings, bindings.nextMarker(column.getName().getReference()),
463-
ignoreCase);
490+
Expression begin = bind(pair.getFirst(), valueType, bindings, bindMarker(nameHint, bindings), ignoreCase);
491+
Expression end = bind(pair.getSecond(), valueType, bindings, bindMarker(nameHint, bindings), ignoreCase);
464492

465493
return comparator == Comparator.BETWEEN ? Conditions.between(columnExpression, begin, end)
466494
: Conditions.notBetween(columnExpression, begin, end);
467495
}
468496

469-
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
497+
BindMarker bindMarker = bindMarker(nameHint, bindings);
498+
;
470499

471-
switch (comparator) {
472-
case EQ: {
500+
return switch (comparator) {
501+
case EQ -> {
473502
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
474-
return Conditions.isEqual(columnExpression, expression);
503+
yield Conditions.isEqual(columnExpression, expression);
475504
}
476-
case NEQ: {
505+
case NEQ -> {
477506
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
478-
return Conditions.isEqual(columnExpression, expression).not();
507+
yield Conditions.isEqual(columnExpression, expression).not();
479508
}
480-
case LT: {
509+
case LT -> {
481510
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
482-
return column.isLess(expression);
511+
yield Conditions.isLess(source, expression);
483512
}
484-
case LTE: {
513+
case LTE -> {
485514
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
486-
return column.isLessOrEqualTo(expression);
515+
yield Conditions.isLessOrEqualTo(source, expression);
487516
}
488-
case GT: {
517+
case GT -> {
489518
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
490-
return column.isGreater(expression);
519+
yield Conditions.isGreater(source, expression);
491520
}
492-
case GTE: {
521+
case GTE -> {
493522
Expression expression = bind(mappedValue, valueType, bindings, bindMarker);
494-
return column.isGreaterOrEqualTo(expression);
523+
yield Conditions.isGreaterOrEqualTo(source, expression);
495524
}
496-
case LIKE: {
525+
case LIKE -> {
497526
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
498-
return Conditions.like(columnExpression, expression);
527+
yield Conditions.like(columnExpression, expression);
499528
}
500-
case NOT_LIKE: {
529+
case NOT_LIKE -> {
501530
Expression expression = bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
502-
return Conditions.notLike(columnExpression, expression);
531+
yield Conditions.notLike(columnExpression, expression);
503532
}
504-
default:
505-
throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
506-
}
533+
default -> throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
534+
};
507535
}
508536

509537
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIdentifier key) {
@@ -515,10 +543,6 @@ Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, SqlIde
515543
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
516544
}
517545

518-
Class<?> getTypeHint(@Nullable Object mappedValue, Class<?> propertyType) {
519-
return propertyType;
520-
}
521-
522546
Class<?> getTypeHint(@Nullable Object mappedValue, Class<?> propertyType, Parameter parameter) {
523547

524548
if (mappedValue == null || propertyType.equals(Object.class)) {
@@ -550,13 +574,18 @@ private Expression bind(@Nullable Object mappedValue, Class<?> valueType, Mutabl
550574
: SQL.bindMarker(bindMarker.getPlaceholder());
551575
}
552576

553-
private Expression booleanBind(Column column, Object mappedValue, Class<?> valueType, MutableBindings bindings,
554-
boolean ignoreCase) {
555-
BindMarker bindMarker = bindings.nextMarker(column.getName().getReference());
577+
private Expression booleanBind(@Nullable String nameHint, Object mappedValue, Class<?> valueType,
578+
MutableBindings bindings, boolean ignoreCase) {
579+
580+
BindMarker bindMarker = bindMarker(nameHint, bindings);
556581

557582
return bind(mappedValue, valueType, bindings, bindMarker, ignoreCase);
558583
}
559584

585+
private static BindMarker bindMarker(@Nullable String nameHint, MutableBindings bindings) {
586+
return StringUtils.hasText(nameHint) ? bindings.nextMarker(nameHint) : bindings.nextMarker();
587+
}
588+
560589
/**
561590
* Value object to represent a field and its meta-information.
562591
*/
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/*
2+
* Copyright 2025 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.query;
17+
18+
import org.springframework.data.relational.core.conversion.RelationalConverter;
19+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
20+
import org.springframework.data.relational.core.query.EvaluationContextSupport;
21+
import org.springframework.data.relational.core.query.QueryExpression;
22+
import org.springframework.data.relational.core.sql.BindMarker;
23+
import org.springframework.data.relational.core.sql.Table;
24+
import org.springframework.lang.Nullable;
25+
import org.springframework.r2dbc.core.binding.BindTarget;
26+
import org.springframework.r2dbc.core.binding.MutableBindings;
27+
28+
/**
29+
* R2DBC-specific {@link QueryExpression.EvaluationContext} implementation.
30+
*
31+
* @author Mark Paluch
32+
*/
33+
class R2dbcEvaluationContext extends EvaluationContextSupport {
34+
35+
private final QueryExpression.ExpressionTypeContext type;
36+
private final MutableBindings bindings;
37+
38+
public R2dbcEvaluationContext(Table table, RelationalConverter converter,
39+
@Nullable RelationalPersistentEntity<?> entity, MutableBindings bindings) {
40+
41+
super(table, converter, entity);
42+
43+
this.type = QueryExpression.ExpressionTypeContext.object();
44+
this.bindings = bindings;
45+
}
46+
47+
public R2dbcEvaluationContext(R2dbcEvaluationContext previous, QueryExpression.ExpressionTypeContext type) {
48+
49+
super(previous);
50+
51+
this.type = type;
52+
this.bindings = previous.bindings;
53+
}
54+
55+
@Override
56+
public QueryExpression.EvaluationContext withType(QueryExpression.ExpressionTypeContext type) {
57+
return new R2dbcEvaluationContext(this, type);
58+
}
59+
60+
@Override
61+
public BindMarker bind(Object value) {
62+
63+
Object valueToUse = getConverter().writeValue(value, type.getTargetType());
64+
return new BindMarkerAdapter(bindings.bind(valueToUse));
65+
}
66+
67+
@Override
68+
public BindMarker bind(String name, Object value) {
69+
70+
Object valueToUse = getConverter().writeValue(value, type.getTargetType());
71+
org.springframework.r2dbc.core.binding.BindMarker bindMarker = bindings.nextMarker(name);
72+
bindings.bind(bindMarker, valueToUse);
73+
return new BindMarkerAdapter(bindMarker);
74+
}
75+
76+
private static class BindMarkerAdapter extends BindMarker
77+
implements org.springframework.r2dbc.core.binding.BindMarker {
78+
79+
private final org.springframework.r2dbc.core.binding.BindMarker bindMarker;
80+
81+
public BindMarkerAdapter(org.springframework.r2dbc.core.binding.BindMarker bindMarker) {
82+
this.bindMarker = bindMarker;
83+
}
84+
85+
@Override
86+
public String getPlaceholder() {
87+
return bindMarker.getPlaceholder();
88+
}
89+
90+
@Override
91+
public void bind(BindTarget bindTarget, Object value) {
92+
bindMarker.bind(bindTarget, value);
93+
}
94+
95+
@Override
96+
public void bindNull(BindTarget bindTarget, Class<?> valueType) {
97+
bindMarker.bindNull(bindTarget, valueType);
98+
}
99+
100+
@Override
101+
public String toString() {
102+
return "[" + getPlaceholder() + "]";
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)