#64 - API polishing.

Document fluent API. Add fluent API for update. Introduce StatementMapper. Migrate Insert to StatementMapper. Refactoring and cleanup. Migrate Select to StatementMapper.

Original pull request: #106.
This commit is contained in:
Mark Paluch
2019-05-06 14:13:21 +02:00
committed by Oliver Drotbohm
parent ff69a41162
commit 774d2e8b09
28 changed files with 2369 additions and 1734 deletions

View File

@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.domain;
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
@@ -27,19 +27,21 @@ import java.util.Map;
import java.util.Spliterator;
import java.util.function.Consumer;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.data.util.Streamable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}.
* Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}. Bindings
* are typically immutable.
*
* @author Mark Paluch
*/
public class Bindings implements Streamable<Bindings.Binding> {
private static final Bindings EMPTY = new Bindings();
private final Map<BindMarker, Binding> bindings;
/**
@@ -67,8 +69,17 @@ public class Bindings implements Streamable<Bindings.Binding> {
this.bindings = bindings;
}
/**
* Create a new, empty {@link Bindings} object.
*
* @return a new, empty {@link Bindings} object.
*/
public static Bindings empty() {
return EMPTY;
}
protected Map<BindMarker, Binding> getBindings() {
return bindings;
return this.bindings;
}
/**
@@ -92,14 +103,24 @@ public class Bindings implements Streamable<Bindings.Binding> {
}
/**
* Apply the bindings to a {@link Statement}.
* Merge this bindings with an other {@link Bindings} object and create a new merged {@link Bindings} object.
*
* @param statement the statement to apply to.
* @param other the object to merge with.
* @return a new, merged {@link Bindings} object.
*/
public void apply(Statement statement) {
public Bindings and(Bindings other) {
return merge(this, other);
}
Assert.notNull(statement, "Statement must not be null");
this.bindings.forEach((marker, binding) -> binding.apply(statement));
/**
* Apply the bindings to a {@link BindTarget}.
*
* @param bindTarget the target to apply bindings to.
*/
public void apply(BindTarget bindTarget) {
Assert.notNull(bindTarget, "BindTarget must not be null");
this.bindings.forEach((marker, binding) -> binding.apply(bindTarget));
}
/**
@@ -146,7 +167,7 @@ public class Bindings implements Streamable<Bindings.Binding> {
* @return the associated {@link BindMarker}.
*/
public BindMarker getBindMarker() {
return marker;
return this.marker;
}
/**
@@ -174,11 +195,11 @@ public class Bindings implements Streamable<Bindings.Binding> {
public abstract Object getValue();
/**
* Applies the binding to a {@link Statement}.
* Applies the binding to a {@link BindTarget}.
*
* @param statement the statement to apply to.
* @param bindTarget the target to apply bindings to.
*/
public abstract void apply(Statement statement);
public abstract void apply(BindTarget bindTarget);
}
/**
@@ -206,7 +227,7 @@ public class Bindings implements Streamable<Bindings.Binding> {
* @see org.springframework.data.r2dbc.function.query.Bindings.Binding#getValue()
*/
public Object getValue() {
return value;
return this.value;
}
/*
@@ -214,8 +235,8 @@ public class Bindings implements Streamable<Bindings.Binding> {
* @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement)
*/
@Override
public void apply(Statement statement) {
getBindMarker().bind(statement, getValue());
public void apply(BindTarget bindTarget) {
getBindMarker().bind(bindTarget, getValue());
}
}
@@ -249,16 +270,16 @@ public class Bindings implements Streamable<Bindings.Binding> {
}
public Class<?> getValueType() {
return valueType;
return this.valueType;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement)
* @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(BindTarget)
*/
@Override
public void apply(Statement statement) {
getBindMarker().bindNull(statement, getValueType());
public void apply(BindTarget bindTarget) {
getBindMarker().bindNull(bindTarget, getValueType());
}
}
}

View File

@@ -13,14 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.r2dbc.domain;
package org.springframework.data.r2dbc.dialect;
import io.r2dbc.spi.Statement;
import java.util.LinkedHashMap;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.util.Assert;
/**

View File

@@ -30,7 +30,9 @@ 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.domain.SettableValue;
import org.springframework.data.r2dbc.function.query.Criteria;
import org.springframework.data.r2dbc.function.query.Update;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
/**
@@ -59,6 +61,11 @@ public interface DatabaseClient {
*/
InsertIntoSpec insert();
/**
* Prepare an SQL UPDATE call.
*/
UpdateTableSpec update();
/**
* Prepare an SQL DELETE call.
*/
@@ -290,6 +297,29 @@ public interface DatabaseClient {
<T> TypedInsertSpec<T> into(Class<T> table);
}
/**
* Contract for specifying {@code UPDATE} options leading to the exchange.
*/
interface UpdateTableSpec {
/**
* Specify the target {@literal table} to update.
*
* @param table must not be {@literal null} or empty.
* @return a {@link GenericUpdateSpec} for further configuration of the update. Guaranteed to be not
* {@literal null}.
*/
GenericUpdateSpec table(String table);
/**
* Specify the target table to update to using the {@link Class entity class}.
*
* @param table must not be {@literal null}.
* @return a {@link TypedUpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}.
*/
<T> TypedUpdateSpec<T> table(Class<T> table);
}
/**
* Contract for specifying {@code DELETE} options leading to the exchange.
*/
@@ -299,18 +329,18 @@ public interface DatabaseClient {
* Specify the source {@literal table} to delete from.
*
* @param table must not be {@literal null} or empty.
* @return a {@link GenericSelectSpec} for further configuration of the delete. Guaranteed to be not
* @return a {@link DeleteMatchingSpec} for further configuration of the delete. Guaranteed to be not
* {@literal null}.
*/
DeleteSpec from(String table);
DeleteMatchingSpec from(String table);
/**
* Specify the source table to delete from to using the {@link Class entity class}.
*
* @param table must not be {@literal null}.
* @return a {@link DeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}.
* @return a {@link TypedDeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}.
*/
DeleteSpec from(Class<?> table);
<T> TypedDeleteSpec<T> from(Class<T> table);
}
/**
@@ -388,7 +418,7 @@ public interface DatabaseClient {
*
* @param criteria must not be {@literal null}.
*/
S where(Criteria criteria);
S matching(Criteria criteria);
/**
* Configure {@link Sort}.
@@ -397,12 +427,21 @@ public interface DatabaseClient {
*/
S orderBy(Sort sort);
/**
* Configure {@link Sort}.
*
* @param orders must not be {@literal null}.
*/
default S orderBy(Sort.Order... orders) {
return orderBy(Sort.by(orders));
}
/**
* Configure pagination. Overrides {@link Sort} if the {@link Pageable} contains a {@link Sort} object.
*
* @param page must not be {@literal null}.
* @param pageable must not be {@literal null}.
*/
S page(Pageable page);
S page(Pageable pageable);
}
/**
@@ -425,12 +464,23 @@ public interface DatabaseClient {
*
* @param field must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @deprecated will be removed soon. Use {@link #nullValue(String)}.
*/
GenericInsertSpec<T> nullValue(String field, Class<?> type);
@Deprecated
default GenericInsertSpec<T> nullValue(String field, Class<?> type) {
return value(field, SettableValue.empty(type));
}
/**
* Specify a {@literal null} value to insert.
*
* @param field must not be {@literal null} or empty.
*/
GenericInsertSpec<T> nullValue(String field);
}
/**
* Contract for specifying {@code SELECT} options leading the exchange.
* Contract for specifying {@code INSERT} options leading the exchange.
*/
interface TypedInsertSpec<T> {
@@ -473,7 +523,7 @@ public interface DatabaseClient {
/**
* Configure a result mapping {@link java.util.function.BiFunction function}.
*
* @param mappwingFunction must not be {@literal null}.
* @param mappingFunction must not be {@literal null}.
* @param <R> result type.
* @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}.
*/
@@ -493,16 +543,110 @@ public interface DatabaseClient {
}
/**
* Contract for specifying {@code DELETE} options leading to the exchange.
* Contract for specifying {@code UPDATE} options leading to the exchange.
*/
interface DeleteSpec {
interface GenericUpdateSpec {
/**
* Specify an {@link Update} object containing assignments.
*
* @param update must not be {@literal null}.
*/
UpdateMatchingSpec using(Update update);
}
/**
* Contract for specifying {@code UPDATE} options leading to the exchange.
*/
interface TypedUpdateSpec<T> {
/**
* Update the given {@code objectToUpdate}.
*
* @param objectToUpdate the object of which the attributes will provide the values for the update and the primary
* key. Must not be {@literal null}.
* @return a {@link UpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}.
*/
UpdateSpec using(T objectToUpdate);
/**
* Use the given {@code tableName} as update target.
*
* @param tableName must not be {@literal null} or empty.
* @return a {@link TypedUpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}.
*/
TypedUpdateSpec<T> table(String tableName);
}
/**
* Contract for specifying {@code UPDATE} options leading to the exchange.
*/
interface UpdateMatchingSpec extends UpdateSpec {
/**
* Configure a filter {@link Criteria}.
*
* @param criteria must not be {@literal null}.
*/
DeleteSpec where(Criteria criteria);
UpdateSpec matching(Criteria criteria);
}
/**
* Contract for specifying {@code UPDATE} options leading to the exchange.
*/
interface UpdateSpec {
/**
* Perform the SQL call and retrieve the result.
*/
UpdatedRowsFetchSpec fetch();
/**
* Perform the SQL call and return a {@link Mono} that completes without result on statement completion.
*
* @return a {@link Mono} ignoring its payload (actively dropping).
*/
Mono<Void> then();
}
/**
* Contract for specifying {@code DELETE} options leading to the exchange.
*/
interface TypedDeleteSpec<T> extends DeleteSpec {
/**
* Use the given {@code tableName} as delete target.
*
* @param tableName must not be {@literal null} or empty.
* @return a {@link TypedDeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}.
*/
TypedDeleteSpec<T> table(String tableName);
/**
* Configure a filter {@link Criteria}.
*
* @param criteria must not be {@literal null}.
*/
DeleteSpec matching(Criteria criteria);
}
/**
* Contract for specifying {@code DELETE} options leading to the exchange.
*/
interface DeleteMatchingSpec extends DeleteSpec {
/**
* Configure a filter {@link Criteria}.
*
* @param criteria must not be {@literal null}.
*/
DeleteSpec matching(Criteria criteria);
}
/**
* Contract for specifying {@code DELETE} options leading to the exchange.
*/
interface DeleteSpec {
/**
* Perform the SQL call and retrieve the result.

View File

@@ -58,13 +58,9 @@ 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;
import org.springframework.data.r2dbc.function.operation.BindableOperation;
import org.springframework.data.r2dbc.function.query.BoundCondition;
import org.springframework.data.r2dbc.function.query.Criteria;
import org.springframework.data.r2dbc.function.query.Update;
import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -101,7 +97,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
@Override
public Builder mutate() {
return builder;
return this.builder;
}
@Override
@@ -119,6 +115,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return new DefaultInsertIntoSpec();
}
@Override
public UpdateTableSpec update() {
return new DefaultUpdateTableSpec();
}
@Override
public DeleteFromSpec delete() {
return new DefaultDeleteFromSpec();
@@ -206,7 +207,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
* @throws IllegalStateException in case of no DataSource set
*/
protected ConnectionFactory obtainConnectionFactory() {
return connector;
return this.connector;
}
/**
@@ -230,7 +231,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
*/
protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) {
DataAccessException dae = exceptionTranslator.translate(task, sql, ex);
DataAccessException dae = this.exceptionTranslator.translate(task, sql, ex);
return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex));
}
@@ -355,9 +356,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(),
new MapBindParameterSource(byName));
new MapBindParameterSource(this.byName));
String expanded = operation.toQuery();
String expanded = getRequiredSql(operation);
if (logger.isTraceEnabled()) {
logger.trace("Expanded SQL [" + expanded + "]");
}
@@ -365,7 +366,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Statement statement = it.createStatement(expanded);
BindTarget bindTarget = new StatementWrapper(statement);
byName.forEach((name, o) -> {
this.byName.forEach((name, o) -> {
if (o.getValue() != null) {
operation.bind(bindTarget, name, o.getValue());
@@ -374,7 +375,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
});
bindByIndex(statement, byIndex);
bindByIndex(statement, this.byIndex);
return statement;
};
@@ -570,7 +571,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
@Override
public FetchSpec<T> fetch() {
return exchange(this.sqlSupplier, mappingFunction);
return exchange(this.sqlSupplier, this.mappingFunction);
}
@Override
@@ -606,7 +607,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
@Override
protected DefaultTypedExecuteSpec<T> createInstance(Map<Integer, SettableValue> byIndex,
Map<String, SettableValue> byName, Supplier<String> sqlSupplier) {
return createTypedExecuteSpec(byIndex, byName, sqlSupplier, typeToRead);
return createTypedExecuteSpec(byIndex, byName, sqlSupplier, this.typeToRead);
}
}
@@ -656,37 +657,38 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
projectedFields.addAll(this.projectedFields);
projectedFields.addAll(Arrays.asList(selectedFields));
return createInstance(table, projectedFields, criteria, sort, page);
return createInstance(this.table, projectedFields, this.criteria, this.sort, this.page);
}
public DefaultSelectSpecSupport where(Criteria whereCriteria) {
Assert.notNull(whereCriteria, "Criteria must not be null!");
return createInstance(table, projectedFields, whereCriteria, sort, page);
return createInstance(this.table, this.projectedFields, whereCriteria, this.sort, this.page);
}
public DefaultSelectSpecSupport orderBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null!");
return createInstance(table, projectedFields, criteria, sort, page);
return createInstance(this.table, this.projectedFields, this.criteria, sort, this.page);
}
public DefaultSelectSpecSupport page(Pageable page) {
Assert.notNull(page, "Pageable must not be null!");
return createInstance(table, projectedFields, criteria, sort, page);
return createInstance(this.table, this.projectedFields, this.criteria, this.sort, page);
}
<R> FetchSpec<R> execute(PreparedOperation<?> preparedOperation, BiFunction<Row, RowMetadata, R> mappingFunction) {
Function<Connection, Statement> selectFunction = wrapPreparedOperation(preparedOperation);
String sql = getRequiredSql(preparedOperation);
Function<Connection, Statement> selectFunction = wrapPreparedOperation(sql, preparedOperation);
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(selectFunction.apply(it).execute());
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
preparedOperation.toQuery(), //
sql, //
resultFunction, //
it -> Mono.error(new UnsupportedOperationException("Not available for SELECT")), //
mappingFunction);
@@ -711,8 +713,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(resultType, "Result type must not be null!");
return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, resultType,
dataAccessStrategy.getRowMapper(resultType));
return new DefaultTypedSelectSpec<>(this.table, this.projectedFields, this.criteria, this.sort, this.page,
resultType, dataAccessStrategy.getRowMapper(resultType));
}
@Override
@@ -729,7 +731,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
@Override
public DefaultGenericSelectSpec where(Criteria criteria) {
public DefaultGenericSelectSpec matching(Criteria criteria) {
return (DefaultGenericSelectSpec) super.where(criteria);
}
@@ -739,8 +741,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
@Override
public DefaultGenericSelectSpec page(Pageable page) {
return (DefaultGenericSelectSpec) super.page(page);
public DefaultGenericSelectSpec page(Pageable pageable) {
return (DefaultGenericSelectSpec) super.page(pageable);
}
@Override
@@ -750,19 +752,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private <R> FetchSpec<R> exchange(BiFunction<Row, RowMetadata, R> mappingFunction) {
PreparedOperation<Select> operation = dataAccessStrategy.getStatements().select(table, this.projectedFields,
(t, configurer) -> {
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
configurer.withPageRequest(page).withSort(sort);
StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.table).withProjection(this.projectedFields)
.withSort(this.sort).withPage(this.page);
if (criteria != null) {
BoundCondition boundCondition = dataAccessStrategy.getMappedCriteria(criteria, t);
configurer.withWhere(boundCondition.getCondition()).withBindings(boundCondition.getBindings());
}
});
if (this.criteria != null) {
selectSpec = selectSpec.withCriteria(this.criteria);
}
PreparedOperation<?> operation = mapper.getMappedObject(selectSpec);
return execute(operation, mappingFunction);
}
@@ -824,7 +823,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
@Override
public DefaultTypedSelectSpec<T> where(Criteria criteria) {
public DefaultTypedSelectSpec<T> matching(Criteria criteria) {
return (DefaultTypedSelectSpec<T>) super.where(criteria);
}
@@ -834,42 +833,34 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
}
@Override
public DefaultTypedSelectSpec<T> page(Pageable page) {
return (DefaultTypedSelectSpec<T>) super.page(page);
public DefaultTypedSelectSpec<T> page(Pageable pageable) {
return (DefaultTypedSelectSpec<T>) super.page(pageable);
}
@Override
public FetchSpec<T> fetch() {
return exchange(mappingFunction);
return exchange(this.mappingFunction);
}
private <R> FetchSpec<R> exchange(BiFunction<Row, RowMetadata, R> mappingFunction) {
List<String> columns;
StatementMapper mapper = dataAccessStrategy.getStatementMapper().forType(this.typeToRead);
if (this.projectedFields.isEmpty()) {
columns = dataAccessStrategy.getAllColumns(typeToRead);
columns = dataAccessStrategy.getAllColumns(this.typeToRead);
} else {
columns = this.projectedFields;
}
PreparedOperation<Select> operation = dataAccessStrategy.getStatements().select(table, columns,
(table, configurer) -> {
StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.table).withProjection(columns)
.withPage(this.page).withSort(this.sort);
Sort sortToUse;
if (this.sort.isSorted()) {
sortToUse = dataAccessStrategy.getMappedSort(this.sort, this.typeToRead);
} else {
sortToUse = this.sort;
}
if (this.criteria != null) {
selectSpec = selectSpec.withCriteria(this.criteria);
}
configurer.withPageRequest(page).withSort(sortToUse);
if (criteria != null) {
BoundCondition boundCondition = dataAccessStrategy.getMappedCriteria(criteria, table, this.typeToRead);
configurer.withWhere(boundCondition.getCondition()).withBindings(boundCondition.getBindings());
}
});
PreparedOperation<?> operation = mapper.getMappedObject(selectSpec);
return execute(operation, mappingFunction);
}
@@ -877,7 +868,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
@Override
protected DefaultTypedSelectSpec<T> createInstance(String table, List<String> projectedFields, Criteria criteria,
Sort sort, Pageable page) {
return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, typeToRead, mappingFunction);
return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, this.typeToRead,
this.mappingFunction);
}
}
@@ -915,18 +907,22 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
() -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field));
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
byName.put(field, SettableValue.fromOrEmpty(value, value.getClass()));
if (value instanceof SettableValue) {
byName.put(field, (SettableValue) value);
} else {
byName.put(field, SettableValue.fromOrEmpty(value, value.getClass()));
}
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
}
@Override
public GenericInsertSpec nullValue(String field, Class<?> type) {
public GenericInsertSpec nullValue(String field) {
Assert.notNull(field, "Field must not be null!");
Map<String, SettableValue> byName = new LinkedHashMap<>(this.byName);
byName.put(field, SettableValue.empty(type));
byName.put(field, null);
return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
}
@@ -951,30 +947,19 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private <R> FetchSpec<R> exchange(BiFunction<Row, RowMetadata, R> mappingFunction) {
if (byName.isEmpty()) {
if (this.byName.isEmpty()) {
throw new IllegalStateException("Insert fields is empty!");
}
PreparedOperation<Insert> operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(),
it -> {
byName.forEach(it::bind);
});
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
StatementMapper.InsertSpec insert = mapper.createInsert(this.table);
String sql = getRequiredSql(operation);
for (String column : this.byName.keySet()) {
insert = insert.withColumn(column, this.byName.get(column));
}
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(), //
resultFunction, //
it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), //
mappingFunction);
PreparedOperation<?> operation = mapper.getMappedObject(insert);
return exchangeInsert(mappingFunction, operation);
}
}
@@ -1002,7 +987,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.hasText(tableName, "Table name must not be null or empty!");
return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert, this.mappingFunction);
return new DefaultTypedInsertSpec<>(this.typeToInsert, tableName, this.objectToInsert, this.mappingFunction);
}
@Override
@@ -1010,7 +995,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(objectToInsert, "Object to insert must not be null!");
return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert), this.mappingFunction);
return new DefaultTypedInsertSpec<>(this.typeToInsert, this.table, Mono.just(objectToInsert),
this.mappingFunction);
}
@Override
@@ -1018,7 +1004,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
Assert.notNull(objectToInsert, "Publisher to insert must not be null!");
return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert, this.mappingFunction);
return new DefaultTypedInsertSpec<>(this.typeToInsert, this.table, objectToInsert, this.mappingFunction);
}
@Override
@@ -1036,7 +1022,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
@Override
public Mono<Void> then() {
return Mono.from(objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then();
return Mono.from(this.objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then();
}
private <MR> FetchSpec<MR> exchange(BiFunction<Row, RowMetadata, MR> mappingFunction) {
@@ -1069,34 +1055,167 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(toInsert);
PreparedOperation<Insert> operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(),
it -> {
outboundRow.forEach((k, v) -> {
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
StatementMapper.InsertSpec insert = mapper.createInsert(this.table);
if (v.hasValue()) {
it.bind(k, v);
}
});
});
for (String column : outboundRow.keySet()) {
SettableValue settableValue = outboundRow.get(column);
if (settableValue.hasValue()) {
insert = insert.withColumn(column, settableValue);
}
}
String sql = getRequiredSql(operation);
Function<Connection, Flux<Result>> resultFunction = it -> {
PreparedOperation<?> operation = mapper.getMappedObject(insert);
return exchangeInsert(mappingFunction, operation);
}
}
Statement statement = it.createStatement(sql);
operation.bindTo(new StatementWrapper(statement));
statement.returnGeneratedValues();
/**
* Default {@link DatabaseClient.UpdateTableSpec} implementation.
*/
class DefaultUpdateTableSpec implements UpdateTableSpec {
return Flux.from(statement.execute());
};
@Override
public GenericUpdateSpec table(String table) {
return new DefaultGenericUpdateSpec(null, table, null, null);
}
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
operation.toQuery(), //
resultFunction, //
it -> resultFunction //
.apply(it) //
.flatMap(Result::getRowsUpdated) //
.collect(Collectors.summingInt(Integer::intValue)), //
mappingFunction);
@Override
public <T> TypedUpdateSpec<T> table(Class<T> table) {
return new DefaultTypedUpdateSpec<>(table, null, null);
}
}
@RequiredArgsConstructor
class DefaultGenericUpdateSpec implements GenericUpdateSpec, UpdateMatchingSpec {
private final @Nullable Class<?> typeToUpdate;
private final @Nullable String table;
private final Update assignments;
private final Criteria where;
@Override
public UpdateMatchingSpec using(Update update) {
Assert.notNull(update, "Update must not be null");
return new DefaultGenericUpdateSpec(this.typeToUpdate, this.table, update, this.where);
}
@Override
public UpdateSpec matching(Criteria criteria) {
Assert.notNull(criteria, "Criteria must not be null");
return new DefaultGenericUpdateSpec(this.typeToUpdate, this.table, this.assignments, criteria);
}
@Override
public UpdatedRowsFetchSpec fetch() {
String table;
if (StringUtils.isEmpty(this.table)) {
table = dataAccessStrategy.getTableName(this.typeToUpdate);
} else {
table = this.table;
}
return exchange(table);
}
@Override
public Mono<Void> then() {
return fetch().rowsUpdated().then();
}
private UpdatedRowsFetchSpec exchange(String table) {
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
if (this.typeToUpdate != null) {
mapper = mapper.forType(this.typeToUpdate);
}
StatementMapper.UpdateSpec update = mapper.createUpdate(table, this.assignments);
if (this.where != null) {
update = update.withCriteria(this.where);
}
PreparedOperation<?> operation = mapper.getMappedObject(update);
return exchangeUpdate(operation);
}
}
@RequiredArgsConstructor
class DefaultTypedUpdateSpec<T> implements TypedUpdateSpec<T>, UpdateSpec {
private final @Nullable Class<T> typeToUpdate;
private final @Nullable String table;
private final T objectToUpdate;
@Override
public UpdateSpec using(T objectToUpdate) {
Assert.notNull(objectToUpdate, "Object to update must not be null");
return new DefaultTypedUpdateSpec<>(this.typeToUpdate, this.table, objectToUpdate);
}
@Override
public TypedUpdateSpec<T> table(String tableName) {
Assert.hasText(tableName, "Table name must not be null or empty!");
return new DefaultTypedUpdateSpec<>(this.typeToUpdate, tableName, this.objectToUpdate);
}
@Override
public UpdatedRowsFetchSpec fetch() {
String table;
if (StringUtils.isEmpty(this.table)) {
table = dataAccessStrategy.getTableName(this.typeToUpdate);
} else {
table = this.table;
}
return exchange(table);
}
@Override
public Mono<Void> then() {
return fetch().rowsUpdated().then();
}
private UpdatedRowsFetchSpec exchange(String table) {
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
Map<String, SettableValue> columns = dataAccessStrategy.getOutboundRow(this.objectToUpdate);
List<String> ids = dataAccessStrategy.getIdentifierColumns(this.typeToUpdate);
if (ids.isEmpty()) {
throw new IllegalStateException("No identifier columns in " + this.typeToUpdate.getName() + "!");
}
Object id = columns.remove(ids.get(0)); // do not update the Id column.
Update update = null;
for (String column : columns.keySet()) {
if (update == null) {
update = Update.update(column, columns.get(column));
} else {
update = update.set(column, columns.get(column));
}
}
PreparedOperation<?> operation = mapper
.getMappedObject(mapper.createUpdate(table, update).withCriteria(Criteria.where(ids.get(0)).is(id)));
return exchangeUpdate(operation);
}
}
@@ -1106,13 +1225,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
class DefaultDeleteFromSpec implements DeleteFromSpec {
@Override
public DeleteSpec from(String table) {
return new DefaultDeleteSpec(null, table, null);
public DefaultDeleteSpec<?> from(String table) {
return new DefaultDeleteSpec<>(null, table, null);
}
@Override
public DeleteSpec from(Class<?> table) {
return new DefaultDeleteSpec(table, null, null);
public <T> DefaultDeleteSpec<T> from(Class<T> table) {
return new DefaultDeleteSpec<>(table, null, null);
}
}
@@ -1120,15 +1239,26 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
* Default implementation of {@link DatabaseClient.TypedInsertSpec}.
*/
@RequiredArgsConstructor
class DefaultDeleteSpec implements DeleteSpec {
class DefaultDeleteSpec<T> implements DeleteMatchingSpec, TypedDeleteSpec<T> {
private final @Nullable Class<?> typeToDelete;
private final @Nullable Class<T> typeToDelete;
private final @Nullable String table;
private final Criteria where;
@Override
public DeleteSpec where(Criteria criteria) {
return new DefaultDeleteSpec(this.typeToDelete, this.table, criteria);
public DeleteSpec matching(Criteria criteria) {
Assert.notNull(criteria, "Criteria must not be null!");
return new DefaultDeleteSpec<>(this.typeToDelete, this.table, criteria);
}
@Override
public TypedDeleteSpec<T> table(String tableName) {
Assert.hasText(tableName, "Table name must not be null or empty!");
return new DefaultDeleteSpec<>(this.typeToDelete, tableName, this.where);
}
@Override
@@ -1152,42 +1282,65 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
private UpdatedRowsFetchSpec exchange(String table) {
PreparedOperation<Delete> operation = dataAccessStrategy.getStatements().delete(table, (t, configurer) -> {
StatementMapper mapper = dataAccessStrategy.getStatementMapper();
if (this.where != null) {
if (this.typeToDelete != null) {
mapper = mapper.forType(this.typeToDelete);
}
BoundCondition condition;
if (this.table != null) {
condition = dataAccessStrategy.getMappedCriteria(this.where, t);
} else {
condition = dataAccessStrategy.getMappedCriteria(this.where, t, this.typeToDelete);
}
StatementMapper.DeleteSpec delete = mapper.createDelete(table);
configurer.withWhere(condition.getCondition()).withBindings(condition.getBindings());
}
});
if (this.where != null) {
delete = delete.withCriteria(this.where);
}
Function<Connection, Statement> deleteFunction = wrapPreparedOperation(operation);
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(deleteFunction.apply(it).execute());
PreparedOperation<?> operation = mapper.getMappedObject(delete);
return new DefaultSqlResult<>(DefaultDatabaseClient.this, //
operation.toQuery(), //
resultFunction, //
it -> resultFunction //
.apply(it) //
.flatMap(Result::getRowsUpdated) //
.collect(Collectors.summingInt(Integer::intValue)), //
(row, rowMetadata) -> rowMetadata);
return exchangeUpdate(operation);
}
}
private Function<Connection, Statement> wrapPreparedOperation(PreparedOperation<?> operation) {
private <R> FetchSpec<R> exchangeInsert(BiFunction<Row, RowMetadata, R> mappingFunction,
PreparedOperation<?> operation) {
String sql = getRequiredSql(operation);
Function<Connection, Statement> insertFunction = wrapPreparedOperation(sql, operation)
.andThen(statement -> statement.returnGeneratedValues());
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(insertFunction.apply(it).execute());
return new DefaultSqlResult<>(this, //
sql, //
resultFunction, //
it -> sumRowsUpdated(resultFunction, it), //
mappingFunction);
}
private UpdatedRowsFetchSpec exchangeUpdate(PreparedOperation<?> operation) {
String sql = getRequiredSql(operation);
Function<Connection, Statement> executeFunction = wrapPreparedOperation(sql, operation);
Function<Connection, Flux<Result>> resultFunction = it -> Flux.from(executeFunction.apply(it).execute());
return new DefaultSqlResult<>(this, //
sql, //
resultFunction, //
it -> sumRowsUpdated(resultFunction, it), //
(row, rowMetadata) -> rowMetadata);
}
private static Mono<Integer> sumRowsUpdated(Function<Connection, Flux<Result>> resultFunction, Connection it) {
return resultFunction.apply(it) //
.flatMap(Result::getRowsUpdated) //
.collect(Collectors.summingInt(Integer::intValue));
}
private Function<Connection, Statement> wrapPreparedOperation(String sql, PreparedOperation<?> operation) {
return it -> {
String sql = operation.toQuery();
if (logger.isDebugEnabled()) {
logger.debug("Executing SQL statement [" + sql + "]");
if (this.logger.isDebugEnabled()) {
this.logger.debug("Executing SQL statement [" + sql + "]");
}
Statement statement = it.createStatement(sql);
@@ -1309,7 +1462,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
return Mono.defer(() -> {
if (compareAndSet(false, true)) {
return Mono.from(closeFunction.apply(connection));
return Mono.from(this.closeFunction.apply(this.connection));
}
return Mono.empty();

View File

@@ -26,11 +26,8 @@ import java.util.function.Function;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.data.convert.CustomConversions.StoreConversions;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.ArrayColumns;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.OutboundRow;
@@ -39,14 +36,11 @@ import org.springframework.data.r2dbc.function.convert.EntityRowMapper;
import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
import org.springframework.data.r2dbc.function.query.BoundCondition;
import org.springframework.data.r2dbc.function.query.Criteria;
import org.springframework.data.r2dbc.function.query.CriteriaMapper;
import org.springframework.data.r2dbc.function.query.UpdateMapper;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.render.NamingStrategies;
import org.springframework.data.relational.core.sql.render.RenderContext;
import org.springframework.data.relational.core.sql.render.RenderNamingStrategy;
@@ -64,9 +58,9 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
private final Dialect dialect;
private final R2dbcConverter converter;
private final CriteriaMapper criteriaMapper;
private final UpdateMapper updateMapper;
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
private final StatementFactory statements;
private final StatementMapper statementMapper;
/**
* Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}.
@@ -103,7 +97,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
Assert.notNull(converter, "RelationalConverter must not be null");
this.converter = converter;
this.criteriaMapper = new CriteriaMapper(converter);
this.updateMapper = new UpdateMapper(converter);
this.mappingContext = (MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty>) this.converter
.getMappingContext();
this.dialect = dialect;
@@ -130,17 +124,17 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
}
};
this.statements = new DefaultStatementFactory(this.dialect, renderContext);
this.statementMapper = new DefaultStatementMapper(dialect, renderContext, this.updateMapper, this.mappingContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getAllFields(java.lang.Class)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getAllColumns(java.lang.Class)
*/
@Override
public List<String> getAllColumns(Class<?> typeToRead) {
public List<String> getAllColumns(Class<?> entityType) {
RelationalPersistentEntity<?> persistentEntity = getPersistentEntity(typeToRead);
RelationalPersistentEntity<?> persistentEntity = getPersistentEntity(entityType);
if (persistentEntity == null) {
return Collections.singletonList("*");
@@ -154,6 +148,26 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
return columnNames;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getIdentifierColumns(java.lang.Class)
*/
@Override
public List<String> getIdentifierColumns(Class<?> entityType) {
RelationalPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(entityType);
List<String> columnNames = new ArrayList<>();
for (RelationalPersistentProperty property : persistentEntity) {
if (property.isIdProperty()) {
columnNames.add(property.getColumnName());
}
}
return columnNames;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getOutboundRow(java.lang.Object)
@@ -164,7 +178,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
OutboundRow row = new OutboundRow();
converter.write(object, row);
this.converter.write(object, row);
RelationalPersistentEntity<?> entity = getRequiredPersistentEntity(ClassUtils.getUserClass(object));
@@ -187,77 +201,25 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
private SettableValue getArrayValue(SettableValue value, RelationalPersistentProperty property) {
ArrayColumns arrayColumns = dialect.getArraySupport();
ArrayColumns arrayColumns = this.dialect.getArraySupport();
if (!arrayColumns.isSupported()) {
throw new InvalidDataAccessResourceUsageException(
"Dialect " + dialect.getClass().getName() + " does not support array columns");
"Dialect " + this.dialect.getClass().getName() + " does not support array columns");
}
return SettableValue.fromOrEmpty(converter.getArrayValue(arrayColumns, property, value.getValue()),
return SettableValue.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()),
property.getActualType());
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedSort(java.lang.Class, org.springframework.data.domain.Sort)
*/
@Override
public Sort getMappedSort(Sort sort, Class<?> typeToRead) {
RelationalPersistentEntity<?> entity = getPersistentEntity(typeToRead);
if (entity == null) {
return sort;
}
List<Order> mappedOrder = new ArrayList<>();
for (Order order : sort) {
RelationalPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty());
if (persistentProperty == null) {
mappedOrder.add(order);
} else {
mappedOrder
.add(Order.by(persistentProperty.getColumnName()).with(order.getNullHandling()).with(order.getDirection()));
}
}
return Sort.by(mappedOrder);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedCriteria(org.springframework.data.r2dbc.function.query.Criteria, org.springframework.data.relational.core.sql.Table)
*/
@Override
public BoundCondition getMappedCriteria(Criteria criteria, Table table) {
return getMappedCriteria(criteria, table, null);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedCriteria(org.springframework.data.r2dbc.function.query.Criteria, org.springframework.data.relational.core.sql.Table, java.lang.Class)
*/
@Override
public BoundCondition getMappedCriteria(Criteria criteria, Table table, @Nullable Class<?> typeToRead) {
BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create();
RelationalPersistentEntity<?> entity = typeToRead != null ? mappingContext.getRequiredPersistentEntity(typeToRead)
: null;
return criteriaMapper.getMappedObject(bindMarkers, criteria, table, entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getRowMapper(java.lang.Class)
*/
@Override
public <T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead) {
return new EntityRowMapper<>(typeToRead, converter);
return new EntityRowMapper<>(typeToRead, this.converter);
}
/*
@@ -271,11 +233,11 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getStatements()
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getStatementMapper()
*/
@Override
public StatementFactory getStatements() {
return this.statements;
public StatementMapper getStatementMapper() {
return this.statementMapper;
}
/*
@@ -284,7 +246,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
*/
@Override
public BindMarkersFactory getBindMarkersFactory() {
return dialect.getBindMarkersFactory();
return this.dialect.getBindMarkersFactory();
}
/*
@@ -292,19 +254,19 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
* @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getConverter()
*/
public R2dbcConverter getConverter() {
return converter;
return this.converter;
}
public MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> getMappingContext() {
return mappingContext;
return this.mappingContext;
}
private RelationalPersistentEntity<?> getRequiredPersistentEntity(Class<?> typeToRead) {
return mappingContext.getRequiredPersistentEntity(typeToRead);
return this.mappingContext.getRequiredPersistentEntity(typeToRead);
}
@Nullable
private RelationalPersistentEntity<?> getPersistentEntity(Class<?> typeToRead) {
return mappingContext.getPersistentEntity(typeToRead);
return this.mappingContext.getPersistentEntity(typeToRead);
}
}

View File

@@ -15,678 +15,21 @@
*/
package org.springframework.data.r2dbc.function;
import io.r2dbc.spi.Statement;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
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.domain.Bindings;
import org.springframework.data.r2dbc.domain.PreparedOperation;
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.r2dbc.support.StatementRenderUtil;
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.OrderByField;
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;
import org.springframework.util.Assert;
/**
* Default {@link StatementFactory} implementation.
*
* @author Mark Paluch
*/
// TODO: Move DefaultPreparedOperation et al to a better place. Probably StatementMapper.
@RequiredArgsConstructor
class DefaultStatementFactory implements StatementFactory {
class DefaultStatementFactory {
private final Dialect dialect;
private final RenderContext renderContext;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#select(java.lang.String, java.util.Collection, java.util.function.Consumer)
*/
@Override
public PreparedOperation<Select> select(String tableName, Collection<String> columnNames,
Consumer<StatementBinderBuilder> binderConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notEmpty(columnNames, "Columns must not be empty");
Assert.notNull(binderConsumer, "Binder Consumer must not be null");
DefaultBinderBuilder binderBuilder = new DefaultBinderBuilder() {
@Override
public void bind(String identifier, SettableValue settable) {
throw new InvalidDataAccessApiUsageException("Binding for SELECT not supported. Use filterBy(…)");
}
};
binderConsumer.accept(binderBuilder);
return withDialect((dialect, renderContext) -> {
Table table = Table.create(tableName);
List<Column> columns = table.columns(columnNames);
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table);
BindMarkers bindMarkers = dialect.getBindMarkersFactory().create();
Binding binding = binderBuilder.build(table, bindMarkers);
Select select;
if (binding.hasCondition()) {
select = selectBuilder.where(binding.getCondition()).build();
} else {
select = selectBuilder.build();
}
return new DefaultPreparedOperation<>( //
select, //
renderContext, //
binding //
);
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#select(java.lang.String, java.util.Collection, java.util.function.BiConsumer)
*/
@Override
public PreparedOperation<Select> select(String tableName, Collection<String> columnNames,
BiConsumer<Table, SelectConfigurer> configurerConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notEmpty(columnNames, "Columns must not be empty");
Assert.notNull(configurerConsumer, "Configurer Consumer must not be null");
return withDialect((dialect, renderContext) -> {
DefaultSelectConfigurer configurer = new DefaultSelectConfigurer(dialect.getBindMarkersFactory().create());
Table table = Table.create(tableName);
configurerConsumer.accept(table, configurer);
List<Column> columns = table.columns(columnNames);
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table);
if (configurer.condition != null) {
selectBuilder.where(configurer.condition);
}
if (configurer.sort != null) {
selectBuilder.orderBy(createOrderByFields(table, configurer.sort));
}
Select select = selectBuilder.build();
return new DefaultPreparedOperation<Select>(select, renderContext, configurer.bindings) {
@Override
public String toQuery() {
return StatementRenderUtil.render(select, configurer.limit, configurer.offset, dialect);
}
};
});
}
private Collection<? extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {
List<OrderByField> fields = new ArrayList<>();
for (Sort.Order order : sortToUse) {
OrderByField orderByField = OrderByField.from(table.column(order.getProperty()));
if (order.getDirection() != null) {
fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());
} else {
fields.add(orderByField);
}
}
return fields;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#insert(java.lang.String, java.util.Collection, java.util.function.Consumer)
*/
@Override
public PreparedOperation<Insert> insert(String tableName, Collection<String> generatedKeysNames,
Consumer<StatementBinderBuilder> binderConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notNull(generatedKeysNames, "Generated key names must not be null");
Assert.notNull(binderConsumer, "Binder Consumer must not be null");
DefaultBinderBuilder binderBuilder = new DefaultBinderBuilder() {
@Override
public void filterBy(String identifier, SettableValue settable) {
throw new InvalidDataAccessApiUsageException("Filter-Binding for INSERT not supported. Use bind(…)");
}
};
binderConsumer.accept(binderBuilder);
return withDialect((dialect, renderContext) -> {
BindMarkers bindMarkers = dialect.getBindMarkersFactory().create();
Table table = Table.create(tableName);
Map<BindMarker, SettableValue> expressionBindings = new LinkedHashMap<>();
List<Expression> expressions = new ArrayList<>();
binderBuilder.forEachBinding((column, settableValue) -> {
BindMarker bindMarker = bindMarkers.next(column);
expressions.add(SQL.bindMarker(bindMarker.getPlaceholder()));
expressionBindings.put(bindMarker, settableValue);
});
if (expressions.isEmpty()) {
throw new IllegalStateException("INSERT contains no value expressions");
}
Binding binding = binderBuilder.build(table, bindMarkers).withBindings(expressionBindings);
Insert insert = StatementBuilder.insert().into(table).columns(table.columns(binderBuilder.bindings.keySet()))
.values(expressions).build();
return new DefaultPreparedOperation<Insert>(insert, renderContext, binding.toBindings());
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#update(java.lang.String, java.util.function.Consumer)
*/
@Override
public PreparedOperation<Update> update(String tableName, Consumer<StatementBinderBuilder> binderConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notNull(binderConsumer, "Binder Consumer must not be null");
DefaultBinderBuilder binderBuilder = new DefaultBinderBuilder();
binderConsumer.accept(binderBuilder);
return withDialect((dialect, renderContext) -> {
BindMarkers bindMarkers = dialect.getBindMarkersFactory().create();
Table table = Table.create(tableName);
Map<BindMarker, SettableValue> assignmentBindings = new LinkedHashMap<>();
List<Assignment> assignments = new ArrayList<>();
binderBuilder.forEachBinding((column, settableValue) -> {
BindMarker bindMarker = bindMarkers.next(column);
AssignValue assignment = table.column(column).set(SQL.bindMarker(bindMarker.getPlaceholder()));
assignments.add(assignment);
assignmentBindings.put(bindMarker, settableValue);
});
if (assignments.isEmpty()) {
throw new IllegalStateException("UPDATE contains no assignments");
}
UpdateBuilder.UpdateWhere updateBuilder = StatementBuilder.update(table).set(assignments);
Binding binding = binderBuilder.build(table, bindMarkers).withBindings(assignmentBindings);
Update update;
if (binding.hasCondition()) {
update = updateBuilder.where(binding.getCondition()).build();
} else {
update = updateBuilder.build();
}
return new DefaultPreparedOperation<>(update, renderContext, binding.toBindings());
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory#delete(java.lang.String, java.util.function.Consumer)
*/
@Override
public PreparedOperation<Delete> delete(String tableName, Consumer<StatementBinderBuilder> binderConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notNull(binderConsumer, "Binder Consumer must not be null");
DefaultBinderBuilder binderBuilder = new DefaultBinderBuilder() {
@Override
public void bind(String identifier, SettableValue settable) {
throw new InvalidDataAccessApiUsageException("Binding for DELETE not supported. Use filterBy(…)");
}
};
binderConsumer.accept(binderBuilder);
return withDialect((dialect, renderContext) -> {
Table table = Table.create(tableName);
DeleteBuilder.DeleteWhere deleteBuilder = StatementBuilder.delete().from(table);
BindMarkers bindMarkers = dialect.getBindMarkersFactory().create();
Binding binding = binderBuilder.build(table, bindMarkers);
Delete delete;
if (binding.hasCondition()) {
delete = deleteBuilder.where(binding.getCondition()).build();
} else {
delete = deleteBuilder.build();
}
return new DefaultPreparedOperation<>(delete, renderContext, binding.toBindings());
});
}
@Override
public PreparedOperation<Delete> delete(String tableName, BiConsumer<Table, BindConfigurer> configurerConsumer) {
Assert.hasText(tableName, "Table must not be empty");
Assert.notNull(configurerConsumer, "Configurer Consumer must not be null");
return withDialect((dialect, renderContext) -> {
Table table = Table.create(tableName);
DeleteBuilder.DeleteWhere deleteBuilder = StatementBuilder.delete().from(table);
BindMarkers bindMarkers = dialect.getBindMarkersFactory().create();
DefaultBindConfigurer configurer = new DefaultBindConfigurer(bindMarkers);
configurerConsumer.accept(table, configurer);
Delete delete;
if (configurer.condition != null) {
delete = deleteBuilder.where(configurer.condition).build();
} else {
delete = deleteBuilder.build();
}
return new DefaultPreparedOperation<>(delete, renderContext, configurer.bindings);
});
}
private <T> T withDialect(BiFunction<Dialect, RenderContext, T> action) {
Assert.notNull(action, "Action must not be null");
return action.apply(this.dialect, this.renderContext);
}
/**
* Default {@link StatementBinderBuilder} implementation.
*/
static class DefaultBinderBuilder implements StatementBinderBuilder {
final Map<String, SettableValue> filters = new LinkedHashMap<>();
final Map<String, SettableValue> bindings = new LinkedHashMap<>();
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.StatementBinderBuilder#filterBy(java.lang.String, org.springframework.data.r2dbc.domain.SettableValue)
*/
@Override
public void filterBy(String identifier, SettableValue settable) {
Assert.hasText(identifier, "FilterBy identifier must not be empty");
Assert.notNull(settable, "SettableValue for Filter must not be null");
this.filters.put(identifier, settable);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.StatementBinderBuilder#bind(java.lang.String, org.springframework.data.r2dbc.domain.SettableValue)
*/
@Override
public void bind(String identifier, SettableValue settable) {
Assert.hasText(identifier, "Bind value identifier must not be empty");
Assert.notNull(settable, "SettableValue must not be null");
this.bindings.put(identifier, settable);
}
/**
* Call {@link BiConsumer} for each filter binding.
*
* @param consumer the consumer to notify.
*/
void forEachFilter(BiConsumer<String, SettableValue> consumer) {
filters.forEach(consumer);
}
/**
* Call {@link BiConsumer} for each value binding.
*
* @param consumer the consumer to notify.
*/
void forEachBinding(BiConsumer<String, SettableValue> consumer) {
bindings.forEach(consumer);
}
Binding build(Table table, BindMarkers bindMarkers) {
Map<BindMarker, Object> values = new LinkedHashMap<>();
Map<BindMarker, SettableValue> nulls = new LinkedHashMap<>();
AtomicReference<Condition> conditionRef = new AtomicReference<>();
forEachFilter((k, v) -> {
Condition condition = toCondition(bindMarkers, table.column(k), v, values, nulls);
Condition current = conditionRef.get();
if (current == null) {
current = condition;
} else {
current = current.and(condition);
}
conditionRef.set(current);
});
return new Binding(values, nulls, conditionRef.get());
}
private static Condition toCondition(BindMarkers bindMarkers, Column column, SettableValue value,
Map<BindMarker, Object> values, Map<BindMarker, SettableValue> nulls) {
if (value.hasValue()) {
Object bindValue = value.getValue();
if (bindValue instanceof Iterable) {
Iterable<?> iterable = (Iterable<?>) bindValue;
List<Expression> expressions = new ArrayList<>();
for (Object o : iterable) {
BindMarker marker = bindMarkers.next(column.getName());
if (o == null) {
nulls.put(marker, value);
} else {
values.put(marker, o);
}
expressions.add(SQL.bindMarker(marker.getPlaceholder()));
}
return column.in(expressions.toArray(new Expression[0]));
}
BindMarker marker = bindMarkers.next(column.getName());
values.put(marker, value.getValue());
return column.isEqualTo(SQL.bindMarker(marker.getPlaceholder()));
}
return column.isNull();
}
}
/**
* Value object holding value and {@code NULL} bindings.
*
* @see SettableValue
*/
@RequiredArgsConstructor
@Getter
static class Binding {
private final Map<BindMarker, Object> values;
private final Map<BindMarker, SettableValue> nulls;
private final @Nullable Condition condition;
boolean hasCondition() {
return condition != null;
}
/**
* Append bindings.
*
* @param assignmentBindings
* @return
*/
Binding withBindings(Map<BindMarker, SettableValue> assignmentBindings) {
assignmentBindings.forEach(((bindMarker, settableValue) -> {
if (settableValue.isEmpty()) {
nulls.put(bindMarker, settableValue);
} else {
values.put(bindMarker, settableValue.getValue());
}
}));
return this;
}
/**
* Apply bindings to a {@link Statement}.
*
* @param to
*/
void apply(BindTarget to) {
values.forEach((marker, value) -> marker.bind(to, value));
nulls.forEach((marker, value) -> marker.bindNull(to, value.getType()));
}
Bindings toBindings() {
List<Bindings.Binding> bindings = new ArrayList<>(values.size() + nulls.size());
values.forEach((marker, value) -> bindings.add(new Bindings.ValueBinding(marker, value)));
nulls.forEach((marker, value) -> bindings.add(new Bindings.NullBinding(marker, value.getType())));
return new Bindings(bindings);
}
}
/**
* Default implementation of {@link PreparedOperation}.
*
* @param <T>
*/
@RequiredArgsConstructor
static class DefaultPreparedOperation<T> implements PreparedOperation<T> {
private final T source;
private final RenderContext renderContext;
private final Bindings bindings;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.PreparedOperation#getSource()
*/
@Override
public T getSource() {
return this.source;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
*/
@Override
public String toQuery() {
SqlRenderer sqlRenderer = SqlRenderer.create(renderContext);
if (this.source instanceof Select) {
return sqlRenderer.render((Select) this.source);
}
if (this.source instanceof Insert) {
return sqlRenderer.render((Insert) this.source);
}
if (this.source instanceof Update) {
return sqlRenderer.render((Update) this.source);
}
if (this.source instanceof Delete) {
return sqlRenderer.render((Delete) this.source);
}
throw new IllegalStateException("Cannot render " + this.getSource());
}
@Override
public void bindTo(BindTarget target) {
bindings.apply(target);
}
}
/**
* Default {@link SelectConfigurer} implementation.
*/
static class DefaultSelectConfigurer extends DefaultBindConfigurer implements SelectConfigurer {
OptionalLong limit = OptionalLong.empty();
OptionalLong offset = OptionalLong.empty();
Sort sort = Sort.unsorted();
DefaultSelectConfigurer(BindMarkers bindMarkers) {
super(bindMarkers);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withBindings(org.springframework.data.r2dbc.function.Bindings)
*/
@Override
public SelectConfigurer withBindings(Bindings bindings) {
super.withBindings(bindings);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withWhere(org.springframework.data.relational.core.sql.Condition)
*/
@Override
public SelectConfigurer withWhere(Condition condition) {
super.withWhere(condition);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withLimit(long)
*/
@Override
public SelectConfigurer withLimit(long limit) {
this.limit = OptionalLong.of(limit);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withOffset(long)
*/
@Override
public SelectConfigurer withOffset(long offset) {
this.offset = OptionalLong.of(offset);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withSort(org.springframework.data.domain.Sort)
*/
@Override
public SelectConfigurer withSort(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
return this;
}
}
/**
* Default {@link SelectConfigurer} implementation.
*/
static class DefaultBindConfigurer implements BindConfigurer {
private final BindMarkers bindMarkers;
@Nullable Condition condition;
Bindings bindings = new Bindings();
DefaultBindConfigurer(BindMarkers bindMarkers) {
this.bindMarkers = bindMarkers;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#bindMarkers()
*/
@Override
public BindMarkers bindMarkers() {
return this.bindMarkers;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withBindings(org.springframework.data.r2dbc.function.Bindings)
*/
@Override
public BindConfigurer withBindings(Bindings bindings) {
Assert.notNull(bindings, "Bindings must not be null");
this.bindings = Bindings.merge(this.bindings, bindings);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementFactory.SelectConfigurer#withWhere(org.springframework.data.relational.core.sql.Condition)
*/
@Override
public BindConfigurer withWhere(Condition condition) {
Assert.notNull(condition, "Condition must not be null");
this.condition = condition;
return this;
}
}
}

View File

@@ -0,0 +1,385 @@
/*
* 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 lombok.RequiredArgsConstructor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.OptionalLong;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.Bindings;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.BindTarget;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.function.query.BoundAssignments;
import org.springframework.data.r2dbc.function.query.BoundCondition;
import org.springframework.data.r2dbc.function.query.UpdateMapper;
import org.springframework.data.r2dbc.support.StatementRenderUtil;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
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.Delete;
import org.springframework.data.relational.core.sql.DeleteBuilder;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.InsertBuilder;
import org.springframework.data.relational.core.sql.InsertBuilder.InsertValuesWithBuild;
import org.springframework.data.relational.core.sql.OrderByField;
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;
import org.springframework.util.Assert;
/**
* Default {@link StatementMapper} implementation.
*
* @author Mark Paluch
*/
@RequiredArgsConstructor
class DefaultStatementMapper implements StatementMapper {
private final Dialect dialect;
private final RenderContext renderContext;
private final UpdateMapper updateMapper;
private final MappingContext<RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> mappingContext;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#forType(java.lang.Class)
*/
@Override
@SuppressWarnings("unchecked")
public <T> TypedStatementMapper<T> forType(Class<T> type) {
Assert.notNull(type, "Type must not be null!");
return new DefaultTypedStatementMapper<>(
(RelationalPersistentEntity<T>) this.mappingContext.getRequiredPersistentEntity(type));
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.SelectSpec)
*/
@Override
public PreparedOperation<?> getMappedObject(SelectSpec selectSpec) {
return getMappedObject(selectSpec, null);
}
private PreparedOperation<Select> getMappedObject(SelectSpec selectSpec,
@Nullable RelationalPersistentEntity<?> entity) {
Table table = Table.create(selectSpec.getTable());
List<Column> columns = table.columns(selectSpec.getProjectedFields());
SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table);
BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create();
Bindings bindings = Bindings.empty();
if (selectSpec.getCriteria() != null) {
BoundCondition mappedObject = this.updateMapper.getMappedObject(bindMarkers, selectSpec.getCriteria(), table,
entity);
bindings = mappedObject.getBindings();
selectBuilder.where(mappedObject.getCondition());
}
if (selectSpec.getSort().isSorted()) {
Sort mappedSort = this.updateMapper.getMappedObject(selectSpec.getSort(), entity);
selectBuilder.orderBy(createOrderByFields(table, mappedSort));
}
OptionalLong limit;
OptionalLong offset;
if (selectSpec.getPage().isPaged()) {
Pageable page = selectSpec.getPage();
limit = OptionalLong.of(page.getPageSize());
offset = OptionalLong.of(page.getOffset());
} else {
limit = OptionalLong.empty();
offset = OptionalLong.empty();
}
Select select = selectBuilder.build();
return new DefaultPreparedOperation<Select>(select, this.renderContext, bindings) {
@Override
public String toQuery() {
return StatementRenderUtil.render(select, limit, offset, DefaultStatementMapper.this.dialect);
}
};
}
private Collection<? extends OrderByField> createOrderByFields(Table table, Sort sortToUse) {
List<OrderByField> fields = new ArrayList<>();
for (Sort.Order order : sortToUse) {
OrderByField orderByField = OrderByField.from(table.column(order.getProperty()));
if (order.getDirection() != null) {
fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());
} else {
fields.add(orderByField);
}
}
return fields;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.InsertSpec)
*/
@Override
public PreparedOperation<Insert> getMappedObject(InsertSpec insertSpec) {
return getMappedObject(insertSpec, null);
}
private PreparedOperation<Insert> getMappedObject(InsertSpec insertSpec,
@Nullable RelationalPersistentEntity<?> entity) {
BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create();
Table table = Table.create(insertSpec.getTable());
BoundAssignments boundAssignments = this.updateMapper.getMappedObject(bindMarkers, insertSpec.getAssignments(),
table, entity);
Bindings bindings;
if (boundAssignments.getAssignments().isEmpty()) {
throw new IllegalStateException("INSERT contains no values");
}
bindings = boundAssignments.getBindings();
InsertBuilder.InsertIntoColumnsAndValues insertBuilder = StatementBuilder.insert(table);
InsertValuesWithBuild withBuild = (InsertValuesWithBuild) insertBuilder;
for (Assignment assignment : boundAssignments.getAssignments()) {
if (assignment instanceof AssignValue) {
AssignValue assignValue = (AssignValue) assignment;
insertBuilder.column(assignValue.getColumn());
withBuild = insertBuilder.value(assignValue.getValue());
}
}
return new DefaultPreparedOperation<>(withBuild.build(), this.renderContext, bindings);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.UpdateSpec)
*/
@Override
public PreparedOperation<Update> getMappedObject(UpdateSpec updateSpec) {
return getMappedObject(updateSpec, null);
}
private PreparedOperation<Update> getMappedObject(UpdateSpec updateSpec,
@Nullable RelationalPersistentEntity<?> entity) {
BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create();
Table table = Table.create(updateSpec.getTable());
BoundAssignments boundAssignments = this.updateMapper.getMappedObject(bindMarkers,
updateSpec.getUpdate().getAssignments(), table, entity);
Bindings bindings;
if (boundAssignments.getAssignments().isEmpty()) {
throw new IllegalStateException("UPDATE contains no assignments");
}
bindings = boundAssignments.getBindings();
UpdateBuilder.UpdateWhere updateBuilder = StatementBuilder.update(table).set(boundAssignments.getAssignments());
Update update;
if (updateSpec.getCriteria() != null) {
BoundCondition boundCondition = this.updateMapper.getMappedObject(bindMarkers, updateSpec.getCriteria(), table,
entity);
bindings = bindings.and(boundCondition.getBindings());
update = updateBuilder.where(boundCondition.getCondition()).build();
} else {
update = updateBuilder.build();
}
return new DefaultPreparedOperation<>(update, this.renderContext, bindings);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.DeleteSpec)
*/
@Override
public PreparedOperation<Delete> getMappedObject(DeleteSpec deleteSpec) {
return getMappedObject(deleteSpec, null);
}
private PreparedOperation<Delete> getMappedObject(DeleteSpec deleteSpec,
@Nullable RelationalPersistentEntity<?> entity) {
BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create();
Table table = Table.create(deleteSpec.getTable());
DeleteBuilder.DeleteWhere deleteBuilder = StatementBuilder.delete(table);
Bindings bindings = Bindings.empty();
Delete delete;
if (deleteSpec.getCriteria() != null) {
BoundCondition boundCondition = this.updateMapper.getMappedObject(bindMarkers, deleteSpec.getCriteria(), table,
entity);
bindings = boundCondition.getBindings();
delete = deleteBuilder.where(boundCondition.getCondition()).build();
} else {
delete = deleteBuilder.build();
}
return new DefaultPreparedOperation<>(delete, this.renderContext, bindings);
}
/**
* Default implementation of {@link PreparedOperation}.
*
* @param <T>
*/
@RequiredArgsConstructor
static class DefaultPreparedOperation<T> implements PreparedOperation<T> {
private final T source;
private final RenderContext renderContext;
private final Bindings bindings;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.PreparedOperation#getSource()
*/
@Override
public T getSource() {
return this.source;
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.QueryOperation#toQuery()
*/
@Override
public String toQuery() {
SqlRenderer sqlRenderer = SqlRenderer.create(this.renderContext);
if (this.source instanceof Select) {
return sqlRenderer.render((Select) this.source);
}
if (this.source instanceof Insert) {
return sqlRenderer.render((Insert) this.source);
}
if (this.source instanceof Update) {
return sqlRenderer.render((Update) this.source);
}
if (this.source instanceof Delete) {
return sqlRenderer.render((Delete) this.source);
}
throw new IllegalStateException("Cannot render " + this.getSource());
}
@Override
public void bindTo(BindTarget to) {
this.bindings.apply(to);
}
}
@RequiredArgsConstructor
class DefaultTypedStatementMapper<T> implements TypedStatementMapper<T> {
final RelationalPersistentEntity<T> entity;
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#forType(java.lang.Class)
*/
@Override
public <TC> TypedStatementMapper<TC> forType(Class<TC> type) {
return DefaultStatementMapper.this.forType(type);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.SelectSpec)
*/
@Override
public PreparedOperation<?> getMappedObject(SelectSpec selectSpec) {
return DefaultStatementMapper.this.getMappedObject(selectSpec, this.entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.InsertSpec)
*/
@Override
public PreparedOperation<?> getMappedObject(InsertSpec insertSpec) {
return DefaultStatementMapper.this.getMappedObject(insertSpec, this.entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.UpdateSpec)
*/
@Override
public PreparedOperation<?> getMappedObject(UpdateSpec updateSpec) {
return DefaultStatementMapper.this.getMappedObject(updateSpec, this.entity);
}
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.StatementMapper#getMappedObject(org.springframework.data.r2dbc.function.StatementMapper.DeleteSpec)
*/
@Override
public PreparedOperation<?> getMappedObject(DeleteSpec deleteSpec) {
return DefaultStatementMapper.this.getMappedObject(deleteSpec, this.entity);
}
}
}

View File

@@ -21,22 +21,17 @@ import io.r2dbc.spi.RowMetadata;
import java.util.List;
import java.util.function.BiFunction;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.BindableOperation;
import org.springframework.data.r2dbc.domain.Bindings;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.r2dbc.function.query.BoundCondition;
import org.springframework.data.r2dbc.function.query.Criteria;
import org.springframework.data.relational.core.sql.Table;
/**
* Draft of a data access strategy that generalizes convenience operations using mapped entities. Typically used
* internally by {@link DatabaseClient} and repository support. SQL creation is limited to single-table operations and
* single-column primary keys.
* Data access strategy that generalizes convenience operations using mapped entities. Typically used internally by
* {@link DatabaseClient} and repository support. SQL creation is limited to single-table operations and single-column
* primary keys.
*
* @author Mark Paluch
* @see BindableOperation
@@ -44,10 +39,16 @@ import org.springframework.data.relational.core.sql.Table;
public interface ReactiveDataAccessStrategy {
/**
* @param typeToRead
* @return all field names for a specific type.
* @param entityType
* @return all column names for a specific type.
*/
List<String> getAllColumns(Class<?> typeToRead);
List<String> getAllColumns(Class<?> entityType);
/**
* @param entityType
* @return all Id column names for a specific type.
*/
List<String> getIdentifierColumns(Class<?> entityType);
/**
* Returns a {@link OutboundRow} that maps column names to a {@link SettableValue} value.
@@ -57,34 +58,6 @@ public interface ReactiveDataAccessStrategy {
*/
OutboundRow getOutboundRow(Object object);
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
*
* @param sort must not be {@literal null}.
* @param typeToRead must not be {@literal null}.
* @return
*/
Sort getMappedSort(Sort sort, Class<?> typeToRead);
/**
* Map the {@link Criteria} object to apply value mapping and return a {@link BoundCondition} with {@link Bindings}.
*
* @param criteria must not be {@literal null}.
* @param table must not be {@literal null}.
* @return
*/
BoundCondition getMappedCriteria(Criteria criteria, Table table);
/**
* Map the {@link Criteria} object to apply value and field name mapping and return a {@link BoundCondition} with
* {@link Bindings}.
*
* @param criteria must not be {@literal null}.
* @param table must not be {@literal null}.
* @return
*/
BoundCondition getMappedCriteria(Criteria criteria, Table table, Class<?> typeToRead);
// TODO: Broaden T to Mono<T>/Flux<T> for reactive relational data access?
<T> BiFunction<Row, RowMetadata, T> getRowMapper(Class<T> typeToRead);
@@ -95,11 +68,11 @@ public interface ReactiveDataAccessStrategy {
String getTableName(Class<?> type);
/**
* Returns the {@link Dialect}-specific {@link StatementFactory}.
* Returns the {@link Dialect}-specific {@link StatementMapper}.
*
* @return the {@link Dialect}-specific {@link StatementFactory}.
* @return the {@link Dialect}-specific {@link StatementMapper}.
*/
StatementFactory getStatements();
StatementMapper getStatementMapper();
/**
* Returns the configured {@link BindMarkersFactory} to create native parameter placeholder markers.

View File

@@ -1,244 +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 java.util.Collection;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.Dialect;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.Bindings;
import org.springframework.data.r2dbc.domain.PreparedOperation;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.relational.core.sql.Insert;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.core.sql.Update;
import org.springframework.util.Assert;
/**
* Interface declaring statement methods that are commonly used for {@code SELECT/INSERT/UPDATE/DELETE} operations.
* These methods consider {@link Dialect} specifics and accept bind parameters with values.
*
* @author Mark Paluch
* @see PreparedOperation
*/
public interface StatementFactory {
/**
* Creates a {@link Select} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param columnNames the columns to project, must not be {@literal null} or empty.
* @param binderConsumer customizer for bindings. Supports only
* {@link StatementBinderBuilder#filterBy(String, SettableValue)} bindings.
* @return the {@link PreparedOperation} to select the given columns.
*/
PreparedOperation<Select> select(String tableName, Collection<String> columnNames,
Consumer<StatementBinderBuilder> binderConsumer);
/**
* Creates a {@link Select} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param columnNames the columns to project, must not be {@literal null} or empty.
* @param configurerConsumer customizer for {@link SelectConfigurer}.
* @return the {@link PreparedOperation} to select the given columns.
*/
PreparedOperation<Select> select(String tableName, Collection<String> columnNames,
BiConsumer<Table, SelectConfigurer> configurerConsumer);
/**
* Creates a {@link Insert} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param generatedKeysNames must not be {@literal null}.
* @param binderConsumer customizer for bindings. Supports only
* {@link StatementBinderBuilder#bind(String, SettableValue)} bindings.
* @return the {@link PreparedOperation} to update values in {@code tableName} assigning bound values.
*/
PreparedOperation<Insert> insert(String tableName, Collection<String> generatedKeysNames,
Consumer<StatementBinderBuilder> binderConsumer);
/**
* Creates a {@link Update} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param binderConsumer customizer for bindings.
* @return the {@link PreparedOperation} to update values in {@code tableName} assigning bound values.
*/
PreparedOperation<Update> update(String tableName, Consumer<StatementBinderBuilder> binderConsumer);
/**
* Creates a {@link Delete} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param binderConsumer customizer for bindings. Supports only
* {@link StatementBinderBuilder#filterBy(String, SettableValue)} bindings.
* @return the {@link PreparedOperation} to delete rows from {@code tableName}.
*/
PreparedOperation<Delete> delete(String tableName, Consumer<StatementBinderBuilder> binderConsumer);
/**
* Creates a {@link Delete} statement.
*
* @param tableName must not be {@literal null} or empty.
* @param configurerConsumer customizer for {@link SelectConfigurer}.
* @return the {@link PreparedOperation} to delete rows from {@code tableName}.
*/
PreparedOperation<Delete> delete(String tableName, BiConsumer<Table, BindConfigurer> configurerConsumer);
/**
* Binder to specify parameter bindings by name. Bindings match to equals comparisons.
*/
interface StatementBinderBuilder {
/**
* Bind the given Id {@code value} to this builder using the underlying binding strategy to express a filter
* condition. {@link Collection} type values translate to {@code IN} matching.
*
* @param identifier named identifier that is considered by the underlying binding strategy.
* @param settable must not be {@literal null}. Use {@link SettableValue#empty(Class)} for {@code NULL} values.
*/
void filterBy(String identifier, SettableValue settable);
/**
* Bind the given {@code value} to this builder using the underlying binding strategy.
*
* @param identifier named identifier that is considered by the underlying binding strategy.
* @param settable must not be {@literal null}. Use {@link SettableValue#empty(Class)} for {@code NULL} values.
*/
void bind(String identifier, SettableValue settable);
}
/**
* Binder to specify parameter bindings by name. Bindings match to equals comparisons.
*/
interface SelectConfigurer extends BindConfigurer {
/**
* Returns the {@link BindMarkers} that are currently in use. Bind markers are stateful and represent the current
* state.
*
* @return the {@link BindMarkers} that are currently in use.
* @see #withBindings(Bindings)
*/
BindMarkers bindMarkers();
/**
* Apply {@link Bindings} and merge these with already existing bindings.
*
* @param bindings must not be {@literal null}.
* @return {@code this} {@link SelectConfigurer}.
* @see #bindMarkers()
*/
SelectConfigurer withBindings(Bindings bindings);
/**
* Apply a {@code WHERE} {@link Condition}. Replaces a previously configured {@link Condition}.
*
* @param condition must not be {@literal null}.
* @return {@code this} {@link SelectConfigurer}.
*/
SelectConfigurer withWhere(Condition condition);
/**
* Apply limit/offset and {@link Sort} from {@link Pageable}.
*
* @param pageable must not be {@literal null}.
* @return {@code this} {@link SelectConfigurer}.
*/
default SelectConfigurer withPageRequest(Pageable pageable) {
Assert.notNull(pageable, "Pageable must not be null");
if (pageable.isPaged()) {
SelectConfigurer configurer = withLimit(pageable.getPageSize()).withOffset(pageable.getOffset());
if (pageable.getSort().isSorted()) {
return configurer.withSort(pageable.getSort());
}
return configurer;
}
return this;
}
/**
* Apply a row limit.
*
* @param limit
* @return {@code this} {@link SelectConfigurer}.
*/
SelectConfigurer withLimit(long limit);
/**
* Apply a row offset.
*
* @param offset
* @return {@code this} {@link SelectConfigurer}.
*/
SelectConfigurer withOffset(long offset);
/**
* Apply an {@code ORDER BY} {@link Sort}. Replaces a previously configured {@link Sort}.
*
* @param sort must not be {@literal null}.
* @return {@code this} {@link SelectConfigurer}.
*/
SelectConfigurer withSort(Sort sort);
}
/**
* Binder to specify parameter bindings by name. Bindings match to equals comparisons.
*/
interface BindConfigurer {
/**
* Returns the {@link BindMarkers} that are currently in use. Bind markers are stateful and represent the current
* state.
*
* @return the {@link BindMarkers} that are currently in use.
* @see #withBindings(Bindings)
*/
BindMarkers bindMarkers();
/**
* Apply {@link Bindings} and merge these with already existing bindings.
*
* @param bindings must not be {@literal null}.
* @return {@code this} {@link BindConfigurer}.
* @see #bindMarkers()
*/
BindConfigurer withBindings(Bindings bindings);
/**
* Apply a {@code WHERE} {@link Condition}. Replaces a previously configured {@link Condition}.
*
* @param condition must not be {@literal null}.
* @return {@code this} {@link BindConfigurer}.
*/
BindConfigurer withWhere(Condition condition);
}
}

View File

@@ -0,0 +1,377 @@
/*
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.dialect.BindMarkers;
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.r2dbc.function.query.Criteria;
import org.springframework.data.r2dbc.function.query.Update;
import org.springframework.lang.Nullable;
/**
* Mapper for statement specifications to {@link PreparedOperation}. Statement mapping applies a
* {@link Dialect}-specific transformation considering {@link BindMarkers} and vendor-specific SQL differences.
*
* @author Mark Paluch
*/
public interface StatementMapper {
/**
* Create a typed {@link StatementMapper} that considers type-specific mapping metadata.
*
* @param type must not be {@literal null}.
* @param <T>
* @return the typed {@link StatementMapper}.
*/
<T> TypedStatementMapper<T> forType(Class<T> type);
/**
* Map a select specification to a {@link PreparedOperation}.
*
* @param selectSpec the insert operation definition, must not be {@literal null}.
* @return the {@link PreparedOperation} for {@link SelectSpec}.
*/
PreparedOperation<?> getMappedObject(SelectSpec selectSpec);
/**
* Map a insert specification to a {@link PreparedOperation}.
*
* @param insertSpec the insert operation definition, must not be {@literal null}.
* @return the {@link PreparedOperation} for {@link InsertSpec}.
*/
PreparedOperation<?> getMappedObject(InsertSpec insertSpec);
/**
* Map a update specification to a {@link PreparedOperation}.
*
* @param updateSpec the update operation definition, must not be {@literal null}.
* @return the {@link PreparedOperation} for {@link UpdateSpec}.
*/
PreparedOperation<?> getMappedObject(UpdateSpec updateSpec);
/**
* Map a delete specification to a {@link PreparedOperation}.
*
* @param deleteSpec the update operation definition, must not be {@literal null}.
* @return the {@link PreparedOperation} for {@link DeleteSpec}.
*/
PreparedOperation<?> getMappedObject(DeleteSpec deleteSpec);
/**
* Extension to {@link StatementMapper} that is associated with a type.
*
* @param <T>
*/
interface TypedStatementMapper<T> extends StatementMapper {}
/**
* Create a {@code SELECT} specification for {@code table}.
*
* @param table
* @return the {@link SelectSpec}.
*/
default SelectSpec createSelect(String table) {
return SelectSpec.create(table);
}
/**
* Create an {@code INSERT} specification for {@code table}.
*
* @param table
* @return the {@link InsertSpec}.
*/
default InsertSpec createInsert(String table) {
return InsertSpec.create(table);
}
/**
* Create an {@code UPDATE} specification for {@code table}.
*
* @param table
* @return the {@link UpdateSpec}.
*/
default UpdateSpec createUpdate(String table, Update update) {
return UpdateSpec.create(table, update);
}
/**
* Create a {@code DELETE} specification for {@code table}.
*
* @param table
* @return the {@link DeleteSpec}.
*/
default DeleteSpec createDelete(String table) {
return DeleteSpec.create(table);
}
/**
* {@code SELECT} specification.
*/
class SelectSpec {
private final String table;
private final List<String> projectedFields;
private final @Nullable Criteria criteria;
private final Sort sort;
private final Pageable page;
protected SelectSpec(String table, List<String> projectedFields, @Nullable Criteria criteria, Sort sort,
Pageable page) {
this.table = table;
this.projectedFields = projectedFields;
this.criteria = criteria;
this.sort = sort;
this.page = page;
}
/**
* Create an {@code SELECT} specification for {@code table}.
*
* @param table
* @return the {@link SelectSpec}.
*/
public static SelectSpec create(String table) {
return new SelectSpec(table, Collections.emptyList(), null, Sort.unsorted(), Pageable.unpaged());
}
/**
* Associate {@code projectedFields} with the select and create a new {@link SelectSpec}.
*
* @param projectedFields
* @return the {@link SelectSpec}.
*/
public SelectSpec withProjection(Collection<String> projectedFields) {
List<String> fields = new ArrayList<>(this.projectedFields);
fields.addAll(projectedFields);
return new SelectSpec(this.table, fields, this.criteria, this.sort, this.page);
}
/**
* Associate a {@link Criteria} with the select and return a new {@link SelectSpec}.
*
* @param criteria
* @return the {@link SelectSpec}.
*/
public SelectSpec withCriteria(Criteria criteria) {
return new SelectSpec(this.table, this.projectedFields, criteria, this.sort, this.page);
}
/**
* Associate {@link Sort} with the select and create a new {@link SelectSpec}.
*
* @param sort
* @return the {@link SelectSpec}.
*/
public SelectSpec withSort(Sort sort) {
return new SelectSpec(this.table, this.projectedFields, this.criteria, sort, this.page);
}
/**
* Associate a {@link Pageable} with the select and create a new {@link SelectSpec}.
*
* @param page
* @return the {@link SelectSpec}.
*/
public SelectSpec withPage(Pageable page) {
if (page.isPaged()) {
Sort sort = page.getSort();
return new SelectSpec(this.table, this.projectedFields, this.criteria, sort.isSorted() ? sort : this.sort,
page);
}
return new SelectSpec(this.table, this.projectedFields, this.criteria, this.sort, page);
}
public String getTable() {
return this.table;
}
public List<String> getProjectedFields() {
return Collections.unmodifiableList(this.projectedFields);
}
@Nullable
public Criteria getCriteria() {
return this.criteria;
}
public Sort getSort() {
return this.sort;
}
public Pageable getPage() {
return this.page;
}
}
/**
* {@code INSERT} specification.
*/
class InsertSpec {
private final String table;
private final Map<String, SettableValue> assignments;
protected InsertSpec(String table, Map<String, SettableValue> assignments) {
this.table = table;
this.assignments = assignments;
}
/**
* Create an {@code INSERT} specification for {@code table}.
*
* @param table
* @return the {@link InsertSpec}.
*/
public static InsertSpec create(String table) {
return new InsertSpec(table, Collections.emptyMap());
}
/**
* Associate a column with a {@link SettableValue} and create a new {@link InsertSpec}.
*
* @param column
* @param value
* @return the {@link InsertSpec}.
*/
public InsertSpec withColumn(String column, SettableValue value) {
Map<String, SettableValue> values = new LinkedHashMap<>(this.assignments);
values.put(column, value);
return new InsertSpec(this.table, values);
}
public String getTable() {
return this.table;
}
public Map<String, SettableValue> getAssignments() {
return Collections.unmodifiableMap(this.assignments);
}
}
/**
* {@code UPDATE} specification.
*/
class UpdateSpec {
private final String table;
private final Update update;
private final @Nullable Criteria criteria;
protected UpdateSpec(String table, Update update, @Nullable Criteria criteria) {
this.table = table;
this.update = update;
this.criteria = criteria;
}
/**
* Create an {@code INSERT} specification for {@code table}.
*
* @param table
* @return the {@link InsertSpec}.
*/
public static UpdateSpec create(String table, Update update) {
return new UpdateSpec(table, update, null);
}
/**
* Associate a {@link Criteria} with the update and return a new {@link UpdateSpec}.
*
* @param criteria
* @return the {@link UpdateSpec}.
*/
public UpdateSpec withCriteria(Criteria criteria) {
return new UpdateSpec(this.table, this.update, criteria);
}
public String getTable() {
return this.table;
}
public Update getUpdate() {
return this.update;
}
@Nullable
public Criteria getCriteria() {
return this.criteria;
}
}
/**
* {@code DELETE} specification.
*/
class DeleteSpec {
private final String table;
private final @Nullable Criteria criteria;
protected DeleteSpec(String table, @Nullable Criteria criteria) {
this.table = table;
this.criteria = criteria;
}
/**
* Create an {@code DELETE} specification for {@code table}.
*
* @param table
* @return the {@link DeleteSpec}.
*/
public static DeleteSpec create(String table) {
return new DeleteSpec(table, null);
}
/**
* Associate a {@link Criteria} with the delete and return a new {@link DeleteSpec}.
*
* @param criteria
* @return the {@link DeleteSpec}.
*/
public DeleteSpec withCriteria(Criteria criteria) {
return new DeleteSpec(this.table, criteria);
}
public String getTable() {
return this.table;
}
@Nullable
public Criteria getCriteria() {
return this.criteria;
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.query;
import java.util.List;
import org.springframework.data.r2dbc.dialect.Bindings;
import org.springframework.data.relational.core.sql.Assignment;
import org.springframework.util.Assert;
/**
* Value object representing {@link Assignment}s with their {@link Bindings}.
*
* @author Mark Paluch
*/
public class BoundAssignments {
private final Bindings bindings;
private final List<Assignment> assignments;
public BoundAssignments(Bindings bindings, List<Assignment> assignments) {
Assert.notNull(bindings, "Bindings must not be null!");
Assert.notNull(assignments, "Assignments must not be null!");
this.bindings = bindings;
this.assignments = assignments;
}
public Bindings getBindings() {
return bindings;
}
public List<Assignment> getAssignments() {
return assignments;
}
}

View File

@@ -15,7 +15,7 @@
*/
package org.springframework.data.r2dbc.function.query;
import org.springframework.data.r2dbc.domain.Bindings;
import org.springframework.data.r2dbc.dialect.Bindings;
import org.springframework.data.relational.core.sql.Condition;
import org.springframework.util.Assert;
@@ -32,8 +32,8 @@ public class BoundCondition {
public BoundCondition(Bindings bindings, Condition condition) {
Assert.notNull(bindings, "Bindings must not be null");
Assert.notNull(condition, "Condition must not be null");
Assert.notNull(bindings, "Bindings must not be null!");
Assert.notNull(condition, "Condition must not be null!");
this.bindings = bindings;
this.condition = condition;

View File

@@ -36,68 +36,68 @@ public class Criteria {
private final @Nullable Criteria previous;
private final Combinator combinator;
private final String property;
private final String column;
private final Comparator comparator;
private final @Nullable Object value;
private Criteria(String property, Comparator comparator, @Nullable Object value) {
this(null, Combinator.INITIAL, property, comparator, value);
private Criteria(String column, Comparator comparator, @Nullable Object value) {
this(null, Combinator.INITIAL, column, comparator, value);
}
private Criteria(@Nullable Criteria previous, Combinator combinator, String property, Comparator comparator,
private Criteria(@Nullable Criteria previous, Combinator combinator, String column, Comparator comparator,
@Nullable Object value) {
this.previous = previous;
this.combinator = combinator;
this.property = property;
this.column = column;
this.comparator = comparator;
this.value = value;
}
/**
* Static factory method to create a Criteria using the provided {@code property} name.
* Static factory method to create a Criteria using the provided {@code column} name.
*
* @param property
* @param column
* @return a new {@link CriteriaStep} object to complete the first {@link Criteria}.
*/
public static CriteriaStep of(String property) {
public static CriteriaStep where(String column) {
Assert.notNull(property, "Property name must not be null!");
Assert.hasText(column, "Column name must not be null or empty!");
return new DefaultCriteriaStep(property);
return new DefaultCriteriaStep(column);
}
/**
* Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code property} name.
* Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code column} name.
*
* @param property
* @param column
* @return a new {@link CriteriaStep} object to complete the next {@link Criteria}.
*/
public CriteriaStep and(String property) {
public CriteriaStep and(String column) {
Assert.notNull(property, "Property name must not be null!");
Assert.hasText(column, "Column name must not be null or empty!");
return new DefaultCriteriaStep(property) {
return new DefaultCriteriaStep(column) {
@Override
protected Criteria createCriteria(Comparator comparator, Object value) {
return new Criteria(Criteria.this, Combinator.AND, property, comparator, value);
return new Criteria(Criteria.this, Combinator.AND, column, comparator, value);
}
};
}
/**
* Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code property} name.
* Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code column} name.
*
* @param property
* @param column
* @return a new {@link CriteriaStep} object to complete the next {@link Criteria}.
*/
public CriteriaStep or(String property) {
public CriteriaStep or(String column) {
Assert.notNull(property, "Property name must not be null!");
Assert.hasText(column, "Column name must not be null or empty!");
return new DefaultCriteriaStep(property) {
return new DefaultCriteriaStep(column) {
@Override
protected Criteria createCriteria(Comparator comparator, Object value) {
return new Criteria(Criteria.this, Combinator.OR, property, comparator, value);
return new Criteria(Criteria.this, Combinator.OR, column, comparator, value);
}
};
}
@@ -128,8 +128,8 @@ public class Criteria {
/**
* @return the property name.
*/
String getProperty() {
return property;
String getColumn() {
return column;
}
/**
@@ -273,31 +273,31 @@ public class Criteria {
private final String property;
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#is(java.lang.Object)
*/
@Override
public Criteria is(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.EQ, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#not(java.lang.Object)
*/
@Override
public Criteria not(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.NEQ, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.lang.Object[])
*/
@@ -326,7 +326,7 @@ public class Criteria {
return createCriteria(Comparator.IN, values);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.lang.Object[])
*/
@@ -355,67 +355,67 @@ public class Criteria {
return createCriteria(Comparator.NOT_IN, values);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThan(java.lang.Object)
*/
@Override
public Criteria lessThan(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.LT, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#lessThanOrEquals(java.lang.Object)
*/
@Override
public Criteria lessThanOrEquals(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.LTE, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThan(java.lang.Object)
*/
@Override
public Criteria greaterThan(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.GT, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#greaterThanOrEquals(java.lang.Object)
*/
@Override
public Criteria greaterThanOrEquals(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.GTE, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#like(java.lang.Object)
*/
@Override
public Criteria like(Object value) {
Assert.notNull(value, "Value must not be null");
Assert.notNull(value, "Value must not be null!");
return createCriteria(Comparator.LIKE, value);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNull()
*/
@@ -424,7 +424,7 @@ public class Criteria {
return createCriteria(Comparator.IS_NULL, null);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNotNull()
*/

View File

@@ -15,14 +15,13 @@
*/
package org.springframework.data.r2dbc.function.query;
import static org.springframework.data.r2dbc.function.query.Criteria.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.PropertyReferenceException;
@@ -30,9 +29,12 @@ import org.springframework.data.mapping.context.InvalidPersistentPropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.domain.Bindings;
import org.springframework.data.r2dbc.domain.MutableBindings;
import org.springframework.data.r2dbc.dialect.Bindings;
import org.springframework.data.r2dbc.dialect.MutableBindings;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.r2dbc.function.query.Criteria.Combinator;
import org.springframework.data.r2dbc.function.query.Criteria.Comparator;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
import org.springframework.data.relational.core.sql.Column;
@@ -44,24 +46,25 @@ import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Maps a {@link Criteria} to {@link Condition} considering mapping metadata.
* Maps {@link Criteria} and {@link Sort} objects considering mapping metadata and dialect-specific conversion.
*
* @author Mark Paluch
*/
public class CriteriaMapper {
public class QueryMapper {
private final R2dbcConverter converter;
private final MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
/**
* Creates a new {@link CriteriaMapper} with the given {@link R2dbcConverter}.
* Creates a new {@link QueryMapper} with the given {@link R2dbcConverter}.
*
* @param converter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
public CriteriaMapper(R2dbcConverter converter) {
@SuppressWarnings({ "unchecked", "rawtypes" })
public QueryMapper(R2dbcConverter converter) {
Assert.notNull(converter, "R2dbcConverter must not be null!");
@@ -69,20 +72,50 @@ public class CriteriaMapper {
this.mappingContext = (MappingContext) converter.getMappingContext();
}
/**
* Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
*
* @param sort must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return
*/
public Sort getMappedObject(Sort sort, @Nullable RelationalPersistentEntity<?> entity) {
if (entity == null) {
return sort;
}
List<Sort.Order> mappedOrder = new ArrayList<>();
for (Sort.Order order : sort) {
RelationalPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty());
if (persistentProperty == null) {
mappedOrder.add(order);
} else {
mappedOrder.add(
Sort.Order.by(persistentProperty.getColumnName()).with(order.getNullHandling()).with(order.getDirection()));
}
}
return Sort.by(mappedOrder);
}
/**
* Map a {@link Criteria} object into {@link Condition} and consider value/{@code NULL} {@link Bindings}.
*
* @param markers bind markers object, must not be {@literal null}.
* @param criteria criteria to map, must not be {@literal null}.
* @param criteria criteria definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}.
* @return the mapped bindings.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link BoundCondition}.
*/
public BoundCondition getMappedObject(BindMarkers markers, Criteria criteria, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Assert.notNull(markers, "BindMarkers must not be null!");
Assert.notNull(criteria, "Criteria must not be null!");
Assert.notNull(table, "Table must not be null!");
Criteria current = criteria;
MutableBindings bindings = new MutableBindings(markers);
@@ -118,36 +151,53 @@ public class CriteriaMapper {
private Condition getCondition(Criteria criteria, MutableBindings bindings, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Field propertyField = createPropertyField(entity, criteria.getProperty(), this.mappingContext);
Field propertyField = createPropertyField(entity, criteria.getColumn(), this.mappingContext);
Column column = table.column(propertyField.getMappedColumnName());
Object mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
TypeInformation<?> actualType = propertyField.getTypeHint().getRequiredActualType();
return createCondition(column, mappedValue, actualType.getType(), bindings, criteria.getComparator());
Object mappedValue;
Class<?> typeHint;
if (criteria.getValue() instanceof SettableValue) {
SettableValue settableValue = (SettableValue) criteria.getValue();
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue);
} else {
mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
typeHint = actualType.getType();
}
return createCondition(column, mappedValue, typeHint, bindings, criteria.getComparator());
}
@Nullable
private Object convertValue(@Nullable Object value, TypeInformation<?> typeInformation) {
protected Object convertValue(@Nullable Object value, TypeInformation<?> typeInformation) {
if (value == null) {
return null;
}
if (typeInformation.isCollectionLike()) {
converter.writeValue(value, typeInformation);
this.converter.writeValue(value, typeInformation);
} else if (value instanceof Iterable) {
List<Object> mapped = new ArrayList<>();
for (Object o : (Iterable<?>) value) {
mapped.add(converter.writeValue(o, typeInformation));
mapped.add(this.converter.writeValue(o, typeInformation));
}
return mapped;
}
return converter.writeValue(value, typeInformation);
return this.converter.writeValue(value, typeInformation);
}
protected MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> getMappingContext() {
return this.mappingContext;
}
private Condition createCondition(Column column, @Nullable Object mappedValue, Class<?> valueType,
@@ -213,11 +263,24 @@ public class CriteriaMapper {
throw new UnsupportedOperationException("Comparator " + comparator + " not supported");
}
protected Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, String key,
Field createPropertyField(@Nullable RelationalPersistentEntity<?> entity, String key,
MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext) {
return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext);
}
Class<?> getTypeHint(Object mappedValue, Class<?> propertyType, SettableValue settableValue) {
if (mappedValue == null || propertyType.equals(Object.class)) {
return settableValue.getType();
}
if (mappedValue.getClass().equals(settableValue.getValue().getClass())) {
return settableValue.getType();
}
return propertyType;
}
private Expression bind(@Nullable Object mappedValue, Class<?> valueType, MutableBindings bindings,
BindMarker bindMarker) {
@@ -248,35 +311,13 @@ public class CriteriaMapper {
this.name = name;
}
/**
* Returns the underlying {@link RelationalPersistentProperty} backing the field. For path traversals this will be
* the property that represents the value to handle. This means it'll be the leaf property for plain paths or the
* association property in case we refer to an association somewhere in the path.
*
* @return can be {@literal null}.
*/
@Nullable
public RelationalPersistentProperty getProperty() {
return null;
}
/**
* Returns the {@link RelationalPersistentEntity} that field is owned by.
*
* @return can be {@literal null}.
*/
@Nullable
public RelationalPersistentEntity<?> getPropertyEntity() {
return null;
}
/**
* Returns the key to be used in the mapped document eventually.
*
* @return
*/
public String getMappedColumnName() {
return name;
return this.name;
}
public TypeInformation<?> getTypeHint() {
@@ -289,8 +330,6 @@ public class CriteriaMapper {
*/
protected static class MetadataBackedField extends Field {
private static final String INVALID_ASSOCIATION_REFERENCE = "Invalid path reference %s! Associations can only be pointed to directly or via their id property!";
private final RelationalPersistentEntity<?> entity;
private final MappingContext<? extends RelationalPersistentEntity<?>, RelationalPersistentProperty> mappingContext;
private final RelationalPersistentProperty property;
@@ -330,32 +369,12 @@ public class CriteriaMapper {
this.mappingContext = context;
this.path = getPath(name);
this.property = path == null ? property : path.getLeafProperty();
}
@Override
public RelationalPersistentProperty getProperty() {
return property;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getEntity()
*/
@Override
public RelationalPersistentEntity<?> getPropertyEntity() {
RelationalPersistentProperty property = getProperty();
return property == null ? null : mappingContext.getPersistentEntity(property);
this.property = this.path == null ? property : this.path.getLeafProperty();
}
@Override
public String getMappedColumnName() {
return path == null ? name : path.toDotPath(RelationalPersistentProperty::getColumnName);
}
@Nullable
protected PersistentPropertyPath<RelationalPersistentProperty> getPath() {
return path;
return this.path == null ? this.name : this.path.toDotPath(RelationalPersistentProperty::getColumnName);
}
/**
@@ -369,24 +388,20 @@ public class CriteriaMapper {
try {
PropertyPath path = PropertyPath.from(pathExpression, entity.getTypeInformation());
PropertyPath path = PropertyPath.from(pathExpression, this.entity.getTypeInformation());
if (isPathToJavaLangClassProperty(path)) {
return null;
}
return mappingContext.getPersistentPropertyPath(path);
return this.mappingContext.getPersistentPropertyPath(path);
} catch (PropertyReferenceException | InvalidPersistentPropertyPath e) {
return null;
}
}
private boolean isPathToJavaLangClassProperty(PropertyPath path) {
if (path.getType().equals(Class.class) && path.getLeafProperty().getOwningType().getType().equals(Class.class)) {
return true;
}
return false;
return path.getType().equals(Class.class) && path.getLeafProperty().getOwningType().getType().equals(Class.class);
}
/*
@@ -396,18 +411,20 @@ public class CriteriaMapper {
@Override
public TypeInformation<?> getTypeHint() {
RelationalPersistentProperty property = getProperty();
if (property == null) {
if (this.property == null) {
return super.getTypeHint();
}
if (property.getActualType().isInterface()
|| java.lang.reflect.Modifier.isAbstract(property.getActualType().getModifiers())) {
if (this.property.getActualType().isPrimitive()) {
return ClassTypeInformation.from(ClassUtils.resolvePrimitiveIfNecessary(this.property.getActualType()));
}
if (this.property.getActualType().isInterface()
|| java.lang.reflect.Modifier.isAbstract(this.property.getActualType().getModifiers())) {
return ClassTypeInformation.OBJECT;
}
return property.getTypeInformation();
return this.property.getTypeInformation();
}
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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.query;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* Class to easily construct SQL update assignments.
*
* @author Mark Paluch
*/
public class Update {
private static final Update EMPTY = new Update(Collections.emptyMap());
private final Map<String, Object> columnsToUpdate;
private Update(Map<String, Object> columnsToUpdate) {
this.columnsToUpdate = columnsToUpdate;
}
/**
* Static factory method to create an {@link Update} using the provided column.
*
* @param column
* @param value
* @return
*/
public static Update update(String column, @Nullable Object value) {
return EMPTY.set(column, value);
}
/**
* Update a column by assigning a value.
*
* @param column
* @param value
* @return
*/
public Update set(String column, @Nullable Object value) {
return addMultiFieldOperation(column, value);
}
private Update addMultiFieldOperation(String key, Object value) {
Assert.hasText(key, "Column for update must not be null or blank");
Map<String, Object> updates = new LinkedHashMap<>(this.columnsToUpdate);
updates.put(key, value);
return new Update(updates);
}
/**
* Performs the given action for each column-value tuple in this {@link Update object} until all entries have been
* processed or the action throws an exception.
*
* @param action must not be {@literal null}.
*/
void forEachColumn(BiConsumer<? super String, ? super Object> action) {
this.columnsToUpdate.forEach(action);
}
public Map<String, Object> getAssignments() {
return Collections.unmodifiableMap(this.columnsToUpdate);
}
}

View File

@@ -0,0 +1,139 @@
/*
* 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.query;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.data.r2dbc.dialect.BindMarker;
import org.springframework.data.r2dbc.dialect.BindMarkers;
import org.springframework.data.r2dbc.dialect.MutableBindings;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.sql.AssignValue;
import org.springframework.data.relational.core.sql.Assignment;
import org.springframework.data.relational.core.sql.Assignments;
import org.springframework.data.relational.core.sql.Column;
import org.springframework.data.relational.core.sql.SQL;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* A subclass of {@link QueryMapper} that maps {@link Update} to update assignments.
*
* @author Mark Paluch
*/
public class UpdateMapper extends QueryMapper {
/**
* Creates a new {@link QueryMapper} with the given {@link R2dbcConverter}.
*
* @param converter must not be {@literal null}.
*/
public UpdateMapper(R2dbcConverter converter) {
super(converter);
}
/**
* Map a {@link Update} object to {@link BoundAssignments} and consider value/{@code NULL} {@link Bindings}.
*
* @param markers bind markers object, must not be {@literal null}.
* @param update update definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link BoundAssignments}.
*/
public BoundAssignments getMappedObject(BindMarkers markers, Update update, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
return getMappedObject(markers, update.getAssignments(), table, entity);
}
/**
* Map a {@code assignments} object to {@link BoundAssignments} and consider value/{@code NULL} {@link Bindings}.
*
* @param markers bind markers object, must not be {@literal null}.
* @param assignments update/insert definition to map, must not be {@literal null}.
* @param table must not be {@literal null}.
* @param entity related {@link RelationalPersistentEntity}, can be {@literal null}.
* @return the mapped {@link BoundAssignments}.
*/
public BoundAssignments getMappedObject(BindMarkers markers, Map<String, ? extends Object> assignments, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Assert.notNull(markers, "BindMarkers must not be null!");
Assert.notNull(assignments, "Assignments must not be null!");
Assert.notNull(table, "Table must not be null!");
MutableBindings bindings = new MutableBindings(markers);
List<Assignment> result = new ArrayList<>();
assignments.forEach((column, value) -> {
Assignment assignment = getAssignment(column, value, bindings, table, entity);
result.add(assignment);
});
return new BoundAssignments(bindings, result);
}
private Assignment getAssignment(String columnName, Object value, MutableBindings bindings, Table table,
@Nullable RelationalPersistentEntity<?> entity) {
Field propertyField = createPropertyField(entity, columnName, getMappingContext());
Column column = table.column(propertyField.getMappedColumnName());
TypeInformation<?> actualType = propertyField.getTypeHint().getRequiredActualType();
Object mappedValue;
Class<?> typeHint;
if (value instanceof SettableValue) {
SettableValue settableValue = (SettableValue) value;
mappedValue = convertValue(settableValue.getValue(), propertyField.getTypeHint());
typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue);
} else {
mappedValue = convertValue(value, propertyField.getTypeHint());
if (mappedValue == null) {
return Assignments.value(column, SQL.nullLiteral());
}
typeHint = actualType.getType();
}
return createAssignment(column, mappedValue, typeHint, bindings);
}
private Assignment createAssignment(Column column, Object value, Class<?> type, MutableBindings bindings) {
BindMarker bindMarker = bindings.nextMarker(column.getName());
AssignValue assignValue = Assignments.value(column, SQL.bindMarker(bindMarker.getPlaceholder()));
if (value == null) {
bindings.bindNull(bindMarker, type);
} else {
bindings.bind(bindMarker, value);
}
return assignValue;
}
}

View File

@@ -21,23 +21,20 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.List;
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.ReactiveDataAccessStrategy;
import org.springframework.data.r2dbc.function.StatementMapper;
import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
import org.springframework.data.relational.core.sql.Delete;
import org.springframework.data.r2dbc.function.query.Criteria;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.Select;
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.render.SqlRenderer;
import org.springframework.data.relational.repository.query.RelationalEntityInformation;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
@@ -64,27 +61,19 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(objectToSave, "Object to save must not be null!");
if (entity.isNew(objectToSave)) {
if (this.entity.isNew(objectToSave)) {
return databaseClient.insert() //
.into(entity.getJavaType()) //
.using(objectToSave) //
.map(converter.populateIdIfNecessary(objectToSave)) //
return this.databaseClient.insert() //
.into(this.entity.getJavaType()) //
.table(this.entity.getTableName()).using(objectToSave) //
.map(this.converter.populateIdIfNecessary(objectToSave)) //
.first() //
.defaultIfEmpty(objectToSave);
}
Object id = entity.getRequiredId(objectToSave);
Map<String, SettableValue> columns = accessStrategy.getOutboundRow(objectToSave);
columns.remove(getIdColumnName()); // do not update the Id column.
String idColumnName = getIdColumnName();
PreparedOperation<Update> operation = accessStrategy.getStatements().update(entity.getTableName(), binder -> {
columns.forEach(binder::bind);
binder.filterBy(idColumnName, SettableValue.from(id));
});
return databaseClient.execute().sql(operation).as(entity.getJavaType()) //
return this.databaseClient.update() //
.table(this.entity.getJavaType()) //
.table(this.entity.getTableName()).using(objectToSave) //
.then() //
.thenReturn(objectToSave);
}
@@ -119,16 +108,18 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(id, "Id must not be null!");
Set<String> columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
List<String> columns = this.accessStrategy.getAllColumns(this.entity.getJavaType());
String idColumnName = getIdColumnName();
PreparedOperation<Select> operation = accessStrategy.getStatements().select(entity.getTableName(), columns,
binder -> {
binder.filterBy(idColumnName, SettableValue.from(id));
});
StatementMapper mapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType());
StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.entity.getTableName()) //
.withProjection(columns) //
.withCriteria(Criteria.where(idColumnName).is(id));
return databaseClient.execute().sql(operation) //
.as(entity.getJavaType()) //
PreparedOperation<?> operation = mapper.getMappedObject(selectSpec);
return this.databaseClient.execute().sql(operation) //
.as(this.entity.getJavaType()) //
.fetch() //
.one();
}
@@ -151,12 +142,14 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
String idColumnName = getIdColumnName();
PreparedOperation<Select> operation = accessStrategy.getStatements().select(entity.getTableName(),
Collections.singleton(idColumnName), binder -> {
binder.filterBy(idColumnName, SettableValue.from(id));
});
StatementMapper mapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType());
StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.entity.getTableName())
.withProjection(Collections.singletonList(idColumnName)) //
.withCriteria(Criteria.where(idColumnName).is(id));
return databaseClient.execute().sql(operation) //
PreparedOperation<?> operation = mapper.getMappedObject(selectSpec);
return this.databaseClient.execute().sql(operation) //
.map((r, md) -> r) //
.first() //
.hasElement();
@@ -175,7 +168,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
*/
@Override
public Flux<T> findAll() {
return databaseClient.select().from(entity.getJavaType()).fetch().all();
return this.databaseClient.select().from(this.entity.getJavaType()).fetch().all();
}
/* (non-Javadoc)
@@ -203,15 +196,17 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
return Flux.empty();
}
Set<String> columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
List<String> columns = this.accessStrategy.getAllColumns(this.entity.getJavaType());
String idColumnName = getIdColumnName();
PreparedOperation<Select> operation = accessStrategy.getStatements().select(entity.getTableName(), columns,
binder -> {
binder.filterBy(idColumnName, SettableValue.from(ids));
});
StatementMapper mapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType());
StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.entity.getTableName()) //
.withProjection(columns) //
.withCriteria(Criteria.where(idColumnName).in(ids));
return databaseClient.execute().sql(operation).as(entity.getJavaType()).fetch().all();
PreparedOperation<?> operation = mapper.getMappedObject(selectSpec);
return this.databaseClient.execute().sql(operation).as(this.entity.getJavaType()).fetch().all();
});
}
@@ -221,17 +216,16 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
@Override
public Mono<Long> count() {
Table table = Table.create(entity.getTableName());
Table table = Table.create(this.entity.getTableName());
Select select = StatementBuilder //
.select(Functions.count(table.column(getIdColumnName()))) //
.from(table) //
.build();
return databaseClient.execute().sql(SqlRenderer.toString(select)) //
return this.databaseClient.execute().sql(SqlRenderer.toString(select)) //
.map((r, md) -> r.get(0, Long.class)) //
.first() //
.defaultIfEmpty(0L);
}
/* (non-Javadoc)
@@ -242,11 +236,10 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(id, "Id must not be null!");
PreparedOperation<Delete> delete = accessStrategy.getStatements().delete(entity.getTableName(), binder -> {
binder.filterBy(getIdColumnName(), SettableValue.from(id));
});
return databaseClient.execute().sql(delete) //
return this.databaseClient.delete() //
.from(this.entity.getJavaType()) //
.table(this.entity.getTableName()) //
.matching(Criteria.where(getIdColumnName()).is(id)) //
.fetch() //
.rowsUpdated() //
.then();
@@ -259,6 +252,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
public Mono<Void> deleteById(Publisher<ID> idPublisher) {
Assert.notNull(idPublisher, "The Id Publisher must not be null!");
StatementMapper statementMapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType());
return Flux.from(idPublisher).buffer().filter(ids -> !ids.isEmpty()).concatMap(ids -> {
@@ -266,11 +260,12 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
return Flux.empty();
}
PreparedOperation<Delete> delete = accessStrategy.getStatements().delete(entity.getTableName(), binder -> {
binder.filterBy(getIdColumnName(), SettableValue.from(ids));
});
return databaseClient.execute().sql(delete).then();
return this.databaseClient.delete() //
.from(this.entity.getJavaType()) //
.table(this.entity.getTableName()) //
.matching(Criteria.where(getIdColumnName()).in(ids)) //
.fetch() //
.rowsUpdated();
}).then();
}
@@ -282,7 +277,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(objectToDelete, "Object to delete must not be null!");
return deleteById(entity.getRequiredId(objectToDelete));
return deleteById(this.entity.getRequiredId(objectToDelete));
}
/* (non-Javadoc)
@@ -305,7 +300,7 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
Assert.notNull(objectPublisher, "The Object Publisher must not be null!");
Flux<ID> idPublisher = Flux.from(objectPublisher) //
.map(entity::getRequiredId);
.map(this.entity::getRequiredId);
return deleteById(idPublisher);
}
@@ -315,16 +310,14 @@ public class SimpleR2dbcRepository<T, ID> implements ReactiveCrudRepository<T, I
*/
@Override
public Mono<Void> deleteAll() {
return databaseClient.execute().sql(String.format("DELETE FROM %s", entity.getTableName())) //
.then();
return this.databaseClient.delete().from(this.entity.getTableName()).then();
}
private String getIdColumnName() {
return converter //
return this.converter //
.getMappingContext() //
.getRequiredPersistentEntity(entity.getJavaType()) //
.getRequiredPersistentEntity(this.entity.getJavaType()) //
.getRequiredIdProperty() //
.getColumnName();
}