diff --git a/pom.xml b/pom.xml
index 4fe0260..6271593 100644
--- a/pom.xml
+++ b/pom.xml
@@ -30,8 +30,11 @@
0.1.4
2.4.1
42.2.5
+ 7.1.2.jre8-preview
1.0.0.M6
1.0.0.M6
+ 1.0.0.M6
+ 1.0.0.M6
1.10.1
@@ -235,6 +238,12 @@
test
+
+ com.microsoft.sqlserver
+ mssql-jdbc
+ ${mssql-jdbc.version}
+
+
io.r2dbc
r2dbc-postgresql
@@ -242,6 +251,20 @@
test
+
+ io.r2dbc
+ r2dbc-h2
+ ${r2dbc-h2.version}
+ test
+
+
+
+ io.r2dbc
+ r2dbc-mssql
+ ${r2dbc-mssql.version}
+ test
+
+
de.schauderhaft.degraph
degraph-check
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
similarity index 76%
rename from src/main/java/org/springframework/data/r2dbc/repository/config/AbstractR2dbcConfiguration.java
rename to src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
index 561a513..6f3dbb1 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/config/AbstractR2dbcConfiguration.java
+++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
@@ -13,13 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.data.r2dbc.repository.config;
+package org.springframework.data.r2dbc.config;
+
+import io.r2dbc.spi.ConnectionFactory;
import java.util.Optional;
-import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.data.r2dbc.dialect.Database;
+import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
@@ -37,7 +40,7 @@ import org.springframework.util.Assert;
* @author Mark Paluch
* @see ConnectionFactory
* @see DatabaseClient
- * @see EnableR2dbcRepositories
+ * @see org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
*/
@Configuration
public abstract class AbstractR2dbcConfiguration {
@@ -50,6 +53,24 @@ public abstract class AbstractR2dbcConfiguration {
*/
public abstract ConnectionFactory connectionFactory();
+ /**
+ * Return a {@link Dialect} for the given {@link ConnectionFactory}. This method attempts to resolve a {@link Dialect}
+ * from {@link io.r2dbc.spi.ConnectionFactoryMetadata}. Override this method to specify a dialect instead of
+ * attempting to resolve one.
+ *
+ * @param connectionFactory the configured {@link ConnectionFactory}.
+ * @return the resolved {@link Dialect}.
+ * @throws UnsupportedOperationException if the {@link Dialect} cannot be determined.
+ */
+ public Dialect getDialect(ConnectionFactory connectionFactory) {
+
+ return Database.findDatabase(connectionFactory)
+ .orElseThrow(() -> new UnsupportedOperationException(
+ String.format("Cannot determine a dialect for %s using %s. Please provide a Dialect.",
+ connectionFactory.getMetadata().getName(), connectionFactory)))
+ .latestDialect();
+ }
+
/**
* Register a {@link DatabaseClient} using {@link #connectionFactory()} and {@link RelationalMappingContext}.
*
@@ -86,18 +107,21 @@ public abstract class AbstractR2dbcConfiguration {
}
/**
- * Creates a {@link ReactiveDataAccessStrategy} using the configured {@link #r2dbcMappingContext(Optional) RelationalMappingContext}.
+ * Creates a {@link ReactiveDataAccessStrategy} using the configured {@link #r2dbcMappingContext(Optional)
+ * RelationalMappingContext}.
*
* @param mappingContext the configured {@link RelationalMappingContext}.
* @return must not be {@literal null}.
* @see #r2dbcMappingContext(Optional)
+ * @see #getDialect(ConnectionFactory)
* @throws IllegalArgumentException if any of the {@literal mappingContext} is {@literal null}.
*/
@Bean
public ReactiveDataAccessStrategy reactiveDataAccessStrategy(RelationalMappingContext mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
- return new DefaultReactiveDataAccessStrategy(new BasicRelationalConverter(mappingContext));
+ return new DefaultReactiveDataAccessStrategy(getDialect(connectionFactory()),
+ new BasicRelationalConverter(mappingContext));
}
/**
diff --git a/src/main/java/org/springframework/data/r2dbc/config/package-info.java b/src/main/java/org/springframework/data/r2dbc/config/package-info.java
new file mode 100644
index 0000000..e4e7fb5
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/config/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Configuration classes for Spring Data R2DBC.
+ */
+@org.springframework.lang.NonNullApi
+@org.springframework.lang.NonNullFields
+package org.springframework.data.r2dbc.config;
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
index a6971bf..415a628 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
@@ -28,7 +28,7 @@ public interface BindMarker {
* {@literal null} values.
* @see Statement#bind
*/
- void bindValue(Statement> statement, Object value);
+ void bind(Statement> statement, Object value);
/**
* Bind a {@literal null} value to the {@link Statement} using the underlying binding strategy.
@@ -37,6 +37,5 @@ public interface BindMarker {
* @param valueType value type, must not be {@literal null}.
* @see Statement#bindNull
*/
-
void bindNull(Statement> statement, Class> valueType);
}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Database.java b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java
new file mode 100644
index 0000000..ee3f4e1
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java
@@ -0,0 +1,92 @@
+package org.springframework.data.r2dbc.dialect;
+
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryMetadata;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Optional;
+
+import org.springframework.util.Assert;
+
+/**
+ * Enumeration of known Databases for offline {@link Dialect} resolution. R2DBC {@link io.r2dbc.spi.ConnectionFactory}
+ * provides {@link io.r2dbc.spi.ConnectionFactoryMetadata metadata} that allows resolving an appropriate {@link Dialect}
+ * if none was configured explicitly.
+ *
+ * @author Mark Paluch
+ */
+public enum Database {
+
+ POSTGRES {
+ @Override
+ public String driverName() {
+ return "PostgreSQL";
+ }
+
+ @Override
+ public Dialect latestDialect() {
+ return PostgresDialect.INSTANCE;
+ }
+ },
+
+ SQL_SERVER {
+ @Override
+ public String driverName() {
+ return "Microsoft SQL Server";
+ }
+
+ @Override
+ public Dialect latestDialect() {
+ return SqlServerDialect.INSTANCE;
+ }
+ },
+
+ H2 {
+ @Override
+ public String driverName() {
+ return "H2";
+ }
+
+ @Override
+ public Dialect latestDialect() {
+ return H2Dialect.INSTANCE;
+ }
+ };
+
+ /**
+ * Find a {@link Database} type using {@link ConnectionFactory} and its metadata.
+ *
+ * @param connectionFactory must not be {@literal null}.
+ * @return the resolved {@link Database} or {@link Optional#empty()} if the database type cannot be determined from
+ * {@link ConnectionFactory}.
+ */
+ public static Optional findDatabase(ConnectionFactory connectionFactory) {
+
+ Assert.notNull(connectionFactory, "ConnectionFactor must not be null!");
+
+ ConnectionFactoryMetadata metadata = connectionFactory.getMetadata();
+
+ return Arrays.stream(values()).filter(it -> matches(metadata, it.driverName())).findFirst();
+ }
+
+ private static boolean matches(ConnectionFactoryMetadata metadata, String databaseType) {
+ return metadata.getName().toLowerCase(Locale.ENGLISH).contains(databaseType.toLowerCase(Locale.ENGLISH));
+ }
+
+ /**
+ * Returns the driver name.
+ *
+ * @return the driver name.
+ * @see ConnectionFactoryMetadata#getName()
+ */
+ public abstract String driverName();
+
+ /**
+ * Returns the latest {@link Dialect} for the underlying database.
+ *
+ * @return the latest {@link Dialect} for the underlying database.
+ */
+ public abstract Dialect latestDialect();
+
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java
new file mode 100644
index 0000000..f73a3ff
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java
@@ -0,0 +1,31 @@
+package org.springframework.data.r2dbc.dialect;
+
+/**
+ * Represents a dialect that is implemented by a particular database.
+ *
+ * @author Mark Paluch
+ */
+public interface Dialect {
+
+ /**
+ * Returns the {@link BindMarkersFactory} used by this dialect.
+ *
+ * @return the {@link BindMarkersFactory} used by this dialect.
+ */
+ BindMarkersFactory getBindMarkersFactory();
+
+ /**
+ * Returns the statement to include for returning generated keys. The returned query is directly appended to
+ * {@code INSERT} statements.
+ *
+ * @return the statement to include for returning generated keys.
+ */
+ String returnGeneratedKeys();
+
+ /**
+ * Return the {@link LimitClause} used by this dialect.
+ *
+ * @return the {@link LimitClause} used by this dialect.
+ */
+ LimitClause limit();
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
new file mode 100644
index 0000000..18e1acb
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
@@ -0,0 +1,23 @@
+package org.springframework.data.r2dbc.dialect;
+
+/**
+ * An SQL dialect for H2 in Postgres Compatibility mode.
+ *
+ * @author Mark Paluch
+ */
+public class H2Dialect extends PostgresDialect {
+
+ /**
+ * Singleton instance.
+ */
+ public static final H2Dialect INSTANCE = new H2Dialect();
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
+ */
+ @Override
+ public String returnGeneratedKeys() {
+ return "";
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
index d77c467..fa59c11 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
@@ -18,6 +18,7 @@ class IndexedBindMarkers implements BindMarkers {
// access via COUNTER_INCREMENTER
@SuppressWarnings("unused") private volatile int counter;
+ private final int offset;
private final String prefix;
/**
@@ -29,9 +30,10 @@ class IndexedBindMarkers implements BindMarkers {
IndexedBindMarkers(String prefix, int beginWith) {
this.counter = beginWith;
this.prefix = prefix;
+ this.offset = 0 - beginWith;
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarkers#next()
*/
@@ -40,7 +42,7 @@ class IndexedBindMarkers implements BindMarkers {
int index = COUNTER_INCREMENTER.getAndIncrement(this);
- return new IndexedBindMarker(prefix + "" + index, index);
+ return new IndexedBindMarker(prefix + "" + index, index + offset);
}
/**
@@ -57,7 +59,7 @@ class IndexedBindMarkers implements BindMarkers {
this.index = index;
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#getPlaceholder()
*/
@@ -66,16 +68,16 @@ class IndexedBindMarkers implements BindMarkers {
return placeholder;
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
*/
@Override
- public void bindValue(Statement> statement, Object value) {
+ public void bind(Statement> statement, Object value) {
statement.bind(this.index, value);
}
- /*
+ /*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
*/
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/LimitClause.java b/src/main/java/org/springframework/data/r2dbc/dialect/LimitClause.java
new file mode 100644
index 0000000..7d1eb48
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/LimitClause.java
@@ -0,0 +1,42 @@
+package org.springframework.data.r2dbc.dialect;
+
+/**
+ * A clause representing Dialect-specific {@code LIMIT}.
+ *
+ * @author Mark Paluch
+ */
+public interface LimitClause {
+
+ /**
+ * Returns the {@code LIMIT} clause
+ *
+ * @param limit the actual limit to use.
+ * @return rendered limit clause.
+ */
+ String getClause(long limit);
+
+ /**
+ * Returns the {@code LIMIT} clause
+ *
+ * @param limit the actual limit to use.
+ * @param offset the offset to start from.
+ * @return rendered limit clause.
+ */
+ String getClause(long limit, long offset);
+
+ /**
+ * Returns the {@link Position} where to apply the {@link #getClause(long) clause}.
+ */
+ Position getClausePosition();
+
+ /**
+ * Enumeration of where to render the clause within the SQL statement.
+ */
+ enum Position {
+
+ /**
+ * Append the clause at the end of the statement.
+ */
+ END
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
index ff7e572..7b94767 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
@@ -101,7 +101,7 @@ class NamedBindMarkers implements BindMarkers {
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
*/
@Override
- public void bindValue(Statement> statement, Object value) {
+ public void bind(Statement> statement, Object value) {
statement.bind(this.identifier, value);
}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java
new file mode 100644
index 0000000..271f272
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java
@@ -0,0 +1,73 @@
+package org.springframework.data.r2dbc.dialect;
+
+/**
+ * An SQL dialect for Postgres.
+ *
+ * @author Mark Paluch
+ */
+public class PostgresDialect implements Dialect {
+
+ /**
+ * Singleton instance.
+ */
+ public static final PostgresDialect INSTANCE = new PostgresDialect();
+
+ private static final BindMarkersFactory INDEXED = BindMarkersFactory.indexed("$", 1);
+
+ private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long, long)
+ */
+ @Override
+ public String getClause(long limit, long offset) {
+ return String.format("LIMIT %d OFFSET %d", limit, offset);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long)
+ */
+ @Override
+ public String getClause(long limit) {
+ return "LIMIT " + limit;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClausePosition()
+ */
+ @Override
+ public Position getClausePosition() {
+ return Position.END;
+ }
+ };
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory()
+ */
+ @Override
+ public BindMarkersFactory getBindMarkersFactory() {
+ return INDEXED;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
+ */
+ @Override
+ public String returnGeneratedKeys() {
+ return "RETURNING *";
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#limit()
+ */
+ @Override
+ public LimitClause limit() {
+ return LIMIT_CLAUSE;
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java
new file mode 100644
index 0000000..3047d77
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java
@@ -0,0 +1,95 @@
+package org.springframework.data.r2dbc.dialect;
+
+/**
+ * An SQL dialect for Microsoft SQL Server.
+ *
+ * @author Mark Paluch
+ */
+public class SqlServerDialect implements Dialect {
+
+ /**
+ * Singleton instance.
+ */
+ public static final SqlServerDialect INSTANCE = new SqlServerDialect();
+
+ private static final BindMarkersFactory NAMED = BindMarkersFactory.named("@", "P", 32,
+ SqlServerDialect::filterBindMarker);
+
+ private static final LimitClause LIMIT_CLAUSE = new LimitClause() {
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long)
+ */
+ @Override
+ public String getClause(long limit) {
+ return "OFFSET 0 ROWS FETCH NEXT " + limit + " ROWS ONLY";
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long, long)
+ */
+ @Override
+ public String getClause(long limit, long offset) {
+ return String.format("OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", offset, limit);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.LimitClause#getClausePosition()
+ */
+ @Override
+ public Position getClausePosition() {
+ return Position.END;
+ }
+ };
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory()
+ */
+ @Override
+ public BindMarkersFactory getBindMarkersFactory() {
+ return NAMED;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
+ */
+ @Override
+ public String returnGeneratedKeys() {
+ return "select SCOPE_IDENTITY() AS GENERATED_KEYS";
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.dialect.Dialect#limit()
+ */
+ @Override
+ public LimitClause limit() {
+ return LIMIT_CLAUSE;
+ }
+
+ private static String filterBindMarker(CharSequence input) {
+
+ StringBuilder builder = new StringBuilder();
+
+ for (int i = 0; i < input.length(); i++) {
+
+ char ch = input.charAt(i);
+
+ // ascii letter or digit
+ if (Character.isLetterOrDigit(ch) && ch < 127) {
+ builder.append(ch);
+ }
+ }
+
+ if (builder.length() == 0) {
+ return "";
+ }
+
+ return "_" + builder.toString();
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java
new file mode 100644
index 0000000..08c9361
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java
@@ -0,0 +1,32 @@
+package org.springframework.data.r2dbc.function;
+
+import io.r2dbc.spi.Statement;
+
+/**
+ * Extension to {@link BindableOperation} for operations that allow parameter substitution for a single {@code id}
+ * column that accepts either a single value or multiple values, depending on the underlying operation.
+ *
+ * @author Mark Paluch
+ * @see Statement#bind
+ * @see Statement#bindNull
+ */
+public interface BindIdOperation extends BindableOperation {
+
+ /**
+ * Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
+ *
+ * @param statement the statement to bind the value to.
+ * @param value the actual value. Must not be {@literal null}.
+ * @see Statement#bind
+ */
+ void bindId(Statement> statement, Object value);
+
+ /**
+ * Bind the given {@code values} to the {@link Statement} using the underlying binding strategy.
+ *
+ * @param statement the statement to bind the value to.
+ * @param values the actual values.
+ * @see Statement#bind
+ */
+ void bindIds(Statement> statement, Iterable extends Object> values);
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
new file mode 100644
index 0000000..ebe26d2
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
@@ -0,0 +1,36 @@
+package org.springframework.data.r2dbc.function;
+
+import io.r2dbc.spi.Statement;
+
+/**
+ * Extension to {@link QueryOperation} for operations that allow parameter substitution by binding parameter values.
+ * {@link BindableOperation} is typically created with a {@link Set} of column names or parameter names that accept bind
+ * parameters by calling {@link #bind(Statement, String, Object)}.
+ *
+ * @author Mark Paluch
+ * @see Statement#bind
+ * @see Statement#bindNull
+ */
+public interface BindableOperation extends QueryOperation {
+
+ /**
+ * Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
+ *
+ * @param statement the statement to bind the value to.
+ * @param identifier named identifier that is considered by the underlying binding strategy.
+ * @param value the actual value. Must not be {@literal null}. Use {@link #bindNull(Statement, Class)} for
+ * {@literal null} values.
+ * @see Statement#bind
+ */
+ void bind(Statement> statement, String identifier, Object value);
+
+ /**
+ * Bind a {@literal null} value to the {@link Statement} using the underlying binding strategy.
+ *
+ * @param statement the statement to bind the value to.
+ * @param identifier named identifier that is considered by the underlying binding strategy.
+ * @param valueType value type, must not be {@literal null}.
+ * @see Statement#bindNull
+ */
+ void bindNull(Statement> statement, String identifier, Class> valueType);
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
index b363f7b..badd872 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -34,14 +34,13 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
-import java.util.concurrent.atomic.AtomicInteger;
+import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -49,8 +48,6 @@ import org.reactivestreams.Publisher;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
-import org.springframework.data.domain.Sort.NullHandling;
-import org.springframework.data.domain.Sort.Order;
import org.springframework.data.r2dbc.UncategorizedR2dbcException;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
@@ -59,7 +56,6 @@ import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
/**
* Default implementation of {@link DatabaseClient}.
@@ -239,7 +235,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return new DefaultGenericExecuteSpec(sqlSupplier);
}
- private static void doBind(Statement statement, Map byName,
+ private static void doBind(Statement> statement, Map byName,
Map byIndex) {
byIndex.forEach((i, o) -> {
@@ -308,13 +304,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
SqlResult exchange(String sql, BiFunction mappingFunction) {
- Function executeFunction = it -> {
+ Function> executeFunction = it -> {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
}
- Statement statement = it.createStatement(sql);
+ Statement> statement = it.createStatement(sql);
doBind(statement, byName, byIndex);
return statement;
@@ -571,37 +567,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return createInstance(table, projectedFields, sort, page);
}
- StringBuilder getLimitOffset(Pageable pageable) {
- return new StringBuilder().append("LIMIT").append(' ').append(pageable.getPageSize()) //
- .append(' ').append("OFFSET").append(' ').append(pageable.getOffset());
- }
-
- StringBuilder getSortClause(Sort sort) {
-
- StringBuilder sortClause = new StringBuilder();
-
- for (Order order : sort) {
-
- if (sortClause.length() != 0) {
- sortClause.append(',').append(' ');
- }
-
- sortClause.append(order.getProperty()).append(' ').append(order.getDirection().isAscending() ? "ASC" : "DESC");
-
- if (order.getNullHandling() == NullHandling.NULLS_FIRST) {
- sortClause.append(' ').append("NULLS FIRST");
- }
-
- if (order.getNullHandling() == NullHandling.NULLS_LAST) {
- sortClause.append(' ').append("NULLS LAST");
- }
- }
- return sortClause;
- }
-
SqlResult execute(String sql, BiFunction mappingFunction) {
- Function selectFunction = it -> {
+ Function> selectFunction = it -> {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
@@ -666,28 +634,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private SqlResult exchange(BiFunction mappingFunction) {
- List projectedFields;
+ Set columns;
if (this.projectedFields.isEmpty()) {
- projectedFields = Collections.singletonList("*");
+ columns = Collections.singleton("*");
} else {
- projectedFields = this.projectedFields;
+ columns = new LinkedHashSet<>(this.projectedFields);
}
- StringBuilder selectBuilder = new StringBuilder();
- selectBuilder.append("SELECT").append(' ') //
- .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') //
- .append("FROM").append(' ').append(table);
+ QueryOperation select = dataAccessStrategy.select(table, columns, sort, page);
- if (sort.isSorted()) {
- selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(sort));
- }
-
- if (page.isPaged()) {
- selectBuilder.append(' ').append(getLimitOffset(page));
- }
-
- return execute(selectBuilder.toString(), mappingFunction);
+ return execute(select.toQuery(), mappingFunction);
}
@Override
@@ -765,30 +722,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private SqlResult exchange(BiFunction mappingFunction) {
- List projectedFields;
+ List columns;
if (this.projectedFields.isEmpty()) {
- projectedFields = dataAccessStrategy.getAllFields(typeToRead);
+ columns = dataAccessStrategy.getAllColumns(typeToRead);
} else {
- projectedFields = this.projectedFields;
+ columns = this.projectedFields;
}
+ Sort sortToUse = sort.isSorted() ? dataAccessStrategy.getMappedSort(typeToRead, sort) : Sort.unsorted();
- StringBuilder selectBuilder = new StringBuilder();
- selectBuilder.append("SELECT").append(' ') //
- .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') //
- .append("FROM").append(' ').append(table);
+ QueryOperation select = dataAccessStrategy.select(table, new LinkedHashSet<>(columns), sortToUse, page);
- if (sort.isSorted()) {
-
- Sort mappedSort = dataAccessStrategy.getMappedSort(typeToRead, sort);
- selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(mappedSort));
- }
-
- if (page.isPaged()) {
- selectBuilder.append(' ').append(getLimitOffset(page));
- }
-
- return execute(selectBuilder.toString(), mappingFunction);
+ return execute(select.get(), mappingFunction);
}
@Override
@@ -861,27 +806,29 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
throw new IllegalStateException("Insert fields is empty!");
}
- StringBuilder builder = new StringBuilder();
- String fieldNames = byName.keySet().stream().collect(Collectors.joining(","));
- String placeholders = IntStream.range(0, byName.size()).mapToObj(i -> "$" + (i + 1))
- .collect(Collectors.joining(","));
+ BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, byName.keySet());
- builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
- .append(placeholders).append(") RETURNING *");
-
- String sql = builder.toString();
+ String sql = bindableInsert.toQuery();
Function insertFunction = it -> {
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
}
- Statement statement = it.createStatement(sql);
- doBind(statement);
+
+ Statement> statement = it.createStatement(sql);
+
+ byName.forEach((k, v) -> {
+
+ if (v.getValue() == null) {
+ bindableInsert.bindNull(statement, k, v.getType());
+ } else {
+ bindableInsert.bind(statement, k, v.getValue());
+ }
+ });
return statement;
};
- Function> resultFunction = it -> Flux
- .from(insertFunction.apply(it).execute());
+ Function> resultFunction = it -> Flux.from(insertFunction.apply(it).execute());
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
sql, //
@@ -889,25 +836,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), //
mappingFunction);
}
-
- /**
- * PostgreSQL-specific bind.
- *
- * @param statement
- */
- private void doBind(Statement statement) {
-
- AtomicInteger index = new AtomicInteger();
-
- for (SettableValue value : byName.values()) {
-
- if (value.getValue() != null) {
- statement.bind(index.getAndIncrement(), value.getValue());
- } else {
- statement.bindNull("$" + (index.getAndIncrement() + 1), value.getType());
- }
- }
- }
}
/**
@@ -963,18 +891,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private SqlResult exchange(Object toInsert, BiFunction mappingFunction) {
- StringBuilder builder = new StringBuilder();
+ List insertValues = dataAccessStrategy.getValuesToInsert(toInsert);
+ Set columns = new LinkedHashSet<>();
- List insertValues = dataAccessStrategy.getInsert(toInsert);
- String fieldNames = insertValues.stream().map(SettableValue::getIdentifier).map(Object::toString)
- .collect(Collectors.joining(","));
- String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1))
- .collect(Collectors.joining(","));
+ for (SettableValue insertValue : insertValues) {
+ columns.add(insertValue.getIdentifier().toString());
+ }
- builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
- .append(placeholders).append(") RETURNING *");
+ BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, columns);
- String sql = builder.toString();
+ String sql = bindableInsert.toQuery();
Function insertFunction = it -> {
@@ -982,16 +908,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
logger.debug("Executing SQL statement [" + sql + "]");
}
- Statement statement = it.createStatement(sql);
-
- AtomicInteger index = new AtomicInteger();
+ Statement> statement = it.createStatement(sql);
for (SettableValue settable : insertValues) {
- if (settable.getValue() != null) {
- statement.bind(index.getAndIncrement(), settable.getValue());
+ if (settable.getValue() == null) {
+ bindableInsert.bindNull(statement, settable.getIdentifier().toString(), settable.getType());
} else {
- statement.bindNull("$" + (index.getAndIncrement() + 1), settable.getType());
+ bindableInsert.bind(statement, settable.getIdentifier().toString(), settable.getValue());
}
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
index a4e4e4e..d21e91d 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
@@ -20,6 +20,8 @@ import io.r2dbc.spi.ConnectionFactory;
import java.util.function.Consumer;
+import org.springframework.data.r2dbc.dialect.Database;
+import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.function.DatabaseClient.Builder;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator;
@@ -33,9 +35,9 @@ import org.springframework.util.Assert;
*/
class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
- private @Nullable ConnectionFactory connector;
+ private @Nullable ConnectionFactory connectionFactory;
private @Nullable R2dbcExceptionTranslator exceptionTranslator;
- private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy();
+ private ReactiveDataAccessStrategy accessStrategy;
DefaultDatabaseClientBuilder() {}
@@ -43,7 +45,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!");
- this.connector = other.connector;
+ this.connectionFactory = other.connectionFactory;
this.exceptionTranslator = other.exceptionTranslator;
this.accessStrategy = other.accessStrategy;
}
@@ -53,7 +55,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
Assert.notNull(factory, "ConnectionFactory must not be null!");
- this.connector = factory;
+ this.connectionFactory = factory;
return this;
}
@@ -81,10 +83,21 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
R2dbcExceptionTranslator exceptionTranslator = this.exceptionTranslator;
if (exceptionTranslator == null) {
- exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connector);
+ exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connectionFactory);
}
- return doBuild(this.connector, exceptionTranslator, this.accessStrategy, new DefaultDatabaseClientBuilder(this));
+ ReactiveDataAccessStrategy accessStrategy = this.accessStrategy;
+
+ if (accessStrategy == null) {
+
+ Dialect dialect = Database.findDatabase(this.connectionFactory)
+ .orElseThrow(() -> new UnsupportedOperationException(
+ "Cannot determine a Dialect. Configure the dialect by providing DefaultReactiveDataAccessStrategy(Dialect)"))
+ .latestDialect();
+ accessStrategy = new DefaultReactiveDataAccessStrategy(dialect);
+ }
+
+ return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, new DefaultDatabaseClientBuilder(this));
}
protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator,
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
index 7a4a282..e6666a4 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -17,16 +17,27 @@ package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
+import io.r2dbc.spi.Statement;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.function.BiFunction;
-import java.util.stream.Collectors;
+import java.util.function.Function;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.r2dbc.dialect.BindMarker;
+import org.springframework.data.r2dbc.dialect.BindMarkers;
+import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.LimitClause;
+import org.springframework.data.r2dbc.dialect.LimitClause.Position;
import org.springframework.data.r2dbc.function.convert.EntityRowMapper;
import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
@@ -34,27 +45,51 @@ import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
-import org.springframework.data.util.StreamUtils;
import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
/**
+ * Default {@link ReactiveDataAccessStrategy} implementation.
+ *
* @author Mark Paluch
*/
public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
private final RelationalConverter relationalConverter;
+ private final Dialect dialect;
- public DefaultReactiveDataAccessStrategy() {
- this(new BasicRelationalConverter(new RelationalMappingContext()));
+ /**
+ * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}.
+ *
+ * @param dialect the {@link Dialect} to use.
+ */
+ public DefaultReactiveDataAccessStrategy(Dialect dialect) {
+ this(dialect, new BasicRelationalConverter(new RelationalMappingContext()));
}
- public DefaultReactiveDataAccessStrategy(RelationalConverter converter) {
+ /**
+ * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and {@link RelationalConverter}.
+ *
+ * @param dialect the {@link Dialect} to use.
+ * @param converter must not be {@literal null}.
+ */
+ public DefaultReactiveDataAccessStrategy(Dialect dialect, RelationalConverter converter) {
+
+ Assert.notNull(dialect, "Dialect must not be null");
+ Assert.notNull(converter, "RelationalConverter must not be null");
+
this.relationalConverter = converter;
+ this.dialect = dialect;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getAllFields(java.lang.Class)
+ */
@Override
- public List getAllFields(Class> typeToRead) {
+ public List getAllColumns(Class> typeToRead) {
RelationalPersistentEntity> persistentEntity = getPersistentEntity(typeToRead);
@@ -62,13 +97,20 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
return Collections.singletonList("*");
}
- return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) //
- .map(RelationalPersistentProperty::getColumnName) //
- .collect(Collectors.toList());
+ List columnNames = new ArrayList<>();
+ for (RelationalPersistentProperty property : persistentEntity) {
+ columnNames.add(property.getColumnName());
+ }
+
+ return columnNames;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getValuesToInsert(java.lang.Object)
+ */
@Override
- public List getInsert(Object object) {
+ public List getValuesToInsert(Object object) {
Class> userClass = ClassUtils.getUserClass(object);
@@ -91,6 +133,10 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
return values;
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedSort(java.lang.Class, org.springframework.data.domain.Sort)
+ */
@Override
public Sort getMappedSort(Class> typeToRead, Sort sort) {
@@ -115,12 +161,20 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
return Sort.by(mappedOrder);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getRowMapper(java.lang.Class)
+ */
@Override
public BiFunction getRowMapper(Class typeToRead) {
return new EntityRowMapper((RelationalPersistentEntity) getRequiredPersistentEntity(typeToRead),
relationalConverter);
}
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getTableName(java.lang.Class)
+ */
@Override
public String getTableName(Class> type) {
return getRequiredPersistentEntity(type).getTableName();
@@ -134,4 +188,414 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
private RelationalPersistentEntity> getPersistentEntity(Class> typeToRead) {
return relationalConverter.getMappingContext().getPersistentEntity(typeToRead);
}
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#insertAndReturnGeneratedKeys(java.lang.String, java.util.Set)
+ */
+ @Override
+ public BindableOperation insertAndReturnGeneratedKeys(String table, Set columns) {
+ return new DefaultBindableInsert(dialect.getBindMarkersFactory().create(), table, columns,
+ dialect.returnGeneratedKeys());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#select(java.lang.String, java.util.Set, org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable)
+ */
+ @Override
+ public QueryOperation select(String table, Set columns, Sort sort, Pageable page) {
+
+ StringBuilder selectBuilder = new StringBuilder();
+
+ selectBuilder.append("SELECT").append(' ') //
+ .append(StringUtils.collectionToDelimitedString(columns, ", ")).append(' ') //
+ .append("FROM").append(' ').append(table);
+
+ if (sort.isSorted()) {
+ selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(sort));
+ }
+
+ if (page.isPaged()) {
+
+ LimitClause limitClause = dialect.limit();
+
+ if (limitClause.getClausePosition() == Position.END) {
+
+ selectBuilder.append(' ').append(limitClause.getClause(page.getPageSize(), page.getOffset()));
+ }
+ }
+
+ return selectBuilder::toString;
+ }
+
+ private StringBuilder getSortClause(Sort sort) {
+
+ StringBuilder sortClause = new StringBuilder();
+
+ for (Order order : sort) {
+
+ if (sortClause.length() != 0) {
+ sortClause.append(',').append(' ');
+ }
+
+ sortClause.append(order.getProperty()).append(' ').append(order.getDirection().isAscending() ? "ASC" : "DESC");
+ }
+ return sortClause;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectById(java.lang.String, java.util.Set, java.lang.String)
+ */
+ @Override
+ public BindIdOperation selectById(String table, Set columns, String idColumn) {
+
+ return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(), marker -> {
+
+ String columnClause = StringUtils.collectionToDelimitedString(columns, ", ");
+
+ return String.format("SELECT %s FROM %s WHERE %s = %s", columnClause, table, idColumn, marker.getPlaceholder());
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectById(java.lang.String, java.util.Set, java.lang.String, int)
+ */
+ @Override
+ public BindIdOperation selectById(String table, Set columns, String idColumn, int limit) {
+
+ LimitClause limitClause = dialect.limit();
+
+ return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(), marker -> {
+
+ String columnClause = StringUtils.collectionToDelimitedString(columns, ", ");
+
+ if (limitClause.getClausePosition() == Position.END) {
+
+ return String.format("SELECT %s FROM %s WHERE %s = %s ORDER BY %s %s", columnClause, table, idColumn,
+ marker.getPlaceholder(), idColumn, limitClause.getClause(limit));
+ }
+
+ throw new UnsupportedOperationException(
+ String.format("Limit clause position %s not supported!", limitClause.getClausePosition()));
+ });
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectByIdIn(java.lang.String, java.util.Set, java.lang.String)
+ */
+ @Override
+ public BindIdOperation selectByIdIn(String table, Set columns, String idColumn) {
+
+ String query = String.format("SELECT %s FROM %s", StringUtils.collectionToDelimitedString(columns, ", "), table);
+ return new DefaultBindIdIn(dialect.getBindMarkersFactory().create(), query, idColumn);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#updateById(java.lang.String, java.util.Set, java.lang.String)
+ */
+ @Override
+ public BindIdOperation updateById(String table, Set columns, String idColumn) {
+ return new DefaultBindableUpdate(dialect.getBindMarkersFactory().create(), table, columns, idColumn);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#deleteById(java.lang.String, java.lang.String)
+ */
+ @Override
+ public BindIdOperation deleteById(String table, String idColumn) {
+
+ return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(),
+ marker -> String.format("DELETE FROM %s WHERE %s = %s", table, idColumn, marker.getPlaceholder()));
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#deleteByIdIn(java.lang.String, java.lang.String)
+ */
+ @Override
+ public BindIdOperation deleteByIdIn(String table, String idColumn) {
+
+ String query = String.format("DELETE FROM %s", table);
+ return new DefaultBindIdIn(dialect.getBindMarkersFactory().create(), query, idColumn);
+ }
+
+ /**
+ * Default {@link BindableOperation} implementation for a {@code INSERT} operation.
+ */
+ static class DefaultBindableInsert implements BindableOperation {
+
+ private final Map markers = new LinkedHashMap<>();
+ private final String query;
+
+ DefaultBindableInsert(BindMarkers bindMarkers, String table, Collection columns,
+ String returningStatement) {
+
+ StringBuilder builder = new StringBuilder();
+ List placeholders = new ArrayList<>(columns.size());
+
+ for (String column : columns) {
+ BindMarker marker = markers.computeIfAbsent(column, bindMarkers::next);
+ placeholders.add(marker.getPlaceholder());
+ }
+
+ String columnsString = StringUtils.collectionToDelimitedString(columns, ", ");
+ String placeholdersString = StringUtils.collectionToDelimitedString(placeholders, ", ");
+
+ builder.append("INSERT INTO ").append(table).append(" (").append(columnsString).append(")").append(" VALUES(")
+ .append(placeholdersString).append(")");
+
+ if (StringUtils.hasText(returningStatement)) {
+ builder.append(' ').append(returningStatement);
+ }
+
+ this.query = builder.toString();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void bind(Statement> statement, String identifier, Object value) {
+ markers.get(identifier).bind(statement, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
+ */
+ @Override
+ public void bindNull(Statement> statement, String identifier, Class> valueType) {
+ markers.get(identifier).bindNull(statement, valueType);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
+ */
+ @Override
+ public String toQuery() {
+ return this.query;
+ }
+ }
+
+ /**
+ * Default {@link BindIdOperation} implementation for a {@code UPDATE} operation using a single key.
+ */
+ static class DefaultBindableUpdate implements BindIdOperation {
+
+ private final Map markers = new LinkedHashMap<>();
+ private final BindMarker idMarker;
+ private final String query;
+
+ DefaultBindableUpdate(BindMarkers bindMarkers, String tableName, Set columns, String idColumnName) {
+
+ this.idMarker = bindMarkers.next();
+
+ StringBuilder setClause = new StringBuilder();
+
+ for (String column : columns) {
+
+ BindMarker marker = markers.computeIfAbsent(column, bindMarkers::next);
+
+ if (setClause.length() != 0) {
+ setClause.append(", ");
+ }
+
+ setClause.append(column).append(" = ").append(marker.getPlaceholder());
+ }
+
+ this.query = String.format("UPDATE %s SET %s WHERE %s = %s", tableName, setClause, idColumnName,
+ idMarker.getPlaceholder());
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void bind(Statement> statement, String identifier, Object value) {
+ markers.get(identifier).bind(statement, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
+ */
+ @Override
+ public void bindNull(Statement> statement, String identifier, Class> valueType) {
+ markers.get(identifier).bindNull(statement, valueType);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
+ */
+ @Override
+ public void bindId(Statement> statement, Object value) {
+ idMarker.bind(statement, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
+ */
+ @Override
+ public void bindIds(Statement> statement, Iterable extends Object> values) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
+ */
+ @Override
+ public String toQuery() {
+ return this.query;
+ }
+ }
+
+ /**
+ * Default {@link BindIdOperation} implementation for a {@code SELECT} or {@code DELETE} operation using a single key
+ * in the {@code WHERE} predicate.
+ */
+ static class DefaultBindIdOperation implements BindIdOperation {
+
+ private final BindMarker idMarker;
+ private final String query;
+
+ DefaultBindIdOperation(BindMarkers bindMarkers, Function queryFunction) {
+
+ this.idMarker = bindMarkers.next();
+ this.query = queryFunction.apply(this.idMarker);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void bind(Statement> statement, String identifier, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
+ */
+ @Override
+ public void bindNull(Statement> statement, String identifier, Class> valueType) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
+ */
+ @Override
+ public void bindId(Statement> statement, Object value) {
+ idMarker.bind(statement, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
+ */
+ @Override
+ public void bindIds(Statement> statement, Iterable extends Object> values) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
+ */
+ @Override
+ public String toQuery() {
+ return this.query;
+ }
+ }
+
+ /**
+ * Default {@link BindIdOperation} implementation for a {@code SELECT … WHERE id IN (…)} or
+ * {@code DELETE … WHERE id IN (…)}.
+ */
+ static class DefaultBindIdIn implements BindIdOperation {
+
+ private final List markers = new ArrayList<>();
+ private final BindMarkers bindMarkers;
+ private final String baseQuery;
+ private final String idColumnName;
+
+ DefaultBindIdIn(BindMarkers bindMarkers, String baseQuery, String idColumnName) {
+
+ this.bindMarkers = bindMarkers;
+ this.baseQuery = baseQuery;
+ this.idColumnName = idColumnName;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
+ */
+ @Override
+ public void bind(Statement> statement, String identifier, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
+ */
+ @Override
+ public void bindNull(Statement> statement, String identifier, Class> valueType) {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
+ */
+ @Override
+ public void bindId(Statement> statement, Object value) {
+
+ BindMarker bindMarker = bindMarkers.next();
+ markers.add(bindMarker.getPlaceholder());
+ bindMarker.bind(statement, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
+ */
+ @Override
+ public void bindIds(Statement> statement, Iterable extends Object> values) {
+
+ for (Object value : values) {
+ bindId(statement, value);
+ }
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
+ */
+ @Override
+ public String toQuery() {
+
+ if (this.markers.isEmpty()) {
+ throw new UnsupportedOperationException();
+ }
+
+ String in = StringUtils.collectionToDelimitedString(this.markers, ", ");
+
+ return String.format("%s WHERE %s IN (%s)", this.baseQuery, this.idColumnName, in);
+ }
+ }
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/QueryOperation.java b/src/main/java/org/springframework/data/r2dbc/function/QueryOperation.java
new file mode 100644
index 0000000..b26923e
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/function/QueryOperation.java
@@ -0,0 +1,26 @@
+package org.springframework.data.r2dbc.function;
+
+import java.util.function.Supplier;
+
+/**
+ * Interface declaring a query operation that can be represented with a query string. This interface is typically
+ * implemented by classes representing a SQL operation such as {@code SELECT}, {@code INSERT}, and such.
+ *
+ * @author Mark Paluch
+ */
+@FunctionalInterface
+public interface QueryOperation extends Supplier {
+
+ /**
+ * Returns the string-representation of this operation to be used with {@link io.r2dbc.spi.Statement} creation.
+ *
+ * @return the operation as SQL string.
+ * @see io.r2dbc.spi.Connection#createStatement(String)
+ */
+ String toQuery();
+
+ @Override
+ default String get() {
+ return toQuery();
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
index 20024a2..f3c8a9f 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
@@ -17,15 +17,23 @@ package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
+import io.r2dbc.spi.Statement;
import java.util.List;
+import java.util.Set;
import java.util.function.BiFunction;
+import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.function.convert.SettableValue;
/**
+ * Draft of a data access strategy that generalizes convenience operations using mapped entities. Typically used
+ * internally by {@link DatabaseClient} and repository support. SQL creation is limited to single-table operations and
+ * single-column primary keys.
+ *
* @author Mark Paluch
+ * @see BindableOperation
*/
public interface ReactiveDataAccessStrategy {
@@ -33,13 +41,13 @@ public interface ReactiveDataAccessStrategy {
* @param typeToRead
* @return all field names for a specific type.
*/
- List getAllFields(Class> typeToRead);
+ List getAllColumns(Class> typeToRead);
/**
* @param object
* @return {@link SettableValue} that represent an {@code INSERT} of {@code object}.
*/
- List getInsert(Object object);
+ List getValuesToInsert(Object object);
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
@@ -58,4 +66,96 @@ public interface ReactiveDataAccessStrategy {
* @return the table name for the {@link Class entity type}.
*/
String getTableName(Class> type);
+
+ // -------------------------------------------------------------------------
+ // Methods creating SQL operations.
+ // Subject to be moved into a SQL creation DSL.
+ // -------------------------------------------------------------------------
+
+ /**
+ * Create an {@code INSERT} operation for the given {@code table} to insert {@code columns}.
+ *
+ * @param table the table to insert data to.
+ * @param columns column names that will be bound.
+ * @return the {@link BindableOperation} representing the {@code INSERT} statement.
+ */
+ BindableOperation insertAndReturnGeneratedKeys(String table, Set columns);
+
+ /**
+ * Create a {@code SELECT … ORDER BY … LIMIT …} operation for the given {@code table} using {@code columns} to
+ * project.
+ *
+ * @param table the table to insert data to.
+ * @param columns columns to return.
+ * @param sort
+ * @param page
+ * @return
+ */
+ QueryOperation select(String table, Set columns, Sort sort, Pageable page);
+
+ /**
+ * Create a {@code SELECT … WHERE id = ?} operation for the given {@code table} using {@code columns} to project and
+ * {@code idColumn}.
+ *
+ * @param table the table to insert data to.
+ * @param columns columns to return.
+ * @param idColumn name of the primary key.
+ * @return
+ */
+ BindIdOperation selectById(String table, Set columns, String idColumn);
+
+ /**
+ * Create a {@code SELECT … WHERE id = ?} operation for the given {@code table} using {@code columns} to project and
+ * {@code idColumn} applying a limit (TOP, LIMIT, …).
+ *
+ * @param table the table to insert data to.
+ * @param columns columns to return.
+ * @param idColumn name of the primary key.
+ * @param limit number of rows to return.
+ * @return
+ */
+ BindIdOperation selectById(String table, Set columns, String idColumn, int limit);
+
+ /**
+ * Create a {@code SELECT … WHERE id IN (?)} operation for the given {@code table} using {@code columns} to project
+ * and {@code idColumn}. The actual {@link BindableOperation#toQuery() query} string depends on
+ * {@link BindIdOperation#bindIds(Statement, Iterable) bound parameters}.
+ *
+ * @param table the table to insert data to.
+ * @param columns columns to return.
+ * @param idColumn name of the primary key.
+ * @return
+ */
+ BindIdOperation selectByIdIn(String table, Set columns, String idColumn);
+
+ /**
+ * Create a {@code UPDATE … SET … WHERE id = ?} operation for the given {@code table} updating {@code columns} and
+ * {@code idColumn}.
+ *
+ * @param table the table to insert data to.
+ * @param columns columns to update.
+ * @param idColumn name of the primary key.
+ * @return
+ */
+ BindIdOperation updateById(String table, Set columns, String idColumn);
+
+ /**
+ * Create a {@code DELETE … WHERE id = ?} operation for the given {@code table} and {@code idColumn}.
+ *
+ * @param table the table to insert data to.
+ * @param idColumn name of the primary key.
+ * @return
+ */
+ BindIdOperation deleteById(String table, String idColumn);
+
+ /**
+ * Create a {@code DELETE … WHERE id IN (?)} operation for the given {@code table} and {@code idColumn}. The actual
+ * {@link BindableOperation#toQuery() query} string depends on {@link BindIdOperation#bindIds(Statement, Iterable)
+ * bound parameters}.
+ *
+ * @param table the table to insert data to.
+ * @param idColumn name of the primary key.
+ * @return
+ */
+ BindIdOperation deleteByIdIn(String table, String idColumn);
}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
index 49fb815..6b291e8 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
@@ -15,6 +15,7 @@
*/
package org.springframework.data.r2dbc.function.convert;
+import io.r2dbc.spi.ColumnMetadata;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
@@ -26,6 +27,7 @@ import java.util.function.BiFunction;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
import org.springframework.data.relational.core.conversion.RelationalConverter;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -41,7 +43,25 @@ public class MappingR2dbcConverter {
private final RelationalConverter relationalConverter;
+ /**
+ * Creates a new {@link MappingR2dbcConverter} given {@link MappingContext}.
+ *
+ * @param context must not be {@literal null}.
+ */
+ public MappingR2dbcConverter(
+ MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> context) {
+ this(new BasicRelationalConverter(context));
+ }
+
+ /**
+ * Creates a new {@link MappingR2dbcConverter} given {@link RelationalConverter}.
+ *
+ * @param converter must not be {@literal null}.
+ */
public MappingR2dbcConverter(RelationalConverter converter) {
+
+ Assert.notNull(converter, "RelationalConverter must not be null!");
+
this.relationalConverter = converter;
}
@@ -52,7 +72,7 @@ public class MappingR2dbcConverter {
* @param object must not be {@literal null}.
* @return
*/
- public Map getFieldsToUpdate(Object object) {
+ public Map getColumnsToUpdate(Object object) {
Assert.notNull(object, "Entity object must not be null!");
@@ -93,18 +113,52 @@ public class MappingR2dbcConverter {
if (propertyAccessor.getProperty(idProperty) == null) {
- ConversionService conversionService = relationalConverter.getConversionService();
- Object value = row.get(idProperty.getColumnName());
-
- propertyAccessor.setProperty(idProperty, conversionService.convert(value, idProperty.getType()));
-
- return (T) propertyAccessor.getBean();
+ if (potentiallySetId(row, metadata, propertyAccessor, idProperty)) {
+ return (T) propertyAccessor.getBean();
+ }
}
return object;
};
}
+ private boolean potentiallySetId(Row row, RowMetadata metadata, PersistentPropertyAccessor> propertyAccessor,
+ RelationalPersistentProperty idProperty) {
+
+ Map columns = createMetadataMap(metadata);
+ Object generatedIdValue = null;
+
+ if (columns.containsKey(idProperty.getColumnName())) {
+ generatedIdValue = row.get(idProperty.getColumnName());
+ }
+
+ if (columns.size() == 1) {
+
+ String key = columns.keySet().iterator().next();
+ generatedIdValue = row.get(key);
+ }
+
+ if (generatedIdValue != null) {
+
+ ConversionService conversionService = relationalConverter.getConversionService();
+ propertyAccessor.setProperty(idProperty, conversionService.convert(generatedIdValue, idProperty.getType()));
+ return true;
+ }
+
+ return false;
+ }
+
+ private static Map createMetadataMap(RowMetadata metadata) {
+
+ Map columns = new LinkedHashMap<>();
+
+ for (ColumnMetadata column : metadata.getColumnMetadatas()) {
+ columns.put(column.getName(), column);
+ }
+
+ return columns;
+ }
+
public MappingContext extends RelationalPersistentEntity>, ? extends RelationalPersistentProperty> getMappingContext() {
return relationalConverter.getMappingContext();
}
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java
index c7cc63a..efad3af 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java
@@ -98,6 +98,7 @@ public class R2dbcRepositoryConfigurationExtension extends RepositoryConfigurati
AnnotationAttributes attributes = config.getAttributes();
builder.addPropertyReference("databaseClient", attributes.getString("databaseClientRef"));
+ builder.addPropertyReference("dataAccessStrategy", "reactiveDataAccessStrategy");
}
/*
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
index d3fb559..6bbb32a 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java
@@ -88,21 +88,19 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery {
T bindSpecToUse = bindSpec;
- // TODO: Encapsulate PostgreSQL-specific bindings
-
Parameters, ?> bindableParameters = accessor.getBindableParameters();
- int index = 1;
+ int index = 0;
for (Object value : accessor.getValues()) {
- Parameter bindableParameter = bindableParameters.getBindableParameter(index - 1);
+ Parameter bindableParameter = bindableParameters.getBindableParameter(index);
if (value == null) {
if (accessor.hasBindableNullValue()) {
- bindSpecToUse = bindSpecToUse.bindNull("$" + (index++), bindableParameter.getType());
+ bindSpecToUse = bindSpecToUse.bindNull(index++, bindableParameter.getType());
}
} else {
- bindSpecToUse = bindSpecToUse.bind("$" + (index++), value);
+ bindSpecToUse = bindSpecToUse.bind(index++, value);
}
}
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecWrapper.java b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecWrapper.java
new file mode 100644
index 0000000..bd991e5
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecWrapper.java
@@ -0,0 +1,103 @@
+package org.springframework.data.r2dbc.repository.support;
+
+import io.r2dbc.spi.Result;
+import io.r2dbc.spi.Statement;
+
+import org.reactivestreams.Publisher;
+import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec;
+
+/**
+ * Wrapper for {@link BindSpec} to be used with {@link org.springframework.data.r2dbc.dialect.BindMarker} binding.
+ * Binding parameters updates the {@link BindSpec}
+ *
+ * @param type of the bind specification.
+ * @author Mark Paluch
+ */
+class BindSpecWrapper> implements Statement> {
+
+ private S bindSpec;
+
+ private BindSpecWrapper(S bindSpec) {
+ this.bindSpec = bindSpec;
+ }
+
+ /**
+ * Create a new {@link BindSpecWrapper} for the given {@link BindSpec}.
+ *
+ * @param bindSpec the bind specification.
+ * @param type of the bind spec to retain the type through {@link #getBoundOperation()}.
+ * @return {@link BindSpecWrapper} for the {@link BindSpec}.
+ */
+ public static > BindSpecWrapper create(S bindSpec) {
+ return new BindSpecWrapper<>(bindSpec);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#add()
+ */
+ @Override
+ public BindSpecWrapper add() {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#execute()
+ */
+ @Override
+ public Publisher extends Result> execute() {
+ throw new UnsupportedOperationException();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#bind(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public BindSpecWrapper bind(Object identifier, Object value) {
+
+ this.bindSpec = bindSpec.bind((String) identifier, value);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#bind(int, java.lang.Object)
+ */
+ @Override
+ public BindSpecWrapper bind(int index, Object value) {
+
+ this.bindSpec = bindSpec.bind(index, value);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#bindNull(java.lang.Object, java.lang.Class)
+ */
+ @Override
+ public BindSpecWrapper bindNull(Object identifier, Class> type) {
+
+ this.bindSpec = bindSpec.bindNull((String) identifier, type);
+ return this;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see io.r2dbc.spi.Statement#bindNull(int, java.lang.Class)
+ */
+ @Override
+ public BindSpecWrapper bindNull(int index, Class> type) {
+
+ this.bindSpec = bindSpec.bindNull(index, type);
+ return this;
+ }
+
+ /**
+ * @return the bound (final) bind specification.
+ */
+ public S getBoundOperation() {
+ return bindSpec;
+ }
+}
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java
index 4d60545..b5e749a 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java
@@ -24,6 +24,7 @@ import java.util.Optional;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.r2dbc.function.DatabaseClient;
+import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
@@ -57,6 +58,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
private final DatabaseClient databaseClient;
private final MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext;
private final MappingR2dbcConverter converter;
+ private final ReactiveDataAccessStrategy dataAccessStrategy;
/**
* Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}.
@@ -65,13 +67,16 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
* @param mappingContext must not be {@literal null}.
*/
public R2dbcRepositoryFactory(DatabaseClient databaseClient,
- MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext) {
+ MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext,
+ ReactiveDataAccessStrategy dataAccessStrategy) {
Assert.notNull(databaseClient, "DatabaseClient must not be null!");
Assert.notNull(mappingContext, "MappingContext must not be null!");
+ Assert.notNull(dataAccessStrategy, "ReactiveDataAccessStrategy must not be null!");
this.databaseClient = databaseClient;
this.mappingContext = mappingContext;
+ this.dataAccessStrategy = dataAccessStrategy;
this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext));
}
@@ -94,7 +99,8 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
RelationalEntityInformation, ?> entityInformation = getEntityInformation(information.getDomainType(),
information);
- return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter);
+ return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter,
+ dataAccessStrategy);
}
/*
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java
index 4b06aa2..8a2ea87 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java
@@ -19,6 +19,7 @@ import java.io.Serializable;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.function.DatabaseClient;
+import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -40,8 +41,8 @@ public class R2dbcRepositoryFactoryBean, S, ID exten
extends RepositoryFactoryBeanSupport {
private @Nullable DatabaseClient client;
- private @Nullable
- MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext;
+ private @Nullable MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext;
+ private @Nullable ReactiveDataAccessStrategy dataAccessStrategy;
private boolean mappingContextConfigured = false;
@@ -80,6 +81,10 @@ public class R2dbcRepositoryFactoryBean, S, ID exten
}
}
+ public void setDataAccessStrategy(@Nullable ReactiveDataAccessStrategy dataAccessStrategy) {
+ this.dataAccessStrategy = dataAccessStrategy;
+ }
+
/*
* (non-Javadoc)
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createRepositoryFactory()
@@ -98,7 +103,7 @@ public class R2dbcRepositoryFactoryBean, S, ID exten
*/
protected RepositoryFactorySupport getFactoryInstance(DatabaseClient client,
MappingContext extends RelationalPersistentEntity>, RelationalPersistentProperty> mappingContext) {
- return new R2dbcRepositoryFactory(client, mappingContext);
+ return new R2dbcRepositoryFactory(client, mappingContext, dataAccessStrategy);
}
/*
@@ -109,6 +114,7 @@ public class R2dbcRepositoryFactoryBean, S, ID exten
public void afterPropertiesSet() {
Assert.state(client != null, "DatabaseClient must not be null!");
+ Assert.state(dataAccessStrategy != null, "ReactiveDataAccessStrategy must not be null!");
if (!mappingContextConfigured) {
setMappingContext(new RelationalMappingContext());
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index 451d2e9..7347518 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -15,25 +15,30 @@
*/
package org.springframework.data.r2dbc.repository.support;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-import java.util.stream.IntStream;
-
+import io.r2dbc.spi.Statement;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
import org.reactivestreams.Publisher;
+import org.springframework.data.r2dbc.function.BindIdOperation;
+import org.springframework.data.r2dbc.function.BindableOperation;
import org.springframework.data.r2dbc.function.DatabaseClient;
-import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec;
import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec;
import org.springframework.data.r2dbc.function.FetchSpec;
+import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.util.Assert;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
/**
* Simple {@link ReactiveCrudRepository} implementation using R2DBC through {@link DatabaseClient}.
@@ -46,6 +51,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository entity;
private final @NonNull DatabaseClient databaseClient;
private final @NonNull MappingR2dbcConverter converter;
+ private final @NonNull ReactiveDataAccessStrategy accessStrategy;
/* (non-Javadoc)
* @see org.springframework.data.repository.reactive.ReactiveCrudRepository#save(S)
@@ -64,51 +70,24 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract(converter.populateIdIfNecessary(objectToSave)).one());
}
- // TODO: Extract in some kind of SQL generator
Object id = entity.getRequiredId(objectToSave);
+ Map columns = converter.getColumnsToUpdate(objectToSave);
+ columns.remove(getIdColumnName()); // do not update the Id column.
+ String idColumnName = getIdColumnName();
+ BindIdOperation update = accessStrategy.updateById(entity.getTableName(), columns.keySet(), idColumnName);
- Map fields = converter.getFieldsToUpdate(objectToSave);
+ GenericExecuteSpec exec = databaseClient.execute().sql(update);
- String setClause = getSetClause(fields);
+ BindSpecWrapper wrapper = BindSpecWrapper.create(exec);
+ columns.forEach(bind(update, wrapper));
+ update.bindId(wrapper, id);
- GenericExecuteSpec exec = databaseClient.execute()
- .sql(String.format("UPDATE %s SET %s WHERE %s = $1", entity.getTableName(), setClause, getIdColumnName())) //
- .bind(0, id);
-
- int index = 1;
- for (SettableValue setValue : fields.values()) {
-
- Object value = setValue.getValue();
- if (value != null) {
- exec = exec.bind(index++, value);
- } else {
- exec = exec.bindNull(index++, setValue.getType());
- }
- }
-
- return exec.as(entity.getJavaType()) //
+ return wrapper.getBoundOperation().as(entity.getJavaType()) //
.exchange() //
.flatMap(FetchSpec::rowsUpdated) //
.thenReturn(objectToSave);
}
- private static String getSetClause(Map fields) {
-
- StringBuilder setClause = new StringBuilder();
-
- int index = 2;
- for (String field : fields.keySet()) {
-
- if (setClause.length() != 0) {
- setClause.append(", ");
- }
-
- setClause.append(field).append('=').append('$').append(index++);
- }
-
- return setClause.toString();
- }
-
/* (non-Javadoc)
* @see org.springframework.data.repository.reactive.ReactiveCrudRepository#saveAll(java.lang.Iterable)
*/
@@ -139,14 +118,17 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
+ String idColumnName = getIdColumnName();
+ BindIdOperation select = accessStrategy.selectById(entity.getTableName(), columns, idColumnName);
+
+ GenericExecuteSpec sql = databaseClient.execute().sql(select);
+ BindSpecWrapper wrapper = BindSpecWrapper.create(sql);
+ select.bindId(wrapper, id);
+
+ return wrapper.getBoundOperation().as(entity.getJavaType()) //
.fetch() //
.one();
-
}
/* (non-Javadoc)
@@ -165,11 +147,15 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecWrapper.create(sql);
+ select.bindId(wrapper, id);
+
+ return wrapper.getBoundOperation().as(entity.getJavaType()) //
.exchange() //
.flatMap(it -> it.extract((r, md) -> r).first()).hasElement();
}
@@ -211,12 +197,18 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository !ids.isEmpty()).concatMap(ids -> {
- String bindings = getInBinding(ids);
+ if (ids.isEmpty()) {
+ return Flux.empty();
+ }
- GenericExecuteSpec exec = databaseClient.execute()
- .sql(String.format("SELECT * FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings));
+ Set columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
+ String idColumnName = getIdColumnName();
+ BindIdOperation select = accessStrategy.selectByIdIn(entity.getTableName(), columns, idColumnName);
- return bind(ids, exec).as(entity.getJavaType()).fetch().all();
+ BindSpecWrapper wrapper = BindSpecWrapper.create(databaseClient.execute().sql(select));
+ select.bindIds(wrapper, ids);
+
+ return wrapper.getBoundOperation().as(entity.getJavaType()).fetch().all();
});
}
@@ -242,9 +234,12 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecWrapper.create(databaseClient.execute().sql(delete));
+
+ delete.bindId(wrapper, id);
+
+ return wrapper.getBoundOperation() //
.fetch() //
.rowsUpdated() //
.then();
@@ -260,12 +255,17 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository !ids.isEmpty()).concatMap(ids -> {
- String bindings = getInBinding(ids);
+ if (ids.isEmpty()) {
+ return Flux.empty();
+ }
- GenericExecuteSpec exec = databaseClient.execute()
- .sql(String.format("DELETE FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings));
+ String idColumnName = getIdColumnName();
+ BindIdOperation delete = accessStrategy.deleteByIdIn(entity.getTableName(), idColumnName);
- return bind(ids, exec).as(entity.getJavaType()).fetch().rowsUpdated();
+ BindSpecWrapper wrapper = BindSpecWrapper.create(databaseClient.execute().sql(delete));
+ delete.bindIds(wrapper, ids);
+
+ return wrapper.getBoundOperation().as(entity.getJavaType()).fetch().rowsUpdated();
}).then();
}
@@ -273,7 +273,6 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository delete(T objectToDelete) {
Assert.notNull(objectToDelete, "Object to delete must not be null!");
@@ -296,7 +295,6 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository deleteAll(Publisher extends T> objectPublisher) {
Assert.notNull(objectPublisher, "The Object Publisher must not be null!");
@@ -318,22 +316,19 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository ids) {
- return IntStream.range(1, ids.size() + 1).mapToObj(i -> "$" + i).collect(Collectors.joining(", "));
- }
-
- @SuppressWarnings("unchecked")
- private > S bind(List it, S bindSpec) {
-
- for (int i = 0; i < it.size(); i++) {
- bindSpec = (S) bindSpec.bind(i, it.get(i));
- }
-
- return bindSpec;
- }
-
private String getIdColumnName() {
return converter.getMappingContext().getRequiredPersistentEntity(entity.getJavaType()).getRequiredIdProperty()
.getColumnName();
}
+
+ private BiConsumer bind(BindableOperation operation, Statement> statement) {
+
+ return (k, v) -> {
+ if (v.getValue() == null) {
+ operation.bindNull(statement, k, v.getType());
+ } else {
+ operation.bind(statement, k, v.getValue());
+ }
+ };
+ }
}
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/DatabaseUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/DatabaseUnitTests.java
new file mode 100644
index 0000000..d28c171
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/DatabaseUnitTests.java
@@ -0,0 +1,56 @@
+package org.springframework.data.r2dbc.dialect;
+
+import static org.assertj.core.api.Assertions.*;
+
+import io.r2dbc.h2.H2ConnectionConfiguration;
+import io.r2dbc.h2.H2ConnectionFactory;
+import io.r2dbc.mssql.MssqlConnectionConfiguration;
+import io.r2dbc.mssql.MssqlConnectionFactory;
+import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
+import io.r2dbc.postgresql.PostgresqlConnectionFactory;
+import io.r2dbc.spi.Connection;
+import io.r2dbc.spi.ConnectionFactory;
+import io.r2dbc.spi.ConnectionFactoryMetadata;
+
+import org.junit.Test;
+import org.reactivestreams.Publisher;
+
+/**
+ * Unit tests for {@link Database}.
+ *
+ * @author Mark Paluch
+ */
+public class DatabaseUnitTests {
+
+ @Test // gh-20
+ public void shouldResolveDatabaseType() {
+
+ PostgresqlConnectionFactory postgres = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder()
+ .host("localhost").database("foo").username("bar").password("password").build());
+ MssqlConnectionFactory mssql = new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host("localhost")
+ .database("foo").username("bar").password("password").build());
+ H2ConnectionFactory h2 = new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("mem").build());
+
+ assertThat(Database.findDatabase(postgres)).contains(Database.POSTGRES);
+ assertThat(Database.findDatabase(mssql)).contains(Database.SQL_SERVER);
+ assertThat(Database.findDatabase(h2)).contains(Database.H2);
+ }
+
+ @Test // gh-20
+ public void shouldNotResolveUnknownDatabase() {
+ assertThat(Database.findDatabase(new UnknownConnectionFactory())).isEmpty();
+ }
+
+ static class UnknownConnectionFactory implements ConnectionFactory {
+
+ @Override
+ public Publisher extends Connection> create() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public ConnectionFactoryMetadata getMetadata() {
+ return () -> "foo";
+ }
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
index 62e1e82..fc2a31a 100644
--- a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
@@ -26,6 +26,25 @@ public class IndexedBindMarkersUnitTests {
assertThat(bindMarkers2.next().getPlaceholder()).isEqualTo("$0");
}
+ @Test // gh-15
+ public void shouldCreateNewBindMarkersWithOffset() {
+
+ Statement> statement = mock(Statement.class);
+
+ BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 1).create();
+
+ BindMarker first = bindMarkers.next();
+ first.bind(statement, "foo");
+
+ BindMarker second = bindMarkers.next();
+ second.bind(statement, "bar");
+
+ assertThat(first.getPlaceholder()).isEqualTo("$1");
+ assertThat(second.getPlaceholder()).isEqualTo("$2");
+ verify(statement).bind(0, "foo");
+ verify(statement).bind(1, "bar");
+ }
+
@Test // gh-15
public void nextShouldIncrementBindMarker() {
@@ -50,8 +69,8 @@ public class IndexedBindMarkersUnitTests {
BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create();
- bindMarkers.next().bindValue(statement, "foo");
- bindMarkers.next().bindValue(statement, "bar");
+ bindMarkers.next().bind(statement, "foo");
+ bindMarkers.next().bind(statement, "bar");
verify(statement).bind(0, "foo");
verify(statement).bind(1, "bar");
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
index ca6afcc..a267ba5 100644
--- a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
@@ -89,8 +89,8 @@ public class NamedBindMarkersUnitTests {
BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32).create();
- bindMarkers.next().bindValue(statement, "foo");
- bindMarkers.next().bindValue(statement, "bar");
+ bindMarkers.next().bind(statement, "foo");
+ bindMarkers.next().bind(statement, "bar");
verify(statement).bind("p0", "foo");
verify(statement).bind("p1", "bar");
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java
new file mode 100644
index 0000000..3b0168d
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java
@@ -0,0 +1,25 @@
+package org.springframework.data.r2dbc.dialect;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link PostgresDialect}.
+ *
+ * @author Mark Paluch
+ */
+public class PostgresDialectUnitTests {
+
+ @Test // gh-20
+ public void shouldUsePostgresPlaceholders() {
+
+ BindMarkers bindMarkers = PostgresDialect.INSTANCE.getBindMarkersFactory().create();
+
+ BindMarker first = bindMarkers.next();
+ BindMarker second = bindMarkers.next("foo");
+
+ assertThat(first.getPlaceholder()).isEqualTo("$1");
+ assertThat(second.getPlaceholder()).isEqualTo("$2");
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java
new file mode 100644
index 0000000..0e84801
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java
@@ -0,0 +1,25 @@
+package org.springframework.data.r2dbc.dialect;
+
+import static org.assertj.core.api.Assertions.*;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link SqlServerDialect}.
+ *
+ * @author Mark Paluch
+ */
+public class SqlServerDialectUnitTests {
+
+ @Test // gh-20
+ public void shouldUseNamedPlaceholders() {
+
+ BindMarkers bindMarkers = SqlServerDialect.INSTANCE.getBindMarkersFactory().create();
+
+ BindMarker first = bindMarkers.next();
+ BindMarker second = bindMarkers.next("'foo!bar");
+
+ assertThat(first.getPlaceholder()).isEqualTo("@P0");
+ assertThat(second.getPlaceholder()).isEqualTo("@P1_foobar");
+ }
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
similarity index 80%
rename from src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java
rename to src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
index 614d797..3ebf5bc 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
@@ -23,8 +23,11 @@ import lombok.Data;
import reactor.core.publisher.Hooks;
import reactor.test.StepVerifier;
+import javax.sql.DataSource;
+
import org.junit.Before;
import org.junit.Test;
+import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
@@ -33,11 +36,11 @@ import org.springframework.data.relational.core.mapping.Table;
import org.springframework.jdbc.core.JdbcTemplate;
/**
- * Integration tests for {@link DatabaseClient} against PostgreSQL.
+ * Integration tests for {@link DatabaseClient}.
*
* @author Mark Paluch
*/
-public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport {
+public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport {
private ConnectionFactory connectionFactory;
@@ -50,24 +53,56 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
connectionFactory = createConnectionFactory();
- String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n"
- + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n"
- + " manual integer NULL\n" + ");";
-
jdbc = createJdbcTemplate(createDataSource());
- jdbc.execute(tableToCreate);
- jdbc.execute("DELETE FROM legoset");
+
+ try {
+ jdbc.execute("DROP TABLE legoset");
+ } catch (DataAccessException e) {}
+ jdbc.execute(getCreateTableStatement());
}
+ /**
+ * Creates a {@link DataSource} to be used in this test.
+ *
+ * @return the {@link DataSource} to be used in this test.
+ */
+ protected abstract DataSource createDataSource();
+
+ /**
+ * Creates a {@link ConnectionFactory} to be used in this test.
+ *
+ * @return the {@link ConnectionFactory} to be used in this test.
+ */
+ protected abstract ConnectionFactory createConnectionFactory();
+
+ /**
+ * Returns the the CREATE TABLE statement for table {@code legoset} with the following three columns:
+ *
+ * - id integer (primary key), not null
+ * - name varchar(255), nullable
+ * - manual integer, nullable
+ *
+ *
+ * @return the CREATE TABLE statement for table {@code legoset} with three columns.
+ */
+ protected abstract String getCreateTableStatement();
+
+ /**
+ * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values.
+ *
+ * @return
+ */
+ protected abstract String getInsertIntoLegosetStatement();
+
@Test
public void executeInsert() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
- databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
- .bindNull("$3", Integer.class) //
+ .bindNull(2, Integer.class) //
.fetch().rowsUpdated() //
.as(StepVerifier::create) //
.expectNext(1) //
@@ -83,10 +118,10 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
executeInsert();
- databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
- .bindNull("$3", Integer.class) //
+ .bindNull(2, Integer.class) //
.fetch().rowsUpdated() //
.as(StepVerifier::create) //
.expectErrorSatisfies(exception -> {
@@ -104,7 +139,6 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
- // TODO: Driver/Decode does not support decoding null values?
databaseClient.execute().sql("SELECT id, name, manual FROM legoset") //
.as(LegoSet.class) //
.fetch().all() //
@@ -127,9 +161,9 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
.value("name", "SCHAUFELRADBAGGER") //
.nullValue("manual", Integer.class) //
.exchange() //
- .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) //
+ .flatMapMany(FetchSpec::rowsUpdated) //
.as(StepVerifier::create) //
- .expectNext(42055).verifyComplete();
+ .expectNext(1).verifyComplete();
assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
}
@@ -162,8 +196,9 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport
databaseClient.insert().into(LegoSet.class)//
.using(legoSet).exchange() //
- .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) //
- .expectNext(42055).verifyComplete();
+ .flatMapMany(FetchSpec::rowsUpdated) //
+ .as(StepVerifier::create) //
+ .expectNext(1).verifyComplete();
assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055);
}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
similarity index 67%
rename from src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java
rename to src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
index 31c641e..c50b305 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
@@ -28,18 +28,21 @@ import java.util.List;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;
+import javax.sql.DataSource;
+
import org.junit.Before;
import org.junit.Test;
+import org.springframework.dao.DataAccessException;
import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.NoTransactionException;
/**
- * Integration tests for {@link TransactionalDatabaseClient}.
+ * Abstract base class for integration tests for {@link TransactionalDatabaseClient}.
*
* @author Mark Paluch
*/
-public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport {
+public abstract class AbstractTransactionalDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport {
private ConnectionFactory connectionFactory;
@@ -52,15 +55,54 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
connectionFactory = createConnectionFactory();
- String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n"
- + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n"
- + " manual integer NULL\n" + ");";
-
jdbc = createJdbcTemplate(createDataSource());
- jdbc.execute(tableToCreate);
+ try {
+ jdbc.execute("DROP TABLE legoset");
+ } catch (DataAccessException e) {}
+ jdbc.execute(getCreateTableStatement());
jdbc.execute("DELETE FROM legoset");
}
+ /**
+ * Creates a {@link DataSource} to be used in this test.
+ *
+ * @return the {@link DataSource} to be used in this test.
+ */
+ protected abstract DataSource createDataSource();
+
+ /**
+ * Creates a {@link ConnectionFactory} to be used in this test.
+ *
+ * @return the {@link ConnectionFactory} to be used in this test.
+ */
+ protected abstract ConnectionFactory createConnectionFactory();
+
+ /**
+ * Returns the the CREATE TABLE statement for table {@code legoset} with the following three columns:
+ *
+ * - id integer (primary key), not null
+ * - name varchar(255), nullable
+ * - manual integer, nullable
+ *
+ *
+ * @return the CREATE TABLE statement for table {@code legoset} with three columns.
+ */
+ protected abstract String getCreateTableStatement();
+
+ /**
+ * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values.
+ *
+ * @return
+ */
+ protected abstract String getInsertIntoLegosetStatement();
+
+ /**
+ * Get a statement that returns the current transactionId.
+ *
+ * @return
+ */
+ protected abstract String getCurrentTransactionIdStatement();
+
@Test
public void executeInsertInManagedTransaction() {
@@ -68,10 +110,10 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
Flux integerFlux = databaseClient.inTransaction(db -> {
- return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ return db.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
- .bindNull("$3", Integer.class) //
+ .bindNull(2, Integer.class) //
.fetch().rowsUpdated();
});
@@ -87,11 +129,10 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
- Mono integerFlux = databaseClient.execute()
- .sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ Mono integerFlux = databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
- .bindNull("$3", Integer.class) //
+ .bindNull(2, Integer.class) //
.fetch().rowsUpdated();
integerFlux.as(StepVerifier::create) //
@@ -107,7 +148,7 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
Queue transactionIds = new ArrayBlockingQueue<>(5);
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
- Flux txId = databaseClient.execute().sql("SELECT txid_current();").exchange()
+ Flux txId = databaseClient.execute().sql(getCurrentTransactionIdStatement()).exchange()
.flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all());
Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() //
@@ -144,10 +185,10 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
Flux integerFlux = databaseClient.inTransaction(db -> {
- return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
+ return db.execute().sql(getInsertIntoLegosetStatement()) //
.bind(0, 42055) //
.bind(1, "SCHAUFELRADBAGGER") //
- .bindNull("$3", Integer.class) //
+ .bindNull(2, Integer.class) //
.fetch().rowsUpdated().then(Mono.error(new IllegalStateException("failed")));
});
@@ -155,7 +196,8 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
.expectError(IllegalStateException.class) //
.verify();
- assertThat(jdbc.queryForMap("SELECT count(*) FROM legoset")).containsEntry("count", 0L);
+ Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class);
+ assertThat(count).isEqualTo(0);
}
@Test
@@ -163,10 +205,10 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
- Flux transactionIds = databaseClient.inTransaction(db -> {
+ Flux