#2 - Add DatabaseClient.

Provide DatabaseClient and add ExternalDatabase class to provide database connection details/encapsulate a dockerized testcontainer.
This commit is contained in:
Mark Paluch
2018-06-04 16:03:34 +02:00
parent 8cc1535d4d
commit c59e9a58eb
13 changed files with 2548 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
/*
* 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.jdbc.core.function;
import io.r2dbc.spi.ColumnMetadata;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import java.util.Collection;
import java.util.Map;
import java.util.function.BiFunction;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedCaseInsensitiveMap;
/**
* {@link RowMapper} implementation that creates a {@link Map} for each row, representing all columns as key-value
* pairs: one entry for each column, with the column name as key.
* <p>
* The {@link Map} implementation to use and the key to use for each column in the column Map can be customized through
* overriding {@link #createColumnMap} and {@link #getColumnKey}, respectively.
* <p>
* <b>Note:</b> By default, {@link ColumnMapRowMapper} will try to build a linked {@link Map} with case-insensitive
* keys, to preserve column order as well as allow any casing to be used for column names. This requires Commons
* Collections on the classpath (which will be autodetected). Else, the fallback is a standard linked
* {@link java.util.HashMap}, which will still preserve column order but requires the application to specify the column
* names in the same casing as exposed by the driver.
*
* @author Mark Paluch
*/
public class ColumnMapRowMapper implements BiFunction<Row, RowMetadata, Map<String, Object>> {
public final static ColumnMapRowMapper INSTANCE = new ColumnMapRowMapper();
@Override
public Map<String, Object> apply(Row row, RowMetadata rowMetadata) {
Collection<? extends ColumnMetadata> columns = IterableUtils.toCollection(rowMetadata.getColumnMetadatas());
int columnCount = columns.size();
Map<String, Object> mapOfColValues = createColumnMap(columnCount);
int index = 0;
for (ColumnMetadata column : columns) {
String key = getColumnKey(column.getName());
Object obj = getColumnValue(row, index++);
mapOfColValues.put(key, obj);
}
return mapOfColValues;
}
/**
* Create a {@link Map} instance to be used as column map.
* <p>
* By default, a linked case-insensitive Map will be created.
*
* @param columnCount the column count, to be used as initial capacity for the Map.
* @return the new {@link Map} instance.
* @see LinkedCaseInsensitiveMap
*/
protected Map<String, Object> createColumnMap(int columnCount) {
return new LinkedCaseInsensitiveMap<>(columnCount);
}
/**
* Determine the key to use for the given column in the column {@link Map}.
*
* @param columnName the column name as returned by the {@link Row}.
* @return the column key to use.
* @see ColumnMetadata#getName()
*/
protected String getColumnKey(String columnName) {
return columnName;
}
/**
* Retrieve a R2DBC object value for the specified column.
* <p>
* The default implementation uses the {@link Row#get(Object, Class)} method.
*
* @param row is the {@link Row} holding the data.
* @param index is the column index.
* @return the Object returned.
*/
@Nullable
protected Object getColumnValue(Row row, int index) {
return row.get(index, Object.class);
}
}

View File

@@ -0,0 +1,451 @@
/*
* 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.jdbc.core.function;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
/**
* A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides
* a higher level, common API over R2DBC client libraries.
* <p>
* Use one of the static factory methods {@link #create(ConnectionFactory)} or obtain a {@link DatabaseClient#builder()}
* to create an instance.
*
* @author Mark Paluch
*/
public interface DatabaseClient {
/**
* Prepare an SQL call returning a result.
*/
SqlSpec execute();
/**
* Prepare an SQL SELECT call.
*/
SelectFromSpec select();
/**
* Prepare an SQL INSERT call.
*/
InsertIntoSpec insert();
/**
* Return a builder to mutate properties of this database client.
*/
DatabaseClient.Builder mutate();
// Static, factory methods
/**
* A variant of {@link #create()} that accepts a {@link io.r2dbc.spi.ConnectionFactory}
*/
static DatabaseClient create(ConnectionFactory factory) {
return new DefaultDatabaseClientBuilder().connectionFactory(factory).build();
}
/**
* Obtain a {@code WebClient} builder.
*/
static DatabaseClient.Builder builder() {
return new DefaultDatabaseClientBuilder();
}
/**
* A mutable builder for creating a {@link DatabaseClient}.
*/
interface Builder {
/**
* Configures the {@link ConnectionFactory R2DBC connector}.
*
* @param factory must not be {@literal null}.
* @return {@code this} {@link Builder}.
*/
Builder connectionFactory(ConnectionFactory factory);
/**
* Configures a {@link SQLExceptionTranslator}.
*
* @param exceptionTranslator must not be {@literal null}.
* @return {@code this} {@link Builder}.
*/
Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator);
/**
* Configures a {@link ReactiveDataAccessStrategy}.
*
* @param accessStrategy must not be {@literal null}.
* @return {@code this} {@link Builder}.
*/
Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy);
/**
* Configures a {@link Consumer} to configure this builder.
*
* @param builderConsumer must not be {@literal null}.
* @return {@code this} {@link Builder}.
*/
Builder apply(Consumer<Builder> builderConsumer);
/**
* Builder the {@link DatabaseClient} instance.
*/
DatabaseClient build();
}
/**
* Contract for specifying a SQL call along with options leading to the exchange.
*/
interface SqlSpec {
/**
* Specify a static {@code sql} string to execute.
*
* @param sql must not be {@literal null} or empty.
* @return a new {@link GenericExecuteSpec}.
*/
GenericExecuteSpec sql(String sql);
/**
* Specify a static {@link Supplier SQL supplier} that provides SQL to execute.
*
* @param sqlSupplier must not be {@literal null}.
* @return a new {@link GenericExecuteSpec}.
*/
GenericExecuteSpec sql(Supplier<String> sqlSupplier);
}
/**
* Contract for specifying a SQL call along with options leading to the exchange.
*/
interface GenericExecuteSpec extends BindSpec<GenericExecuteSpec> {
/**
* Define the target type the result should be mapped to. <br />
* Skip this step if you are anyway fine with the default conversion.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
*/
<R> TypedExecuteSpec<R> as(Class<R> resultType);
/**
* Perform the SQL call and retrieve the result.
*/
FetchSpec<Map<String, Object>> fetch();
/**
* Perform the SQL request and return a {@link SqlResult}.
*
* @return a {@code Mono} for the result
*/
Mono<SqlResult<Map<String, Object>>> exchange();
}
/**
* Contract for specifying a SQL call along with options leading to the exchange.
*/
interface TypedExecuteSpec<T> extends BindSpec<TypedExecuteSpec<T>> {
/**
* Define the target type the result should be mapped to. <br />
* Skip this step if you are anyway fine with the default conversion.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
*/
<R> TypedExecuteSpec<R> as(Class<R> resultType);
/**
* Perform the SQL call and retrieve the result.
*/
FetchSpec<T> fetch();
/**
* Perform the SQL request and return a {@link SqlResult}.
*
* @return a {@code Mono} for the result
*/
Mono<SqlResult<T>> exchange();
}
/**
* Contract for specifying {@code SELECT} options leading to the exchange.
*/
interface SelectFromSpec {
GenericSelectSpec from(String table);
<T> TypedSelectSpec<T> from(Class<T> table);
}
/**
* Contract for specifying {@code SELECT} options leading to the exchange.
*/
interface InsertIntoSpec {
/**
* Specify the target {@literal table} to insert into.
*
* @param table must not be {@literal null} or empty.
* @return
*/
GenericInsertSpec into(String table);
<T> TypedInsertSpec<T> into(Class<T> table);
}
/**
* Contract for specifying {@code SELECT} options leading to the exchange.
*/
interface GenericSelectSpec extends SelectSpec<GenericSelectSpec> {
/**
* Define the target type the result should be mapped to. <br />
* Skip this step if you are anyway fine with the default conversion.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
*/
<R> TypedSelectSpec<R> as(Class<R> resultType);
/**
* Perform the SQL call and retrieve the result.
*/
FetchSpec<Map<String, Object>> fetch();
/**
* Perform the SQL request and return a {@link SqlResult}.
*
* @return a {@code Mono} for the result
*/
Mono<SqlResult<Map<String, Object>>> exchange();
}
/**
* Contract for specifying {@code SELECT} options leading to the exchange.
*/
interface TypedSelectSpec<T> extends SelectSpec<TypedSelectSpec<T>> {
/**
* Define the target type the result should be mapped to. <br />
* Skip this step if you are anyway fine with the default conversion.
*
* @param resultType must not be {@literal null}.
* @param <R> result type.
*/
<R> TypedSelectSpec<R> as(Class<R> resultType);
/**
* Configure a result mapping {@link java.util.function.Function}.
*
* @param mappingFunction must not be {@literal null}.
* @param <R> result type.
* @return
*/
<R> TypedSelectSpec<R> extract(BiFunction<Row, RowMetadata, R> mappingFunction);
/**
* Perform the SQL call and retrieve the result.
*/
FetchSpec<T> fetch();
/**
* Perform the SQL request and return a {@link SqlResult}.
*
* @return a {@code Mono} for the result
*/
Mono<SqlResult<T>> exchange();
}
/**
* Contract for specifying {@code SELECT} options leading to the exchange.
*/
interface SelectSpec<S extends SelectSpec<S>> {
S project(String... selectedFields);
S where(Object criteriaDefinition);
S orderBy(Sort sort);
S page(Pageable page);
}
/**
* Contract for specifying {@code INSERT} options leading to the exchange.
*/
interface GenericInsertSpec extends InsertSpec {
/**
* Specify a field and non-{@literal null} value to insert.
*
* @param field must not be {@literal null} or empty.
* @param value must not be {@literal null}
*/
GenericInsertSpec value(String field, Object value);
/**
* Specify a {@literal null} value to insert.
*
* @param field must not be {@literal null} or empty.
*/
GenericInsertSpec nullValue(String field);
}
/**
* Contract for specifying {@code SELECT} options leading the exchange.
*/
interface TypedInsertSpec<T> {
/**
* Insert the given {@code objectToInsert}.
*
* @param objectToInsert
* @return
*/
InsertSpec using(T objectToInsert);
/**
* Use the given {@code tableName} as insert target.
*
* @param tableName must not be {@literal null} or empty.
* @return
*/
TypedInsertSpec<T> table(String tableName);
/**
* Insert the given {@link Publisher} to insert one or more objects.
*
* @param objectToInsert
* @return
*/
InsertSpec using(Publisher<T> objectToInsert);
}
/**
* Contract for specifying {@code INSERT} options leading to the exchange.
*/
interface InsertSpec {
/**
* Perform the SQL call.
*/
Mono<Void> then();
/**
* Perform the SQL request and return a {@link SqlResult}.
*
* @return a {@code Mono} for the result
*/
Mono<SqlResult<Map<String, Object>>> exchange();
}
/**
* Contract for specifying parameter bindings.
*/
interface BindSpec<S extends BindSpec<S>> {
/**
* Bind a non-{@literal null} value to a parameter identified by its {@code index}.
*
* @param index
* @param value must not be {@literal null}.
*/
S bind(int index, Object value);
/**
* Bind a {@literal null} value to a parameter identified by its {@code index}.
*
* @param index
*/
S bindNull(int index);
/**
* Bind a non-{@literal null} value to a parameter identified by its {@code name}.
*
* @param name must not be {@literal null} or empty.
* @param value must not be {@literal null}.
*/
S bind(String name, Object value);
/**
* Bind a {@literal null} value to a parameter identified by its {@code name}.
*
* @param name must not be {@literal null} or empty.
*/
S bindNull(String name);
/**
* Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names.
*
* @param bean must not be {@literal null}.
*/
S bind(Object bean);
}
/**
* Contract for fetching results.
*/
interface FetchSpec<T> {
/**
* Get exactly zero or one result.
*
* @return {@link Mono#empty()} if no match found. Never {@literal null}.
* @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found.
*/
Mono<T> one();
/**
* Get the first or no result.
*
* @return {@link Mono#empty()} if no match found. Never {@literal null}.
*/
Mono<T> first();
/**
* Get all matching elements.
*
* @return never {@literal null}.
*/
Flux<T> all();
/**
* Get the number of updated rows.
*
* @return {@link Mono} emitting the number of updated rows. Never {@literal null}.
*/
Mono<Integer> rowsUpdated();
}
}

View File

@@ -0,0 +1,793 @@
/*
* 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.jdbc.core.function;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Result;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
import io.r2dbc.spi.Statement;
import lombok.RequiredArgsConstructor;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.SQLException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
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;
import org.reactivestreams.Publisher;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.dao.UncategorizedDataAccessException;
import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy;
import org.springframework.data.util.Pair;
import org.springframework.jdbc.core.SqlProvider;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Default implementation of {@link DatabaseClient}.
*
* @author Mark Paluch
*/
class DefaultDatabaseClient implements DatabaseClient {
/** Logger available to subclasses */
protected final Log logger = LogFactory.getLog(getClass());
private final ConnectionFactory connector;
private final SQLExceptionTranslator exceptionTranslator;
private final ReactiveDataAccessStrategy dataAccessStrategy;
private final DefaultDatabaseClientBuilder builder;
public DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator,
ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) {
this.connector = connector;
this.exceptionTranslator = exceptionTranslator;
this.dataAccessStrategy = dataAccessStrategy;
this.builder = builder;
}
@Override
public Builder mutate() {
return builder;
}
@Override
public SqlSpec execute() {
return new DefaultSqlSpec();
}
@Override
public SelectFromSpec select() {
throw new UnsupportedOperationException("Implement me");
}
@Override
public InsertIntoSpec insert() {
return new DefaultInsertIntoSpec();
}
public <T> Flux<T> execute(Function<Connection, Flux<T>> action) throws DataAccessException {
Assert.notNull(action, "Callback object must not be null");
Mono<Connection> connectionMono = Mono.from(obtainConnectionFactory().create());
// Create close-suppressing Connection proxy, also preparing returned Statements.
return connectionMono.flatMapMany(connection -> {
Connection connectionToUse = createConnectionProxy(connection);
// TODO: Release connection
return doInConnection(action, connectionToUse);
}).onErrorMap(SQLException.class, ex -> {
String sql = getSql(action);
return translateException("ConnectionCallback", sql, ex);
});
}
/**
* Obtain the {@link ConnectionFactory} for actual use.
*
* @return the ConnectionFactory (never {@code null})
* @throws IllegalStateException in case of no DataSource set
*/
protected ConnectionFactory obtainConnectionFactory() {
return connector;
}
/**
* Create a close-suppressing proxy for the given R2DBC Connection. Called by the {@code execute} method.
*
* @param con the R2DBC Connection to create a proxy for
* @return the Connection proxy
*/
protected Connection createConnectionProxy(Connection con) {
return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(),
new Class<?>[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(con));
}
/**
* Translate the given {@link SQLException} into a generic {@link DataAccessException}.
*
* @param task readable text describing the task being attempted
* @param sql SQL query or update that caused the problem (may be {@code null})
* @param ex the offending {@code SQLException}
* @return a DataAccessException wrapping the {@code SQLException} (never {@code null})
*/
protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) {
DataAccessException dae = exceptionTranslator.translate(task, sql, ex);
return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex));
}
static void doBind(Statement statement, Map<String, Optional<Object>> byName,
Map<Integer, Optional<Object>> byIndex) {
byIndex.forEach((i, o) -> {
if (o.isPresent()) {
o.ifPresent(v -> statement.bind(i, v));
} else {
statement.bindNull(i, 0); // TODO: What is type?
}
});
byName.forEach((name, o) -> {
if (o.isPresent()) {
o.ifPresent(v -> statement.bind(name, v));
} else {
statement.bindNull(name, 0); // TODO: What is type?
}
});
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlSpec} implementation.
*/
private class DefaultSqlSpec implements SqlSpec {
@Override
public GenericExecuteSpec sql(String sql) {
Assert.hasText(sql, "SQL must not be null or empty!");
return sql(() -> sql);
}
@Override
public GenericExecuteSpec sql(Supplier<String> sqlSupplier) {
Assert.notNull(sqlSupplier, "SQL Supplier must not be null!");
return new DefaultGenericExecuteSpec(sqlSupplier);
}
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
*/
@RequiredArgsConstructor
private class GenericExecuteSpecSupport {
final Map<Integer, Optional<Object>> byIndex;
final Map<String, Optional<Object>> byName;
final Supplier<String> sqlSupplier;
GenericExecuteSpecSupport(Supplier<String> sqlSupplier) {
this.byIndex = Collections.emptyMap();
this.byName = Collections.emptyMap();
this.sqlSupplier = sqlSupplier;
}
protected String getSql() {
String sql = sqlSupplier.get();
Assert.state(sql != null, "SQL supplier returned null!");
return sql;
}
<T> Mono<SqlResult<T>> exchange(String sql, BiFunction<Row, RowMetadata, T> mappingFunction) {
return execute(it -> {
Statement statement = it.createStatement(sql);
doBind(statement, byName, byIndex);
return Flux
.just((SqlResult<T>) new DefaultSqlResult<>(sql, Flux.from(statement.add().execute()), mappingFunction));
}).next();
}
public GenericExecuteSpecSupport bind(int index, Object value) {
Map<Integer, Optional<Object>> byIndex = new LinkedHashMap<>(this.byIndex);
byIndex.put(index, Optional.of(value));
return createInstance(byIndex, this.byName, this.sqlSupplier);
}
public GenericExecuteSpecSupport bindNull(int index) {
Map<Integer, Optional<Object>> byIndex = new LinkedHashMap<>(this.byIndex);
byIndex.put(index, Optional.empty());
return createInstance(byIndex, this.byName, this.sqlSupplier);
}
public GenericExecuteSpecSupport bind(String name, Object value) {
Assert.hasText(name, "Parameter name must not be null or empty!");
Map<String, Optional<Object>> byName = new LinkedHashMap<>(this.byName);
byName.put(name, Optional.of(value));
return createInstance(this.byIndex, byName, this.sqlSupplier);
}
public GenericExecuteSpecSupport bindNull(String name) {
Assert.hasText(name, "Parameter name must not be null or empty!");
Map<String, Optional<Object>> byName = new LinkedHashMap<>(this.byName);
byName.put(name, Optional.empty());
return createInstance(this.byIndex, byName, this.sqlSupplier);
}
protected GenericExecuteSpecSupport createInstance(Map<Integer, Optional<Object>> byIndex,
Map<String, Optional<Object>> byName, Supplier<String> sqlSupplier) {
return new GenericExecuteSpecSupport(byIndex, byName, sqlSupplier);
}
public GenericExecuteSpecSupport bind(Object bean) {
Assert.notNull(bean, "Bean must not be null!");
throw new UnsupportedOperationException("Implement me!");
}
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
*/
private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec {
DefaultGenericExecuteSpec(Map<Integer, Optional<Object>> byIndex, Map<String, Optional<Object>> byName,
Supplier<String> sqlSupplier) {
super(byIndex, byName, sqlSupplier);
}
DefaultGenericExecuteSpec(Supplier<String> sqlSupplier) {
super(sqlSupplier);
}
@Override
public <R> TypedExecuteSpec<R> as(Class<R> resultType) {
Assert.notNull(resultType, "Result type must not be null!");
return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType);
}
@Override
public FetchSpec<Map<String, Object>> fetch() {
String sql = getSql();
return new DefaultFetchSpec<>(sql, exchange(sql, ColumnMapRowMapper.INSTANCE).flatMapMany(SqlResult::all),
exchange(sql, ColumnMapRowMapper.INSTANCE).flatMap(FetchSpec::rowsUpdated));
}
@Override
public Mono<SqlResult<Map<String, Object>>> exchange() {
return exchange(getSql(), ColumnMapRowMapper.INSTANCE);
}
@Override
public DefaultGenericExecuteSpec bind(int index, Object value) {
return (DefaultGenericExecuteSpec) super.bind(index, value);
}
@Override
public DefaultGenericExecuteSpec bindNull(int index) {
return (DefaultGenericExecuteSpec) super.bindNull(index);
}
@Override
public DefaultGenericExecuteSpec bind(String name, Object value) {
return (DefaultGenericExecuteSpec) super.bind(name, value);
}
@Override
public DefaultGenericExecuteSpec bindNull(String name) {
return (DefaultGenericExecuteSpec) super.bindNull(name);
}
@Override
public DefaultGenericExecuteSpec bind(Object bean) {
return (DefaultGenericExecuteSpec) super.bind(bean);
}
@Override
protected GenericExecuteSpecSupport createInstance(Map<Integer, Optional<Object>> byIndex,
Map<String, Optional<Object>> byName, Supplier<String> sqlSupplier) {
return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier);
}
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation.
*/
@SuppressWarnings("unchecked")
private class DefaultTypedGenericExecuteSpec<T> extends GenericExecuteSpecSupport implements TypedExecuteSpec<T> {
private final Class<T> typeToRead;
private final BiFunction<Row, RowMetadata, T> mappingFunction;
DefaultTypedGenericExecuteSpec(Map<Integer, Optional<Object>> byIndex, Map<String, Optional<Object>> byName,
Supplier<String> sqlSupplier, Class<T> typeToRead) {
super(byIndex, byName, sqlSupplier);
this.typeToRead = typeToRead;
this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead);
}
@Override
public <R> TypedExecuteSpec<R> as(Class<R> resultType) {
Assert.notNull(resultType, "Result type must not be null!");
return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType);
}
@Override
public FetchSpec<T> fetch() {
String sql = getSql();
return new DefaultFetchSpec<>(sql, exchange(sql, mappingFunction).flatMapMany(SqlResult::all),
exchange(sql, mappingFunction).flatMap(FetchSpec::rowsUpdated));
}
@Override
public Mono<SqlResult<T>> exchange() {
return exchange(getSql(), mappingFunction);
}
@Override
public DefaultTypedGenericExecuteSpec<T> bind(int index, Object value) {
return (DefaultTypedGenericExecuteSpec<T>) super.bind(index, value);
}
@Override
public DefaultTypedGenericExecuteSpec<T> bindNull(int index) {
return (DefaultTypedGenericExecuteSpec<T>) super.bindNull(index);
}
@Override
public DefaultTypedGenericExecuteSpec<T> bind(String name, Object value) {
return (DefaultTypedGenericExecuteSpec) super.bind(name, value);
}
@Override
public DefaultTypedGenericExecuteSpec<T> bindNull(String name) {
return (DefaultTypedGenericExecuteSpec<T>) super.bindNull(name);
}
@Override
public DefaultTypedGenericExecuteSpec<T> bind(Object bean) {
return (DefaultTypedGenericExecuteSpec<T>) super.bind(bean);
}
@Override
protected DefaultTypedGenericExecuteSpec<T> createInstance(Map<Integer, Optional<Object>> byIndex,
Map<String, Optional<Object>> byName, Supplier<String> sqlSupplier) {
return new DefaultTypedGenericExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead);
}
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation.
*/
class DefaultInsertIntoSpec implements InsertIntoSpec {
@Override
public GenericInsertSpec into(String table) {
return new DefaultGenericInsertSpec(table, Collections.emptyMap());
}
@Override
public <T> TypedInsertSpec<T> into(Class<T> table) {
return new DefaultTypedInsertSpec<>(table);
}
}
/**
* Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericInsertSpec}.
*/
@RequiredArgsConstructor
class DefaultGenericInsertSpec implements GenericInsertSpec {
private final String table;
private final Map<String, Optional<Object>> byName;
@Override
public GenericInsertSpec value(String field, Object value) {
Assert.notNull(field, "Field must not be null!");
Map<String, Optional<Object>> byName = new LinkedHashMap<>(this.byName);
byName.put(field, Optional.of(value));
return new DefaultGenericInsertSpec(this.table, byName);
}
@Override
public GenericInsertSpec nullValue(String field) {
Assert.notNull(field, "Field must not be null!");
Map<String, Optional<Object>> byName = new LinkedHashMap<>(this.byName);
byName.put(field, Optional.empty());
return new DefaultGenericInsertSpec(this.table, byName);
}
@Override
public Mono<Void> then() {
return exchange().flatMapMany(FetchSpec::all).then();
}
@Override
public Mono<SqlResult<Map<String, Object>>> exchange() {
if (byName.isEmpty()) {
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(","));
builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
.append(placeholders).append(")");
return execute(it -> {
String sql = builder.toString();
Statement statement = it.createStatement(sql);
AtomicInteger index = new AtomicInteger();
for (Optional<Object> o : byName.values()) {
if (o.isPresent()) {
o.ifPresent(v -> statement.bind(index.getAndIncrement(), v));
} else {
statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type?
}
}
SqlResult<Map<String, Object>> result = new DefaultSqlResult<>(sql,
Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE);
return Flux.just(result);
}).next();
}
}
/**
* Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}.
*/
@RequiredArgsConstructor
class DefaultTypedInsertSpec<T> implements TypedInsertSpec<T>, InsertSpec {
private final Class<?> typeToInsert;
private final String table;
private final Publisher<T> objectToInsert;
public DefaultTypedInsertSpec(Class<?> typeToInsert) {
this.typeToInsert = typeToInsert;
this.table = dataAccessStrategy.getTableName(typeToInsert);
this.objectToInsert = Mono.empty();
}
@Override
public TypedInsertSpec<T> table(String tableName) {
Assert.hasText(tableName, "Table name must not be null or empty!");
return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert);
}
@Override
public InsertSpec using(T objectToInsert) {
Assert.notNull(objectToInsert, "Object to insert must not be null!");
return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert));
}
@Override
public InsertSpec using(Publisher<T> objectToInsert) {
Assert.notNull(objectToInsert, "Publisher to insert must not be null!");
return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert);
}
@Override
public Mono<Void> then() {
return exchange().flatMapMany(FetchSpec::all).then();
}
@Override
public Mono<SqlResult<Map<String, Object>>> exchange() {
return Mono.from(objectToInsert).flatMap(toInsert -> {
StringBuilder builder = new StringBuilder();
List<Pair<String, Object>> insertValues = dataAccessStrategy.getInsert(toInsert);
String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(","));
String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1))
.collect(Collectors.joining(","));
builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(")
.append(placeholders).append(")");
return execute(it -> {
String sql = builder.toString();
Statement statement = it.createStatement(sql);
AtomicInteger index = new AtomicInteger();
for (Pair<String, Object> pair : insertValues) {
if (pair.getSecond() != null) { // TODO: Better type to transport null values.
statement.bind(index.getAndIncrement(), pair.getSecond());
} else {
statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type?
}
}
SqlResult<Map<String, Object>> result = new DefaultSqlResult<>(sql,
Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE);
return Flux.just(result);
}).next();
});
}
}
/**
* Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlResult} implementation.
*/
static class DefaultSqlResult<T> implements SqlResult<T> {
private final String sql;
private final Flux<Result> result;
private final FetchSpec<T> fetchSpec;
DefaultSqlResult(String sql, Flux<Result> result, BiFunction<Row, RowMetadata, T> mappingFunction) {
this.sql = sql;
this.result = result;
this.fetchSpec = new DefaultFetchSpec<>(sql, result.flatMap(it -> it.map(mappingFunction)),
result.flatMap(Result::getRowsUpdated).next());
}
@Override
public <R> SqlResult<R> extract(BiFunction<Row, RowMetadata, R> mappingFunction) {
return new DefaultSqlResult<>(sql, result, mappingFunction);
}
@Override
public Mono<T> one() {
return fetchSpec.one();
}
@Override
public Mono<T> first() {
return fetchSpec.first();
}
@Override
public Flux<T> all() {
return fetchSpec.all();
}
@Override
public Mono<Integer> rowsUpdated() {
return fetchSpec.rowsUpdated();
}
}
@RequiredArgsConstructor
static class DefaultFetchSpec<T> implements FetchSpec<T> {
private final String sql;
private final Flux<T> result;
private final Mono<Integer> updatedRows;
@Override
public Mono<T> one() {
return all().buffer(2) //
.flatMap(it -> {
if (it.isEmpty()) {
return Mono.empty();
}
if (it.size() > 1) {
return Mono.error(new IncorrectResultSizeDataAccessException(
String.format("Query [%s] returned non unique result.", this.sql), 1));
}
return Mono.just(it.get(0));
}).next();
}
@Override
public Mono<T> first() {
return all().next();
}
@Override
public Flux<T> all() {
return result;
}
@Override
public Mono<Integer> rowsUpdated() {
return updatedRows;
}
}
private static <T> Flux<T> doInConnection(Function<Connection, Flux<T>> action, Connection it) {
try {
return action.apply(it);
} catch (RuntimeException e) {
String sql = getSql(action);
return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {});
}
}
/**
* Determine SQL from potential provider object.
*
* @param sqlProvider object that's potentially a SqlProvider
* @return the SQL string, or {@code null}
* @see SqlProvider
*/
@Nullable
private static String getSql(Object sqlProvider) {
if (sqlProvider instanceof SqlProvider) {
return ((SqlProvider) sqlProvider).getSql();
} else {
return null;
}
}
/**
* Invocation handler that suppresses close calls on R2DBC Connections. Also prepares returned Statement
* (Prepared/CallbackStatement) objects.
*
* @see Connection#close()
*/
private class CloseSuppressingInvocationHandler implements InvocationHandler {
private final Connection target;
CloseSuppressingInvocationHandler(Connection target) {
this.target = target;
}
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Invocation on ConnectionProxy interface coming in...
if (method.getName().equals("equals")) {
// Only consider equal when proxies are identical.
return (proxy == args[0]);
} else if (method.getName().equals("hashCode")) {
// Use hashCode of PersistenceManager proxy.
return System.identityHashCode(proxy);
} else if (method.getName().equals("unwrap")) {
if (((Class<?>) args[0]).isInstance(proxy)) {
return proxy;
}
} else if (method.getName().equals("isWrapperFor")) {
if (((Class<?>) args[0]).isInstance(proxy)) {
return true;
}
} else if (method.getName().equals("close")) {
// Handle close method: suppress, not valid.
return Mono.error(new UnsupportedOperationException("Close is not supported!"));
} else if (method.getName().equals("getTargetConnection")) {
// Handle getTargetConnection method: return underlying Connection.
return this.target;
}
// Invoke method on target Connection.
try {
Object retVal = method.invoke(this.target, args);
return retVal;
} catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
private static class UncategorizedSQLException extends UncategorizedDataAccessException {
/** SQL that led to the problem */
@Nullable private final String sql;
/**
* Constructor for UncategorizedSQLException.
*
* @param task name of current task
* @param sql the offending SQL statement
* @param ex the root cause
*/
public UncategorizedSQLException(String task, @Nullable String sql, Exception ex) {
super(String.format("%s; uncategorized SQLException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "",
ex.getMessage()), ex);
this.sql = sql;
}
/**
* Return the SQL that led to the problem (if known).
*/
@Nullable
public String getSql() {
return this.sql;
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.jdbc.core.function;
import java.util.function.Consumer;
import org.springframework.data.jdbc.core.function.DatabaseClient.Builder;
import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator;
import org.springframework.jdbc.support.SQLExceptionTranslator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import io.r2dbc.spi.ConnectionFactory;
/**
* Default implementation of {@link DatabaseClient.Builder}.
*
* @author Mark Paluch
*/
class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
private @Nullable ConnectionFactory connector;
private SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator();
private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy();
DefaultDatabaseClientBuilder() {}
DefaultDatabaseClientBuilder(DefaultDatabaseClientBuilder other) {
Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!");
this.connector = other.connector;
this.exceptionTranslator = exceptionTranslator;
}
@Override
public Builder connectionFactory(ConnectionFactory factory) {
Assert.notNull(factory, "ConnectionFactory must not be null!");
this.connector = factory;
return this;
}
@Override
public Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator) {
Assert.notNull(exceptionTranslator, "SQLExceptionTranslator must not be null!");
this.exceptionTranslator = exceptionTranslator;
return this;
}
@Override
public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) {
Assert.notNull(accessStrategy, "ReactiveDataAccessStrategy must not be null!");
this.accessStrategy = accessStrategy;
return this;
}
@Override
public DatabaseClient build() {
return new DefaultDatabaseClient(this.connector, exceptionTranslator, accessStrategy,
new DefaultDatabaseClientBuilder(this));
}
@Override
public DatabaseClient.Builder clone() {
return new DefaultDatabaseClientBuilder(this);
}
@Override
public DatabaseClient.Builder apply(Consumer<DatabaseClient.Builder> builderConsumer) {
Assert.notNull(builderConsumer, "BuilderConsumer must not be null");
builderConsumer.accept(this);
return this;
}
}

View File

@@ -0,0 +1,75 @@
/*
* 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.jdbc.core.function;
import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.util.Pair;
import org.springframework.util.ClassUtils;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
/**
* @author Mark Paluch
*/
public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
private final EntityInstantiators instantiators = new EntityInstantiators();
private final JdbcMappingContext mappingContext = new JdbcMappingContext();
@Override
public List<Pair<String, Object>> getInsert(Object object) {
Class<?> userClass = ClassUtils.getUserClass(object);
JdbcPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(userClass);
PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object);
List<Pair<String, Object>> values = new ArrayList<>();
for (JdbcPersistentProperty property : entity) {
Object value = propertyAccessor.getProperty(property);
if (value == null) {
continue;
}
values.add(Pair.of(property.getColumnName(), value));
}
return values;
}
@Override
public <T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead) {
return new EntityRowMapper<T>((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead),
instantiators, mappingContext);
}
@Override
public String getTableName(Class<?> type) {
return mappingContext.getRequiredPersistentEntity(type).getTableName();
}
}

View File

@@ -0,0 +1,168 @@
/*
* 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.jdbc.core.function;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.sql.ResultSet;
import java.util.function.BiFunction;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity;
import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.ParameterValueProvider;
import org.springframework.util.ClassUtils;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
/**
* Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced.
*
* @author Mark Paluch
* @since 1.0
*/
public class EntityRowMapper<T> implements BiFunction<Row, RowMetadata, T> {
private final JdbcPersistentEntity<T> entity;
private final EntityInstantiators entityInstantiators;
private final ConversionService conversions;
private final MappingContext<JdbcPersistentEntity<?>, JdbcPersistentProperty> context;
public EntityRowMapper(JdbcPersistentEntity<T> entity, EntityInstantiators entityInstantiators,
JdbcMappingContext context) {
this.entity = entity;
this.entityInstantiators = entityInstantiators;
this.conversions = context.getConversions();
this.context = context;
}
@Override
public T apply(Row row, RowMetadata metadata) {
T result = createInstance(row, "", entity);
ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result),
conversions);
for (JdbcPersistentProperty property : entity) {
if (property.isCollectionLike()) {
throw new UnsupportedOperationException();
} else if (property.isMap()) {
throw new UnsupportedOperationException();
} else {
propertyAccessor.setProperty(property, readFrom(row, property, ""));
}
}
return result;
}
/**
* Read a single value or a complete Entity from the {@link ResultSet} passed as an argument.
*
* @param row the {@link Row} to extract the value from. Must not be {@code null}.
* @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@code null}.
* @param prefix to be used for all column names accessed by this method. Must not be {@code null}.
* @return the value read from the {@link ResultSet}. May be {@code null}.
*/
private Object readFrom(Row row, JdbcPersistentProperty property, String prefix) {
try {
if (property.isEntity()) {
return readEntityFrom(row, property);
}
return row.get(prefix + property.getColumnName(), getType(property));
} catch (Exception o_O) {
throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
}
}
private Class<?> getType(JdbcPersistentProperty property) {
return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType());
}
private <S> S readEntityFrom(Row row, PersistentProperty<?> property) {
String prefix = property.getName() + "_";
@SuppressWarnings("unchecked")
JdbcPersistentEntity<S> entity = (JdbcPersistentEntity<S>) context
.getRequiredPersistentEntity(property.getActualType());
if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) {
return null;
}
S instance = createInstance(row, prefix, entity);
PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions);
for (JdbcPersistentProperty p : entity) {
propertyAccessor.setProperty(p, readFrom(row, p, prefix));
}
return instance;
}
private <S> S createInstance(Row row, String prefix, JdbcPersistentEntity<S> entity) {
return entityInstantiators.getInstantiatorFor(entity).createInstance(entity,
new RowParameterValueProvider(row, entity, conversions, prefix));
}
@RequiredArgsConstructor
private static class RowParameterValueProvider implements ParameterValueProvider<JdbcPersistentProperty> {
@NonNull private final Row resultSet;
@NonNull private final JdbcPersistentEntity<?> entity;
@NonNull private final ConversionService conversionService;
@NonNull private final String prefix;
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
*/
@Override
public <T> T getParameterValue(Parameter<T, JdbcPersistentProperty> parameter) {
String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName();
try {
return conversionService.convert(resultSet.get(column, parameter.getType().getType()),
parameter.getType().getType());
} catch (Exception o_O) {
throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O);
}
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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.jdbc.core.function;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.util.Assert;
/**
* @author Mark Paluch
*/
class IterableUtils {
static <T> Collection<T> toCollection(Iterable<T> iterable) {
Assert.notNull(iterable, "Iterable must not be null!");
if (iterable instanceof Collection) {
return (Collection<T>) iterable;
}
List<T> result = new ArrayList<>();
for (T element : iterable) {
result.add(element);
}
return result;
}
static <T> List<T> toList(Iterable<T> iterable) {
Assert.notNull(iterable, "Iterable must not be null!");
if (iterable instanceof List) {
return (List<T>) iterable;
}
List<T> result = new ArrayList<>();
for (T element : iterable) {
result.add(element);
}
return result;
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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.jdbc.core.function;
import java.util.List;
import java.util.function.BiFunction;
import org.springframework.data.util.Pair;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
/**
* @author Mark Paluch
*/
public interface ReactiveDataAccessStrategy {
List<Pair<String, Object>> getInsert(Object object);
// TODO: Broaden T to Mono<T>/Flux<T> for reactive relational data access?
<T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead);
String getTableName(Class<?> type);
}

View File

@@ -0,0 +1,31 @@
/*
* 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.jdbc.core.function;
import java.util.function.BiFunction;
import org.springframework.data.jdbc.core.function.DatabaseClient.FetchSpec;
import io.r2dbc.spi.Row;
import io.r2dbc.spi.RowMetadata;
/**
* @author Mark Paluch
*/
public interface SqlResult<T> extends FetchSpec<T> {
<R> SqlResult<R> extract(BiFunction<Row, RowMetadata, R> mappingFunction);
}

View File

@@ -0,0 +1,41 @@
/*
* 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.jdbc.core.function.connectionfactory;
import io.r2dbc.spi.Connection;
import java.sql.Wrapper;
/**
* Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target
* Connection.
* <p>
* This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. Alternatively, all
* such connections also support JDBC 4.0's {@link Connection#unwrap}.
*
* @author Mark Paluch
*/
public interface ConnectionProxy extends Connection, Wrapper {
/**
* Return the target Connection of this proxy.
* <p>
* This will typically be the native driver Connection or a wrapper from a connection pool.
*
* @return the underlying Connection (never {@code null})
*/
Connection getTargetConnection();
}