From 38453910cd131c3f790c29f4299fa3da987566cf Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Mon, 29 Jul 2024 14:07:15 +0200 Subject: [PATCH] Add DatabaseClient bind variant for list of parameters Prior to this commit, the `DatabaseClient` interface would allow batch operations for binding parameters by their names and values. Positional parameters did not have such equivalent. This commit adds a new `bindValues(List)` method variant for adding multiple positional arguments in a single call and avoiding allocation overhead when the parameters count is large. Closes gh-33274 --- .../modules/ROOT/pages/data-access/r2dbc.adoc | 22 +++++++++++++++++++ .../r2dbc/core/DatabaseClient.java | 16 +++++++++++++- .../r2dbc/core/DefaultDatabaseClient.java | 13 +++++++++++ ...bstractDatabaseClientIntegrationTests.java | 20 +++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index 672f396ef0f9..1c07b8390e61 100644 --- a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc +++ b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc @@ -364,6 +364,28 @@ Or you may pass in a parameter object with bean properties or record components: .bindProperties(new Person("joe", "Joe", 34); ---- +Alternatively, you can use positional parameters for binding values to statements. +Indices are zero based. + +[source,java] +---- + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind(0, "joe") + .bind(1, "Joe") + .bind(2, 34); +---- + +In case your application is binding to many parameters, the same can be achieved with a single call: + +[source,java] +---- + List values = List.of("joe", "Joe", 34); + db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bindValues(values); +---- + + + .R2DBC Native Bind Markers **** R2DBC uses database-native bind markers that depend on the actual database vendor. diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java index 4ad0cd7c818a..449f4db9e843 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -16,6 +16,7 @@ package org.springframework.r2dbc.core; +import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Consumer; @@ -56,6 +57,7 @@ * * @author Mark Paluch * @author Juergen Hoeller + * @author Brian Clozel * @since 5.3 */ public interface DatabaseClient extends ConnectionAccessor { @@ -191,6 +193,18 @@ interface GenericExecuteSpec { */ GenericExecuteSpec bindNull(String name, Class type); + /** + * Bind the parameter values from the given source list, + * registering each as a positional parameter using their order + * in the given list as their index. + * @param source the source list of parameters, with their order + * as position and each value either a scalar value + * or a {@link io.r2dbc.spi.Parameter} + * @since 6.2 + * @see #bind(int, Object) + */ + GenericExecuteSpec bindValues(List source); + /** * Bind the parameter values from the given source map, * registering each as a parameter with the map key as name. diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java index 6cab172001b8..d535a3989b6a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java @@ -24,6 +24,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; +import java.util.ListIterator; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; @@ -308,6 +309,18 @@ public DefaultGenericExecuteSpec bindNull(String name, Class type) { return new DefaultGenericExecuteSpec(this.byIndex, byName, this.sqlSupplier, this.filterFunction); } + @Override + public GenericExecuteSpec bindValues(List source) { + assertNotPreparedOperation(); + Assert.notNull(source, "Source list must not be null"); + Map byIndex = new LinkedHashMap<>(this.byIndex); + ListIterator listIterator = source.listIterator(); + while (listIterator.hasNext()) { + byIndex.put(listIterator.nextIndex(), resolveParameter(listIterator.next())); + } + return new DefaultGenericExecuteSpec(byIndex, this.byName, this.sqlSupplier, this.filterFunction); + } + @Override public GenericExecuteSpec bindValues(Map source) { assertNotPreparedOperation(); diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index d0625b1bcc0a..19332ecd4033 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/AbstractDatabaseClientIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.r2dbc.core; +import java.util.List; import java.util.Map; import io.r2dbc.spi.ConnectionFactory; @@ -96,6 +97,25 @@ void executeInsert() { .verifyComplete(); } + @Test + void executeInsertWithList() { + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)") + .bindValues(List.of(42055, Parameters.in("SCHAUFELRADBAGGER"), Parameters.in(Integer.class))) + .fetch().rowsUpdated() + .as(StepVerifier::create) + .expectNext(1L) + .verifyComplete(); + + databaseClient.sql("SELECT id FROM legoset") + .mapValue(Integer.class) + .first() + .as(StepVerifier::create) + .assertNext(actual -> assertThat(actual).isEqualTo(42055)) + .verifyComplete(); + } + @Test void executeInsertWithMap() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);