Skip to content

Commit 929c4df

Browse files
committed
#138 - Allow multiple usages of the same named parameter.
We now allow multiple usages of the same named parameter if the underlying database supports identifiable placeholders. SELECT * FROM person where name = :id or lastname = :id gets translated to SELECT * FROM person where name = $1 or lastname = $1
1 parent 7403651 commit 929c4df

File tree

6 files changed

+276
-34
lines changed

6 files changed

+276
-34
lines changed

src/main/java/org/springframework/data/r2dbc/core/NamedParameterExpander.java

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
* This class expands SQL from named parameters to native style placeholders at execution time. It also allows for
3030
* expanding a {@link java.util.List} of values to the appropriate number of placeholders.
3131
* <p>
32+
* References to the same parameter name are substituted with the same bind marker placeholder if a
33+
* {@link BindMarkersFactory} uses {@link BindMarkersFactory#identifiablePlaceholders() identifiable} placeholders.
34+
* <p>
3235
* <b>NOTE: An instance of this class is thread-safe once configured.</b>
3336
*
3437
* @author Mark Paluch

src/main/java/org/springframework/data/r2dbc/core/NamedParameterUtils.java

+102-31
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,17 @@
3131
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
3232
import org.springframework.data.r2dbc.dialect.BindTarget;
3333
import org.springframework.util.Assert;
34+
import org.springframework.util.CollectionUtils;
3435

3536
/**
3637
* Helper methods for named parameter parsing.
3738
* <p>
3839
* Only intended for internal use within Spring's Data's R2DBC framework. Partially extracted from Spring's JDBC named
3940
* parameter support.
4041
* <p>
42+
* References to the same parameter name are substituted with the same bind marker placeholder if a
43+
* {@link BindMarkersFactory} uses {@link BindMarkersFactory#identifiablePlaceholders() identifiable} placeholders.
44+
* <p>
4145
* This is a subset of Spring Frameworks's {@code org.springframework.r2dbc.namedparam.NamedParameterUtils}.
4246
*
4347
* @author Thomas Risberg
@@ -260,7 +264,7 @@ private static int skipCommentsAndQuotes(char[] statement, int position) {
260264
public static PreparedOperation<String> substituteNamedParameters(ParsedSql parsedSql,
261265
BindMarkersFactory bindMarkersFactory, BindParameterSource paramSource) {
262266

263-
BindMarkerHolder markerHolder = new BindMarkerHolder(bindMarkersFactory.create());
267+
NamedParameters markerHolder = new NamedParameters(bindMarkersFactory);
264268

265269
String originalSql = parsedSql.getOriginalSql();
266270
List<String> paramNames = parsedSql.getParameterNames();
@@ -276,9 +280,11 @@ public static PreparedOperation<String> substituteNamedParameters(ParsedSql pars
276280
int startIndex = indexes[0];
277281
int endIndex = indexes[1];
278282
actualSql.append(originalSql, lastIndex, startIndex);
283+
NamedParameters.NamedParameter marker = markerHolder.getOrCreate(paramName);
279284
if (paramSource.hasValue(paramName)) {
280285
Object value = paramSource.getValue(paramName);
281286
if (value instanceof Collection) {
287+
282288
Iterator<?> entryIter = ((Collection<?>) value).iterator();
283289
int k = 0;
284290
while (entryIter.hasNext()) {
@@ -294,19 +300,19 @@ public static PreparedOperation<String> substituteNamedParameters(ParsedSql pars
294300
if (m > 0) {
295301
actualSql.append(", ");
296302
}
297-
actualSql.append(markerHolder.addMarker(paramName));
303+
actualSql.append(marker.addPlaceholder());
298304
}
299305
actualSql.append(')');
300306
} else {
301-
actualSql.append(markerHolder.addMarker(paramName));
307+
actualSql.append(marker.addPlaceholder());
302308
}
303309

304310
}
305311
} else {
306-
actualSql.append(markerHolder.addMarker(paramName));
312+
actualSql.append(marker.getPlaceholder());
307313
}
308314
} else {
309-
actualSql.append(markerHolder.addMarker(paramName));
315+
actualSql.append(marker.getPlaceholder());
310316
}
311317
lastIndex = endIndex;
312318
}
@@ -387,22 +393,79 @@ public int hashCode() {
387393
}
388394

389395
/**
390-
* Holder for bind marker progress.
396+
* Holder for bind markers progress.
391397
*/
392-
private static class BindMarkerHolder {
398+
static class NamedParameters {
393399

394400
private final BindMarkers bindMarkers;
395-
private final Map<String, List<BindMarker>> markers = new TreeMap<>();
401+
private final boolean identifiable;
402+
private final Map<String, List<NamedParameter>> references = new TreeMap<>();
403+
404+
NamedParameters(BindMarkersFactory factory) {
405+
this.bindMarkers = factory.create();
406+
this.identifiable = factory.identifiablePlaceholders();
407+
}
408+
409+
/**
410+
* Get the {@link NamedParameter} identified by {@code namedParameter}.
411+
*
412+
* @param namedParameter
413+
* @return
414+
*/
415+
NamedParameter getOrCreate(String namedParameter) {
416+
417+
List<NamedParameter> reference = this.references.computeIfAbsent(namedParameter, ignore -> new ArrayList<>());
418+
419+
if (reference.isEmpty()) {
420+
NamedParameter param = new NamedParameter(namedParameter);
421+
reference.add(param);
422+
return param;
423+
}
396424

397-
BindMarkerHolder(BindMarkers bindMarkers) {
398-
this.bindMarkers = bindMarkers;
425+
if (this.identifiable) {
426+
return reference.get(0);
427+
}
428+
429+
NamedParameter param = new NamedParameter(namedParameter);
430+
reference.add(param);
431+
return param;
399432
}
400433

401-
String addMarker(String name) {
434+
List<NamedParameter> getMarker(String name) {
435+
return this.references.get(name);
436+
}
437+
438+
class NamedParameter {
439+
440+
private final String namedParameter;
441+
private final List<BindMarker> placeholders = new ArrayList<>();
442+
443+
NamedParameter(String namedParameter) {
444+
this.namedParameter = namedParameter;
445+
}
446+
447+
/**
448+
* Create a placeholder to translate a single value into a bindable parameter.
449+
* <p>
450+
* Can be called multiple times to create placeholders for array/collections.
451+
*
452+
* @return
453+
*/
454+
String addPlaceholder() {
455+
456+
BindMarker bindMarker = NamedParameters.this.bindMarkers.next(this.namedParameter);
457+
this.placeholders.add(bindMarker);
458+
return bindMarker.getPlaceholder();
459+
}
460+
461+
String getPlaceholder() {
462+
463+
if (this.placeholders.isEmpty()) {
464+
return addPlaceholder();
465+
}
402466

403-
BindMarker bindMarker = this.bindMarkers.next(name);
404-
this.markers.computeIfAbsent(name, ignore -> new ArrayList<>()).add(bindMarker);
405-
return bindMarker.getPlaceholder();
467+
return this.placeholders.get(0).getPlaceholder();
468+
}
406469
}
407470
}
408471

@@ -414,13 +477,13 @@ private static class ExpandedQuery implements PreparedOperation<String> {
414477

415478
private final String expandedSql;
416479

417-
private final Map<String, List<BindMarker>> markers;
480+
private final NamedParameters parameters;
418481

419482
private final BindParameterSource parameterSource;
420483

421-
ExpandedQuery(String expandedSql, BindMarkerHolder bindMarkerHolder, BindParameterSource parameterSource) {
484+
ExpandedQuery(String expandedSql, NamedParameters parameters, BindParameterSource parameterSource) {
422485
this.expandedSql = expandedSql;
423-
this.markers = bindMarkerHolder.markers;
486+
this.parameters = parameters;
424487
this.parameterSource = parameterSource;
425488
}
426489

@@ -435,13 +498,7 @@ public void bind(BindTarget target, String identifier, Object value) {
435498
return;
436499
}
437500

438-
if (bindMarkers.size() == 1) {
439-
bindMarkers.get(0).bind(target, value);
440-
} else {
441-
442-
Assert.isInstanceOf(Collection.class, value,
443-
() -> String.format("Value [%s] must be an Collection with a size of [%d]", value, bindMarkers.size()));
444-
501+
if (value instanceof Collection) {
445502
Collection<Object> collection = (Collection<Object>) value;
446503

447504
Iterator<Object> iterator = collection.iterator();
@@ -460,6 +517,10 @@ public void bind(BindTarget target, String identifier, Object value) {
460517
bind(target, markers, valueToBind);
461518
}
462519
}
520+
} else {
521+
for (BindMarker bindMarker : bindMarkers) {
522+
bindMarker.bind(target, value);
523+
}
463524
}
464525
}
465526

@@ -483,16 +544,26 @@ public void bindNull(BindTarget target, String identifier, Class<?> valueType) {
483544
return;
484545
}
485546

486-
if (bindMarkers.size() == 1) {
487-
bindMarkers.get(0).bindNull(target, valueType);
488-
return;
547+
for (BindMarker bindMarker : bindMarkers) {
548+
bindMarker.bindNull(target, valueType);
489549
}
490-
491-
throw new UnsupportedOperationException("bindNull(…) can bind only singular values");
492550
}
493551

494-
private List<BindMarker> getBindMarkers(String identifier) {
495-
return this.markers.get(identifier);
552+
List<BindMarker> getBindMarkers(String identifier) {
553+
554+
List<NamedParameters.NamedParameter> parameters = this.parameters.getMarker(identifier);
555+
556+
if (parameters == null) {
557+
return null;
558+
}
559+
560+
List<BindMarker> markers = new ArrayList<>();
561+
562+
for (NamedParameters.NamedParameter parameter : parameters) {
563+
markers.addAll(parameter.placeholders);
564+
}
565+
566+
return markers;
496567
}
497568

498569
@Override

src/main/java/org/springframework/data/r2dbc/dialect/BindMarkersFactory.java

+25-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,17 @@ public interface BindMarkersFactory {
2626
*/
2727
BindMarkers create();
2828

29+
/**
30+
* Return whether the {@link BindMarkersFactory} uses identifiable placeholders.
31+
*
32+
* @return whether the {@link BindMarkersFactory} uses identifiable placeholders. {@literal false} if multiple
33+
* placeholders cannot be distinguished by just the {@link BindMarker#getPlaceholder() placeholder}
34+
* identifier.
35+
*/
36+
default boolean identifiablePlaceholders() {
37+
return true;
38+
}
39+
2940
/**
3041
* Create index-based {@link BindMarkers} using indexes to bind parameters. Allow customization of the bind marker
3142
* placeholder {@code prefix} to represent the bind marker as placeholder within the query.
@@ -40,6 +51,7 @@ public interface BindMarkersFactory {
4051
static BindMarkersFactory indexed(String prefix, int beginWith) {
4152

4253
Assert.notNull(prefix, "Prefix must not be null!");
54+
4355
return () -> new IndexedBindMarkers(prefix, beginWith);
4456
}
4557

@@ -56,7 +68,19 @@ static BindMarkersFactory indexed(String prefix, int beginWith) {
5668
static BindMarkersFactory anonymous(String placeholder) {
5769

5870
Assert.hasText(placeholder, "Placeholder must not be empty!");
59-
return () -> new AnonymousBindMarkers(placeholder);
71+
72+
return new BindMarkersFactory() {
73+
74+
@Override
75+
public BindMarkers create() {
76+
return new AnonymousBindMarkers(placeholder);
77+
}
78+
79+
@Override
80+
public boolean identifiablePlaceholders() {
81+
return false;
82+
}
83+
};
6084
}
6185

6286
/**

src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java

+12
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ public void executeSelect() {
152152
}).verifyComplete();
153153
}
154154

155+
@Test // gh-2
156+
public void executeSelectNamedParameters() {
157+
158+
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
159+
160+
databaseClient.execute("SELECT id, name, manual FROM legoset WHERE name = :name or manual = :name") //
161+
.bind("name", "unknown").as(LegoSet.class) //
162+
.fetch().all() //
163+
.as(StepVerifier::create) //
164+
.verifyComplete();
165+
}
166+
155167
@Test // gh-2
156168
public void insert() {
157169

0 commit comments

Comments
 (0)