#73 - Introduce BindTarget.

We now apply bindings to a BindTarget that can be overridden without the need to implement all Statement methods.

PreparedOperation no longer has a direct dependency on R2DBC Statement.

Original pull request: #82.
This commit is contained in:
Mark Paluch
2019-05-06 13:43:42 +02:00
parent 02e0cb5026
commit 88945d7d71
25 changed files with 389 additions and 653 deletions

View File

@@ -2,6 +2,8 @@ package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import org.springframework.data.r2dbc.domain.BindTarget;
/**
* A bind marker represents a single bindable parameter within a query. Bind markers are dialect-specific and provide a
* {@link #getPlaceholder() placeholder} that is used in the actual query.
@@ -23,19 +25,19 @@ public interface BindMarker {
/**
* Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
*
* @param statement the statement to bind the value to.
* @param bindTarget the target to bind the value to.
* @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, Object value);
void bind(BindTarget bindTarget, 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 bindTarget the target to bind the value to.
* @param valueType value type, must not be {@literal null}.
* @see Statement#bindNull
*/
void bindNull(Statement statement, Class<?> valueType);
void bindNull(BindTarget bindTarget, Class<?> valueType);
}

View File

@@ -15,12 +15,12 @@
*/
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import org.springframework.data.r2dbc.domain.BindTarget;
/**
* A single indexed bind marker.
*/
public class IndexedBindMarker implements BindMarker {
class IndexedBindMarker implements BindMarker {
private final String placeholder;
@@ -42,20 +42,20 @@ public class IndexedBindMarker implements BindMarker {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(org.springframework.data.r2dbc.dialect.BindTarget, java.lang.Object)
*/
@Override
public void bind(Statement statement, Object value) {
statement.bind(this.index, value);
public void bind(BindTarget target, Object value) {
target.bind(this.index, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(org.springframework.data.r2dbc.dialect.BindTarget, java.lang.Class)
*/
@Override
public void bindNull(Statement statement, Class<?> valueType) {
statement.bindNull(this.index, valueType);
public void bindNull(BindTarget target, Class<?> valueType) {
target.bindNull(this.index, valueType);
}

View File

@@ -1,10 +1,9 @@
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
import java.util.function.Function;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.util.Assert;
/**
@@ -98,20 +97,20 @@ class NamedBindMarkers implements BindMarkers {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(org.springframework.data.r2dbc.dialect.BindTarget, java.lang.Object)
*/
@Override
public void bind(Statement statement, Object value) {
statement.bind(this.identifier, value);
public void bind(BindTarget target, Object value) {
target.bind(this.identifier, value);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
* @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(org.springframework.data.r2dbc.dialect.BindTarget, java.lang.Class)
*/
@Override
public void bindNull(Statement statement, Class<?> valueType) {
statement.bindNull(this.identifier, valueType);
public void bindNull(BindTarget target, Class<?> valueType) {
target.bindNull(this.identifier, valueType);
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2019 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
*
* https://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.domain;
/**
* Target to apply bindings to.
*
* @author Mark Paluch
* @see PreparedOperation
* @see io.r2dbc.spi.Statement#bind
* @see io.r2dbc.spi.Statement#bindNull
*/
public interface BindTarget {
/**
* Bind a value.
*
* @param identifier the identifier to bind to.
* @param value the value to bind.
*/
void bind(Object identifier, Object value);
/**
* Bind a value to an index. Indexes are zero-based.
*
* @param index the index to bind to.
* @param value the value to bind.
*/
void bind(int index, Object value);
/**
* Bind a {@code null} value.
*
* @param identifier the identifier to bind to.
* @param type the type of {@literal null} value.
*/
void bindNull(Object identifier, Class<?> type);
/**
* Bind a {@code null} value.
*
* @param index the index to bind to.
* @param type the type of {@literal null} value.
*/
void bindNull(int index, Class<?> type);
}

View File

@@ -39,7 +39,7 @@ public class OutboundRow implements Map<String, SettableValue> {
* Creates an empty {@link OutboundRow} instance.
*/
public OutboundRow() {
rowAsMap = new LinkedHashMap<>();
this.rowAsMap = new LinkedHashMap<>();
}
/**
@@ -51,7 +51,7 @@ public class OutboundRow implements Map<String, SettableValue> {
Assert.notNull(map, "Map must not be null");
rowAsMap = new LinkedHashMap<>(map);
this.rowAsMap = new LinkedHashMap<>(map);
}
/**
@@ -61,8 +61,8 @@ public class OutboundRow implements Map<String, SettableValue> {
* @param value value.
*/
public OutboundRow(String key, SettableValue value) {
rowAsMap = new LinkedHashMap<>();
rowAsMap.put(key, value);
this.rowAsMap = new LinkedHashMap<>();
this.rowAsMap.put(key, value);
}
/**
@@ -78,7 +78,7 @@ public class OutboundRow implements Map<String, SettableValue> {
* @return this
*/
public OutboundRow append(String key, SettableValue value) {
rowAsMap.put(key, value);
this.rowAsMap.put(key, value);
return this;
}
@@ -88,7 +88,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public int size() {
return rowAsMap.size();
return this.rowAsMap.size();
}
/*
@@ -97,7 +97,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public boolean isEmpty() {
return rowAsMap.isEmpty();
return this.rowAsMap.isEmpty();
}
/*
@@ -106,7 +106,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public boolean containsKey(Object key) {
return rowAsMap.containsKey(key);
return this.rowAsMap.containsKey(key);
}
/*
@@ -115,7 +115,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public boolean containsValue(Object value) {
return rowAsMap.containsValue(value);
return this.rowAsMap.containsValue(value);
}
/*
@@ -124,7 +124,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public SettableValue get(Object key) {
return rowAsMap.get(key);
return this.rowAsMap.get(key);
}
/*
@@ -133,7 +133,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public SettableValue put(String key, SettableValue value) {
return rowAsMap.put(key, value);
return this.rowAsMap.put(key, value);
}
/*
@@ -142,7 +142,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public SettableValue remove(Object key) {
return rowAsMap.remove(key);
return this.rowAsMap.remove(key);
}
/*
@@ -151,7 +151,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public void putAll(Map<? extends String, ? extends SettableValue> m) {
rowAsMap.putAll(m);
this.rowAsMap.putAll(m);
}
/*
@@ -160,7 +160,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public void clear() {
rowAsMap.clear();
this.rowAsMap.clear();
}
/*
@@ -169,7 +169,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public Set<String> keySet() {
return rowAsMap.keySet();
return this.rowAsMap.keySet();
}
/*
@@ -178,7 +178,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public Collection<SettableValue> values() {
return rowAsMap.values();
return this.rowAsMap.values();
}
/*
@@ -187,7 +187,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public Set<Entry<String, SettableValue>> entrySet() {
return rowAsMap.entrySet();
return this.rowAsMap.entrySet();
}
/*
@@ -207,7 +207,7 @@ public class OutboundRow implements Map<String, SettableValue> {
OutboundRow row = (OutboundRow) o;
return rowAsMap.equals(row.rowAsMap);
return this.rowAsMap.equals(row.rowAsMap);
}
/*
@@ -216,7 +216,7 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public int hashCode() {
return rowAsMap.hashCode();
return this.rowAsMap.hashCode();
}
/*
@@ -225,11 +225,11 @@ public class OutboundRow implements Map<String, SettableValue> {
*/
@Override
public String toString() {
return "OutboundRow[" + rowAsMap + "]";
return "OutboundRow[" + this.rowAsMap + "]";
}
@Override
public void forEach(BiConsumer<? super String, ? super SettableValue> action) {
rowAsMap.forEach(action);
this.rowAsMap.forEach(action);
}
}

View File

@@ -13,25 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.function;
package org.springframework.data.r2dbc.domain;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Statement;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* Extension to {@link QueryOperation} for a prepared SQL query {@link Supplier} with bound parameters. Contains
* parameter bindings that can be {@link #bind(Statement)} bound to a {@link Statement}.
* parameter bindings that can be {@link #bindTo bound} bound to a {@link BindTarget}.
* <p>
* Can be executed with {@link DatabaseClient}.
* Can be executed with {@link org.springframework.data.r2dbc.function.DatabaseClient}.
* </p>
*
* @param <T> underlying operation source.
* @author Mark Paluch
* @see DatabaseClient
* @see DatabaseClient.SqlSpec#sql(Supplier)
* @see org.springframework.data.r2dbc.function.DatabaseClient
* @see org.springframework.data.r2dbc.function.DatabaseClient.SqlSpec#sql(Supplier)
*/
public interface PreparedOperation<T> extends QueryOperation {
@@ -41,15 +37,10 @@ public interface PreparedOperation<T> extends QueryOperation {
T getSource();
/**
* create a {@link Statement} from the generated SQL after applying the SQL filter and then applying the
* {@link org.springframework.data.r2dbc.function.DefaultStatementFactory.Binding} after filtering those as well.
* Apply bindings to {@link BindTarget}.
*
* @param connection the {@link Connection} used for constructing a statement
* @return the bound statement.
* @param target the target to apply bindings to.
*/
Statement createBoundStatement(Connection connection);
void bindTo(BindTarget target);
void addSqlFilter(Function<String, String> filter);
void addBindingFilter(Function<Bindings, Bindings> filter);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2019 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
*
* https://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.domain;
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
* @see PreparedOperation
*/
@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();
}
}

View File

@@ -84,7 +84,7 @@ public class SettableValue {
*/
@Nullable
public Object getValue() {
return value;
return this.value;
}
/**
@@ -93,7 +93,7 @@ public class SettableValue {
* @return the column value type
*/
public Class<?> getType() {
return type;
return this.type;
}
/**
@@ -102,7 +102,7 @@ public class SettableValue {
* @return whether this {@link SettableValue} has a value. {@literal false} if {@link #getValue()} is {@literal null}.
*/
public boolean hasValue() {
return value != null;
return this.value != null;
}
/**
@@ -111,7 +111,7 @@ public class SettableValue {
* @return whether this {@link SettableValue} is empty. {@literal true} if {@link #getValue()} is {@literal null}.
*/
public boolean isEmpty() {
return value == null;
return this.value == null;
}
@Override
@@ -121,20 +121,20 @@ public class SettableValue {
if (!(o instanceof SettableValue))
return false;
SettableValue value1 = (SettableValue) o;
return Objects.equals(value, value1.value) && Objects.equals(type, value1.type);
return Objects.equals(this.value, value1.value) && Objects.equals(this.type, value1.type);
}
@Override
public int hashCode() {
return Objects.hash(value, type);
return Objects.hash(this.value, this.type);
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer();
sb.append("SettableValue");
sb.append("[value=").append(value);
sb.append(", type=").append(type);
sb.append("[value=").append(this.value);
sb.append(", type=").append(this.type);
sb.append(']');
return sb.toString();
}

View File

@@ -2,6 +2,9 @@ package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Statement;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.QueryOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
@@ -11,46 +14,46 @@ import org.springframework.data.r2dbc.domain.SettableValue;
*
* @author Mark Paluch
* @see Statement#bind
* @see Statement#bindNull
* @see Statement#bindNull TODO: Refactor to {@link PreparedOperation}.
*/
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 bindTarget the bindTarget 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);
void bind(BindTarget bindTarget, 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 bindTarget the bindTarget 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);
void bindNull(BindTarget bindTarget, String identifier, Class<?> valueType);
/**
* Bind a {@link SettableValue} to the {@link Statement} using the underlying binding strategy. Binds either the
* {@link SettableValue#getValue()} or {@literal null}, depending on whether the value is {@literal null}.
*
* @param statement the statement to bind the value to.
* @param bindTarget the bindTarget to bind the value to.
* @param value the settable value
* @see Statement#bind
* @see Statement#bindNull
*/
default void bind(Statement statement, String identifier, SettableValue value) {
default void bind(BindTarget bindTarget, String identifier, SettableValue value) {
if (value.getValue() == null) {
bindNull(statement, identifier, value.getType());
bindNull(bindTarget, identifier, value.getType());
} else {
bind(statement, identifier, value.getValue());
bind(bindTarget, identifier, value.getValue());
}
}

View File

@@ -1,104 +0,0 @@
/*
* Copyright 2019 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
*
* https://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.Statement;
import lombok.RequiredArgsConstructor;
import java.util.List;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
* @author Jens Schauder
*/
public class Bindings {
private final List<SingleBinding> bindings;
public Bindings(List<SingleBinding> bindings) {
this.bindings = bindings;
}
public void apply(Statement statement) {
bindings.forEach(sb -> sb.bindTo(statement));
}
@RequiredArgsConstructor
public static abstract class SingleBinding<T> {
final T identifier;
final SettableValue value;
public abstract void bindTo(Statement statement);
public abstract boolean isIndexed();
public final boolean isNamed() {
return !isIndexed();
}
}
public static class IndexedSingleBinding extends SingleBinding<Integer> {
public IndexedSingleBinding(Integer identifier, SettableValue value) {
super(identifier, value);
}
@Override
public void bindTo(Statement statement) {
if (value.isEmpty()) {
statement.bindNull((int) identifier, value.getType());
} else {
statement.bind((int) identifier, value.getValue());
}
}
@Override
public boolean isIndexed() {
return true;
}
}
public static class NamedExpandedSingleBinding extends SingleBinding<String> {
private final BindableOperation operation;
public NamedExpandedSingleBinding(String identifier, SettableValue value, BindableOperation operation) {
super(identifier, value);
this.operation = operation;
}
@Override
public void bindTo(Statement statement) {
if (value != null) {
operation.bind(statement, identifier, value);
} else {
operation.bindNull(statement, identifier, value.getType());
}
}
@Override
public boolean isIndexed() {
return false;
}
}
}

View File

@@ -29,6 +29,7 @@ import org.reactivestreams.Publisher;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
/**

View File

@@ -52,7 +52,9 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.UncategorizedR2dbcException;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
@@ -60,6 +62,7 @@ import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link DatabaseClient}.
@@ -322,24 +325,51 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
this.sqlSupplier = sqlSupplier;
}
protected String getSql() {
<T> FetchSpec<T> exchange(Supplier<String> sqlSupplier, BiFunction<Row, RowMetadata, T> mappingFunction) {
String sql = sqlSupplier.get();
Assert.state(sql != null, "SQL supplier returned null!");
return sql;
}
String sql = getRequiredSql(sqlSupplier);
<T> FetchSpec<T> exchange(String sql, BiFunction<Row, RowMetadata, T> mappingFunction) {
Function<Connection, Statement> executeFunction = it -> {
PreparedOperation pop;
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
}
if (sqlSupplier instanceof PreparedOperation<?>) {
pop = ((PreparedOperation<?>) sqlSupplier);
} else {
pop = new ExpandedPreparedOperation(sql, namedParameters, dataAccessStrategy, byName, byIndex);
}
if (sqlSupplier instanceof PreparedOperation<?>) {
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(pop.createBoundStatement(it).execute());
Statement statement = it.createStatement(sql);
BindTarget bindTarget = new StatementWrapper(statement);
((PreparedOperation<?>) sqlSupplier).bindTo(bindTarget);
return statement;
}
BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(),
new MapBindParameterSource(byName));
String expanded = operation.toQuery();
if (logger.isTraceEnabled()) {
logger.trace("Expanded SQL [" + expanded + "]");
}
Statement statement = it.createStatement(expanded);
BindTarget bindTarget = new StatementWrapper(statement);
byName.forEach((name, o) -> {
if (o.getValue() != null) {
operation.bind(bindTarget, name, o.getValue());
} else {
operation.bindNull(bindTarget, name, o.getType());
}
});
bindByIndex(statement, byIndex);
return statement;
};
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(executeFunction.apply(it).execute());
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
sql, //
@@ -395,7 +425,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
private void assertNotPreparedOperation() {
if (sqlSupplier instanceof PreparedOperation<?>) {
if (this.sqlSupplier instanceof PreparedOperation<?>) {
throw new InvalidDataAccessApiUsageException("Cannot add bindings to a PreparedOperation");
}
}
@@ -440,12 +470,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(mappingFunction, "Mapping function must not be null!");
return exchange(getSql(), mappingFunction);
return exchange(this.sqlSupplier, mappingFunction);
}
@Override
public FetchSpec<Map<String, Object>> fetch() {
return exchange(getSql(), ColumnMapRowMapper.INSTANCE);
return exchange(this.sqlSupplier, ColumnMapRowMapper.INSTANCE);
}
@Override
@@ -525,12 +555,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(mappingFunction, "Mapping function must not be null!");
return exchange(getSql(), mappingFunction);
return exchange(this.sqlSupplier, mappingFunction);
}
@Override
public FetchSpec<T> fetch() {
return exchange(getSql(), mappingFunction);
return exchange(this.sqlSupplier, mappingFunction);
}
@Override
@@ -882,10 +912,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
byName.forEach(it::bind);
});
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(operation.createBoundStatement(it).execute());
String sql = getRequiredSql(operation);
Function<Connection, Flux<Result>> resultFunction = it -> {
Statement statement = it.createStatement(sql);
operation.bindTo(new StatementWrapper(statement));
return Flux.from(statement.execute());
};
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
operation.toQuery(), //
sql, //
resultFunction, //
it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), //
mappingFunction);
@@ -993,10 +1031,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
});
});
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(operation.createBoundStatement(it).execute());
String sql = getRequiredSql(operation);
Function<Connection, Flux<Result>> resultFunction = it -> {
Statement statement = it.createStatement(sql);
operation.bindTo(new StatementWrapper(statement));
statement.returnGeneratedValues();
return Flux.from(statement.execute());
};
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
operation.toQuery(), //
sql, //
resultFunction, //
it -> resultFunction //
.apply(it) //
@@ -1045,6 +1091,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
}
private static String getRequiredSql(Supplier<String> sqlSupplier) {
String sql = sqlSupplier.get();
Assert.state(StringUtils.hasText(sql), "SQL returned by SQL supplier must not be empty!");
return sql;
}
/**
* Invocation handler that suppresses close calls on R2DBC Connections. Also prepares returned Statement
* (Prepared/CallbackStatement) objects.
@@ -1118,4 +1171,31 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
});
}
}
@RequiredArgsConstructor
static class StatementWrapper implements BindTarget {
final Statement statement;
@Override
public void bind(Object identifier, Object value) {
this.statement.bind(identifier, value);
}
@Override
public void bind(int index, Object value) {
this.statement.bind(index, value);
}
@Override
public void bindNull(Object identifier, Class<?> type) {
this.statement.bindNull(identifier, type);
}
@Override
public void bindNull(int index, Class<?> type) {
this.statement.bindNull(index, type);
}
}
}

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Connection;
import io.r2dbc.spi.Statement;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -29,16 +28,29 @@ import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.InvalidDataAccessApiUsageException;
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.IndexedBindMarker;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.sql.*;
import org.springframework.data.relational.core.sql.AssignValue;
import org.springframework.data.relational.core.sql.Assignment;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.relational.core.sql.DeleteBuilder;
import org.springframework.data.relational.core.sql.Expression;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.SelectBuilder;
import org.springframework.data.relational.core.sql.StatementBuilder;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.Update;
import org.springframework.data.relational.core.sql.UpdateBuilder;
import org.springframework.data.relational.core.sql.render.RenderContext;
import org.springframework.data.relational.core.sql.render.SqlRenderer;
import org.springframework.lang.Nullable;
@@ -94,42 +106,11 @@ class DefaultStatementFactory implements StatementFactory {
return new DefaultPreparedOperation<>( //
select, //
renderContext, //
createBindings(binding) //
binding //
);
});
}
@NotNull
private static Bindings createBindings(Binding binding) {
List<Bindings.SingleBinding> singleBindings = new ArrayList<>();
binding.getNulls().forEach( //
(bindMarker, settableValue) -> {
if (bindMarker instanceof IndexedBindMarker) {
singleBindings //
.add(new Bindings.IndexedSingleBinding( //
((IndexedBindMarker) bindMarker).getIndex(), //
settableValue) //
);
}
});
binding.getValues().forEach( //
(bindMarker, value) -> {
if (bindMarker instanceof IndexedBindMarker) {
singleBindings //
.add(new Bindings.IndexedSingleBinding( //
((IndexedBindMarker) bindMarker).getIndex(), //
value instanceof SettableValue ? (SettableValue) value : SettableValue.from(value)) //
);
}
});
return new Bindings(singleBindings);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#insert(java.lang.String, java.util.Collection, java.util.function.Consumer)
@@ -174,12 +155,7 @@ class DefaultStatementFactory implements StatementFactory {
Insert insert = StatementBuilder.insert().into(table).columns(table.columns(binderBuilder.bindings.keySet()))
.values(expressions).build();
return new DefaultPreparedOperation<Insert>(insert, renderContext, createBindings(binding)) {
@Override
public Statement bind(Statement to) {
return super.bind(to).returnGeneratedValues(generatedKeysNames.toArray(new String[0]));
}
};
return new DefaultPreparedOperation<Insert>(insert, renderContext, binding);
});
}
@@ -228,7 +204,7 @@ class DefaultStatementFactory implements StatementFactory {
update = updateBuilder.build();
}
return new DefaultPreparedOperation<>(update, renderContext, createBindings(binding));
return new DefaultPreparedOperation<>(update, renderContext, binding);
});
}
@@ -266,7 +242,7 @@ class DefaultStatementFactory implements StatementFactory {
delete = deleteBuilder.build();
}
return new DefaultPreparedOperation<>(delete, renderContext, createBindings(binding));
return new DefaultPreparedOperation<>(delete, renderContext, binding);
});
}
@@ -429,97 +405,24 @@ class DefaultStatementFactory implements StatementFactory {
*
* @param to
*/
void apply(Statement to) {
void apply(BindTarget to) {
values.forEach((marker, value) -> marker.bind(to, value));
nulls.forEach((marker, value) -> marker.bindNull(to, value.getType()));
}
}
static abstract class PreparedOperationSupport<T> implements PreparedOperation<T> {
private Function<String, String> sqlFilter = s -> s;
private Function<Bindings, Bindings> bindingFilter = b -> b;
private final Bindings bindings;
protected PreparedOperationSupport(Bindings bindings) {
this.bindings = bindings;
}
abstract protected String createBaseSql();
Bindings getBaseBinding() {
return bindings;
};
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
*/
@Override
public String toQuery() {
return sqlFilter.apply(createBaseSql());
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.PreparedOperation#bind(io.r2dbc.spi.Statement)
*/
protected Statement bind(Statement to) {
bindingFilter.apply(getBaseBinding()).apply(to);
return to;
}
@Override
public Statement createBoundStatement(Connection connection) {
// TODO add back logging
// if (logger.isDebugEnabled()) {
// logger.debug("Executing SQL statement [" + sql + "]");
// }
return bind(connection.createStatement(toQuery()));
}
@Override
public void addSqlFilter(Function<String, String> filter) {
Assert.notNull(filter, "Filter must not be null.");
sqlFilter = filter;
}
@Override
public void addBindingFilter(Function<Bindings, Bindings> filter) {
Assert.notNull(filter, "Filter must not be null.");
bindingFilter = filter;
}
}
/**
* Default implementation of {@link PreparedOperation}.
*
* @param <T>
*/
static class DefaultPreparedOperation<T> extends PreparedOperationSupport<T> {
@RequiredArgsConstructor
static class DefaultPreparedOperation<T> implements PreparedOperation<T> {
private final T source;
private final RenderContext renderContext;
DefaultPreparedOperation(T source, RenderContext renderContext, Bindings bindings) {
super(bindings);
this.source = source;
this.renderContext = renderContext;
}
private final Binding binding;
/*
* (non-Javadoc)
@@ -530,8 +433,12 @@ class DefaultStatementFactory implements StatementFactory {
return this.source;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
*/
@Override
protected String createBaseSql() {
public String toQuery() {
SqlRenderer sqlRenderer = SqlRenderer.create(renderContext);
@@ -554,5 +461,9 @@ class DefaultStatementFactory implements StatementFactory {
throw new IllegalStateException("Cannot render " + this.getSource());
}
@Override
public void bindTo(BindTarget target) {
binding.apply(target);
}
}
}

View File

@@ -1,121 +0,0 @@
/*
* Copyright 2019 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
*
* https://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.Connection;
import io.r2dbc.spi.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
* @author Jens Schauder
*/
public class ExpandedPreparedOperation
extends DefaultStatementFactory.PreparedOperationSupport<BindableOperation> {
private final BindableOperation operation;
private final Map<String, SettableValue> byName;
private final Map<Integer, SettableValue> byIndex;
private ExpandedPreparedOperation(BindableOperation operation, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {
super(createBindings(operation, byName, byIndex));
this.operation = operation;
this.byName = byName;
this.byIndex = byIndex;
}
private static Bindings createBindings(BindableOperation operation, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {
List<Bindings.SingleBinding> bindings = new ArrayList<>();
byName.forEach(
(identifier, settableValue) -> bindings.add(new Bindings.NamedExpandedSingleBinding(identifier, settableValue, operation)));
byIndex.forEach((identifier, settableValue) -> bindings.add(new Bindings.IndexedSingleBinding(identifier, settableValue)));
return new Bindings(bindings);
}
ExpandedPreparedOperation(String sql, NamedParameterExpander namedParameters,
ReactiveDataAccessStrategy dataAccessStrategy, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {
this( //
namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(), new MapBindParameterSource(byName)), //
byName, //
byIndex //
);
}
@Override
protected String createBaseSql() {
return toQuery();
}
@Override
public BindableOperation getSource() {
return operation;
}
@Override
public Statement createBoundStatement(Connection connection) {
Statement statement = connection.createStatement(operation.toQuery());
bindByName(statement, byName);
bindByIndex(statement, byIndex);
return statement;
}
@Override
public String toQuery() {
return operation.toQuery();
}
// todo that is a weird assymmetry between bindByName and bindByIndex
private void bindByName(Statement statement, Map<String, SettableValue> byName) {
byName.forEach((name, o) -> {
if (o.getValue() != null) {
operation.bind(statement, name, o.getValue());
} else {
operation.bindNull(statement, name, o.getType());
}
});
}
private static void bindByIndex(Statement statement, Map<Integer, SettableValue> byIndex) {
byIndex.forEach((i, o) -> {
if (o.getValue() != null) {
statement.bind(i.intValue(), o.getValue());
} else {
statement.bindNull(i.intValue(), o.getType());
}
});
}
}

View File

@@ -15,14 +15,13 @@
*/
package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Statement;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.domain.BindTarget;
/**
* SQL translation support allowing the use of named parameters rather than native placeholders.
@@ -147,13 +146,13 @@ public class NamedParameterExpander {
return new BindableOperation() {
@Override
public void bind(Statement statement, String identifier, Object value) {
statement.bind(identifier, value);
public void bind(BindTarget target, String identifier, Object value) {
target.bind(identifier, value);
}
@Override
public void bindNull(Statement statement, String identifier, Class<?> valueType) {
statement.bindNull(identifier, valueType);
public void bindNull(BindTarget target, String identifier, Class<?> valueType) {
target.bindNull(identifier, valueType);
}
@Override

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Statement;
import lombok.Value;
import java.util.ArrayList;
@@ -31,6 +30,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.util.Assert;
/**
@@ -391,22 +391,22 @@ abstract class NamedParameterUtils {
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
* @see org.springframework.data.r2dbc.function.BindableOperation#bind(BindTarget, java.lang.String, java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public void bind(Statement statement, String identifier, Object value) {
public void bind(BindTarget target, String identifier, Object value) {
List<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
statement.bind(identifier, value);
target.bind(identifier, value);
return;
}
if (bindMarkers.size() == 1) {
bindMarkers.get(0).bind(statement, value);
bindMarkers.get(0).bind(target, value);
} else {
Assert.isInstanceOf(Collection.class, value,
@@ -424,36 +424,36 @@ abstract class NamedParameterUtils {
if (valueToBind instanceof Object[]) {
Object[] objects = (Object[]) valueToBind;
for (Object object : objects) {
bind(statement, markers, object);
bind(target, markers, object);
}
} else {
bind(statement, markers, valueToBind);
bind(target, markers, valueToBind);
}
}
}
}
private void bind(Statement statement, Iterator<BindMarker> markers, Object valueToBind) {
private void bind(BindTarget target, Iterator<BindMarker> markers, Object valueToBind) {
Assert.isTrue(markers.hasNext(),
() -> String.format(
"No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.",
valueToBind, toQuery()));
markers.next().bind(statement, valueToBind);
markers.next().bind(target, valueToBind);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
* @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(BindTarget, java.lang.String, java.lang.Class)
*/
@Override
public void bindNull(Statement statement, String identifier, Class<?> valueType) {
public void bindNull(BindTarget target, String identifier, Class<?> valueType) {
List<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers.size() == 1) {
bindMarkers.get(0).bindNull(statement, valueType);
bindMarkers.get(0).bindNull(target, valueType);
return;
}

View File

@@ -1,109 +0,0 @@
/*
* Copyright 2019 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
*
* https://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.Connection;
import io.r2dbc.spi.Statement;
import java.util.Map;
import java.util.function.Function;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
* @author Jens Schauder
*/
public class OldParameterbindingPreparedOperation implements PreparedOperation<BindableOperation> {
private final BindableOperation operation;
private final Map<String, SettableValue> byName;
private final Map<Integer, SettableValue> byIndex;
private OldParameterbindingPreparedOperation(BindableOperation operation, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {
this.operation = operation;
this.byName = byName;
this.byIndex = byIndex;
}
OldParameterbindingPreparedOperation(String sql, NamedParameterExpander namedParameters,
ReactiveDataAccessStrategy dataAccessStrategy, Map<String, SettableValue> byName,
Map<Integer, SettableValue> byIndex) {
this( //
namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(), new MapBindParameterSource(byName)), //
byName, //
byIndex //
);
}
@Override
public BindableOperation getSource() {
return operation;
}
@Override
public Statement createBoundStatement(Connection connection) {
Statement statement = connection.createStatement(operation.toQuery());
bindByName(statement, byName);
bindByIndex(statement, byIndex);
return statement;
}
@Override
public void addSqlFilter(Function<String, String> filter) {
}
@Override
public void addBindingFilter(Function<Bindings, Bindings> filter) {
}
@Override
public String toQuery() {
return operation.toQuery();
}
// todo that is a weird assymmetry between bindByName and bindByIndex
private void bindByName(Statement statement, Map<String, SettableValue> byName) {
byName.forEach((name, o) -> {
if (o.getValue() != null) {
operation.bind(statement,name, o.getValue());
} else {
operation.bindNull(statement, name, o.getType());
}
});
}
private static void bindByIndex(Statement statement, Map<Integer, SettableValue> byIndex) {
byIndex.forEach((i, o) -> {
if (o.getValue() != null) {
statement.bind(i.intValue(), o.getValue());
} else {
statement.bindNull(i.intValue(), o.getType());
}
});
}
}

View File

@@ -1,26 +0,0 @@
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();
}
}

View File

@@ -19,6 +19,7 @@ import java.util.Collection;
import java.util.function.Consumer;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.relational.core.sql.Insert;

View File

@@ -27,9 +27,9 @@ import java.util.Set;
import org.reactivestreams.Publisher;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.DatabaseClient;
import org.springframework.data.r2dbc.function.PreparedOperation;
import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.StatementFactory;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;