From 68f1e0b0b36793762a0a370ddd1f713a707dfc56 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 21 Nov 2018 13:48:07 +0100 Subject: [PATCH] #20 - Add Dialect initial support for H2, PostgreSQL, and Microsoft SQL Server. We now provide dialect support for H2, PostgreSQL, and Microsoft SQL Server databases, configurable through AbstractR2dbcConfiguration. By default, we obtain the Dialect by inspecting ConnectionFactoryMetadata to identify the database and the most likely dialect to use. BindableOperation encapsulates statements/queries that can accept parameters. Use BindableOperation for statements through DatabaseClient. Extract SQL creation. Split integration test into abstract base class that can be implemented with a database-specific test class. Original pull request: #24. --- pom.xml | 23 + .../config/AbstractR2dbcConfiguration.java | 34 +- .../data/r2dbc/config/package-info.java | 6 + .../data/r2dbc/dialect/BindMarker.java | 3 +- .../data/r2dbc/dialect/Database.java | 92 ++++ .../data/r2dbc/dialect/Dialect.java | 31 ++ .../data/r2dbc/dialect/H2Dialect.java | 23 + .../r2dbc/dialect/IndexedBindMarkers.java | 14 +- .../data/r2dbc/dialect/LimitClause.java | 42 ++ .../data/r2dbc/dialect/NamedBindMarkers.java | 2 +- .../data/r2dbc/dialect/PostgresDialect.java | 73 +++ .../data/r2dbc/dialect/SqlServerDialect.java | 95 ++++ .../data/r2dbc/function/BindIdOperation.java | 32 ++ .../r2dbc/function/BindableOperation.java | 36 ++ .../r2dbc/function/DefaultDatabaseClient.java | 160 ++---- .../DefaultDatabaseClientBuilder.java | 25 +- .../DefaultReactiveDataAccessStrategy.java | 484 +++++++++++++++++- .../data/r2dbc/function/QueryOperation.java | 26 + .../function/ReactiveDataAccessStrategy.java | 104 +++- .../convert/MappingR2dbcConverter.java | 68 ++- ...R2dbcRepositoryConfigurationExtension.java | 1 + .../query/StringBasedR2dbcQuery.java | 10 +- .../repository/support/BindSpecWrapper.java | 103 ++++ .../support/R2dbcRepositoryFactory.java | 10 +- .../support/R2dbcRepositoryFactoryBean.java | 12 +- .../support/SimpleR2dbcRepository.java | 159 +++--- .../data/r2dbc/dialect/DatabaseUnitTests.java | 56 ++ .../dialect/IndexedBindMarkersUnitTests.java | 23 +- .../dialect/NamedBindMarkersUnitTests.java | 4 +- .../dialect/PostgresDialectUnitTests.java | 25 + .../dialect/SqlServerDialectUnitTests.java | 25 + ...stractDatabaseClientIntegrationTests.java} | 69 ++- ...tionalDatabaseClientIntegrationTests.java} | 80 ++- ...ltReactiveDataAccessStrategyUnitTests.java | 104 ++++ ...ostgresDatabaseClientIntegrationTests.java | 63 +++ ...ctionalDatabaseClientIntegrationTests.java | 45 ++ ...lServerDatabaseClientIntegrationTests.java | 54 ++ ...ctionalDatabaseClientIntegrationTests.java | 45 ++ ...tractR2dbcRepositoryIntegrationTests.java} | 90 ++-- ...stgresR2dbcRepositoryIntegrationTests.java | 94 ++++ ...ServerR2dbcRepositoryIntegrationTests.java | 99 ++++ .../R2dbcRepositoriesRegistrarTests.java | 6 + .../query/StringBasedR2dbcQueryUnitTests.java | 8 +- ...impleR2dbcRepositoryIntegrationTests.java} | 163 +++--- ...SimpleR2dbcRepositoryIntegrationTests.java | 60 +++ .../R2dbcRepositoryFactoryUnitTests.java | 6 +- ...SimpleR2dbcRepositoryIntegrationTests.java | 60 +++ .../data/r2dbc/testing/ExternalDatabase.java | 3 +- .../r2dbc/testing/PostgresTestSupport.java | 75 +++ .../testing/R2dbcIntegrationTestSupport.java | 35 -- .../r2dbc/testing/SqlServerTestSupport.java | 79 +++ 51 files changed, 2594 insertions(+), 445 deletions(-) rename src/main/java/org/springframework/data/r2dbc/{repository => }/config/AbstractR2dbcConfiguration.java (76%) create mode 100644 src/main/java/org/springframework/data/r2dbc/config/package-info.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/Database.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/LimitClause.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/QueryOperation.java create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecWrapper.java create mode 100644 src/test/java/org/springframework/data/r2dbc/dialect/DatabaseUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java rename src/test/java/org/springframework/data/r2dbc/function/{DatabaseClientIntegrationTests.java => AbstractDatabaseClientIntegrationTests.java} (80%) rename src/test/java/org/springframework/data/r2dbc/function/{TransactionalDatabaseClientIntegrationTests.java => AbstractTransactionalDatabaseClientIntegrationTests.java} (67%) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java rename src/test/java/org/springframework/data/r2dbc/repository/{R2dbcRepositoryIntegrationTests.java => AbstractR2dbcRepositoryIntegrationTests.java} (69%) create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java rename src/test/java/org/springframework/data/r2dbc/repository/support/{SimpleR2dbcRepositoryIntegrationTests.java => AbstractSimpleR2dbcRepositoryIntegrationTests.java} (57%) create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java diff --git a/pom.xml b/pom.xml index 4fe02600..62715933 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 561a5130..6f3dbb13 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 00000000..e4e7fb58 --- /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 a6971bf5..415a6281 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 00000000..ee3f4e11 --- /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 00000000..f73a3ff6 --- /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 00000000..18e1acb4 --- /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 d77c4671..fa59c112 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 00000000..7d1eb482 --- /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 ff7e5722..7b94767a 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 00000000..271f2721 --- /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 00000000..3047d776 --- /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 00000000..08c9361c --- /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 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 00000000..ebe26d26 --- /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 b363f7b5..badd8720 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 a4e4e4e8..d21e91d3 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 7a4a2823..e6666a46 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 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 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 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 00000000..b26923e4 --- /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 20024a2b..f3c8a9f2 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 49fb815d..6b291e86 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 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 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 c7cc63aa..efad3af3 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 d3fb559f..6bbb32ae 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 00000000..bd991e52 --- /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 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 4d605451..b5e749a0 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, 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, RelationalPersistentProperty> mappingContext) { + MappingContext, 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 4b06aa29..8a2ea87f 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, RelationalPersistentProperty> mappingContext; + private @Nullable MappingContext, 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, 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 451d2e97..7347518e 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 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 00000000..d28c171f --- /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 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 62e1e82e..fc2a31a4 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 ca6afcce..a267ba5f 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 00000000..3b0168d5 --- /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 00000000..0e848015 --- /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 614d7972..3ebf5bc3 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 31c641e4..c50b3050 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 transactionIds = databaseClient.inTransaction(db -> { - Flux txId = db.execute().sql("SELECT txid_current();").exchange() - .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).exchange() + .flatMapMany(it -> it.extract((r, md) -> r.get(0)).all()); return txId.concatWith(txId); }); diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java new file mode 100644 index 00000000..df279946 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java @@ -0,0 +1,104 @@ +package org.springframework.data.r2dbc.function; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Statement; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.junit.Test; +import org.springframework.data.r2dbc.dialect.PostgresDialect; + +/** + * Unit tests for {@link DefaultReactiveDataAccessStrategy}. + * + * @author Mark Paluch + */ +public class DefaultReactiveDataAccessStrategyUnitTests { + + DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + + @Test // gh-20 + public void shouldRenderInsertAndReturnGeneratedKeysQuery() { + + BindableOperation operation = strategy.insertAndReturnGeneratedKeys("table", + new HashSet<>(Arrays.asList("firstname", "lastname"))); + + assertThat(operation.toQuery()).isEqualTo("INSERT INTO table (firstname, lastname) VALUES($1, $2) RETURNING *"); + } + + @Test // gh-20 + public void shouldRenderUpdateByIdQuery() { + + BindableOperation operation = strategy.updateById("table", new HashSet<>(Arrays.asList("firstname", "lastname")), + "id"); + + assertThat(operation.toQuery()).isEqualTo("UPDATE table SET firstname = $2, lastname = $3 WHERE id = $1"); + } + + @Test // gh-20 + public void shouldRenderSelectByIdQuery() { + + BindableOperation operation = strategy.selectById("table", new HashSet<>(Arrays.asList("firstname", "lastname")), + "id"); + + assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id = $1"); + } + + @Test // gh-20 + public void shouldRenderSelectByIdQueryWithLimit() { + + BindableOperation operation = strategy.selectById("table", new HashSet<>(Arrays.asList("firstname", "lastname")), + "id", 10); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT firstname, lastname FROM table WHERE id = $1 ORDER BY id LIMIT 10"); + } + + @Test // gh-20 + public void shouldFailRenderingSelectByIdInQueryWithoutBindings() { + + BindableOperation operation = strategy.selectByIdIn("table", new HashSet<>(Arrays.asList("firstname", "lastname")), + "id"); + + assertThatThrownBy(operation::toQuery).isInstanceOf(UnsupportedOperationException.class); + } + + @Test // gh-20 + public void shouldRenderSelectByIdInQuery() { + + Statement statement = mock(Statement.class); + BindIdOperation operation = strategy.selectByIdIn("table", new HashSet<>(Arrays.asList("firstname", "lastname")), + "id"); + + operation.bindId(statement, Collections.singleton("foo")); + assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id IN ($1)"); + + operation.bindId(statement, "bar"); + assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id IN ($1, $2)"); + } + + @Test // gh-20 + public void shouldRenderDeleteByIdQuery() { + + BindableOperation operation = strategy.deleteById("table", "id"); + + assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id = $1"); + } + + @Test // gh-20 + public void shouldRenderDeleteByIdInQuery() { + + Statement statement = mock(Statement.class); + BindIdOperation operation = strategy.deleteByIdIn("table", "id"); + + operation.bindId(statement, Collections.singleton("foo")); + assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1)"); + + operation.bindId(statement, "bar"); + assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1, $2)"); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..df86faf7 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.PostgresTestSupport; + +/** + * Integration tests for {@link DatabaseClient} against PostgreSQL. + * + * @author Mark Paluch + */ +public class PostgresDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return PostgresTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return PostgresTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return PostgresTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getInsertIntoLegosetStatement() { + return PostgresTestSupport.INSERT_INTO_LEGOSET; + } + + @Ignore("Adding RETURNING * lets Postgres report 0 affected rows.") + @Override + public void insert() {} + + @Ignore("Adding RETURNING * lets Postgres report 0 affected rows.") + @Override + public void insertTypedObject() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..cfd88545 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,45 @@ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.PostgresTestSupport; + +/** + * Integration tests for {@link TransactionalDatabaseClient} against PostgreSQL. + * + * @author Mark Paluch + */ +public class PostgresTransactionalDatabaseClientIntegrationTests + extends AbstractTransactionalDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return PostgresTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return PostgresTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return PostgresTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getInsertIntoLegosetStatement() { + return PostgresTestSupport.INSERT_INTO_LEGOSET; + } + + @Override + protected String getCurrentTransactionIdStatement() { + return "SELECT txid_current();"; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..b9ee4bc1 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.SqlServerTestSupport; + +/** + * Integration tests for {@link DatabaseClient} against Microsoft SQL Server. + * + * @author Mark Paluch + */ +public class SqlServerDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = SqlServerTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return SqlServerTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return SqlServerTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return SqlServerTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getInsertIntoLegosetStatement() { + return SqlServerTestSupport.INSERT_INTO_LEGOSET; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..7485a4db --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,45 @@ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.SqlServerTestSupport; + +/** + * Integration tests for {@link TransactionalDatabaseClient} against Microsoft SQL Server. + * + * @author Mark Paluch + */ +public class SqlServerTransactionalDatabaseClientIntegrationTests + extends AbstractTransactionalDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = SqlServerTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return SqlServerTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return SqlServerTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return SqlServerTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getInsertIntoLegosetStatement() { + return SqlServerTestSupport.INSERT_INTO_LEGOSET; + } + + @Override + protected String getCurrentTransactionIdStatement() { + return "SELECT CURRENT_TRANSACTION_ID();"; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java similarity index 69% rename from src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index aa33cd89..74650c25 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -30,54 +30,37 @@ import java.util.Arrays; import java.util.Collections; import java.util.Map; +import javax.sql.DataSource; + import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.ComponentScan.Filter; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.FilterType; +import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; +import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; -import org.springframework.data.r2dbc.repository.config.AbstractR2dbcConfiguration; -import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; -import org.springframework.data.r2dbc.repository.query.Query; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; /** - * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}. + * Abstract base class for integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}. * * @author Mark Paluch */ -@RunWith(SpringRunner.class) -@ContextConfiguration -public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { +public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { private static RelationalMappingContext mappingContext = new RelationalMappingContext(); @Autowired private LegoSetRepository repository; private JdbcTemplate jdbc; - @Configuration - @EnableR2dbcRepositories(considerNestedRepositories = true, - includeFilters = @Filter(classes = LegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) - static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { - - @Override - public ConnectionFactory connectionFactory() { - return createConnectionFactory(); - } - } - @Before public void before() { @@ -85,13 +68,41 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport this.jdbc = createJdbcTemplate(createDataSource()); - String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n" - + " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");"; + try { + this.jdbc.execute("DROP TABLE legoset"); + } catch (DataAccessException e) {} - this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset"); - this.jdbc.execute(tableToCreate); + this.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, auto-increment
  • + *
  • name varchar(255), nullable
  • + *
  • manual integer, nullable
  • + *
+ * + * @return the CREATE TABLE statement for table {@code legoset} with three columns. + */ + protected abstract String getCreateTableStatement(); + + protected abstract Class getRepositoryInterfaceType(); + @Test public void shouldInsertNewItems() { @@ -148,13 +159,14 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport @Test public void shouldInsertItemsTransactional() { + Database database = Database.findDatabase(createConnectionFactory()).get(); + DefaultReactiveDataAccessStrategy dataAccessStrategy = new DefaultReactiveDataAccessStrategy( + database.latestDialect(), new BasicRelationalConverter(mappingContext)); TransactionalDatabaseClient client = TransactionalDatabaseClient.builder() - .connectionFactory(createConnectionFactory()) - .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(new BasicRelationalConverter(mappingContext))) - .build(); + .connectionFactory(createConnectionFactory()).dataAccessStrategy(dataAccessStrategy).build(); - LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext) - .getRepository(LegoSetRepository.class); + LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext, dataAccessStrategy) + .getRepository(getRepositoryInterfaceType()); LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); @@ -162,33 +174,31 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport Flux> transactional = client.inTransaction(db -> { return transactionalRepository.save(legoSet1) // - .map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) FROM legoset")); }); Mono> nonTransactional = transactionalRepository.save(legoSet2) // - .map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) FROM legoset")); transactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 0L)).verifyComplete(); nonTransactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 2L)).verifyComplete(); - Map count = jdbc.queryForMap("SELECT count(*) FROM repo_legoset"); + Map count = jdbc.queryForMap("SELECT count(*) FROM legoset"); assertThat(count).containsEntry("count", 2L); } + @NoRepositoryBean interface LegoSetRepository extends ReactiveCrudRepository { - @Query("SELECT * FROM repo_legoset WHERE name like $1") Flux findByNameContains(String name); - @Query("SELECT * FROM repo_legoset") Flux findAsProjection(); - @Query("SELECT * FROM repo_legoset WHERE manual = $1") Mono findByManual(int manual); } @Data - @Table("repo_legoset") + @Table("legoset") @AllArgsConstructor @NoArgsConstructor static class LegoSet { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..1899ae5b --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.PostgresTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Postgres. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = PostgresLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return PostgresTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return PostgresTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return PostgresTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return PostgresTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return PostgresLegoSetRepository.class; + } + + interface PostgresLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT * FROM legoset WHERE name like $1") + Flux findByNameContains(String name); + + @Override + @Query("SELECT * FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = $1") + Mono findByManual(int manual); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..5d27027d --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.SqlServerTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against Microsoft SQL Server. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class SqlServerR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = SqlServerTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = SqlServerLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return SqlServerTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return SqlServerTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return SqlServerTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return SqlServerTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return SqlServerLegoSetRepository.class; + } + + @Ignore("SQL server locks a SELECT COUNT so we cannot proceed.") + @Override + public void shouldInsertItemsTransactional() {} + + interface SqlServerLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT * FROM legoset WHERE name like @name") + Flux findByNameContains(String name); + + @Override + @Query("SELECT * FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = @P0") + Mono findByManual(int manual); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index b5c29df4..6db3bf59 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -24,6 +24,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -44,6 +45,11 @@ public class R2dbcRepositoriesRegistrarTests { public DatabaseClient databaseClient() { return mock(DatabaseClient.class); } + + @Bean + public ReactiveDataAccessStrategy reactiveDataAccessStrategy() { + return mock(ReactiveDataAccessStrategy.class); + } } @Autowired PersonRepository personRepository; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index bf71533d..3902b6fe 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -16,8 +16,7 @@ package org.springframework.data.r2dbc.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -60,7 +59,6 @@ public class StringBasedR2dbcQueryUnitTests { private RepositoryMetadata metadata; @Before - @SuppressWarnings("unchecked") public void setUp() { this.mappingContext = new RelationalMappingContext(); @@ -68,7 +66,7 @@ public class StringBasedR2dbcQueryUnitTests { this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.factory = new SpelAwareProxyProjectionFactory(); - when(bindSpec.bind(anyString(), any())).thenReturn(bindSpec); + when(bindSpec.bind(anyInt(), any())).thenReturn(bindSpec); } @Test @@ -82,7 +80,7 @@ public class StringBasedR2dbcQueryUnitTests { assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = $1"); assertThat(stringQuery.bind(bindSpec)).isNotNull(); - verify(bindSpec).bind("$1", "White"); + verify(bindSpec).bind(0, "White"); } private StringBasedR2dbcQuery getQueryMethod(String name, Class... args) { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java similarity index 57% rename from src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index 14c48842..5201362c 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -17,7 +17,6 @@ package org.springframework.data.r2dbc.repository.support; import static org.assertj.core.api.Assertions.*; -import io.r2dbc.spi.ConnectionFactory; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -28,17 +27,19 @@ import reactor.test.StepVerifier; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.Map; +import javax.sql.DataSource; + import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; 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.config.AbstractR2dbcConfiguration; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -47,34 +48,23 @@ import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringRunner; /** - * Integration tests for {@link SimpleR2dbcRepository}. + * Abstract integration tests for {@link SimpleR2dbcRepository} to be ran against various databases. * * @author Mark Paluch */ -@RunWith(SpringRunner.class) -@ContextConfiguration -public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { +public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { @Autowired private DatabaseClient databaseClient; @Autowired private RelationalMappingContext mappingContext; + @Autowired private ReactiveDataAccessStrategy strategy; + private SimpleR2dbcRepository repository; private JdbcTemplate jdbc; - @Configuration - static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { - - @Override - public ConnectionFactory connectionFactory() { - return createConnectionFactory(); - } - } - @Before public void before() { @@ -84,17 +74,35 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, - new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext))); + new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext)), strategy); this.jdbc = createJdbcTemplate(createDataSource()); + try { + this.jdbc.execute("DROP TABLE legoset"); + } catch (DataAccessException e) {} - String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n" - + " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");"; - - this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset"); - this.jdbc.execute(tableToCreate); + this.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(); + + /** + * Returns the the CREATE TABLE statement for table {@code legoset} with the following three columns: + *
    + *
  • id integer (primary key), not null, auto-increment
  • + *
  • name varchar(255), nullable
  • + *
  • manual integer, nullable
  • + *
+ * + * @return the CREATE TABLE statement for table {@code legoset} with three columns. + */ + protected abstract String getCreateTableStatement(); + @Test public void shouldSaveNewObject() { @@ -107,16 +115,17 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS assertThat(actual.getId()).isNotNull(); }).verifyComplete(); - Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); + Map map = jdbc.queryForMap("SELECT * FROM legoset"); assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 12).containsKey("id"); } @Test public void shouldUpdateObject() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet = new LegoSet(id, "SCHAUFELRADBAGGER", 12); legoSet.setManual(14); repository.save(legoSet) // @@ -124,7 +133,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .expectNextCount(1) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); + Map map = jdbc.queryForMap("SELECT * FROM legoset"); assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 14).containsKey("id"); } @@ -145,8 +154,8 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .expectNext(15) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 4L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(4); } @Test @@ -160,20 +169,21 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .expectNextCount(2) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 2L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(2); } @Test public void shouldFindById() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - repository.findById(42055) // + repository.findById(id) // .as(StepVerifier::create) // .assertNext(actual -> { - assertThat(actual.getId()).isEqualTo(42055); + assertThat(actual.getId()).isEqualTo(id); assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER"); assertThat(actual.getManual()).isEqualTo(12); }).verifyComplete(); @@ -182,9 +192,10 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldExistsById() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - repository.existsById(42055) // + repository.existsById(id) // .as(StepVerifier::create) // .expectNext(true)// .verifyComplete(); @@ -198,9 +209,10 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldExistsByIdPublisher() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - repository.existsById(Mono.just(42055)) // + repository.existsById(Mono.just(id)) // .as(StepVerifier::create) // .expectNext(true)// .verifyComplete(); @@ -214,8 +226,8 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldFindByAll() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('FORSCHUNGSSCHIFF', 13)"); repository.findAll() // .map(LegoSet::getName) // @@ -230,10 +242,12 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldFindAllByIdUsingIterable() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('FORSCHUNGSSCHIFF', 13)"); - repository.findAllById(Arrays.asList(42055, 42064)) // + List ids = jdbc.queryForList("SELECT id FROM legoset", Integer.class); + + repository.findAllById(ids) // .map(LegoSet::getName) // .collectList() // .as(StepVerifier::create) // @@ -246,10 +260,12 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldFindAllByIdUsingPublisher() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('FORSCHUNGSSCHIFF', 13)"); - repository.findAllById(Flux.just(42055, 42064)) // + List ids = jdbc.queryForList("SELECT id FROM legoset", Integer.class); + + repository.findAllById(Flux.fromIterable(ids)) // .map(LegoSet::getName) // .collectList() // .as(StepVerifier::create) // @@ -267,8 +283,8 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .expectNext(0L) // .verifyComplete(); - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('FORSCHUNGSSCHIFF', 13)"); repository.count() // .as(StepVerifier::create) // @@ -279,76 +295,81 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS @Test public void shouldDeleteById() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - repository.deleteById(42055) // + repository.deleteById(id) // .as(StepVerifier::create) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 0L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); } @Test public void shouldDeleteByIdPublisher() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - repository.deleteById(Mono.just(42055)) // + repository.deleteById(Mono.just(id)) // .as(StepVerifier::create) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 0L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); } @Test public void shouldDelete() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet = new LegoSet(id, "SCHAUFELRADBAGGER", 12); repository.delete(legoSet) // .as(StepVerifier::create) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 0L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); } @Test public void shouldDeleteAllUsingIterable() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet = new LegoSet(id, "SCHAUFELRADBAGGER", 12); repository.deleteAll(Collections.singletonList(legoSet)) // .as(StepVerifier::create) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 0L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); } @Test public void shouldDeleteAllUsingPublisher() { - jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 12)"); + Integer id = jdbc.queryForObject("SELECT id FROM legoset", Integer.class); - LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet = new LegoSet(id, "SCHAUFELRADBAGGER", 12); repository.deleteAll(Mono.just(legoSet)) // .as(StepVerifier::create) // .verifyComplete(); - Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 0L); + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); } @Data - @Table("repo_legoset") + @Table("legoset") @AllArgsConstructor @NoArgsConstructor static class LegoSet { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..11157ab0 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository.support; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.PostgresTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link SimpleR2dbcRepository} against Postgres. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class PostgresSimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + + @Configuration + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return PostgresTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return PostgresTestSupport.createDataSource(database); + } + + @Override + protected String getCreateTableStatement() { + return PostgresTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 30f91bbe..79dbfbdb 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -25,6 +25,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; 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.RelationalPersistentEntity; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; @@ -41,6 +42,7 @@ public class R2dbcRepositoryFactoryUnitTests { @Mock DatabaseClient databaseClient; @Mock @SuppressWarnings("rawtypes") MappingContext mappingContext; @Mock @SuppressWarnings("rawtypes") RelationalPersistentEntity entity; + @Mock ReactiveDataAccessStrategy dataAccessStrategy; @Before @SuppressWarnings("unchecked") @@ -52,7 +54,7 @@ public class R2dbcRepositoryFactoryUnitTests { @SuppressWarnings("unchecked") public void usesMappingRelationalEntityInformationIfMappingContextSet() { - R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext, dataAccessStrategy); RelationalEntityInformation entityInformation = factory.getEntityInformation(Person.class); assertThat(entityInformation).isInstanceOf(MappingRelationalEntityInformation.class); @@ -62,7 +64,7 @@ public class R2dbcRepositoryFactoryUnitTests { @SuppressWarnings("unchecked") public void createsRepositoryWithIdTypeLong() { - R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext, dataAccessStrategy); MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); assertThat(repository).isNotNull(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..6a16d4e8 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository.support; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.SqlServerTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link SimpleR2dbcRepository} against Microsoft SQL Server. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class SqlServerSimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = SqlServerTestSupport.database(); + + @Configuration + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return SqlServerTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return SqlServerTestSupport.createDataSource(database); + } + + @Override + protected String getCreateTableStatement() { + return SqlServerTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index 7a021e21..2582983f 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -57,12 +57,11 @@ public abstract class ExternalDatabase extends ExternalResource { protected void before() { try (Socket socket = new Socket()) { - ; socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5))); } catch (IOException e) { throw new AssumptionViolatedException( - String.format("Cannot connect to %s:%d. Skiping tests.", getHostname(), getPort())); + String.format("Cannot connect to %s:%d. Skipping tests.", getHostname(), getPort())); } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java new file mode 100644 index 00000000..d7608f09 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -0,0 +1,75 @@ +package org.springframework.data.r2dbc.testing; + +import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; +import io.r2dbc.postgresql.PostgresqlConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; + +/** + * Utility class for testing against Postgres. + * + * @author Mark Paluch + */ +public class PostgresTestSupport { + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer CONSTRAINT id PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id serial CONSTRAINT id PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + public static String INSERT_INTO_LEGOSET = "INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)"; + + /** + * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + * + * @return + */ + public static ExternalDatabase database() { + return local(); + } + + /** + * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + * + * @return + */ + private static ExternalDatabase local() { + return ProvidedDatabase.builder().hostname("localhost").port(5432).database("postgres").username("postgres") + .password("").build(); + } + + /** + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder().host(database.getHostname()) + .database(database.getDatabase()).username(database.getUsername()).password(database.getPassword()).build()); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + public static DataSource createDataSource(ExternalDatabase database) { + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + + return dataSource; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java index 54ecbf71..d20fc476 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java @@ -15,15 +15,8 @@ */ package org.springframework.data.r2dbc.testing; -import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; -import io.r2dbc.postgresql.PostgresqlConnectionFactory; -import io.r2dbc.spi.ConnectionFactory; - import javax.sql.DataSource; -import org.junit.ClassRule; -import org.postgresql.ds.PGSimpleDataSource; -import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; import org.springframework.jdbc.core.JdbcTemplate; /** @@ -33,34 +26,6 @@ import org.springframework.jdbc.core.JdbcTemplate; */ public abstract class R2dbcIntegrationTestSupport { - /** - * Local test database at {@code postgres:@localhost:5432/postgres}. - */ - @ClassRule public static final ExternalDatabase database = ProvidedDatabase.builder().hostname("localhost").port(5432) - .database("postgres").username("postgres").password("").build(); - - /** - * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. - */ - protected static ConnectionFactory createConnectionFactory() { - return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder().host(database.getHostname()) - .database(database.getDatabase()).username(database.getUsername()).password(database.getPassword()).build()); - } - - /** - * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. - */ - protected static DataSource createDataSource() { - - PGSimpleDataSource dataSource = new PGSimpleDataSource(); - dataSource.setUser(database.getUsername()); - dataSource.setPassword(database.getPassword()); - dataSource.setDatabaseName(database.getDatabase()); - dataSource.setServerName(database.getHostname()); - dataSource.setPortNumber(database.getPort()); - return dataSource; - } - /** * Creates a new {@link JdbcTemplate} for a {@link DataSource}. */ diff --git a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java new file mode 100644 index 00000000..f8cfc2cb --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java @@ -0,0 +1,79 @@ +package org.springframework.data.r2dbc.testing; + +import io.r2dbc.mssql.MssqlConnectionConfiguration; +import io.r2dbc.mssql.MssqlConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; + +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; + +/** + * Utility class for testing against Microsoft SQL Server. + * + * @author Mark Paluch + */ +public class SqlServerTestSupport { + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer IDENTITY(1,1) PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + public static String INSERT_INTO_LEGOSET = "INSERT INTO legoset (id, name, manual) VALUES(@P0, @P1, @P3)"; + + /** + * Returns a locally provided database at {@code sqlserver:@localhost:1433/master}. + * + * @return + */ + public static ExternalDatabase database() { + return local(); + } + + /** + * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + * + * @return + */ + private static ExternalDatabase local() { + return ProvidedDatabase.builder().hostname("localhost").port(1433).database("master").username("sa") + .password("my1.password").build(); + } + + /** + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + return new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host(database.getHostname()) // + .database(database.getDatabase()) // + .username(database.getUsername()) // + .password(database.getPassword()) // + .build()); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + public static DataSource createDataSource(ExternalDatabase database) { + + SQLServerDataSource dataSource = new SQLServerDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + + return dataSource; + } +}