diff --git a/build.gradle b/build.gradle
index d4ac72496f..f2d54c3d29 100644
--- a/build.gradle
+++ b/build.gradle
@@ -346,7 +346,7 @@ configure([rootProject] + javaProjects) { project ->
// "https://junit.org/junit5/docs/5.8.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
- "https://r2dbc.io/spec/0.8.5.RELEASE/api/",
+ "https://r2dbc.io/spec/0.9.1.RELEASE/api/",
// The external Javadoc link for JSR 305 must come last to ensure that types from
// JSR 250 (such as @PostConstruct) are still supported. This is due to the fact
// that JSR 250 and JSR 305 both define types in javax.annotation, which results
diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java
index 557427ecb6..9d5ff8e3cf 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/R2dbcTransactionManager.java
@@ -21,8 +21,10 @@ import java.time.Duration;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.IsolationLevel;
+import io.r2dbc.spi.Option;
import io.r2dbc.spi.R2dbcException;
import io.r2dbc.spi.Result;
+import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import org.springframework.beans.factory.InitializingBean;
@@ -47,7 +49,7 @@ import org.springframework.util.Assert;
*
Note: The {@code ConnectionFactory} that this transaction manager
* operates on needs to return independent {@code Connection}s.
* The {@code Connection}s may come from a pool (the typical case), but the
- * {@code ConnectionFactory} must not return scoped scoped {@code Connection}s
+ * {@code ConnectionFactory} must not return scoped {@code Connection}s
* or the like. This transaction manager will associate {@code Connection}
* with context-bound transactions itself, according to the specified propagation
* behavior. It assumes that a separate, independent {@code Connection} can
@@ -72,6 +74,11 @@ import org.springframework.util.Assert;
* synchronizations (if synchronization is generally active), assuming resources
* operating on the underlying R2DBC {@code Connection}.
*
+ *
Spring's {@code TransactionDefinition} attributes are carried forward to R2DBC drivers
+ * using extensible R2DBC {@link io.r2dbc.spi.TransactionDefinition}. Subclasses may
+ * override {@link #createTransactionDefinition(TransactionDefinition)} to customize
+ * transaction definitions for vendor-specific attributes.
+ *
* @author Mark Paluch
* @since 5.3
* @see ConnectionFactoryUtils#getConnection(ConnectionFactory)
@@ -203,7 +210,8 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}
return connectionMono.flatMap(con -> {
- return prepareTransactionalConnection(con, definition, transaction).then(Mono.from(con.beginTransaction()))
+ return prepareTransactionalConnection(con, definition, transaction)
+ .then(Mono.from(doBegin(definition, con)))
.doOnSuccess(v -> {
txObject.getConnectionHolder().setTransactionActive(true);
Duration timeout = determineTimeout(definition);
@@ -230,6 +238,31 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}).then();
}
+ private Publisher doBegin(TransactionDefinition definition, Connection con) {
+ io.r2dbc.spi.TransactionDefinition transactionDefinition = createTransactionDefinition(definition);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Starting R2DBC transaction on Connection [" + con + "] using [" + transactionDefinition + "]");
+ }
+ return con.beginTransaction(transactionDefinition);
+ }
+
+ /**
+ * Determine the transaction definition from our {@code TransactionDefinition}.
+ * Can be overridden to wrap the R2DBC {@code TransactionDefinition} to adjust or
+ * enhance transaction attributes.
+ * @param definition the transaction definition
+ * @return the actual transaction definition to use
+ * @since 6.0
+ * @see io.r2dbc.spi.TransactionDefinition
+ */
+ protected io.r2dbc.spi.TransactionDefinition createTransactionDefinition(TransactionDefinition definition) {
+ // Apply specific isolation level, if any.
+ IsolationLevel isolationLevelToUse = resolveIsolationLevel(definition.getIsolationLevel());
+ return new ExtendedTransactionDefinition(definition.getName(), definition.isReadOnly(),
+ definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT ? isolationLevelToUse : null,
+ determineTimeout(definition));
+ }
+
/**
* Determine the actual timeout to use for the given definition.
* Will fall back to this manager's default timeout if the
@@ -375,21 +408,6 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
.then();
}
- // Apply specific isolation level, if any.
- IsolationLevel isolationLevelToUse = resolveIsolationLevel(definition.getIsolationLevel());
- if (isolationLevelToUse != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
-
- if (logger.isDebugEnabled()) {
- logger.debug("Changing isolation level of R2DBC Connection [" + con + "] to " + isolationLevelToUse.asSql());
- }
- IsolationLevel currentIsolation = con.getTransactionIsolationLevel();
- if (!currentIsolation.asSql().equalsIgnoreCase(isolationLevelToUse.asSql())) {
-
- txObject.setPreviousIsolationLevel(currentIsolation);
- prepare = prepare.then(Mono.from(con.setTransactionIsolationLevel(isolationLevelToUse)));
- }
- }
-
// Switch to manual commit if necessary. This is very expensive in some R2DBC drivers,
// so we don't want to do it unnecessarily (for example if we've explicitly
// configured the connection pool to set it already).
@@ -436,6 +454,62 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
}
+ /**
+ * Extended R2DBC transaction definition object providing transaction attributes
+ * to R2DBC drivers when starting a transaction.
+ */
+ private record ExtendedTransactionDefinition(@Nullable String transactionName,
+ boolean readOnly,
+ @Nullable IsolationLevel isolationLevel,
+ Duration lockWaitTimeout) implements io.r2dbc.spi.TransactionDefinition {
+
+ private ExtendedTransactionDefinition(@Nullable String transactionName, boolean readOnly,
+ @Nullable IsolationLevel isolationLevel, Duration lockWaitTimeout) {
+ this.transactionName = transactionName;
+ this.readOnly = readOnly;
+ this.isolationLevel = isolationLevel;
+ this.lockWaitTimeout = lockWaitTimeout;
+ }
+
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T getAttribute(Option option) {
+ return (T) doGetValue(option);
+ }
+
+ @Nullable
+ private Object doGetValue(Option> option) {
+ if (io.r2dbc.spi.TransactionDefinition.ISOLATION_LEVEL.equals(option)) {
+ return this.isolationLevel;
+ }
+ if (io.r2dbc.spi.TransactionDefinition.NAME.equals(option)) {
+ return this.transactionName;
+ }
+ if (io.r2dbc.spi.TransactionDefinition.READ_ONLY.equals(option)) {
+ return this.readOnly;
+ }
+ if (io.r2dbc.spi.TransactionDefinition.LOCK_WAIT_TIMEOUT.equals(option)
+ && !this.lockWaitTimeout.isZero()) {
+ return this.lockWaitTimeout;
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getClass().getSimpleName());
+ sb.append(" [transactionName='").append(this.transactionName).append('\'');
+ sb.append(", readOnly=").append(this.readOnly);
+ sb.append(", isolationLevel=").append(this.isolationLevel);
+ sb.append(", lockWaitTimeout=").append(this.lockWaitTimeout);
+ sb.append(']');
+ return sb.toString();
+ }
+ }
+
+
/**
* ConnectionFactory transaction object, representing a ConnectionHolder.
* Used as transaction object by R2dbcTransactionManager.
diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BindParameterSource.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BindParameterSource.java
index 61211b98f5..08d672088c 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BindParameterSource.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BindParameterSource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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,7 +16,7 @@
package org.springframework.r2dbc.core;
-import org.springframework.lang.Nullable;
+import io.r2dbc.spi.Parameter;
/**
* Interface that defines common functionality for objects
@@ -44,24 +44,13 @@ interface BindParameterSource {
boolean hasValue(String paramName);
/**
- * Return the parameter value for the requested named parameter.
+ * Return the parameter for the requested named parameter.
* @param paramName the name of the parameter
- * @return the value of the specified parameter (can be {@code null})
+ * @return the specified parameter
* @throws IllegalArgumentException if there is no value
* for the requested parameter
*/
- @Nullable
- Object getValue(String paramName) throws IllegalArgumentException;
-
- /**
- * Determine the type for the specified named parameter.
- * @param paramName the name of the parameter
- * @return the type of the specified parameter, or
- * {@link Object#getClass()} if not known.
- */
- default Class> getType(String paramName) {
- return Object.class;
- }
+ Parameter getValue(String paramName) throws IllegalArgumentException;
/**
* Return the parameter names of the underlying parameter source.
diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java
index 585b6ff6eb..b68b7dcbb7 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java
@@ -16,7 +16,7 @@
package org.springframework.r2dbc.core;
-import java.util.Collection;
+import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
@@ -55,12 +55,12 @@ public class ColumnMapRowMapper implements BiFunction apply(Row row, RowMetadata rowMetadata) {
- Collection columns = rowMetadata.getColumnNames();
+ List extends ColumnMetadata> columns = rowMetadata.getColumnMetadatas();
int columnCount = columns.size();
Map mapOfColValues = createColumnMap(columnCount);
int index = 0;
- for (String column : columns) {
- String key = getColumnKey(column);
+ for (ColumnMetadata column : columns) {
+ String key = getColumnKey(column.getName());
Object obj = getColumnValue(row, index++);
mapOfColValues.put(key, obj);
}
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 7b0c7ddd59..f47765d172 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-2020 the original author or authors.
+ * Copyright 2002-2022 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,12 +20,17 @@ import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Predicate;
import java.util.function.Supplier;
import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.Readable;
+import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import io.r2dbc.spi.Statement;
+import org.reactivestreams.Publisher;
+import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
@@ -157,9 +162,9 @@ public interface DatabaseClient extends ConnectionAccessor {
/**
* Bind a non-{@code null} value to a parameter identified by its
- * {@code index}. {@code value} can be either a scalar value or {@link Parameter}.
+ * {@code index}. {@code value} can be either a scalar value or {@link io.r2dbc.spi.Parameter}.
* @param index zero based index to bind the parameter to
- * @param value either a scalar value or {@link Parameter}
+ * @param value either a scalar value or {@link io.r2dbc.spi.Parameter}
*/
GenericExecuteSpec bind(int index, Object value);
@@ -213,14 +218,12 @@ public interface DatabaseClient extends ConnectionAccessor {
/**
* Configure a result mapping {@link Function function} and enter the execution stage.
- * @param mappingFunction a function that maps from {@link Row} to the result type
+ * @param mappingFunction a function that maps from {@link Readable} to the result type
* @param the result type
* @return a {@link FetchSpec} for configuration what to fetch
+ * @since 6.0
*/
- default RowsFetchSpec map(Function mappingFunction) {
- Assert.notNull(mappingFunction, "Mapping function must not be null");
- return map((row, rowMetadata) -> mappingFunction.apply(row));
- }
+ RowsFetchSpec map(Function super Readable, R> mappingFunction);
/**
* Configure a result mapping {@link BiFunction function} and enter the execution stage.
@@ -231,6 +234,17 @@ public interface DatabaseClient extends ConnectionAccessor {
*/
RowsFetchSpec map(BiFunction mappingFunction);
+ /**
+ * Perform the SQL call and apply {@link BiFunction function} to the {@link Result}.
+ * @param mappingFunction a function that maps from {@link Result} into a result publisher
+ * @param the result type
+ * @return a {@link Flux} emitting mapped elements
+ * @since 6.0
+ * @see Result#filter(Predicate)
+ * @see Result#flatMap(Function)
+ */
+ Flux flatMap(Function> mappingFunction);
+
/**
* Perform the SQL call and retrieve the result by entering the execution stage.
*/
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 366254ae4c..3f6fec3f07 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -32,7 +32,10 @@ import java.util.stream.Collectors;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.Parameter;
+import io.r2dbc.spi.Parameters;
import io.r2dbc.spi.R2dbcException;
+import io.r2dbc.spi.Readable;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
@@ -188,11 +191,12 @@ class DefaultDatabaseClient implements DatabaseClient {
new CloseSuppressingInvocationHandler(con));
}
- private static Mono sumRowsUpdated(
+ private static Mono sumRowsUpdated(
Function> resultFunction, Connection it) {
return resultFunction.apply(it)
.flatMap(Result::getRowsUpdated)
- .collect(Collectors.summingInt(Integer::intValue));
+ .cast(Number.class)
+ .collect(Collectors.summingLong(Number::longValue));
}
/**
@@ -243,17 +247,21 @@ class DefaultDatabaseClient implements DatabaseClient {
}
@Override
+ @SuppressWarnings("deprecation")
public DefaultGenericExecuteSpec bind(int index, Object value) {
assertNotPreparedOperation();
Assert.notNull(value, () -> String.format(
"Value at index %d must not be null. Use bindNull(…) instead.", index));
Map byIndex = new LinkedHashMap<>(this.byIndex);
- if (value instanceof Parameter) {
- byIndex.put(index, (Parameter) value);
+ if (value instanceof Parameter p) {
+ byIndex.put(index, p);
+ }
+ else if (value instanceof org.springframework.r2dbc.core.Parameter p) {
+ byIndex.put(index, p.hasValue() ? Parameters.in(p.getValue()) : Parameters.in(p.getType()));
}
else {
- byIndex.put(index, Parameter.fromOrEmpty(value, value.getClass()));
+ byIndex.put(index, Parameters.in(value));
}
return new DefaultGenericExecuteSpec(byIndex, this.byName, this.sqlSupplier, this.filterFunction);
@@ -264,12 +272,13 @@ class DefaultDatabaseClient implements DatabaseClient {
assertNotPreparedOperation();
Map byIndex = new LinkedHashMap<>(this.byIndex);
- byIndex.put(index, Parameter.empty(type));
+ byIndex.put(index, Parameters.in(type));
return new DefaultGenericExecuteSpec(byIndex, this.byName, this.sqlSupplier, this.filterFunction);
}
@Override
+ @SuppressWarnings("deprecation")
public DefaultGenericExecuteSpec bind(String name, Object value) {
assertNotPreparedOperation();
@@ -278,11 +287,14 @@ class DefaultDatabaseClient implements DatabaseClient {
"Value for parameter %s must not be null. Use bindNull(…) instead.", name));
Map byName = new LinkedHashMap<>(this.byName);
- if (value instanceof Parameter) {
- byName.put(name, (Parameter) value);
+ if (value instanceof Parameter p) {
+ byName.put(name, p);
+ }
+ else if (value instanceof org.springframework.r2dbc.core.Parameter p) {
+ byName.put(name, p.hasValue() ? Parameters.in(p.getValue()) : Parameters.in(p.getType()));
}
else {
- byName.put(name, Parameter.fromOrEmpty(value, value.getClass()));
+ byName.put(name, Parameters.in(value));
}
return new DefaultGenericExecuteSpec(this.byIndex, byName, this.sqlSupplier, this.filterFunction);
@@ -294,7 +306,7 @@ class DefaultDatabaseClient implements DatabaseClient {
Assert.hasText(name, "Parameter name must not be null or empty!");
Map byName = new LinkedHashMap<>(this.byName);
- byName.put(name, Parameter.empty(type));
+ byName.put(name, Parameters.in(type));
return new DefaultGenericExecuteSpec(this.byIndex, byName, this.sqlSupplier, this.filterFunction);
}
@@ -306,15 +318,27 @@ class DefaultDatabaseClient implements DatabaseClient {
this.byIndex, this.byName, this.sqlSupplier, this.filterFunction.andThen(filter));
}
+ @Override
+ public FetchSpec map(Function super Readable, R> mappingFunction) {
+ Assert.notNull(mappingFunction, "Mapping function must not be null");
+ return execute(this.sqlSupplier, result -> result.map(mappingFunction));
+ }
+
@Override
public FetchSpec map(BiFunction mappingFunction) {
Assert.notNull(mappingFunction, "Mapping function must not be null");
- return execute(this.sqlSupplier, mappingFunction);
+ return execute(this.sqlSupplier, result -> result.map(mappingFunction));
+ }
+
+ @Override
+ public Flux flatMap(Function> mappingFunction) {
+ Assert.notNull(mappingFunction, "Mapping function must not be null");
+ return flatMap(this.sqlSupplier, mappingFunction);
}
@Override
public FetchSpec