#20 - Add Dialect initial support for H2, PostgreSQL, and Microsoft SQL Server.
We now provide dialect support for H2, PostgreSQL, and Microsoft SQL Server databases, configurable through AbstractR2dbcConfiguration. By default, we obtain the Dialect by inspecting ConnectionFactoryMetadata to identify the database and the most likely dialect to use. BindableOperation encapsulates statements/queries that can accept parameters. Use BindableOperation for statements through DatabaseClient. Extract SQL creation. Split integration test into abstract base class that can be implemented with a database-specific test class. Original pull request: #24.
This commit is contained in:
23
pom.xml
23
pom.xml
@@ -30,8 +30,11 @@
|
||||
<degraph-check.version>0.1.4</degraph-check.version>
|
||||
<hsqldb.version>2.4.1</hsqldb.version>
|
||||
<postgresql.version>42.2.5</postgresql.version>
|
||||
<mssql-jdbc.version>7.1.2.jre8-preview</mssql-jdbc.version>
|
||||
<r2dbc-spi.version>1.0.0.M6</r2dbc-spi.version>
|
||||
<r2dbc-postgresql.version>1.0.0.M6</r2dbc-postgresql.version>
|
||||
<r2dbc-h2.version>1.0.0.M6</r2dbc-h2.version>
|
||||
<r2dbc-mssql.version>1.0.0.M6</r2dbc-mssql.version>
|
||||
<testcontainers.version>1.10.1</testcontainers.version>
|
||||
|
||||
</properties>
|
||||
@@ -235,6 +238,12 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.microsoft.sqlserver</groupId>
|
||||
<artifactId>mssql-jdbc</artifactId>
|
||||
<version>${mssql-jdbc.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-postgresql</artifactId>
|
||||
@@ -242,6 +251,20 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-h2</artifactId>
|
||||
<version>${r2dbc-h2.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.r2dbc</groupId>
|
||||
<artifactId>r2dbc-mssql</artifactId>
|
||||
<version>${r2dbc-mssql.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>de.schauderhaft.degraph</groupId>
|
||||
<artifactId>degraph-check</artifactId>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Configuration classes for Spring Data R2DBC.
|
||||
*/
|
||||
@org.springframework.lang.NonNullApi
|
||||
@org.springframework.lang.NonNullFields
|
||||
package org.springframework.data.r2dbc.config;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<Database> 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();
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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 "";
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package org.springframework.data.r2dbc.function;
|
||||
|
||||
import io.r2dbc.spi.Statement;
|
||||
|
||||
/**
|
||||
* Extension to {@link BindableOperation} for operations that allow parameter substitution for a single {@code id}
|
||||
* column that accepts either a single value or multiple values, depending on the underlying operation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @see Statement#bind
|
||||
* @see Statement#bindNull
|
||||
*/
|
||||
public interface BindIdOperation extends BindableOperation {
|
||||
|
||||
/**
|
||||
* Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
|
||||
*
|
||||
* @param statement the statement to bind the value to.
|
||||
* @param value the actual value. Must not be {@literal null}.
|
||||
* @see Statement#bind
|
||||
*/
|
||||
void bindId(Statement<?> statement, Object value);
|
||||
|
||||
/**
|
||||
* Bind the given {@code values} to the {@link Statement} using the underlying binding strategy.
|
||||
*
|
||||
* @param statement the statement to bind the value to.
|
||||
* @param values the actual values.
|
||||
* @see Statement#bind
|
||||
*/
|
||||
void bindIds(Statement<?> statement, Iterable<? extends Object> values);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<String, SettableValue> byName,
|
||||
private static void doBind(Statement<?> statement, Map<String, SettableValue> byName,
|
||||
Map<Integer, SettableValue> byIndex) {
|
||||
|
||||
byIndex.forEach((i, o) -> {
|
||||
@@ -308,13 +304,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
|
||||
|
||||
<T> SqlResult<T> exchange(String sql, BiFunction<Row, RowMetadata, T> mappingFunction) {
|
||||
|
||||
Function<Connection, Statement> executeFunction = it -> {
|
||||
Function<Connection, Statement<?>> 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;
|
||||
}
|
||||
|
||||
<R> SqlResult<R> execute(String sql, BiFunction<Row, RowMetadata, R> mappingFunction) {
|
||||
|
||||
Function<Connection, Statement> selectFunction = it -> {
|
||||
Function<Connection, Statement<?>> selectFunction = it -> {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing SQL statement [" + sql + "]");
|
||||
@@ -666,28 +634,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
|
||||
|
||||
private <R> SqlResult<R> exchange(BiFunction<Row, RowMetadata, R> mappingFunction) {
|
||||
|
||||
List<String> projectedFields;
|
||||
Set<String> 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 <R> SqlResult<R> exchange(BiFunction<Row, RowMetadata, R> mappingFunction) {
|
||||
|
||||
List<String> projectedFields;
|
||||
List<String> 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<Connection, Statement> 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<Connection, Flux<Result>> resultFunction = it -> Flux
|
||||
.from(insertFunction.apply(it).execute());
|
||||
Function<Connection, Flux<Result>> 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 <R> SqlResult<R> exchange(Object toInsert, BiFunction<Row, RowMetadata, R> mappingFunction) {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
List<SettableValue> insertValues = dataAccessStrategy.getValuesToInsert(toInsert);
|
||||
Set<String> columns = new LinkedHashSet<>();
|
||||
|
||||
List<SettableValue> 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<Connection, Statement> 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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String> getAllFields(Class<?> typeToRead) {
|
||||
public List<String> 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<String> 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<SettableValue> getInsert(Object object) {
|
||||
public List<SettableValue> 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 <T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead) {
|
||||
return new EntityRowMapper<T>((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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String, BindMarker> markers = new LinkedHashMap<>();
|
||||
private final String query;
|
||||
|
||||
DefaultBindableInsert(BindMarkers bindMarkers, String table, Collection<String> columns,
|
||||
String returningStatement) {
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
List<String> 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<String, BindMarker> markers = new LinkedHashMap<>();
|
||||
private final BindMarker idMarker;
|
||||
private final String query;
|
||||
|
||||
DefaultBindableUpdate(BindMarkers bindMarkers, String tableName, Set<String> columns, String idColumnName) {
|
||||
|
||||
this.idMarker = bindMarkers.next();
|
||||
|
||||
StringBuilder setClause = new StringBuilder();
|
||||
|
||||
for (String column : columns) {
|
||||
|
||||
BindMarker marker = markers.computeIfAbsent(column, bindMarkers::next);
|
||||
|
||||
if (setClause.length() != 0) {
|
||||
setClause.append(", ");
|
||||
}
|
||||
|
||||
setClause.append(column).append(" = ").append(marker.getPlaceholder());
|
||||
}
|
||||
|
||||
this.query = String.format("UPDATE %s SET %s WHERE %s = %s", tableName, setClause, idColumnName,
|
||||
idMarker.getPlaceholder());
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bind(Statement<?> statement, String identifier, Object value) {
|
||||
markers.get(identifier).bind(statement, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public void bindNull(Statement<?> statement, String identifier, Class<?> valueType) {
|
||||
markers.get(identifier).bindNull(statement, valueType);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bindId(Statement<?> statement, Object value) {
|
||||
idMarker.bind(statement, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
|
||||
*/
|
||||
@Override
|
||||
public void bindIds(Statement<?> statement, Iterable<? extends Object> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
|
||||
*/
|
||||
@Override
|
||||
public String toQuery() {
|
||||
return this.query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default {@link BindIdOperation} implementation for a {@code SELECT} or {@code DELETE} operation using a single key
|
||||
* in the {@code WHERE} predicate.
|
||||
*/
|
||||
static class DefaultBindIdOperation implements BindIdOperation {
|
||||
|
||||
private final BindMarker idMarker;
|
||||
private final String query;
|
||||
|
||||
DefaultBindIdOperation(BindMarkers bindMarkers, Function<BindMarker, String> queryFunction) {
|
||||
|
||||
this.idMarker = bindMarkers.next();
|
||||
this.query = queryFunction.apply(this.idMarker);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bind(Statement<?> statement, String identifier, Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public void bindNull(Statement<?> statement, String identifier, Class<?> valueType) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bindId(Statement<?> statement, Object value) {
|
||||
idMarker.bind(statement, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
|
||||
*/
|
||||
@Override
|
||||
public void bindIds(Statement<?> statement, Iterable<? extends Object> values) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
|
||||
*/
|
||||
@Override
|
||||
public String toQuery() {
|
||||
return this.query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default {@link BindIdOperation} implementation for a {@code SELECT … WHERE id IN (…)} or
|
||||
* {@code DELETE … WHERE id IN (…)}.
|
||||
*/
|
||||
static class DefaultBindIdIn implements BindIdOperation {
|
||||
|
||||
private final List<String> markers = new ArrayList<>();
|
||||
private final BindMarkers bindMarkers;
|
||||
private final String baseQuery;
|
||||
private final String idColumnName;
|
||||
|
||||
DefaultBindIdIn(BindMarkers bindMarkers, String baseQuery, String idColumnName) {
|
||||
|
||||
this.bindMarkers = bindMarkers;
|
||||
this.baseQuery = baseQuery;
|
||||
this.idColumnName = idColumnName;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bind(Statement<?> statement, String identifier, Object value) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public void bindNull(Statement<?> statement, String identifier, Class<?> valueType) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public void bindId(Statement<?> statement, Object value) {
|
||||
|
||||
BindMarker bindMarker = bindMarkers.next();
|
||||
markers.add(bindMarker.getPlaceholder());
|
||||
bindMarker.bind(statement, value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
|
||||
*/
|
||||
@Override
|
||||
public void bindIds(Statement<?> statement, Iterable<? extends Object> values) {
|
||||
|
||||
for (Object value : values) {
|
||||
bindId(statement, value);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
|
||||
*/
|
||||
@Override
|
||||
public String toQuery() {
|
||||
|
||||
if (this.markers.isEmpty()) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
String in = StringUtils.collectionToDelimitedString(this.markers, ", ");
|
||||
|
||||
return String.format("%s WHERE %s IN (%s)", this.baseQuery, this.idColumnName, in);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> {
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> getAllFields(Class<?> typeToRead);
|
||||
List<String> getAllColumns(Class<?> typeToRead);
|
||||
|
||||
/**
|
||||
* @param object
|
||||
* @return {@link SettableValue} that represent an {@code INSERT} of {@code object}.
|
||||
*/
|
||||
List<SettableValue> getInsert(Object object);
|
||||
List<SettableValue> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.r2dbc.function.convert;
|
||||
|
||||
import io.r2dbc.spi.ColumnMetadata;
|
||||
import io.r2dbc.spi.Row;
|
||||
import io.r2dbc.spi.RowMetadata;
|
||||
|
||||
@@ -26,6 +27,7 @@ import java.util.function.BiFunction;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
|
||||
import org.springframework.data.relational.core.conversion.RelationalConverter;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
|
||||
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
|
||||
@@ -41,7 +43,25 @@ public class MappingR2dbcConverter {
|
||||
|
||||
private final RelationalConverter relationalConverter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingR2dbcConverter} given {@link MappingContext}.
|
||||
*
|
||||
* @param context must not be {@literal null}.
|
||||
*/
|
||||
public MappingR2dbcConverter(
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context) {
|
||||
this(new BasicRelationalConverter(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link MappingR2dbcConverter} given {@link RelationalConverter}.
|
||||
*
|
||||
* @param converter must not be {@literal null}.
|
||||
*/
|
||||
public MappingR2dbcConverter(RelationalConverter converter) {
|
||||
|
||||
Assert.notNull(converter, "RelationalConverter must not be null!");
|
||||
|
||||
this.relationalConverter = converter;
|
||||
}
|
||||
|
||||
@@ -52,7 +72,7 @@ public class MappingR2dbcConverter {
|
||||
* @param object must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
public Map<String, SettableValue> getFieldsToUpdate(Object object) {
|
||||
public Map<String, SettableValue> 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<String, ColumnMetadata> 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<String, ColumnMetadata> createMetadataMap(RowMetadata metadata) {
|
||||
|
||||
Map<String, ColumnMetadata> columns = new LinkedHashMap<>();
|
||||
|
||||
for (ColumnMetadata column : metadata.getColumnMetadatas()) {
|
||||
columns.put(column.getName(), column);
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
public MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> getMappingContext() {
|
||||
return relationalConverter.getMappingContext();
|
||||
}
|
||||
|
||||
@@ -98,6 +98,7 @@ public class R2dbcRepositoryConfigurationExtension extends RepositoryConfigurati
|
||||
AnnotationAttributes attributes = config.getAttributes();
|
||||
|
||||
builder.addPropertyReference("databaseClient", attributes.getString("databaseClientRef"));
|
||||
builder.addPropertyReference("dataAccessStrategy", "reactiveDataAccessStrategy");
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <S> type of the bind specification.
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
class BindSpecWrapper<S extends BindSpec<S>> implements Statement<BindSpecWrapper<S>> {
|
||||
|
||||
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 <S> type of the bind spec to retain the type through {@link #getBoundOperation()}.
|
||||
* @return {@link BindSpecWrapper} for the {@link BindSpec}.
|
||||
*/
|
||||
public static <S extends BindSpec<S>> BindSpecWrapper<S> create(S bindSpec) {
|
||||
return new BindSpecWrapper<>(bindSpec);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see io.r2dbc.spi.Statement#add()
|
||||
*/
|
||||
@Override
|
||||
public BindSpecWrapper<S> add() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see io.r2dbc.spi.Statement#execute()
|
||||
*/
|
||||
@Override
|
||||
public Publisher<? extends Result> execute() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see io.r2dbc.spi.Statement#bind(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public BindSpecWrapper<S> 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<S> 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<S> 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<S> bindNull(int index, Class<?> type) {
|
||||
|
||||
this.bindSpec = bindSpec.bindNull(index, type);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the bound (final) bind specification.
|
||||
*/
|
||||
public S getBoundOperation() {
|
||||
return bindSpec;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import java.util.Optional;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.r2dbc.function.DatabaseClient;
|
||||
import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
|
||||
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod;
|
||||
@@ -57,6 +58,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
private final DatabaseClient databaseClient;
|
||||
private final MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
|
||||
private final MappingR2dbcConverter converter;
|
||||
private final ReactiveDataAccessStrategy dataAccessStrategy;
|
||||
|
||||
/**
|
||||
* Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}.
|
||||
@@ -65,13 +67,16 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public R2dbcRepositoryFactory(DatabaseClient databaseClient,
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext) {
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext,
|
||||
ReactiveDataAccessStrategy dataAccessStrategy) {
|
||||
|
||||
Assert.notNull(databaseClient, "DatabaseClient must not be null!");
|
||||
Assert.notNull(mappingContext, "MappingContext must not be null!");
|
||||
Assert.notNull(dataAccessStrategy, "ReactiveDataAccessStrategy must not be null!");
|
||||
|
||||
this.databaseClient = databaseClient;
|
||||
this.mappingContext = mappingContext;
|
||||
this.dataAccessStrategy = dataAccessStrategy;
|
||||
this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext));
|
||||
}
|
||||
|
||||
@@ -94,7 +99,8 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport {
|
||||
RelationalEntityInformation<?, ?> entityInformation = getEntityInformation(information.getDomainType(),
|
||||
information);
|
||||
|
||||
return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter);
|
||||
return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter,
|
||||
dataAccessStrategy);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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<T extends Repository<S, ID>, S, ID exten
|
||||
extends RepositoryFactoryBeanSupport<T, S, ID> {
|
||||
|
||||
private @Nullable DatabaseClient client;
|
||||
private @Nullable
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
|
||||
private @Nullable MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
|
||||
private @Nullable ReactiveDataAccessStrategy dataAccessStrategy;
|
||||
|
||||
private boolean mappingContextConfigured = false;
|
||||
|
||||
@@ -80,6 +81,10 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, 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<T extends Repository<S, ID>, S, ID exten
|
||||
*/
|
||||
protected RepositoryFactorySupport getFactoryInstance(DatabaseClient client,
|
||||
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext) {
|
||||
return new R2dbcRepositoryFactory(client, mappingContext);
|
||||
return new R2dbcRepositoryFactory(client, mappingContext, dataAccessStrategy);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -109,6 +114,7 @@ public class R2dbcRepositoryFactoryBean<T extends Repository<S, ID>, 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());
|
||||
|
||||
@@ -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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
private final @NonNull RelationalEntityInformation<T, ID> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
.flatMap(it -> it.extract(converter.populateIdIfNecessary(objectToSave)).one());
|
||||
}
|
||||
|
||||
// TODO: Extract in some kind of SQL generator
|
||||
Object id = entity.getRequiredId(objectToSave);
|
||||
Map<String, SettableValue> 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<String, SettableValue> fields = converter.getFieldsToUpdate(objectToSave);
|
||||
GenericExecuteSpec exec = databaseClient.execute().sql(update);
|
||||
|
||||
String setClause = getSetClause(fields);
|
||||
BindSpecWrapper<GenericExecuteSpec> 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<String, ?> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
|
||||
Assert.notNull(id, "Id must not be null!");
|
||||
|
||||
// TODO: Generate proper SQL (select, where clause, parameter binding).
|
||||
return databaseClient.execute()
|
||||
.sql(String.format("SELECT * FROM %s WHERE %s = $1", entity.getTableName(), getIdColumnName())) //
|
||||
.bind("$1", id) //
|
||||
.as(entity.getJavaType()) //
|
||||
Set<String> 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<GenericExecuteSpec> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
|
||||
Assert.notNull(id, "Id must not be null!");
|
||||
|
||||
// TODO: Generate proper SQL (select, where clause, parameter binding).
|
||||
return databaseClient.execute()
|
||||
.sql(String.format("SELECT %s FROM %s WHERE %s = $1 LIMIT 1", getIdColumnName(), entity.getTableName(),
|
||||
getIdColumnName())) //
|
||||
.bind("$1", id) //
|
||||
String idColumnName = getIdColumnName();
|
||||
BindIdOperation select = accessStrategy.selectById(entity.getTableName(), Collections.singleton(idColumnName),
|
||||
idColumnName, 10);
|
||||
|
||||
GenericExecuteSpec sql = databaseClient.execute().sql(select);
|
||||
BindSpecWrapper<GenericExecuteSpec> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
|
||||
return Flux.from(idPublisher).buffer().filter(ids -> !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<String> 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<GenericExecuteSpec> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
|
||||
Assert.notNull(id, "Id must not be null!");
|
||||
|
||||
return databaseClient.execute()
|
||||
.sql(String.format("DELETE FROM %s WHERE %s = $1", entity.getTableName(), getIdColumnName())) //
|
||||
.bind("$1", id) //
|
||||
BindIdOperation delete = accessStrategy.deleteById(entity.getTableName(), getIdColumnName());
|
||||
BindSpecWrapper<GenericExecuteSpec> wrapper = BindSpecWrapper.create(databaseClient.execute().sql(delete));
|
||||
|
||||
delete.bindId(wrapper, id);
|
||||
|
||||
return wrapper.getBoundOperation() //
|
||||
.fetch() //
|
||||
.rowsUpdated() //
|
||||
.then();
|
||||
@@ -260,12 +255,17 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
|
||||
|
||||
return Flux.from(idPublisher).buffer().filter(ids -> !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<GenericExecuteSpec> 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<T, ID> implements ReactiveCrudRepository<T, I
|
||||
* @see org.springframework.data.repository.reactive.ReactiveCrudRepository#delete(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Void> delete(T objectToDelete) {
|
||||
|
||||
Assert.notNull(objectToDelete, "Object to delete must not be null!");
|
||||
@@ -296,7 +295,6 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
|
||||
* @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(org.reactivestreams.Publisher)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Mono<Void> deleteAll(Publisher<? extends T> objectPublisher) {
|
||||
|
||||
Assert.notNull(objectPublisher, "The Object Publisher must not be null!");
|
||||
@@ -318,22 +316,19 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
|
||||
.then();
|
||||
}
|
||||
|
||||
private String getInBinding(List<ID> ids) {
|
||||
return IntStream.range(1, ids.size() + 1).mapToObj(i -> "$" + i).collect(Collectors.joining(", "));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <S extends BindSpec<?>> S bind(List<ID> 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<String, SettableValue> 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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.springframework.data.r2dbc.dialect;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import io.r2dbc.h2.H2ConnectionConfiguration;
|
||||
import io.r2dbc.h2.H2ConnectionFactory;
|
||||
import io.r2dbc.mssql.MssqlConnectionConfiguration;
|
||||
import io.r2dbc.mssql.MssqlConnectionFactory;
|
||||
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
|
||||
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import io.r2dbc.spi.ConnectionFactoryMetadata;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.reactivestreams.Publisher;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link Database}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class DatabaseUnitTests {
|
||||
|
||||
@Test // gh-20
|
||||
public void shouldResolveDatabaseType() {
|
||||
|
||||
PostgresqlConnectionFactory postgres = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder()
|
||||
.host("localhost").database("foo").username("bar").password("password").build());
|
||||
MssqlConnectionFactory mssql = new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host("localhost")
|
||||
.database("foo").username("bar").password("password").build());
|
||||
H2ConnectionFactory h2 = new H2ConnectionFactory(H2ConnectionConfiguration.builder().inMemory("mem").build());
|
||||
|
||||
assertThat(Database.findDatabase(postgres)).contains(Database.POSTGRES);
|
||||
assertThat(Database.findDatabase(mssql)).contains(Database.SQL_SERVER);
|
||||
assertThat(Database.findDatabase(h2)).contains(Database.H2);
|
||||
}
|
||||
|
||||
@Test // gh-20
|
||||
public void shouldNotResolveUnknownDatabase() {
|
||||
assertThat(Database.findDatabase(new UnknownConnectionFactory())).isEmpty();
|
||||
}
|
||||
|
||||
static class UnknownConnectionFactory implements ConnectionFactory {
|
||||
|
||||
@Override
|
||||
public Publisher<? extends Connection> create() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConnectionFactoryMetadata getMetadata() {
|
||||
return () -> "foo";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>id integer (primary key), not null</li>
|
||||
* <li>name varchar(255), nullable</li>
|
||||
* <li>manual integer, nullable</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>id integer (primary key), not null</li>
|
||||
* <li>name varchar(255), nullable</li>
|
||||
* <li>manual integer, nullable</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<Integer> 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<Integer> integerFlux = databaseClient.execute()
|
||||
.sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") //
|
||||
Mono<Integer> 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<Long> transactionIds = new ArrayBlockingQueue<>(5);
|
||||
TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
|
||||
|
||||
Flux<Long> txId = databaseClient.execute().sql("SELECT txid_current();").exchange()
|
||||
Flux<Long> txId = databaseClient.execute().sql(getCurrentTransactionIdStatement()).exchange()
|
||||
.flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all());
|
||||
|
||||
Mono<Void> then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() //
|
||||
@@ -144,10 +185,10 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio
|
||||
|
||||
Flux<Integer> 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<Long> transactionIds = databaseClient.inTransaction(db -> {
|
||||
Flux<Object> transactionIds = databaseClient.inTransaction(db -> {
|
||||
|
||||
Flux<Long> txId = db.execute().sql("SELECT txid_current();").exchange()
|
||||
.flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all());
|
||||
Flux<Object> txId = db.execute().sql(getCurrentTransactionIdStatement()).exchange()
|
||||
.flatMapMany(it -> it.extract((r, md) -> r.get(0)).all());
|
||||
return txId.concatWith(txId);
|
||||
});
|
||||
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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();";
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();";
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>id integer (primary key), not null, auto-increment</li>
|
||||
* <li>name varchar(255), nullable</li>
|
||||
* <li>manual integer, nullable</li>
|
||||
* </ul>
|
||||
*
|
||||
* @return the CREATE TABLE statement for table {@code legoset} with three columns.
|
||||
*/
|
||||
protected abstract String getCreateTableStatement();
|
||||
|
||||
protected abstract Class<? extends LegoSetRepository> 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<Map<String, Object>> 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<Map<String, Object>> 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<String, Object> count = jdbc.queryForMap("SELECT count(*) FROM repo_legoset");
|
||||
Map<String, Object> count = jdbc.queryForMap("SELECT count(*) FROM legoset");
|
||||
assertThat(count).containsEntry("count", 2L);
|
||||
}
|
||||
|
||||
@NoRepositoryBean
|
||||
interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
|
||||
|
||||
@Query("SELECT * FROM repo_legoset WHERE name like $1")
|
||||
Flux<LegoSet> findByNameContains(String name);
|
||||
|
||||
@Query("SELECT * FROM repo_legoset")
|
||||
Flux<Named> findAsProjection();
|
||||
|
||||
@Query("SELECT * FROM repo_legoset WHERE manual = $1")
|
||||
Mono<LegoSet> findByManual(int manual);
|
||||
}
|
||||
|
||||
@Data
|
||||
@Table("repo_legoset")
|
||||
@Table("legoset")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
static class LegoSet {
|
||||
@@ -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<? extends LegoSetRepository> getRepositoryInterfaceType() {
|
||||
return PostgresLegoSetRepository.class;
|
||||
}
|
||||
|
||||
interface PostgresLegoSetRepository extends LegoSetRepository {
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM legoset WHERE name like $1")
|
||||
Flux<LegoSet> findByNameContains(String name);
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM legoset")
|
||||
Flux<Named> findAsProjection();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM legoset WHERE manual = $1")
|
||||
Mono<LegoSet> findByManual(int manual);
|
||||
}
|
||||
}
|
||||
@@ -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<? extends LegoSetRepository> 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<LegoSet> findByNameContains(String name);
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM legoset")
|
||||
Flux<Named> findAsProjection();
|
||||
|
||||
@Override
|
||||
@Query("SELECT * FROM legoset WHERE manual = @P0")
|
||||
Mono<LegoSet> findByManual(int manual);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<LegoSet, Integer> 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<LegoSet>) 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:
|
||||
* <ul>
|
||||
* <li>id integer (primary key), not null, auto-increment</li>
|
||||
* <li>name varchar(255), nullable</li>
|
||||
* <li>manual integer, nullable</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<String, Object> map = jdbc.queryForMap("SELECT * FROM repo_legoset");
|
||||
Map<String, Object> 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<String, Object> map = jdbc.queryForMap("SELECT * FROM repo_legoset");
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<Integer> 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<Integer> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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 {
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Person, Long> 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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user