diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java index f6696e8d9cd..630a00b7512 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -513,14 +513,50 @@ abstract class NamedParameterUtils { private final BindParameterSource parameterSource; + ExpandedQuery(String expandedSql, NamedParameters parameters, BindParameterSource parameterSource) { this.expandedSql = expandedSql; this.parameters = parameters; this.parameterSource = parameterSource; } + + @Override + public String toQuery() { + return this.expandedSql; + } + + @Override + public String getSource() { + return this.expandedSql; + } + + @Override + public void bindTo(BindTarget target) { + for (String namedParameter : this.parameterSource.getParameterNames()) { + Parameter parameter = this.parameterSource.getValue(namedParameter); + if (parameter.getValue() == null) { + bindNull(target, namedParameter, parameter); + } + else { + bind(target, namedParameter, parameter); + } + } + } + + private void bindNull(BindTarget target, String identifier, Parameter parameter) { + List bindMarkers = getBindMarkers(identifier); + if (bindMarkers == null) { + target.bind(identifier, parameter); + return; + } + for (BindMarker bindMarker : bindMarkers) { + bindMarker.bind(target, parameter); + } + } + @SuppressWarnings({"rawtypes", "unchecked"}) - public void bind(BindTarget target, String identifier, Parameter parameter) { + private void bind(BindTarget target, String identifier, Parameter parameter) { List bindMarkers = getBindMarkers(identifier); if (bindMarkers == null) { target.bind(identifier, parameter); @@ -555,19 +591,8 @@ abstract class NamedParameterUtils { markers.next().bind(target, valueToBind); } - public void bindNull(BindTarget target, String identifier, Parameter parameter) { - List bindMarkers = getBindMarkers(identifier); - if (bindMarkers == null) { - target.bind(identifier, parameter); - return; - } - for (BindMarker bindMarker : bindMarkers) { - bindMarker.bind(target, parameter); - } - } - @Nullable - List getBindMarkers(String identifier) { + private List getBindMarkers(String identifier) { List parameters = this.parameters.getMarker(identifier); if (parameters == null) { return null; @@ -579,28 +604,6 @@ abstract class NamedParameterUtils { return markers; } - @Override - public String getSource() { - return this.expandedSql; - } - - @Override - public void bindTo(BindTarget target) { - for (String namedParameter : this.parameterSource.getParameterNames()) { - Parameter parameter = this.parameterSource.getValue(namedParameter); - if (parameter.getValue() == null) { - bindNull(target, namedParameter, parameter); - } - else { - bind(target, namedParameter, parameter); - } - } - } - - @Override - public String toQuery() { - return this.expandedSql; - } } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java index 71569702ccc..c66ada2ca17 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/AnonymousBindMarkers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,9 @@ package org.springframework.r2dbc.core.binding; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * Anonymous, index-based bind marker using a static placeholder. - * Instances are bound by the ordinal position ordered by the appearance of + * Anonymous, index-based bind markers that use a static placeholder. + * + *

Instances are bound by the ordinal position ordered by the appearance of * the placeholder. This implementation creates indexed bind markers using * an anonymous placeholder that correlates with an index. * @@ -46,7 +47,7 @@ class AnonymousBindMarkers implements BindMarkers { /** - * Create a new {@link AnonymousBindMarkers} instance given {@code placeholder}. + * Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}. * @param placeholder parameter bind marker */ AnonymousBindMarkers(String placeholder) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java index 5fb53c47c19..40f8a71313e 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,8 @@ import io.r2dbc.spi.Statement; /** * A bind marker represents a single bindable parameter within a query. - * Bind markers are dialect-specific and provide a + * + *

Bind markers are dialect-specific and provide a * {@link #getPlaceholder() placeholder} that is used in the actual query. * * @author Mark Paluch @@ -37,7 +38,8 @@ public interface BindMarker { String getPlaceholder(); /** - * Bind the given {@code value} to the {@link Statement} using the underlying binding strategy. + * Bind the given {@code value} to the {@link Statement} using the underlying + * binding strategy. * @param bindTarget the target to bind the value to * @param value the actual value (must not be {@code null}; * use {@link #bindNull(BindTarget, Class)} for {@code null} values) @@ -46,7 +48,8 @@ public interface BindMarker { void bind(BindTarget bindTarget, Object value); /** - * Bind a {@code null} value to the {@link Statement} using the underlying binding strategy. + * Bind a {@code null} value to the {@link Statement} using the underlying + * binding strategy. * @param bindTarget the target to bind the value to * @param valueType the value type (must not be {@code null}) * @see Statement#bindNull diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java index af5a9fd48ca..5504cbc01e1 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,10 +20,10 @@ package org.springframework.r2dbc.core.binding; * Bind markers represent placeholders in SQL queries for substitution * for an actual parameter. Using bind markers allows creating safe queries * so query strings are not required to contain escaped values but rather - * the driver encodes parameter in the appropriate representation. + * the driver encodes the parameter in the appropriate representation. * *

{@link BindMarkers} is stateful and can be only used for a single binding - * pass of one or more parameters. It maintains bind indexes/bind parameter names. + * pass of one or more parameters. It maintains bind indexes or bind parameter names. * * @author Mark Paluch * @since 5.3 @@ -41,7 +41,7 @@ public interface BindMarkers { /** * Create a new {@link BindMarker} that accepts a {@code hint}. - * Implementations are allowed to consider/ignore/filter + *

Implementations are allowed to consider/ignore/filter * the name hint to create more expressive bind markers. * @param hint an optional name hint that can be used as part of the bind marker * @return a new {@link BindMarker} diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java index c658352846d..d83c70abdd5 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/BindMarkersFactoryResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,8 +30,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap; /** * Resolves a {@link BindMarkersFactory} from a {@link ConnectionFactory} using - * {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's - * {@link SpringFactoriesLoader spring.factories} to determine available extensions. + * a {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's + * {@link SpringFactoriesLoader spring.factories} file to determine available extensions. * * @author Mark Paluch * @since 5.3 @@ -45,8 +45,8 @@ public final class BindMarkersFactoryResolver { /** - * Retrieve a {@link BindMarkersFactory} by inspecting {@link ConnectionFactory} - * and its metadata. + * Retrieve a {@link BindMarkersFactory} by inspecting the supplied + * {@link ConnectionFactory} and its metadata. * @param connectionFactory the connection factory to inspect * @return the resolved {@link BindMarkersFactory} * @throws NoBindMarkersFactoryException if no {@link BindMarkersFactory} can be resolved @@ -69,18 +69,21 @@ public final class BindMarkersFactoryResolver { /** - * SPI to extend Spring's default R2DBC BindMarkersFactory discovery mechanism. - * Implementations of this interface are discovered through Spring's + * SPI to extend Spring's default R2DBC {@link BindMarkersFactory} discovery + * mechanism. + * + *

Implementations of this interface are discovered through Spring's * {@link SpringFactoriesLoader} mechanism. + * * @see SpringFactoriesLoader */ @FunctionalInterface public interface BindMarkerFactoryProvider { /** - * Return a {@link BindMarkersFactory} for a {@link ConnectionFactory}. - * @param connectionFactory the connection factory to be used with the {@link BindMarkersFactory} - * @return the {@link BindMarkersFactory} if the {@link BindMarkerFactoryProvider} + * Return a {@link BindMarkersFactory} for the given {@link ConnectionFactory}. + * @param connectionFactory the connection factory to be used with the {@code BindMarkersFactory} + * @return the {@code BindMarkersFactory} if this {@code BindMarkerFactoryProvider} * can provide a bind marker factory object, otherwise {@code null} */ @Nullable @@ -89,7 +92,7 @@ public final class BindMarkersFactoryResolver { /** - * Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a + * Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a * {@link BindMarkersFactory}. */ @SuppressWarnings("serial") @@ -106,8 +109,11 @@ public final class BindMarkersFactoryResolver { /** - * Built-in bind maker factories. Used typically as last {@link BindMarkerFactoryProvider} - * when other providers register with a higher precedence. + * Built-in bind marker factories. + * + *

Typically used as the last {@link BindMarkerFactoryProvider} when other + * providers are registered with a higher precedence. + * * @see org.springframework.core.Ordered * @see org.springframework.core.annotation.AnnotationAwareOrderComparator */ diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java index 5f14d937b4f..23081c9778c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/IndexedBindMarkers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,7 +19,7 @@ package org.springframework.r2dbc.core.binding; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** - * Index-based bind marker. This implementation creates indexed bind + * Index-based bind markers. This implementation creates indexed bind * markers using a numeric index and an optional prefix for bind markers * to be represented within the query string. * @@ -43,14 +43,15 @@ class IndexedBindMarkers implements BindMarkers { /** - * Create a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}. - * @param prefix bind parameter prefix - * @param beginWith the first index to use + * Create a new {@link IndexedBindMarker} instance for the given {@code prefix} + * and {@code beginWith} value. + * @param prefix the bind parameter prefix + * @param beginIndex the first index to use */ - IndexedBindMarkers(String prefix, int beginWith) { + IndexedBindMarkers(String prefix, int beginIndex) { this.counter = 0; this.prefix = prefix; - this.offset = beginWith; + this.offset = beginIndex; } diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java index 413c218909e..9cd68ee3998 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/NamedParameterUtilsTests.java @@ -28,8 +28,6 @@ import org.junit.jupiter.params.provider.ValueSource; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindTarget; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -320,34 +318,17 @@ class NamedParameterUtilsTests { assertThat(operation.toQuery()) .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); - Map bindings = new HashMap<>(); - - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - bindings.put(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindings) + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) .hasSize(1) .containsEntry(0, Parameters.in("foo")); } @Test - void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValueOnce() { + void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValuesOnce() { String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)"; MapBindParameterSource source = new MapBindParameterSource(Map.of("ids", @@ -357,71 +338,40 @@ class NamedParameterUtilsTests { assertThat(operation.toQuery()) .isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)"); - MultiValueMap bindings = new LinkedMultiValueMap<>(); - - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - bindings.add(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindings) + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) .hasSize(3) - .containsEntry(0, List.of("foo")) - .containsEntry(1, List.of("bar")) - .containsEntry(2, List.of("baz")); + .containsEntry(0, "foo") + .containsEntry(1, "bar") + .containsEntry(2, "baz"); } @Test // gh-34768 @Disabled("Disabled until gh-34768 is addressed") - void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValueTwice() { + void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValuesTwice() { String sql = "SELECT * FROM fund_info WHERE fund_code IN (:fundCodes) OR fund_code IN (:fundCodes)"; - MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo")))); + MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo", "bar", "baz")))); PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source); assertThat(operation.toQuery()) - .isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?) OR fund_code IN (?)"); - - Map bindings = new HashMap<>(); + .isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?, ?, ?) OR fund_code IN (?, ?, ?)"); - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) {} + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); - @Override - public void bind(int index, Object value) { - bindings.put(index, value); - } + operation.bindTo(trackingBindTarget); - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindings) - .hasSize(2) + assertThat(trackingBindTarget.bindings) + .hasSize(6) .containsEntry(0, "foo") - .containsEntry(1, "foo"); + .containsEntry(1, "bar") + .containsEntry(2, "baz") + .containsEntry(3, "foo") + .containsEntry(4, "bar") + .containsEntry(5, "baz"); } @Test @@ -434,28 +384,11 @@ class NamedParameterUtilsTests { assertThat(operation.toQuery()) .isEqualTo("SELECT * FROM person where name = ? or lastname = ?"); - Map bindings = new HashMap<>(); - - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - bindings.put(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindings) + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) .hasSize(2) .containsEntry(0, Parameters.in("foo")) .containsEntry(1, Parameters.in("foo")); @@ -471,28 +404,11 @@ class NamedParameterUtilsTests { assertThat(operation.toQuery()) .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); - Map bindings = new HashMap<>(); - - operation.bindTo(new BindTarget() { - @Override - public void bind(String identifier, Object value) { - throw new UnsupportedOperationException(); - } - @Override - public void bind(int index, Object value) { - bindings.put(index, value); - } - @Override - public void bindNull(String identifier, Class type) { - throw new UnsupportedOperationException(); - } - @Override - public void bindNull(int index, Class type) { - throw new UnsupportedOperationException(); - } - }); - - assertThat(bindings) + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) .hasSize(1) .containsEntry(0, Parameters.in(String.class)); } @@ -508,4 +424,28 @@ class NamedParameterUtilsTests { new MapBindParameterSource()).toQuery(); } + + private static class TrackingBindTarget implements BindTarget { + + final Map bindings = new HashMap<>(); + + @Override + public void bind(String identifier, Object value) {} + + @Override + public void bind(int index, Object value) { + this.bindings.put(index, value); + } + + @Override + public void bindNull(String identifier, Class type) { + throw new UnsupportedOperationException(); + } + + @Override + public void bindNull(int index, Class type) { + throw new UnsupportedOperationException(); + } + } + }