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