Skip to content

Commit 637d06a

Browse files
committed
Apply custom converter for Collection-like values in queries.
We now apply converters only for Collection-like values and no longer to Iterable types. Closes #1452
1 parent 7e1bec2 commit 637d06a

File tree

4 files changed

+130
-57
lines changed

4 files changed

+130
-57
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/QueryMapper.java

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@
4141
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
4242
import org.springframework.data.relational.core.query.ValueFunction;
4343
import org.springframework.data.relational.core.sql.*;
44-
import org.springframework.data.util.TypeInformation;
4544
import org.springframework.data.util.Pair;
4645
import org.springframework.data.util.TypeInformation;
4746
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
@@ -276,17 +275,18 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc
276275
Column column = table.column(propertyField.getMappedColumnName());
277276
Object mappedValue;
278277
SQLType sqlType;
278+
Comparator comparator = criteria.getComparator();
279279

280-
if (criteria.getValue() instanceof JdbcValue settableValue) {
280+
if (criteria.getValue()instanceof JdbcValue settableValue) {
281281

282-
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
282+
mappedValue = convertValue(comparator, settableValue.getValue(), propertyField.getTypeHint());
283283
sqlType = getTypeHint(mappedValue, actualType.getType(), settableValue);
284284
} else if (criteria.getValue() instanceof ValueFunction) {
285285

286286
ValueFunction<Object> valueFunction = (ValueFunction<Object>) criteria.getValue();
287-
Object value = valueFunction.apply(getEscaper(criteria.getComparator()));
287+
Object value = valueFunction.apply(getEscaper(comparator));
288288

289-
mappedValue = convertValue(value, propertyField.getTypeHint());
289+
mappedValue = convertValue(comparator, value, propertyField.getTypeHint());
290290
sqlType = propertyField.getSqlType();
291291

292292
} else if (propertyField instanceof MetadataBackedField //
@@ -296,17 +296,15 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc
296296
RelationalPersistentProperty property = ((MetadataBackedField) propertyField).property;
297297
JdbcValue jdbcValue = convertToJdbcValue(property, criteria.getValue());
298298
mappedValue = jdbcValue.getValue();
299-
sqlType = jdbcValue.getJdbcType() != null ? jdbcValue.getJdbcType()
300-
: propertyField.getSqlType();
299+
sqlType = jdbcValue.getJdbcType() != null ? jdbcValue.getJdbcType() : propertyField.getSqlType();
301300

302301
} else {
303302

304-
mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
303+
mappedValue = convertValue(comparator, criteria.getValue(), propertyField.getTypeHint());
305304
sqlType = propertyField.getSqlType();
306305
}
307306

308-
return createCondition(column, mappedValue, sqlType, parameterSource, criteria.getComparator(),
309-
criteria.isIgnoreCase());
307+
return createCondition(column, mappedValue, sqlType, parameterSource, comparator, criteria.isIgnoreCase());
310308
}
311309

312310
/**
@@ -428,6 +426,23 @@ private Escaper getEscaper(Comparator comparator) {
428426
return Escaper.DEFAULT;
429427
}
430428

429+
@Nullable
430+
private Object convertValue(Comparator comparator, @Nullable Object value, TypeInformation<?> typeHint) {
431+
432+
if (Comparator.IN.equals(comparator) && value instanceof Collection<?> collection && !collection.isEmpty()) {
433+
434+
Collection<Object> mapped = new ArrayList<>(collection.size());
435+
436+
for (Object o : collection) {
437+
mapped.add(convertValue(o, typeHint));
438+
}
439+
440+
return mapped;
441+
}
442+
443+
return convertValue(value, typeHint);
444+
}
445+
431446
@Nullable
432447
protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInformation) {
433448

@@ -450,19 +465,6 @@ protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInf
450465
return Pair.of(first, second);
451466
}
452467

453-
if (value instanceof Iterable) {
454-
455-
List<Object> mapped = new ArrayList<>();
456-
457-
for (Object o : (Iterable<?>) value) {
458-
459-
mapped.add(convertValue(o, typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
460-
: TypeInformation.OBJECT));
461-
}
462-
463-
return mapped;
464-
}
465-
466468
if (value.getClass().isArray()
467469
&& (TypeInformation.OBJECT.equals(typeInformation) || typeInformation.isCollectionLike())) {
468470
return value;
@@ -476,7 +478,7 @@ protected MappingContext<? extends RelationalPersistentEntity<?>, RelationalPers
476478
}
477479

478480
private Condition createCondition(Column column, @Nullable Object mappedValue, SQLType sqlType,
479-
MapSqlParameterSource parameterSource, Comparator comparator, boolean ignoreCase) {
481+
MapSqlParameterSource parameterSource, Comparator comparator, boolean ignoreCase) {
480482

481483
if (comparator.equals(Comparator.IS_NULL)) {
482484
return column.isNull();
@@ -614,12 +616,12 @@ SQLType getTypeHint(@Nullable Object mappedValue, Class<?> propertyType, JdbcVal
614616
}
615617

616618
private Expression bind(@Nullable Object mappedValue, SQLType sqlType, MapSqlParameterSource parameterSource,
617-
String name) {
619+
String name) {
618620
return bind(mappedValue, sqlType, parameterSource, name, false);
619621
}
620622

621-
private Expression bind(@Nullable Object mappedValue, SQLType sqlType, MapSqlParameterSource parameterSource, String name,
622-
boolean ignoreCase) {
623+
private Expression bind(@Nullable Object mappedValue, SQLType sqlType, MapSqlParameterSource parameterSource,
624+
String name, boolean ignoreCase) {
623625

624626
String uniqueName = getUniqueName(parameterSource, name);
625627

@@ -671,7 +673,7 @@ public boolean isEmbedded() {
671673
/**
672674
* Returns the key to be used in the mapped document eventually.
673675
*
674-
* @return the key to be used in the mapped document eventually.
676+
* @return the key to be used in the mapped document eventually.
675677
*/
676678
public SqlIdentifier getMappedColumnName() {
677679
return this.name;

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

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.springframework.data.relational.core.query.CriteriaDefinition.Comparator;
3939
import org.springframework.data.relational.core.query.ValueFunction;
4040
import org.springframework.data.relational.core.sql.*;
41-
import org.springframework.data.util.TypeInformation;
4241
import org.springframework.data.util.Pair;
4342
import org.springframework.data.util.TypeInformation;
4443
import org.springframework.lang.Nullable;
@@ -348,26 +347,27 @@ private Condition mapCondition(CriteriaDefinition criteria, MutableBindings bind
348347
Object mappedValue;
349348
Class<?> typeHint;
350349

350+
Comparator comparator = criteria.getComparator();
351351
if (criteria.getValue() instanceof Parameter) {
352352

353353
Parameter parameter = (Parameter) criteria.getValue();
354354

355-
mappedValue = convertValue(parameter.getValue(), propertyField.getTypeHint());
355+
mappedValue = convertValue(comparator, parameter.getValue(), propertyField.getTypeHint());
356356
typeHint = getTypeHint(mappedValue, actualType.getType(), parameter);
357357
} else if (criteria.getValue() instanceof ValueFunction) {
358358

359359
ValueFunction<Object> valueFunction = (ValueFunction<Object>) criteria.getValue();
360-
Object value = valueFunction.apply(getEscaper(criteria.getComparator()));
360+
Object value = valueFunction.apply(getEscaper(comparator));
361361

362-
mappedValue = convertValue(value, propertyField.getTypeHint());
362+
mappedValue = convertValue(comparator, value, propertyField.getTypeHint());
363363
typeHint = actualType.getType();
364364
} else {
365365

366-
mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
366+
mappedValue = convertValue(comparator, criteria.getValue(), propertyField.getTypeHint());
367367
typeHint = actualType.getType();
368368
}
369369

370-
return createCondition(column, mappedValue, typeHint, bindings, criteria.getComparator(), criteria.isIgnoreCase());
370+
return createCondition(column, mappedValue, typeHint, bindings, comparator, criteria.isIgnoreCase());
371371
}
372372

373373
private Escaper getEscaper(Comparator comparator) {
@@ -395,6 +395,23 @@ public Parameter getBindValue(Parameter value) {
395395
return Parameter.from(convertValue(value.getValue(), TypeInformation.OBJECT));
396396
}
397397

398+
@Nullable
399+
private Object convertValue(Comparator comparator, @Nullable Object value, TypeInformation<?> typeHint) {
400+
401+
if (Comparator.IN.equals(comparator) && value instanceof Collection<?> collection && !collection.isEmpty()) {
402+
403+
Collection<Object> mapped = new ArrayList<>(collection.size());
404+
405+
for (Object o : collection) {
406+
mapped.add(convertValue(o, typeHint));
407+
}
408+
409+
return mapped;
410+
}
411+
412+
return convertValue(value, typeHint);
413+
}
414+
398415
@Nullable
399416
protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInformation) {
400417

@@ -407,33 +424,14 @@ protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInf
407424
Pair<Object, Object> pair = (Pair<Object, Object>) value;
408425

409426
Object first = convertValue(pair.getFirst(),
410-
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
411-
: TypeInformation.OBJECT);
427+
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() : TypeInformation.OBJECT);
412428

413429
Object second = convertValue(pair.getSecond(),
414-
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
415-
: TypeInformation.OBJECT);
430+
typeInformation.getActualType() != null ? typeInformation.getRequiredActualType() : TypeInformation.OBJECT);
416431

417432
return Pair.of(first, second);
418433
}
419434

420-
if (value instanceof Iterable) {
421-
422-
List<Object> mapped = new ArrayList<>();
423-
424-
for (Object o : (Iterable<?>) value) {
425-
mapped.add(convertValue(o, typeInformation.getActualType() != null ? typeInformation.getRequiredActualType()
426-
: TypeInformation.OBJECT));
427-
}
428-
429-
return mapped;
430-
}
431-
432-
if (value.getClass().isArray()
433-
&& (TypeInformation.OBJECT.equals(typeInformation) || typeInformation.isCollectionLike())) {
434-
return value;
435-
}
436-
437435
return this.converter.writeValue(value, typeInformation);
438436
}
439437

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/query/QueryMapperUnitTests.java

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import java.util.Collections;
2323

2424
import org.junit.jupiter.api.Test;
25-
25+
import org.springframework.core.convert.converter.Converter;
2626
import org.springframework.data.domain.Sort;
2727
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
2828
import org.springframework.data.r2dbc.convert.R2dbcConverter;
@@ -39,6 +39,8 @@
3939
import org.springframework.r2dbc.core.Parameter;
4040
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
4141
import org.springframework.r2dbc.core.binding.BindTarget;
42+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.JsonNode;
43+
import org.testcontainers.shaded.com.fasterxml.jackson.databind.node.TextNode;
4244

4345
/**
4446
* Unit tests for {@link QueryMapper}.
@@ -53,7 +55,8 @@ class QueryMapperUnitTests {
5355

5456
QueryMapper createMapper(R2dbcDialect dialect) {
5557

56-
R2dbcCustomConversions conversions = R2dbcCustomConversions.of(dialect);
58+
R2dbcCustomConversions conversions = R2dbcCustomConversions.of(dialect, JsonNodeToStringConverter.INSTANCE,
59+
StringToJsonNodeConverter.INSTANCE);
5760

5861
R2dbcMappingContext context = new R2dbcMappingContext();
5962
context.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
@@ -463,6 +466,28 @@ void shouldMapAndConvertBooleanConditionProperly() {
463466
assertThat(bindings.getBindings().iterator().next().getValue()).isEqualTo((byte) 1);
464467
}
465468

469+
@Test // gh-1452
470+
void shouldMapJsonNodeToString() {
471+
472+
Criteria criteria = Criteria.where("jsonNode").is(new TextNode("foo"));
473+
474+
BoundCondition bindings = map(criteria);
475+
476+
assertThat(bindings.getCondition()).hasToString("person.json_node = ?[$1]");
477+
assertThat(bindings.getBindings().iterator().next().getValue()).isEqualTo("foo");
478+
}
479+
480+
@Test // gh-1452
481+
void shouldMapJsonNodeListToString() {
482+
483+
Criteria criteria = Criteria.where("jsonNode").in(new TextNode("foo"), new TextNode("bar"));
484+
485+
BoundCondition bindings = map(criteria);
486+
487+
assertThat(bindings.getCondition()).hasToString("person.json_node IN (?[$1], ?[$2])");
488+
assertThat(bindings.getBindings().iterator().next().getValue()).isEqualTo("foo");
489+
}
490+
466491
private BoundCondition map(Criteria criteria) {
467492

468493
BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1);
@@ -478,9 +503,29 @@ static class Person {
478503
MyEnum enumValue;
479504

480505
boolean state;
506+
507+
JsonNode jsonNode;
481508
}
482509

483510
enum MyEnum {
484511
ONE, TWO,
485512
}
513+
514+
enum JsonNodeToStringConverter implements Converter<JsonNode, String> {
515+
INSTANCE;
516+
517+
@Override
518+
public String convert(JsonNode source) {
519+
return source.asText();
520+
}
521+
}
522+
523+
enum StringToJsonNodeConverter implements Converter<String, JsonNode> {
524+
INSTANCE;
525+
526+
@Override
527+
public JsonNode convert(String source) {
528+
return new TextNode(source);
529+
}
530+
}
486531
}

spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.relational.core.conversion;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collection;
1820
import java.util.Collections;
1921
import java.util.List;
2022
import java.util.Optional;
@@ -171,6 +173,32 @@ public Object writeValue(@Nullable Object value, TypeInformation<?> type) {
171173
return getPotentiallyConvertedSimpleWrite(value);
172174
}
173175

176+
// TODO: We should add conversion support for arrays, however,
177+
// these should consider multi-dimensional arrays as well.
178+
if (value.getClass().isArray() && (TypeInformation.OBJECT.equals(type) || type.isCollectionLike())) {
179+
return value;
180+
}
181+
182+
if (value instanceof Collection<?>) {
183+
184+
List<Object> mapped = new ArrayList<>();
185+
186+
TypeInformation<?> component = TypeInformation.OBJECT;
187+
if (type.isCollectionLike() && type.getActualType() != null) {
188+
component = type.getRequiredComponentType();
189+
}
190+
191+
for (Object o : (Iterable<?>) value) {
192+
mapped.add(writeValue(o, component));
193+
}
194+
195+
if (type.getType().isInstance(mapped) || !type.isCollectionLike()) {
196+
return mapped;
197+
}
198+
199+
return conversionService.convert(mapped, type.getType());
200+
}
201+
174202
RelationalPersistentEntity<?> persistentEntity = context.getPersistentEntity(value.getClass());
175203

176204
if (persistentEntity != null) {

0 commit comments

Comments
 (0)