From a39d2eb34cf5161149876977d72b73deb25bae96 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Apr 2019 09:19:18 +0200 Subject: [PATCH 1/5] #64 - Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index a577fa59..887d3721 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.gh-64-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC. From c4fc1e89e9e1343b6fd7d16b5d0e349677992eb9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Mar 2019 14:38:32 +0100 Subject: [PATCH 2/5] #64 - Add Criteria API. We now support Criteria creation and mapping to express where conditions with a fluent API. databaseClient.select().from("legoset") .where(Criteria.of("name").like("John%").and("id").lessThanOrEquals(42055)); databaseClient.delete() .from(LegoSet.class) .where(Criteria.of("id").is(42055)) .then() databaseClient.delete() .from(LegoSet.class) .where(Criteria.of("id").is(42055)) .fetch() .rowsUpdated() --- .../BindableOperation.java | 17 +- .../data/r2dbc/domain/Bindings.java | 264 +++++++++++ .../data/r2dbc/domain/MutableBindings.java | 134 ++++++ .../data/r2dbc/function/DatabaseClient.java | 63 ++- .../r2dbc/function/DefaultDatabaseClient.java | 219 +++++++-- .../DefaultReactiveDataAccessStrategy.java | 113 ++--- .../function/DefaultStatementFactory.java | 235 +++++++++- .../function/NamedParameterExpander.java | 2 + .../r2dbc/function/NamedParameterUtils.java | 1 + .../function/ReactiveDataAccessStrategy.java | 54 ++- .../data/r2dbc/function/StatementFactory.java | 141 ++++++ .../r2dbc/function/query/BoundCondition.java | 49 ++ .../data/r2dbc/function/query/Criteria.java | 440 ++++++++++++++++++ .../r2dbc/function/query/CriteriaMapper.java | 413 ++++++++++++++++ .../r2dbc/function/query/package-info.java | 6 + .../support/SimpleR2dbcRepository.java | 3 - .../data/r2dbc/domain/BindingsUnitTests.java | 156 +++++++ ...bstractDatabaseClientIntegrationTests.java | 76 +++ .../DefaultDatabaseClientUnitTests.java | 1 + .../NamedParameterUtilsUnitTests.java | 1 + .../query/CriteriaMapperUnitTests.java | 218 +++++++++ .../function/query/CriteriaUnitTests.java | 173 +++++++ 22 files changed, 2640 insertions(+), 139 deletions(-) rename src/main/java/org/springframework/data/r2dbc/{function => domain}/BindableOperation.java (77%) create mode 100644 src/main/java/org/springframework/data/r2dbc/domain/Bindings.java create mode 100644 src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/package-info.java create mode 100644 src/test/java/org/springframework/data/r2dbc/domain/BindingsUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java similarity index 77% rename from src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java rename to src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java index d37c44f5..22f1baa3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java +++ b/src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java @@ -1,4 +1,19 @@ -package org.springframework.data.r2dbc.function; +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.domain; import io.r2dbc.spi.Statement; diff --git a/src/main/java/org/springframework/data/r2dbc/domain/Bindings.java b/src/main/java/org/springframework/data/r2dbc/domain/Bindings.java new file mode 100644 index 00000000..89c1edf1 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/domain/Bindings.java @@ -0,0 +1,264 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.domain; + +import io.r2dbc.spi.Statement; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +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.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}. + * + * @author Mark Paluch + */ +public class Bindings implements Streamable { + + private final Map bindings; + + /** + * Create empty {@link Bindings}. + */ + public Bindings() { + this.bindings = Collections.emptyMap(); + } + + /** + * Create {@link Bindings} from a {@link Map}. + * + * @param bindings must not be {@literal null}. + */ + public Bindings(Collection bindings) { + + Assert.notNull(bindings, "Bindings must not be null"); + + Map mapping = new LinkedHashMap<>(bindings.size()); + bindings.forEach(it -> mapping.put(it.getBindMarker(), it)); + this.bindings = mapping; + } + + Bindings(Map bindings) { + this.bindings = bindings; + } + + protected Map getBindings() { + return bindings; + } + + /** + * Merge this bindings with an other {@link Bindings} object and create a new merged {@link Bindings} object. + * + * @param left the left object to merge with. + * @param right the right object to merge with. + * @return a new, merged {@link Bindings} object. + */ + public static Bindings merge(Bindings left, Bindings right) { + + Assert.notNull(left, "Left side Bindings must not be null"); + Assert.notNull(right, "Right side Bindings must not be null"); + + List result = new ArrayList<>(left.getBindings().size() + right.getBindings().size()); + + result.addAll(left.getBindings().values()); + result.addAll(right.getBindings().values()); + + return new Bindings(result); + } + + /** + * Apply the bindings to a {@link Statement}. + * + * @param statement the statement to apply to. + */ + public void apply(Statement statement) { + + Assert.notNull(statement, "Statement must not be null"); + this.bindings.forEach((marker, binding) -> binding.apply(statement)); + } + + /** + * Performs the given action for each binding of this {@link Bindings} until all bindings have been processed or the + * action throws an exception. Actions are performed in the order of iteration (if an iteration order is specified). + * Exceptions thrown by the action are relayed to the + * + * @param action The action to be performed for each {@link Binding}. + */ + public void forEach(Consumer action) { + this.bindings.forEach((marker, binding) -> action.accept(binding)); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + @Override + public Iterator iterator() { + return this.bindings.values().iterator(); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#spliterator() + */ + @Override + public Spliterator spliterator() { + return this.bindings.values().spliterator(); + } + + /** + * Base class for value objects representing a value or a {@code NULL} binding. + */ + public abstract static class Binding { + + private final BindMarker marker; + + protected Binding(BindMarker marker) { + this.marker = marker; + } + + /** + * @return the associated {@link BindMarker}. + */ + public BindMarker getBindMarker() { + return marker; + } + + /** + * Return {@literal true} if there is a value present, otherwise {@literal false} for a {@code NULL} binding. + * + * @return {@literal true} if there is a value present, otherwise {@literal false} for a {@code NULL} binding. + */ + public abstract boolean hasValue(); + + /** + * Return {@literal true} if this is is a {@code NULL} binding. + * + * @return {@literal true} if this is is a {@code NULL} binding. + */ + public boolean isNull() { + return !hasValue(); + } + + /** + * Returns the value of this binding. Can be {@literal null} if this is a {@code NULL} binding. + * + * @return value of this binding. Can be {@literal null} if this is a {@code NULL} binding. + */ + @Nullable + public abstract Object getValue(); + + /** + * Applies the binding to a {@link Statement}. + * + * @param statement the statement to apply to. + */ + public abstract void apply(Statement statement); + } + + /** + * Value binding. + */ + public static class ValueBinding extends Binding { + + private final Object value; + + public ValueBinding(BindMarker marker, Object value) { + super(marker); + this.value = value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#hasValue() + */ + public boolean hasValue() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#getValue() + */ + public Object getValue() { + return value; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement) + */ + @Override + public void apply(Statement statement) { + getBindMarker().bind(statement, getValue()); + } + } + + /** + * {@code NULL} binding. + */ + public static class NullBinding extends Binding { + + private final Class valueType; + + public NullBinding(BindMarker marker, Class valueType) { + super(marker); + this.valueType = valueType; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#hasValue() + */ + public boolean hasValue() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#getValue() + */ + @Nullable + public Object getValue() { + return null; + } + + public Class getValueType() { + return valueType; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement) + */ + @Override + public void apply(Statement statement) { + getBindMarker().bindNull(statement, getValueType()); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java b/src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java new file mode 100644 index 00000000..739aa812 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java @@ -0,0 +1,134 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.domain; + +import 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; + +/** + * Mutable extension to {@link Bindings} for Value and {@code NULL} bindings for a {@link Statement} using + * {@link BindMarkers}. + * + * @author Mark Paluch + */ +public class MutableBindings extends Bindings { + + private final BindMarkers markers; + + /** + * Create new {@link MutableBindings}. + * + * @param markers must not be {@literal null}. + */ + public MutableBindings(BindMarkers markers) { + + super(new LinkedHashMap<>()); + + Assert.notNull(markers, "BindMarkers must not be null"); + + this.markers = markers; + } + + /** + * Obtain the next {@link BindMarker}. Increments {@link BindMarkers} state. + * + * @return the next {@link BindMarker}. + */ + public BindMarker nextMarker() { + return markers.next(); + } + + /** + * Obtain the next {@link BindMarker} with a name {@code hint}. Increments {@link BindMarkers} state. + * + * @param hint name hint. + * @return the next {@link BindMarker}. + */ + public BindMarker nextMarker(String hint) { + return markers.next(hint); + } + + /** + * Bind a value to {@link BindMarker}. + * + * @param marker must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@code this} {@link MutableBindings}. + */ + public MutableBindings bind(BindMarker marker, Object value) { + + Assert.notNull(marker, "BindMarker must not be null"); + Assert.notNull(value, "Value must not be null"); + + getBindings().put(marker, new ValueBinding(marker, value)); + + return this; + } + + /** + * Bind a value and return the related {@link BindMarker}. Increments {@link BindMarkers} state. + * + * @param value must not be {@literal null}. + * @return {@code this} {@link MutableBindings}. + */ + public BindMarker bind(Object value) { + + Assert.notNull(value, "Value must not be null"); + + BindMarker marker = nextMarker(); + getBindings().put(marker, new ValueBinding(marker, value)); + + return marker; + } + + /** + * Bind a {@code NULL} value to {@link BindMarker}. + * + * @param marker must not be {@literal null}. + * @param valueType must not be {@literal null}. + * @return {@code this} {@link MutableBindings}. + */ + public MutableBindings bindNull(BindMarker marker, Class valueType) { + + Assert.notNull(marker, "BindMarker must not be null"); + Assert.notNull(valueType, "Value type must not be null"); + + getBindings().put(marker, new NullBinding(marker, valueType)); + + return this; + } + + /** + * Bind a {@code NULL} value and return the related {@link BindMarker}. Increments {@link BindMarkers} state. + * + * @param valueType must not be {@literal null}. + * @return {@code this} {@link MutableBindings}. + */ + public BindMarker bindNull(Class valueType) { + + Assert.notNull(valueType, "Value type must not be null"); + + BindMarker marker = nextMarker(); + getBindings().put(marker, new NullBinding(marker, valueType)); + + return marker; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java index da2cf453..4c9a965d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -30,6 +30,7 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.domain.PreparedOperation; +import org.springframework.data.r2dbc.function.query.Criteria; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** @@ -58,6 +59,11 @@ public interface DatabaseClient { */ InsertIntoSpec insert(); + /** + * Prepare an SQL DELETE call. + */ + DeleteFromSpec delete(); + /** * Return a builder to mutate properties of this database client. */ @@ -262,7 +268,7 @@ interface SelectFromSpec { } /** - * Contract for specifying {@code SELECT} options leading to the exchange. + * Contract for specifying {@code INSERT} options leading to the exchange. */ interface InsertIntoSpec { @@ -284,6 +290,29 @@ interface InsertIntoSpec { TypedInsertSpec into(Class table); } + /** + * Contract for specifying {@code DELETE} options leading to the exchange. + */ + interface DeleteFromSpec { + + /** + * 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 + * {@literal null}. + */ + DeleteSpec 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}. + */ + DeleteSpec from(Class table); + } + /** * Contract for specifying {@code SELECT} options leading to the exchange. */ @@ -354,6 +383,13 @@ interface SelectSpec> { */ S project(String... selectedFields); + /** + * Configure a filter {@link Criteria}. + * + * @param criteria must not be {@literal null}. + */ + S where(Criteria criteria); + /** * Configure {@link Sort}. * @@ -456,6 +492,31 @@ interface InsertSpec { Mono then(); } + /** + * Contract for specifying {@code DELETE} options leading to the exchange. + */ + interface DeleteSpec { + + /** + * Configure a filter {@link Criteria}. + * + * @param criteria must not be {@literal null}. + */ + DeleteSpec where(Criteria criteria); + + /** + * 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 then(); + } + /** * Contract for specifying parameter bindings. */ diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java index ebf2107f..7f99ccfd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -34,7 +34,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -53,13 +52,19 @@ import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.UncategorizedR2dbcException; import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.BindableOperation; import org.springframework.data.r2dbc.domain.OutboundRow; import org.springframework.data.r2dbc.domain.PreparedOperation; import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; +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.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; @@ -114,6 +119,11 @@ public InsertIntoSpec insert() { return new DefaultInsertIntoSpec(); } + @Override + public DeleteFromSpec delete() { + return new DefaultDeleteFromSpec(); + } + /** * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a * {@link Mono}. The connection is released after the {@link Mono} terminates (or the subscription is cancelled). @@ -624,6 +634,7 @@ private abstract class DefaultSelectSpecSupport { final String table; final List projectedFields; + final @Nullable Criteria criteria; final Sort sort; final Pageable page; @@ -633,6 +644,7 @@ private abstract class DefaultSelectSpecSupport { this.table = table; this.projectedFields = Collections.emptyList(); + this.criteria = null; this.sort = Sort.unsorted(); this.page = Pageable.unpaged(); } @@ -644,51 +656,50 @@ public DefaultSelectSpecSupport project(String... selectedFields) { projectedFields.addAll(this.projectedFields); projectedFields.addAll(Arrays.asList(selectedFields)); - return createInstance(table, projectedFields, sort, page); + return createInstance(table, projectedFields, criteria, sort, page); + } + + public DefaultSelectSpecSupport where(Criteria whereCriteria) { + + Assert.notNull(whereCriteria, "Criteria must not be null!"); + + return createInstance(table, projectedFields, whereCriteria, sort, page); } public DefaultSelectSpecSupport orderBy(Sort sort) { Assert.notNull(sort, "Sort must not be null!"); - return createInstance(table, projectedFields, sort, page); + return createInstance(table, projectedFields, criteria, sort, page); } public DefaultSelectSpecSupport page(Pageable page) { Assert.notNull(page, "Pageable must not be null!"); - return createInstance(table, projectedFields, sort, page); + return createInstance(table, projectedFields, criteria, sort, page); } - FetchSpec execute(String sql, BiFunction mappingFunction) { - - Function selectFunction = it -> { - - if (logger.isDebugEnabled()) { - logger.debug("Executing SQL statement [" + sql + "]"); - } - - return it.createStatement(sql); - }; + FetchSpec execute(PreparedOperation preparedOperation, BiFunction mappingFunction) { + Function selectFunction = wrapPreparedOperation(preparedOperation); Function> resultFunction = it -> Flux.from(selectFunction.apply(it).execute()); return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - sql, // + preparedOperation.toQuery(), // resultFunction, // it -> Mono.error(new UnsupportedOperationException("Not available for SELECT")), // mappingFunction); } - protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, Sort sort, - Pageable page); + protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, + Criteria criteria, Sort sort, Pageable page); } private class DefaultGenericSelectSpec extends DefaultSelectSpecSupport implements GenericSelectSpec { - DefaultGenericSelectSpec(String table, List projectedFields, Sort sort, Pageable page) { - super(table, projectedFields, sort, page); + DefaultGenericSelectSpec(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page) { + super(table, projectedFields, criteria, sort, page); } DefaultGenericSelectSpec(String table) { @@ -700,7 +711,7 @@ public TypedSelectSpec as(Class resultType) { Assert.notNull(resultType, "Result type must not be null!"); - return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, resultType, + return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, resultType, dataAccessStrategy.getRowMapper(resultType)); } @@ -717,6 +728,11 @@ public DefaultGenericSelectSpec project(String... selectedFields) { return (DefaultGenericSelectSpec) super.project(selectedFields); } + @Override + public DefaultGenericSelectSpec where(Criteria criteria) { + return (DefaultGenericSelectSpec) super.where(criteria); + } + @Override public DefaultGenericSelectSpec orderBy(Sort sort) { return (DefaultGenericSelectSpec) super.orderBy(sort); @@ -734,15 +750,26 @@ public FetchSpec> fetch() { private FetchSpec exchange(BiFunction mappingFunction) { - String select = dataAccessStrategy.select(table, new LinkedHashSet<>(this.projectedFields), sort, page); + PreparedOperation operation = dataAccessStrategy.getStatements().select(table, columns, + (table, configurer) -> { + + Sort sortToUse; + if (this.sort.isSorted()) { + sortToUse = dataAccessStrategy.getMappedSort(this.sort, this.typeToRead); + } else { + sortToUse = this.sort; + } + + configurer.withPageRequest(page).withSort(sortToUse); - return execute(select, mappingFunction); + if (criteria != null) { + BoundCondition boundCondition = dataAccessStrategy.getMappedCriteria(criteria, table, this.typeToRead); + configurer.withWhere(boundCondition.getCondition()).withBindings(boundCondition.getBindings()); + } + }); + + return execute(operation, mappingFunction); } @Override - protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { - return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Criteria criteria, + Sort sort, Pageable page) { + return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, typeToRead, mappingFunction); } } @@ -923,7 +971,7 @@ private FetchSpec exchange(BiFunction mappingFunctio }; return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - sql, // + operation.toQuery(), // resultFunction, // it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // mappingFunction); @@ -1042,7 +1090,7 @@ private FetchSpec exchange(Object toInsert, BiFunction(DefaultDatabaseClient.this, // - sql, // + operation.toQuery(), // resultFunction, // it -> resultFunction // .apply(it) // @@ -1052,6 +1100,103 @@ private FetchSpec exchange(Object toInsert, BiFunction table) { + return new DefaultDeleteSpec(table, null, null); + } + } + + /** + * Default implementation of {@link DatabaseClient.TypedInsertSpec}. + */ + @RequiredArgsConstructor + class DefaultDeleteSpec implements DeleteSpec { + + private final @Nullable Class typeToDelete; + private final @Nullable String table; + private final Criteria where; + + @Override + public DeleteSpec where(Criteria criteria) { + return new DefaultDeleteSpec(this.typeToDelete, this.table, criteria); + } + + @Override + public UpdatedRowsFetchSpec fetch() { + + String table; + + if (StringUtils.isEmpty(this.table)) { + table = dataAccessStrategy.getTableName(this.typeToDelete); + } else { + table = this.table; + } + + return exchange(table); + } + + @Override + public Mono then() { + return fetch().rowsUpdated().then(); + } + + private UpdatedRowsFetchSpec exchange(String table) { + + PreparedOperation operation = dataAccessStrategy.getStatements().delete(table, (t, configurer) -> { + + if (this.where != null) { + + BoundCondition condition; + if (this.table != null) { + condition = dataAccessStrategy.getMappedCriteria(this.where, t); + } else { + condition = dataAccessStrategy.getMappedCriteria(this.where, t, this.typeToDelete); + } + + configurer.withWhere(condition.getCondition()).withBindings(condition.getBindings()); + } + }); + + Function deleteFunction = wrapPreparedOperation(operation); + Function> resultFunction = it -> Flux.from(deleteFunction.apply(it).execute()); + + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + operation.toQuery(), // + resultFunction, // + it -> resultFunction // + .apply(it) // + .flatMap(Result::getRowsUpdated) // + .collect(Collectors.summingInt(Integer::intValue)), // + (row, rowMetadata) -> rowMetadata); + } + } + + private Function wrapPreparedOperation(PreparedOperation operation) { + + return it -> { + + String sql = operation.toQuery(); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } + + Statement statement = it.createStatement(sql); + operation.bindTo(new StatementWrapper(statement)); + + return statement; + }; + } + private static Flux doInConnectionMany(Connection connection, Function> action) { try { diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java index 17f3ddbd..6089ce3f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -19,21 +19,18 @@ import io.r2dbc.spi.RowMetadata; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.OptionalLong; -import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.convert.CustomConversions.StoreConversions; -import org.springframework.data.domain.Pageable; 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; @@ -42,15 +39,13 @@ 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.support.StatementRenderUtil; +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.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.Expression; -import org.springframework.data.relational.core.sql.OrderByField; import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy; -import org.springframework.data.relational.core.sql.StatementBuilder; 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; @@ -69,6 +64,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final Dialect dialect; private final R2dbcConverter converter; + private final CriteriaMapper criteriaMapper; private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; private final StatementFactory statements; @@ -94,14 +90,6 @@ private static R2dbcConverter createConverter(Dialect dialect) { return new MappingR2dbcConverter(context, customConversions); } - public R2dbcConverter getConverter() { - return converter; - } - - public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { - return mappingContext; - } - /** * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and {@link R2dbcConverter}. * @@ -115,6 +103,7 @@ public DefaultReactiveDataAccessStrategy(Dialect dialect, R2dbcConverter convert Assert.notNull(converter, "RelationalConverter must not be null"); this.converter = converter; + this.criteriaMapper = new CriteriaMapper(converter); this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) this.converter .getMappingContext(); this.dialect = dialect; @@ -215,7 +204,7 @@ private SettableValue getArrayValue(SettableValue value, RelationalPersistentPro * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedSort(java.lang.Class, org.springframework.data.domain.Sort) */ @Override - public Sort getMappedSort(Class typeToRead, Sort sort) { + public Sort getMappedSort(Sort sort, Class typeToRead) { RelationalPersistentEntity entity = getPersistentEntity(typeToRead); if (entity == null) { @@ -238,6 +227,30 @@ public Sort getMappedSort(Class typeToRead, Sort sort) { 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) @@ -274,64 +287,24 @@ public BindMarkersFactory getBindMarkersFactory() { return dialect.getBindMarkersFactory(); } - private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { - return mappingContext.getRequiredPersistentEntity(typeToRead); - } - - @Nullable - private RelationalPersistentEntity getPersistentEntity(Class typeToRead) { - return mappingContext.getPersistentEntity(typeToRead); - } - /* * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#select(java.lang.String, java.util.Set, org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getConverter() */ - @Override - public String select(String tableName, Set columns, Sort sort, Pageable page) { - - Table table = Table.create(tableName); - - Collection selectList; - - if (columns.isEmpty()) { - selectList = Collections.singletonList(table.asterisk()); - } else { - selectList = table.columns(columns); - } - - SelectFromAndOrderBy selectBuilder = StatementBuilder // - .select(selectList) // - .from(tableName) // - .orderBy(createOrderByFields(table, sort)); - - OptionalLong limit = OptionalLong.empty(); - OptionalLong offset = OptionalLong.empty(); - - if (page.isPaged()) { - limit = OptionalLong.of(page.getPageSize()); - offset = OptionalLong.of(page.getOffset()); - } - - // See https://github.com/spring-projects/spring-data-r2dbc/issues/55 - return StatementRenderUtil.render(selectBuilder.build(), limit, offset, this.dialect); + public R2dbcConverter getConverter() { + return converter; } - private Collection createOrderByFields(Table table, Sort sortToUse) { - - List fields = new ArrayList<>(); - - for (Order order : sortToUse) { - - OrderByField orderByField = OrderByField.from(table.column(order.getProperty())); + public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { + return mappingContext; + } - if (order.getDirection() != null) { - fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc()); - } else { - fields.add(orderByField); - } - } + private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { + return mappingContext.getRequiredPersistentEntity(typeToRead); + } - return fields; + @Nullable + private RelationalPersistentEntity getPersistentEntity(Class typeToRead) { + return mappingContext.getPersistentEntity(typeToRead); } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java index 980a6ce4..bfb4cca6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java @@ -24,18 +24,23 @@ 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; @@ -44,6 +49,7 @@ 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; @@ -89,6 +95,7 @@ public void bind(String identifier, SettableValue settable) { binderConsumer.accept(binderBuilder); return withDialect((dialect, renderContext) -> { + Table table = Table.create(tableName); List columns = table.columns(columnNames); SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table); @@ -111,6 +118,63 @@ public void bind(String identifier, SettableValue settable) { }); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.StatementFactory#select(java.lang.String, java.util.Collection, java.util.function.BiConsumer) + */ + @Override + public PreparedOperation(select, renderContext, configurer.bindings) { + @Override + public String toQuery() { + return StatementRenderUtil.render(select, configurer.limit, configurer.offset, dialect); + } + }; + }); + } + + private Collection createOrderByFields(Table table, Sort sortToUse) { + + List 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) @@ -155,7 +219,7 @@ public void filterBy(String identifier, SettableValue settable) { Insert insert = StatementBuilder.insert().into(table).columns(table.columns(binderBuilder.bindings.keySet())) .values(expressions).build(); - return new DefaultPreparedOperation(insert, renderContext, binding); + return new DefaultPreparedOperation(insert, renderContext, binding.toBindings()); }); } @@ -204,7 +268,7 @@ public PreparedOperation update(String tableName, Consumer(update, renderContext, binding); + return new DefaultPreparedOperation<>(update, renderContext, binding.toBindings()); }); } @@ -242,7 +306,35 @@ public void bind(String identifier, SettableValue settable) { delete = deleteBuilder.build(); } - return new DefaultPreparedOperation<>(delete, renderContext, binding); + return new DefaultPreparedOperation<>(delete, renderContext, binding.toBindings()); + }); + } + + @Override + public PreparedOperation delete(String tableName, BiConsumer 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); }); } @@ -325,7 +417,6 @@ Binding build(Table table, BindMarkers bindMarkers) { }); return new Binding(values, nulls, conditionRef.get()); - } private static Condition toCondition(BindMarkers bindMarkers, Column column, SettableValue value, @@ -410,6 +501,16 @@ 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 = 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); + } } /** @@ -422,7 +523,7 @@ static class DefaultPreparedOperation implements PreparedOperation { private final T source; private final RenderContext renderContext; - private final Binding binding; + private final Bindings bindings; /* * (non-Javadoc) @@ -463,7 +564,129 @@ public String toQuery() { @Override public void bindTo(BindTarget target) { - binding.apply(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; } } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java index a913c62c..a7162061 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java @@ -20,8 +20,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.BindableOperation; /** * SQL translation support allowing the use of named parameters rather than native placeholders. diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java index b2efac0c..048d0124 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java @@ -31,6 +31,7 @@ import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.BindableOperation; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 03954484..22311422 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -19,15 +19,19 @@ import io.r2dbc.spi.RowMetadata; import java.util.List; -import java.util.Set; import java.util.function.BiFunction; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.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 @@ -56,11 +60,30 @@ public interface ReactiveDataAccessStrategy { /** * Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}. * - * @param typeToRead - * @param sort + * @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 */ - Sort getMappedSort(Class typeToRead, Sort sort); + 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/Flux for reactive relational data access? BiFunction getRowMapper(Class typeToRead); @@ -71,6 +94,11 @@ public interface ReactiveDataAccessStrategy { */ String getTableName(Class type); + /** + * Returns the {@link Dialect}-specific {@link StatementFactory}. + * + * @return the {@link Dialect}-specific {@link StatementFactory}. + */ StatementFactory getStatements(); /** @@ -87,20 +115,4 @@ public interface ReactiveDataAccessStrategy { */ R2dbcConverter getConverter(); - // ------------------------------------------------------------------------- - // Methods creating SQL operations. - // Subject to be moved into a SQL creation DSL. - // ------------------------------------------------------------------------- - - /** - * Create a {@code SELECT … ORDER BY … LIMIT …} operation for the given {@code table} using {@code columns} to - * project. - * - * @param table the table to insert data to. - * @param columns columns to return. - * @param sort - * @param page - * @return - */ - String select(String table, Set columns, Sort sort, Pageable page); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java b/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java index 20849af0..b03f60d7 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java @@ -16,15 +16,24 @@ 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. @@ -47,6 +56,17 @@ public interface StatementFactory { PreparedOperation select(String tableName, Collection columnNames, + BiConsumer configurerConsumer); + /** * Creates a {@link Insert} statement. * @@ -78,6 +98,15 @@ PreparedOperation insert(String tableName, Collection generatedK */ PreparedOperation delete(String tableName, Consumer 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(String tableName, BiConsumer configurerConsumer); + /** * Binder to specify parameter bindings by name. Bindings match to equals comparisons. */ @@ -100,4 +129,116 @@ interface StatementBinderBuilder { */ 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); + } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java b/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java new file mode 100644 index 00000000..744275f9 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java @@ -0,0 +1,49 @@ +/* + * 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 org.springframework.data.r2dbc.domain.Bindings; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.util.Assert; + +/** + * Value object representing a {@link Condition} with its {@link Bindings}. + * + * @author Mark Paluch + */ +public class BoundCondition { + + private final Bindings bindings; + + private final Condition condition; + + public BoundCondition(Bindings bindings, Condition condition) { + + Assert.notNull(bindings, "Bindings must not be null"); + Assert.notNull(condition, "Condition must not be null"); + + this.bindings = bindings; + this.condition = condition; + } + + public Bindings getBindings() { + return bindings; + } + + public Condition getCondition() { + return condition; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java new file mode 100644 index 00000000..f09bd50d --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java @@ -0,0 +1,440 @@ +/* + * 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 lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Central class for creating queries. It follows a fluent API style so that you can easily chain together multiple + * criteria. Static import of the {@code Criteria.property(…)} method will improve readability as in + * {@code where(property(…).is(…)}. + * + * @author Mark Paluch + */ +public class Criteria { + + private final @Nullable Criteria previous; + private final Combinator combinator; + + private final String property; + 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(@Nullable Criteria previous, Combinator combinator, String property, Comparator comparator, + @Nullable Object value) { + this.previous = previous; + this.combinator = combinator; + this.property = property; + this.comparator = comparator; + this.value = value; + } + + /** + * Static factory method to create a Criteria using the provided {@code property} name. + * + * @param property + * @return a new {@link CriteriaStep} object to complete the first {@link Criteria}. + */ + public static CriteriaStep of(String property) { + + Assert.notNull(property, "Property name must not be null!"); + + return new DefaultCriteriaStep(property); + } + + /** + * Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code property} name. + * + * @param property + * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. + */ + public CriteriaStep and(String property) { + + Assert.notNull(property, "Property name must not be null!"); + + return new DefaultCriteriaStep(property) { + @Override + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(Criteria.this, Combinator.AND, property, comparator, value); + } + }; + } + + /** + * Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code property} name. + * + * @param property + * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. + */ + public CriteriaStep or(String property) { + + Assert.notNull(property, "Property name must not be null!"); + + return new DefaultCriteriaStep(property) { + @Override + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(Criteria.this, Combinator.OR, property, comparator, value); + } + }; + } + + /** + * @return the previous {@link Criteria} object. Can be {@literal null} if there is no previous {@link Criteria}. + * @see #hasPrevious() + */ + @Nullable + Criteria getPrevious() { + return previous; + } + + /** + * @return {@literal true} if this {@link Criteria} has a previous one. + */ + boolean hasPrevious() { + return previous != null; + } + + /** + * @return {@link Combinator} to combine this criteria with a previous one. + */ + Combinator getCombinator() { + return combinator; + } + + /** + * @return the property name. + */ + String getProperty() { + return property; + } + + /** + * @return {@link Comparator}. + */ + Comparator getComparator() { + return comparator; + } + + /** + * @return the comparison value. Can be {@literal null}. + */ + @Nullable + Object getValue() { + return value; + } + + enum Comparator { + EQ, NEQ, LT, LTE, GT, GTE, IS_NULL, IS_NOT_NULL, LIKE, NOT_IN, IN, + } + + enum Combinator { + INITIAL, AND, OR; + } + + /** + * Interface declaring terminal builder methods to build a {@link Criteria}. + */ + public interface CriteriaStep { + + /** + * Creates a {@link Criteria} using equality. + * + * @param value + * @return + */ + Criteria is(Object value); + + /** + * Creates a {@link Criteria} using equality (is not). + * + * @param value + * @return + */ + Criteria not(Object value); + + /** + * Creates a {@link Criteria} using {@code IN}. + * + * @param value + * @return + */ + Criteria in(Object... values); + + /** + * Creates a {@link Criteria} using {@code IN}. + * + * @param value + * @return + */ + Criteria in(Collection values); + + /** + * Creates a {@link Criteria} using {@code NOT IN}. + * + * @param value + * @return + */ + Criteria notIn(Object... values); + + /** + * Creates a {@link Criteria} using {@code NOT IN}. + * + * @param value + * @return + */ + Criteria notIn(Collection values); + + /** + * Creates a {@link Criteria} using less-than ({@literal <}). + * + * @param value + * @return + */ + Criteria lessThan(Object value); + + /** + * Creates a {@link Criteria} using less-than or equal to ({@literal <=}). + * + * @param value + * @return + */ + Criteria lessThanOrEquals(Object value); + + /** + * Creates a {@link Criteria} using greater-than({@literal >}). + * + * @param value + * @return + */ + Criteria greaterThan(Object value); + + /** + * Creates a {@link Criteria} using greater-than or equal to ({@literal >=}). + * + * @param value + * @return + */ + Criteria greaterThanOrEquals(Object value); + + /** + * Creates a {@link Criteria} using {@code LIKE}. + * + * @param value + * @return + */ + Criteria like(Object value); + + /** + * Creates a {@link Criteria} using {@code IS NULL}. + * + * @param value + * @return + */ + Criteria isNull(); + + /** + * Creates a {@link Criteria} using {@code IS NOT NULL}. + * + * @param value + * @return + */ + Criteria isNotNull(); + } + + /** + * Default {@link CriteriaStep} implementation. + */ + @RequiredArgsConstructor + static class DefaultCriteriaStep implements CriteriaStep { + + 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"); + + 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"); + + return createCriteria(Comparator.NEQ, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.lang.Object[]) + */ + @Override + public Criteria in(Object... values) { + + Assert.notNull(values, "Values must not be null!"); + + if (values.length > 1 && values[1] instanceof Collection) { + throw new InvalidDataAccessApiUsageException( + "You can only pass in one argument of type " + values[1].getClass().getName()); + } + + return createCriteria(Comparator.IN, Arrays.asList(values)); + } + + /** + * @param values + * @return + */ + @Override + public Criteria in(Collection values) { + + Assert.notNull(values, "Values must not be null!"); + + return createCriteria(Comparator.IN, values); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.lang.Object[]) + */ + @Override + public Criteria notIn(Object... values) { + + Assert.notNull(values, "Values must not be null!"); + + if (values.length > 1 && values[1] instanceof Collection) { + throw new InvalidDataAccessApiUsageException( + "You can only pass in one argument of type " + values[1].getClass().getName()); + } + + return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); + } + + /** + * @param values + * @return + */ + @Override + public Criteria notIn(Collection values) { + + Assert.notNull(values, "Values must not be null!"); + + 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"); + + 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"); + + 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"); + + 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"); + + 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"); + + return createCriteria(Comparator.LIKE, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNull() + */ + @Override + public Criteria isNull() { + return createCriteria(Comparator.IS_NULL, null); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNotNull() + */ + @Override + public Criteria isNotNull() { + return createCriteria(Comparator.IS_NOT_NULL, null); + } + + protected Criteria createCriteria(Comparator comparator, Object value) { + return new Criteria(property, comparator, value); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java new file mode 100644 index 00000000..5fa947f7 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java @@ -0,0 +1,413 @@ +/* + * 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 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.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.PropertyReferenceException; +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.function.convert.R2dbcConverter; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Maps a {@link Criteria} to {@link Condition} considering mapping metadata. + * + * @author Mark Paluch + */ +public class CriteriaMapper { + + private final R2dbcConverter converter; + private final MappingContext, RelationalPersistentProperty> mappingContext; + + /** + * Creates a new {@link CriteriaMapper} with the given {@link R2dbcConverter}. + * + * @param converter must not be {@literal null}. + */ + @SuppressWarnings("unchecked") + public CriteriaMapper(R2dbcConverter converter) { + + Assert.notNull(converter, "R2dbcConverter must not be null!"); + + this.converter = converter; + this.mappingContext = (MappingContext) converter.getMappingContext(); + } + + /** + * 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 table must not be {@literal null}. + * @param entity related {@link RelationalPersistentEntity}. + * @return the mapped bindings. + */ + 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!"); + + Criteria current = criteria; + MutableBindings bindings = new MutableBindings(markers); + + // reverse unroll criteria chain + Map forwardChain = new HashMap<>(); + + while (current.hasPrevious()) { + forwardChain.put(current.getPrevious(), current); + current = current.getPrevious(); + } + + // perform the actual mapping + Condition mapped = getCondition(current, bindings, table, entity); + while (forwardChain.containsKey(current)) { + + Criteria nextCriteria = forwardChain.get(current); + + if (nextCriteria.getCombinator() == Combinator.AND) { + mapped = mapped.and(getCondition(nextCriteria, bindings, table, entity)); + } + + if (nextCriteria.getCombinator() == Combinator.OR) { + mapped = mapped.or(getCondition(nextCriteria, bindings, table, entity)); + } + + current = nextCriteria; + } + + return new BoundCondition(bindings, mapped); + } + + private Condition getCondition(Criteria criteria, MutableBindings bindings, Table table, + @Nullable RelationalPersistentEntity entity) { + + Field propertyField = createPropertyField(entity, criteria.getProperty(), 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()); + } + + @Nullable + + private Object convertValue(@Nullable Object value, TypeInformation typeInformation) { + + if (value == null) { + return null; + } + + if (typeInformation.isCollectionLike()) { + converter.writeValue(value, typeInformation); + } else if (value instanceof Iterable) { + + List mapped = new ArrayList<>(); + + for (Object o : (Iterable) value) { + + mapped.add(converter.writeValue(o, typeInformation)); + } + return mapped; + } + + return converter.writeValue(value, typeInformation); + } + + private Condition createCondition(Column column, @Nullable Object mappedValue, Class valueType, + MutableBindings bindings, Comparator comparator) { + + switch (comparator) { + case IS_NULL: + return column.isNull(); + case IS_NOT_NULL: + return column.isNotNull(); + } + + if (comparator == Comparator.NOT_IN || comparator == Comparator.IN) { + + Condition condition; + if (mappedValue instanceof Iterable) { + + List expressions = new ArrayList<>( + mappedValue instanceof Collection ? ((Collection) mappedValue).size() : 10); + + for (Object o : (Iterable) mappedValue) { + + BindMarker bindMarker = bindings.nextMarker(column.getName()); + expressions.add(bind(o, valueType, bindings, bindMarker)); + } + + condition = column.in(expressions.toArray(new Expression[0])); + + } else { + BindMarker bindMarker = bindings.nextMarker(column.getName()); + Expression expression = bind(mappedValue, valueType, bindings, bindMarker); + + condition = column.in(expression); + } + + if (comparator == Comparator.NOT_IN) { + condition = condition.not(); + } + + return condition; + } + + BindMarker bindMarker = bindings.nextMarker(column.getName()); + Expression expression = bind(mappedValue, valueType, bindings, bindMarker); + + switch (comparator) { + case EQ: + return column.isEqualTo(expression); + case NEQ: + return column.isNotEqualTo(expression); + case LT: + return column.isLess(expression); + case LTE: + return column.isLessOrEqualTo(expression); + case GT: + return column.isGreater(expression); + case GTE: + return column.isGreaterOrEqualTo(expression); + case LIKE: + return column.like(expression); + } + + throw new UnsupportedOperationException("Comparator " + comparator + " not supported"); + } + + protected Field createPropertyField(@Nullable RelationalPersistentEntity entity, String key, + MappingContext, RelationalPersistentProperty> mappingContext) { + return entity == null ? new Field(key) : new MetadataBackedField(key, entity, mappingContext); + } + + private Expression bind(@Nullable Object mappedValue, Class valueType, MutableBindings bindings, + BindMarker bindMarker) { + + if (mappedValue != null) { + bindings.bind(bindMarker, mappedValue); + } else { + bindings.bindNull(bindMarker, valueType); + } + + return SQL.bindMarker(bindMarker.getPlaceholder()); + } + + /** + * Value object to represent a field and its meta-information. + */ + protected static class Field { + + protected final String name; + + /** + * Creates a new {@link Field} without meta-information but the given name. + * + * @param name must not be {@literal null} or empty. + */ + public Field(String name) { + + Assert.hasText(name, "Name must not be null!"); + 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; + } + + public TypeInformation getTypeHint() { + return ClassTypeInformation.OBJECT; + } + } + + /** + * Extension of {@link Field} to be backed with mapping metadata. + */ + 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, RelationalPersistentProperty> mappingContext; + private final RelationalPersistentProperty property; + private final @Nullable PersistentPropertyPath path; + + /** + * Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and + * {@link MappingContext}. + * + * @param name must not be {@literal null} or empty. + * @param entity must not be {@literal null}. + * @param context must not be {@literal null}. + */ + protected MetadataBackedField(String name, RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> context) { + this(name, entity, context, null); + } + + /** + * Creates a new {@link MetadataBackedField} with the given name, {@link RelationalPersistentEntity} and + * {@link MappingContext} with the given {@link RelationalPersistentProperty}. + * + * @param name must not be {@literal null} or empty. + * @param entity must not be {@literal null}. + * @param context must not be {@literal null}. + * @param property may be {@literal null}. + */ + protected MetadataBackedField(String name, RelationalPersistentEntity entity, + MappingContext, RelationalPersistentProperty> context, + @Nullable RelationalPersistentProperty property) { + + super(name); + + Assert.notNull(entity, "MongoPersistentEntity must not be null!"); + + this.entity = entity; + 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); + } + + @Override + public String getMappedColumnName() { + return path == null ? name : path.toDotPath(RelationalPersistentProperty::getColumnName); + } + + @Nullable + protected PersistentPropertyPath getPath() { + return path; + } + + /** + * Returns the {@link PersistentPropertyPath} for the given {@code pathExpression}. + * + * @param pathExpression + * @return + */ + @Nullable + private PersistentPropertyPath getPath(String pathExpression) { + + try { + + PropertyPath path = PropertyPath.from(pathExpression, entity.getTypeInformation()); + + if (isPathToJavaLangClassProperty(path)) { + return null; + } + + return 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; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.QueryMapper.Field#getTypeHint() + */ + @Override + public TypeInformation getTypeHint() { + + RelationalPersistentProperty property = getProperty(); + + if (property == null) { + return super.getTypeHint(); + } + + if (property.getActualType().isInterface() + || java.lang.reflect.Modifier.isAbstract(property.getActualType().getModifiers())) { + return ClassTypeInformation.OBJECT; + } + + return property.getTypeInformation(); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/package-info.java b/src/main/java/org/springframework/data/r2dbc/function/query/package-info.java new file mode 100644 index 00000000..42e57792 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/package-info.java @@ -0,0 +1,6 @@ +/** + * Query and update support. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.r2dbc.function.query; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 347d7c6e..2f30c487 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -31,7 +31,6 @@ 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.StatementFactory; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.relational.core.sql.Delete; import org.springframework.data.relational.core.sql.Functions; @@ -123,8 +122,6 @@ public Mono findById(ID id) { Set columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType())); String idColumnName = getIdColumnName(); - StatementFactory statements; - PreparedOperation 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 @@ public DefaultTypedSelectSpec project(String... selectedFields) { } @Override - public DefaultTypedSelectSpec where(Criteria criteria) { + public DefaultTypedSelectSpec matching(Criteria criteria) { return (DefaultTypedSelectSpec) super.where(criteria); } @@ -834,42 +833,34 @@ public DefaultTypedSelectSpec orderBy(Sort sort) { } @Override - public DefaultTypedSelectSpec page(Pageable page) { - return (DefaultTypedSelectSpec) super.page(page); + public DefaultTypedSelectSpec page(Pageable pageable) { + return (DefaultTypedSelectSpec) super.page(pageable); } @Override public FetchSpec fetch() { - return exchange(mappingFunction); + return exchange(this.mappingFunction); } private FetchSpec exchange(BiFunction mappingFunction) { List 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(String tableName, Collection columnNames, - Consumer 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 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, renderContext, configurer.bindings) { - @Override - public String toQuery() { - return StatementRenderUtil.render(select, configurer.limit, configurer.offset, dialect); - } - }; - }); - } - - private Collection createOrderByFields(Table table, Sort sortToUse) { - - List 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(String tableName, Collection generatedKeysNames, - Consumer 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 expressionBindings = new LinkedHashMap<>(); - List 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, renderContext, binding.toBindings()); - }); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.StatementFactory#update(java.lang.String, java.util.function.Consumer) - */ - @Override - public PreparedOperation update(String tableName, Consumer 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 assignmentBindings = new LinkedHashMap<>(); - List 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(String tableName, Consumer 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(String tableName, BiConsumer 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 withDialect(BiFunction 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 filters = new LinkedHashMap<>(); - final Map 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 consumer) { - filters.forEach(consumer); - } - - /** - * Call {@link BiConsumer} for each value binding. - * - * @param consumer the consumer to notify. - */ - void forEachBinding(BiConsumer consumer) { - bindings.forEach(consumer); - } - - Binding build(Table table, BindMarkers bindMarkers) { - - Map values = new LinkedHashMap<>(); - Map nulls = new LinkedHashMap<>(); - - AtomicReference 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 values, Map nulls) { - - if (value.hasValue()) { - - Object bindValue = value.getValue(); - - if (bindValue instanceof Iterable) { - - Iterable iterable = (Iterable) bindValue; - List 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 values; - private final Map nulls; - - private final @Nullable Condition condition; - - boolean hasCondition() { - return condition != null; - } - - /** - * Append bindings. - * - * @param assignmentBindings - * @return - */ - Binding withBindings(Map 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 = 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 - */ - @RequiredArgsConstructor - static class DefaultPreparedOperation implements PreparedOperation { - - 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; - } - } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java new file mode 100644 index 00000000..bdf7c31c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java @@ -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, ? extends RelationalPersistentProperty> mappingContext; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.StatementMapper#forType(java.lang.Class) + */ + @Override + @SuppressWarnings("unchecked") + public TypedStatementMapper forType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return new DefaultTypedStatementMapper<>( + (RelationalPersistentEntity) 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, this.renderContext, bindings) { + @Override + public String toQuery() { + return StatementRenderUtil.render(select, limit, offset, DefaultStatementMapper.this.dialect); + } + }; + } + + private Collection createOrderByFields(Table table, Sort sortToUse) { + + List 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 getMappedObject(InsertSpec insertSpec) { + return getMappedObject(insertSpec, null); + } + + private PreparedOperation 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 getMappedObject(UpdateSpec updateSpec) { + return getMappedObject(updateSpec, null); + } + + private PreparedOperation 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 getMappedObject(DeleteSpec deleteSpec) { + return getMappedObject(deleteSpec, null); + } + + private PreparedOperation 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 + */ + @RequiredArgsConstructor + static class DefaultPreparedOperation implements PreparedOperation { + + 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 implements TypedStatementMapper { + + final RelationalPersistentEntity entity; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.StatementMapper#forType(java.lang.Class) + */ + @Override + public TypedStatementMapper forType(Class 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); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 22311422..7e331544 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -21,22 +21,17 @@ 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,46 +39,24 @@ public interface ReactiveDataAccessStrategy { /** - * @param typeToRead - * @return all field names for a specific type. + * @param entityType + * @return all column names for a specific type. */ - List getAllColumns(Class typeToRead); + List getAllColumns(Class entityType); /** - * Returns a {@link OutboundRow} that maps column names to a {@link SettableValue} value. - * - * @param object must not be {@literal null}. - * @return - */ - 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 + * @param entityType + * @return all Id column names for a specific type. */ - Sort getMappedSort(Sort sort, Class typeToRead); + List getIdentifierColumns(Class entityType); /** - * 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}. + * Returns a {@link OutboundRow} that maps column names to a {@link SettableValue} value. * - * @param criteria must not be {@literal null}. - * @param table must not be {@literal null}. + * @param object must not be {@literal null}. * @return */ - BoundCondition getMappedCriteria(Criteria criteria, Table table, Class typeToRead); + OutboundRow getOutboundRow(Object object); // TODO: Broaden T to Mono/Flux for reactive relational data access? BiFunction getRowMapper(Class 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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java b/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java deleted file mode 100644 index b03f60d7..00000000 --- a/src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java +++ /dev/null @@ -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(String tableName, Collection columnNames, - BiConsumer 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(String tableName, Collection generatedKeysNames, - Consumer 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(String tableName, Consumer 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(String tableName, Consumer 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(String tableName, BiConsumer 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); - } -} diff --git a/src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java b/src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java new file mode 100644 index 00000000..0f03e37c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java @@ -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 + * @return the typed {@link StatementMapper}. + */ + TypedStatementMapper forType(Class 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 + */ + interface TypedStatementMapper 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 projectedFields; + private final @Nullable Criteria criteria; + private final Sort sort; + private final Pageable page; + + protected SelectSpec(String table, List 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 projectedFields) { + + List 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 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 assignments; + + protected InsertSpec(String table, Map 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 values = new LinkedHashMap<>(this.assignments); + values.put(column, value); + + return new InsertSpec(this.table, values); + } + + public String getTable() { + return this.table; + } + + public Map 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; + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java b/src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java new file mode 100644 index 00000000..0a3bd3f1 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java @@ -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 assignments; + + public BoundAssignments(Bindings bindings, List 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 getAssignments() { + return assignments; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java b/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java index 744275f9..a9cba313 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java @@ -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; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java index f09bd50d..4e4153f1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java @@ -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 @@ Combinator getCombinator() { /** * @return the property name. */ - String getProperty() { - return property; + String getColumn() { + return column; } /** @@ -273,31 +273,31 @@ static class DefaultCriteriaStep implements CriteriaStep { 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 Criteria in(Collection values) { 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 Criteria notIn(Collection values) { 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 Criteria isNull() { return createCriteria(Comparator.IS_NULL, null); } - /* + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#isNotNull() */ diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java similarity index 70% rename from src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java rename to src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java index 5fa947f7..95b06533 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/CriteriaMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java @@ -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.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.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, 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 CriteriaMapper(R2dbcConverter converter) { 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 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 BoundCondition getMappedObject(BindMarkers markers, Criteria criteria, Ta 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 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, RelationalPersistentProperty> getMappingContext() { + return this.mappingContext; } private Condition createCondition(Column column, @Nullable Object mappedValue, Class valueType, @@ -213,11 +263,24 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, C throw new UnsupportedOperationException("Comparator " + comparator + " not supported"); } - protected Field createPropertyField(@Nullable RelationalPersistentEntity entity, String key, + Field createPropertyField(@Nullable RelationalPersistentEntity entity, String key, MappingContext, 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 Field(String name) { 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 TypeInformation getTypeHint() { */ 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, RelationalPersistentProperty> mappingContext; private final RelationalPersistentProperty property; @@ -330,32 +369,12 @@ protected MetadataBackedField(String name, RelationalPersistentEntity entity, 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 getPath() { - return path; + return this.path == null ? this.name : this.path.toDotPath(RelationalPersistentProperty::getColumnName); } /** @@ -369,24 +388,20 @@ private PersistentPropertyPath getPath(String path 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 @@ private boolean isPathToJavaLangClassProperty(PropertyPath path) { @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(); } } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Update.java b/src/main/java/org/springframework/data/r2dbc/function/query/Update.java new file mode 100644 index 00000000..be2f53b6 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Update.java @@ -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 columnsToUpdate; + + private Update(Map 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 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 action) { + this.columnsToUpdate.forEach(action); + } + + public Map getAssignments() { + return Collections.unmodifiableMap(this.columnsToUpdate); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java new file mode 100644 index 00000000..400a131f --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java @@ -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 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 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; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 2f30c487..94f43b1f 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -21,23 +21,20 @@ 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 Mono save(S objectToSave) { 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 columns = accessStrategy.getOutboundRow(objectToSave); - columns.remove(getIdColumnName()); // do not update the Id column. - String idColumnName = getIdColumnName(); - - PreparedOperation 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 Mono findById(ID id) { Assert.notNull(id, "Id must not be null!"); - Set columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType())); + List columns = this.accessStrategy.getAllColumns(this.entity.getJavaType()); String idColumnName = getIdColumnName(); - PreparedOperation 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)); + + PreparedOperation operation = mapper.getMappedObject(selectSpec); - return databaseClient.execute().sql(operation) // + return this.databaseClient.execute().sql(operation) // .map((r, md) -> r) // .first() // .hasElement(); @@ -175,7 +168,7 @@ public Mono existsById(Publisher publisher) { */ @Override public Flux 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 Flux findAllById(Publisher idPublisher) { return Flux.empty(); } - Set columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType())); + List columns = this.accessStrategy.getAllColumns(this.entity.getJavaType()); String idColumnName = getIdColumnName(); - PreparedOperation select = statements.select("foo", Arrays.asList("bar", "baz"), it -> {}); - - assertThat(select.getSource()).isInstanceOf(Select.class); - assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo"); - - createBoundStatement(select, connectionMock); - - verifyZeroInteractions(statementMock); - } - - @Test - public void shouldToQuerySimpleSelectWithSimpleFilter() { - - PreparedOperation select = statements.select("foo", Arrays.asList("bar", "baz"), it -> { - it.filterBy("doe", SettableValue.from("John")); - it.filterBy("baz", SettableValue.from("Jake")); - }); - - assertThat(select.getSource()).isInstanceOf(Select.class); - assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe = $1 AND foo.baz = $2"); - - createBoundStatement(select, connectionMock); - - verify(statementMock).bind(0, "John"); - verify(statementMock).bind(1, "Jake"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQuerySimpleSelectWithNullFilter() { - - PreparedOperation select = statements.select("foo", Arrays.asList("bar", "baz"), it -> { - it.filterBy("doe", SettableValue.from(Arrays.asList("John", "Jake"))); - }); - - assertThat(select.getSource()).isInstanceOf(Select.class); - assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe IN ($1, $2)"); - - createBoundStatement(select, connectionMock); - verify(statementMock).bind(0, "John"); - verify(statementMock).bind(1, "Jake"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldFailInsertToQueryingWithoutValueBindings() { - - assertThatThrownBy(() -> statements.insert("foo", Collections.emptyList(), it -> {})) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void shouldToQuerySimpleInsert() { - - PreparedOperation insert = statements.insert("foo", Collections.emptyList(), it -> { - it.bind("bar", SettableValue.from("Foo")); - }); - - assertThat(insert.getSource()).isInstanceOf(Insert.class); - assertThat(insert.toQuery()).isEqualTo("INSERT INTO foo (bar) VALUES ($1)"); - - createBoundStatement(insert, connectionMock); - verify(statementMock).bind(0, "Foo"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldFailUpdateToQueryingWithoutValueBindings() { - - assertThatThrownBy(() -> statements.update("foo", it -> it.filterBy("foo", SettableValue.empty(Object.class)))) - .isInstanceOf(IllegalStateException.class); - } - - @Test - public void shouldToQuerySimpleUpdate() { - - PreparedOperation update = statements.update("foo", it -> { - it.bind("bar", SettableValue.from("Foo")); - }); - - assertThat(update.getSource()).isInstanceOf(Update.class); - assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1"); - - createBoundStatement(update, connectionMock); - verify(statementMock).bind(0, "Foo"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQueryNullUpdate() { - - PreparedOperation update = statements.update("foo", it -> { - it.bind("bar", SettableValue.empty(String.class)); - }); - - assertThat(update.getSource()).isInstanceOf(Update.class); - assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1"); - - createBoundStatement(update, connectionMock); - verify(statementMock).bindNull(0, String.class); - - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQueryUpdateWithFilter() { - - PreparedOperation update = statements.update("foo", it -> { - it.bind("bar", SettableValue.from("Foo")); - it.filterBy("baz", SettableValue.from("Baz")); - }); - - assertThat(update.getSource()).isInstanceOf(Update.class); - assertThat(update.toQuery()).isEqualTo("UPDATE foo SET bar = $1 WHERE foo.baz = $2"); - - createBoundStatement(update, connectionMock); - verify(statementMock).bind(0, "Foo"); - verify(statementMock).bind(1, "Baz"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQuerySimpleDeleteWithSimpleFilter() { - - PreparedOperation delete = statements.delete("foo", it -> { - it.filterBy("doe", SettableValue.from("John")); - }); - - assertThat(delete.getSource()).isInstanceOf(Delete.class); - assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe = $1"); - - createBoundStatement(delete, connectionMock); - verify(statementMock).bind(0, "John"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQuerySimpleDeleteWithMultipleFilters() { - - PreparedOperation delete = statements.delete("foo", it -> { - it.filterBy("doe", SettableValue.from("John")); - it.filterBy("baz", SettableValue.from("Jake")); - }); - - assertThat(delete.getSource()).isInstanceOf(Delete.class); - assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe = $1 AND foo.baz = $2"); - - createBoundStatement(delete, connectionMock); - verify(statementMock).bind(0, "John"); - verify(statementMock).bind(1, "Jake"); - verifyNoMoreInteractions(statementMock); - } - - @Test - public void shouldToQuerySimpleDeleteWithNullFilter() { - - PreparedOperation delete = statements.delete("foo", it -> { - it.filterBy("doe", SettableValue.empty(String.class)); - }); - - assertThat(delete.getSource()).isInstanceOf(Delete.class); - assertThat(delete.toQuery()).isEqualTo("DELETE FROM foo WHERE foo.doe IS NULL"); - - createBoundStatement(delete, connectionMock); - verifyZeroInteractions(statementMock); - } - - void createBoundStatement(PreparedOperation operation, Connection connection) { - - Statement statement = connection.createStatement(operation.toQuery()); - operation.bindTo(new DefaultDatabaseClient.StatementWrapper(statement)); - } -} diff --git a/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java new file mode 100644 index 00000000..bd5ed805 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java @@ -0,0 +1,69 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.PreparedOperation; +import org.springframework.data.r2dbc.function.StatementMapper.UpdateSpec; +import org.springframework.data.r2dbc.function.query.Criteria; +import org.springframework.data.r2dbc.function.query.Update; + +/** + * Unit tests for {@link DefaultStatementMapper}. + * + * @author Mark Paluch + */ +public class StatementMapperUnitTests { + + ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + StatementMapper mapper = strategy.getStatementMapper(); + + BindTarget bindTarget = mock(BindTarget.class); + + @Test // gh-64 + public void shouldMapUpdate() { + + UpdateSpec updateSpec = mapper.createUpdate("foo", Update.update("column", "value")); + + PreparedOperation preparedOperation = mapper.getMappedObject(updateSpec); + + assertThat(preparedOperation.toQuery()).isEqualTo("UPDATE foo SET column = $1"); + + preparedOperation.bindTo(bindTarget); + verify(bindTarget).bind(0, "value"); + } + + @Test // gh-64 + public void shouldMapUpdateWithCriteria() { + + UpdateSpec updateSpec = mapper.createUpdate("foo", Update.update("column", "value")) + .withCriteria(Criteria.where("foo").is("bar")); + + PreparedOperation preparedOperation = mapper.getMappedObject(updateSpec); + + assertThat(preparedOperation.toQuery()).isEqualTo("UPDATE foo SET column = $1 WHERE foo.foo = $2"); + + preparedOperation.bindTo(bindTarget); + verify(bindTarget).bind(0, "value"); + verify(bindTarget).bind(1, "bar"); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java index c6ac54cd..32d190c5 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java @@ -34,9 +34,9 @@ public class CriteriaUnitTests { @Test // gh-64 public void andChainedCriteria() { - Criteria criteria = of("foo").is("bar").and("baz").isNotNull(); + Criteria criteria = where("foo").is("bar").and("baz").isNotNull(); - assertThat(criteria.getProperty()).isEqualTo("baz"); + assertThat(criteria.getColumn()).isEqualTo("baz"); assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); assertThat(criteria.getValue()).isNull(); assertThat(criteria.getPrevious()).isNotNull(); @@ -44,7 +44,7 @@ public void andChainedCriteria() { criteria = criteria.getPrevious(); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.EQ); assertThat(criteria.getValue()).isEqualTo("bar"); } @@ -52,9 +52,9 @@ public void andChainedCriteria() { @Test // gh-64 public void orChainedCriteria() { - Criteria criteria = of("foo").is("bar").or("baz").isNotNull(); + Criteria criteria = where("foo").is("bar").or("baz").isNotNull(); - assertThat(criteria.getProperty()).isEqualTo("baz"); + assertThat(criteria.getColumn()).isEqualTo("baz"); assertThat(criteria.getCombinator()).isEqualTo(Combinator.OR); criteria = criteria.getPrevious(); @@ -66,9 +66,9 @@ public void orChainedCriteria() { @Test // gh-64 public void shouldBuildEqualsCriteria() { - Criteria criteria = of("foo").is("bar"); + Criteria criteria = where("foo").is("bar"); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.EQ); assertThat(criteria.getValue()).isEqualTo("bar"); } @@ -76,9 +76,9 @@ public void shouldBuildEqualsCriteria() { @Test // gh-64 public void shouldBuildNotEqualsCriteria() { - Criteria criteria = of("foo").not("bar"); + Criteria criteria = where("foo").not("bar"); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.NEQ); assertThat(criteria.getValue()).isEqualTo("bar"); } @@ -86,9 +86,9 @@ public void shouldBuildNotEqualsCriteria() { @Test // gh-64 public void shouldBuildInCriteria() { - Criteria criteria = of("foo").in("bar", "baz"); + Criteria criteria = where("foo").in("bar", "baz"); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.IN); assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); } @@ -96,9 +96,9 @@ public void shouldBuildInCriteria() { @Test // gh-64 public void shouldBuildNotInCriteria() { - Criteria criteria = of("foo").notIn("bar", "baz"); + Criteria criteria = where("foo").notIn("bar", "baz"); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.NOT_IN); assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); } @@ -106,9 +106,9 @@ public void shouldBuildNotInCriteria() { @Test // gh-64 public void shouldBuildGtCriteria() { - Criteria criteria = of("foo").greaterThan(1); + Criteria criteria = where("foo").greaterThan(1); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.GT); assertThat(criteria.getValue()).isEqualTo(1); } @@ -116,9 +116,9 @@ public void shouldBuildGtCriteria() { @Test // gh-64 public void shouldBuildGteCriteria() { - Criteria criteria = of("foo").greaterThanOrEquals(1); + Criteria criteria = where("foo").greaterThanOrEquals(1); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.GTE); assertThat(criteria.getValue()).isEqualTo(1); } @@ -126,9 +126,9 @@ public void shouldBuildGteCriteria() { @Test // gh-64 public void shouldBuildLtCriteria() { - Criteria criteria = of("foo").lessThan(1); + Criteria criteria = where("foo").lessThan(1); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.LT); assertThat(criteria.getValue()).isEqualTo(1); } @@ -136,9 +136,9 @@ public void shouldBuildLtCriteria() { @Test // gh-64 public void shouldBuildLteCriteria() { - Criteria criteria = of("foo").lessThanOrEquals(1); + Criteria criteria = where("foo").lessThanOrEquals(1); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.LTE); assertThat(criteria.getValue()).isEqualTo(1); } @@ -146,9 +146,9 @@ public void shouldBuildLteCriteria() { @Test // gh-64 public void shouldBuildLikeCriteria() { - Criteria criteria = of("foo").like("hello%"); + Criteria criteria = where("foo").like("hello%"); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.LIKE); assertThat(criteria.getValue()).isEqualTo("hello%"); } @@ -156,18 +156,18 @@ public void shouldBuildLikeCriteria() { @Test // gh-64 public void shouldBuildIsNullCriteria() { - Criteria criteria = of("foo").isNull(); + Criteria criteria = where("foo").isNull(); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NULL); } @Test // gh-64 public void shouldBuildIsNotNullCriteria() { - Criteria criteria = of("foo").isNotNull(); + Criteria criteria = where("foo").isNotNull(); - assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getColumn()).isEqualTo("foo"); assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java similarity index 67% rename from src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java index e47f9c11..966ff4a9 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java @@ -17,12 +17,14 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; - -import io.r2dbc.spi.Statement; +import static org.springframework.data.domain.Sort.Order.*; import org.junit.Test; +import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.relational.core.mapping.Column; @@ -30,33 +32,46 @@ import org.springframework.data.relational.core.sql.Table; /** - * Unit tests for {@link CriteriaMapper}. + * Unit tests for {@link QueryMapper}. * * @author Mark Paluch */ -public class CriteriaMapperUnitTests { +public class QueryMapperUnitTests { R2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); - CriteriaMapper mapper = new CriteriaMapper(converter); - Statement statementMock = mock(Statement.class); + QueryMapper mapper = new QueryMapper(converter); + BindTarget bindTarget = mock(BindTarget.class); @Test // gh-64 public void shouldMapSimpleCriteria() { - Criteria criteria = Criteria.of("name").is("foo"); + Criteria criteria = Criteria.where("name").is("foo"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name = ?[$1]"); + + bindings.getBindings().apply(bindTarget); + verify(bindTarget).bind(0, "foo"); + } + + @Test // gh-64 + public void shouldMapSimpleNullableCriteria() { + + Criteria criteria = Criteria.where("name").is(SettableValue.empty(Integer.class)); BoundCondition bindings = map(criteria); assertThat(bindings.getCondition().toString()).isEqualTo("person.name = ?[$1]"); - bindings.getBindings().apply(statementMock); - verify(statementMock).bind(0, "foo"); + bindings.getBindings().apply(bindTarget); + verify(bindTarget).bindNull(0, Integer.class); } @Test // gh-64 public void shouldConsiderColumnName() { - Criteria criteria = Criteria.of("alternative").is("foo"); + Criteria criteria = Criteria.where("alternative").is("foo"); BoundCondition bindings = map(criteria); @@ -66,21 +81,21 @@ public void shouldConsiderColumnName() { @Test // gh-64 public void shouldMapAndCriteria() { - Criteria criteria = Criteria.of("name").is("foo").and("bar").is("baz"); + Criteria criteria = Criteria.where("name").is("foo").and("bar").is("baz"); BoundCondition bindings = map(criteria); assertThat(bindings.getCondition().toString()).isEqualTo("person.name = ?[$1] AND person.bar = ?[$2]"); - bindings.getBindings().apply(statementMock); - verify(statementMock).bind(0, "foo"); - verify(statementMock).bind(1, "baz"); + bindings.getBindings().apply(bindTarget); + verify(bindTarget).bind(0, "foo"); + verify(bindTarget).bind(1, "baz"); } @Test // gh-64 public void shouldMapOrCriteria() { - Criteria criteria = Criteria.of("name").is("foo").or("bar").is("baz"); + Criteria criteria = Criteria.where("name").is("foo").or("bar").is("baz"); BoundCondition bindings = map(criteria); @@ -90,7 +105,7 @@ public void shouldMapOrCriteria() { @Test // gh-64 public void shouldMapAndOrCriteria() { - Criteria criteria = Criteria.of("name").is("foo") // + Criteria criteria = Criteria.where("name").is("foo") // .and("name").isNotNull() // .or("bar").is("baz") // .and("anotherOne").is("alternative"); @@ -104,7 +119,7 @@ public void shouldMapAndOrCriteria() { @Test // gh-64 public void shouldMapNeq() { - Criteria criteria = Criteria.of("name").not("foo"); + Criteria criteria = Criteria.where("name").not("foo"); BoundCondition bindings = map(criteria); @@ -114,7 +129,7 @@ public void shouldMapNeq() { @Test // gh-64 public void shouldMapIsNull() { - Criteria criteria = Criteria.of("name").isNull(); + Criteria criteria = Criteria.where("name").isNull(); BoundCondition bindings = map(criteria); @@ -124,7 +139,7 @@ public void shouldMapIsNull() { @Test // gh-64 public void shouldMapIsNotNull() { - Criteria criteria = Criteria.of("name").isNotNull(); + Criteria criteria = Criteria.where("name").isNotNull(); BoundCondition bindings = map(criteria); @@ -134,7 +149,7 @@ public void shouldMapIsNotNull() { @Test // gh-64 public void shouldMapIsIn() { - Criteria criteria = Criteria.of("name").in("a", "b", "c"); + Criteria criteria = Criteria.where("name").in("a", "b", "c"); BoundCondition bindings = map(criteria); @@ -144,7 +159,7 @@ public void shouldMapIsIn() { @Test // gh-64 public void shouldMapIsNotIn() { - Criteria criteria = Criteria.of("name").notIn("a", "b", "c"); + Criteria criteria = Criteria.where("name").notIn("a", "b", "c"); BoundCondition bindings = map(criteria); @@ -154,7 +169,7 @@ public void shouldMapIsNotIn() { @Test // gh-64 public void shouldMapIsGt() { - Criteria criteria = Criteria.of("name").greaterThan("a"); + Criteria criteria = Criteria.where("name").greaterThan("a"); BoundCondition bindings = map(criteria); @@ -164,7 +179,7 @@ public void shouldMapIsGt() { @Test // gh-64 public void shouldMapIsGte() { - Criteria criteria = Criteria.of("name").greaterThanOrEquals("a"); + Criteria criteria = Criteria.where("name").greaterThanOrEquals("a"); BoundCondition bindings = map(criteria); @@ -174,7 +189,7 @@ public void shouldMapIsGte() { @Test // gh-64 public void shouldMapIsLt() { - Criteria criteria = Criteria.of("name").lessThan("a"); + Criteria criteria = Criteria.where("name").lessThan("a"); BoundCondition bindings = map(criteria); @@ -184,7 +199,7 @@ public void shouldMapIsLt() { @Test // gh-64 public void shouldMapIsLte() { - Criteria criteria = Criteria.of("name").lessThanOrEquals("a"); + Criteria criteria = Criteria.where("name").lessThanOrEquals("a"); BoundCondition bindings = map(criteria); @@ -194,13 +209,24 @@ public void shouldMapIsLte() { @Test // gh-64 public void shouldMapIsLike() { - Criteria criteria = Criteria.of("name").like("a"); + Criteria criteria = Criteria.where("name").like("a"); BoundCondition bindings = map(criteria); assertThat(bindings.getCondition().toString()).isEqualTo("person.name LIKE ?[$1]"); } + @Test // gh-64 + public void shouldMapSort() { + + Sort sort = Sort.by(desc("alternative")); + + Sort mapped = mapper.getMappedObject(sort, converter.getMappingContext().getRequiredPersistentEntity(Person.class)); + + assertThat(mapped.getOrderFor("another_name")).isEqualTo(desc("another_name")); + assertThat(mapped.getOrderFor("alternative")).isNull(); + } + @SuppressWarnings("unchecked") private BoundCondition map(Criteria criteria) { diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java new file mode 100644 index 00000000..9c239303 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java @@ -0,0 +1,106 @@ +/* + * 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 static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.AssignValue; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Table; + +/** + * Unit tests for {@link UpdateMapper}. + * + * @author Mark Paluch + */ +public class UpdateMapperUnitTests { + + R2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); + UpdateMapper mapper = new UpdateMapper(converter); + BindTarget bindTarget = mock(BindTarget.class); + + @Test // gh-64 + public void shouldMapFieldNamesInUpdate() { + + Update update = Update.update("alternative", "foo"); + + BoundAssignments mapped = map(update); + + Map assignments = mapped.getAssignments().stream().map(it -> (AssignValue) it) + .collect(Collectors.toMap(k -> k.getColumn().getName(), AssignValue::getValue)); + + assertThat(assignments).containsEntry("another_name", SQL.bindMarker("$1")); + } + + @Test // gh-64 + public void shouldUpdateToSettableValue() { + + Update update = Update.update("alternative", SettableValue.empty(String.class)); + + BoundAssignments mapped = map(update); + + Map assignments = mapped.getAssignments().stream().map(it -> (AssignValue) it) + .collect(Collectors.toMap(k -> k.getColumn().getName(), AssignValue::getValue)); + + assertThat(assignments).containsEntry("another_name", SQL.bindMarker("$1")); + + mapped.getBindings().apply(bindTarget); + verify(bindTarget).bindNull(0, String.class); + } + + @Test // gh-64 + public void shouldUpdateToNull() { + + Update update = Update.update("alternative", null); + + BoundAssignments mapped = map(update); + + assertThat(mapped.getAssignments()).hasSize(1); + assertThat(mapped.getAssignments().get(0).toString()).isEqualTo("person.another_name = NULL"); + + mapped.getBindings().apply(bindTarget); + verifyZeroInteractions(bindTarget); + } + + @SuppressWarnings("unchecked") + private BoundAssignments map(Update update) { + + BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1); + + return mapper.getMappedObject(markers.create(), update, Table.create("person"), + converter.getMappingContext().getRequiredPersistentEntity(Person.class)); + } + + static class Person { + + String name; + @Column("another_name") String alternative; + } +} From 2ff58d8c9024b6ba97389ef93609a9b7f1cd7f87 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 7 May 2019 15:12:31 +0200 Subject: [PATCH 5/5] #64 - Polishing. Fixed typos, formatting and minor errors in documentation. --- src/main/asciidoc/reference/r2dbc-core.adoc | 2 +- src/main/asciidoc/reference/r2dbc-fluent.adoc | 6 ++-- src/main/asciidoc/reference/r2dbc-sql.adoc | 4 +-- .../data/r2dbc/dialect/Bindings.java | 2 +- .../data/r2dbc/dialect/MutableBindings.java | 2 +- .../data/r2dbc/function/query/Criteria.java | 29 +++++++++---------- .../r2dbc/function/query/QueryMapper.java | 2 ++ .../r2dbc/function/query/UpdateMapper.java | 1 + 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index aba9b628..87466e3e 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -229,7 +229,7 @@ This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instanc Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC. R2DBC is a relatively young initiative that gains significance by maturing through adoption. -As of writing the following 3 drivers are available: +As of writing the following drivers are available: * https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) * https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) diff --git a/src/main/asciidoc/reference/r2dbc-fluent.adoc b/src/main/asciidoc/reference/r2dbc-fluent.adoc index faf9943e..7ac0eadd 100644 --- a/src/main/asciidoc/reference/r2dbc-fluent.adoc +++ b/src/main/asciidoc/reference/r2dbc-fluent.adoc @@ -192,10 +192,10 @@ Mono update = databaseClient.update() <4> Use `then()` to just update rows an object without consuming further details. Modifying statements allow also consumption of the number of affected rows. ==== -[r2dbc.datbaseclient.fluent-api.delete.methods]] -==== Methods for DELETE operations +[r2dbc.datbaseclient.fluent-api.update.methods]] +==== Methods for UPDATE operations -The `delete()` entry point exposes some additional methods that provide options for the operation: +The `update()` entry point exposes some additional methods that provide options for the operation: * *table* `(Class)` used to specify the target table using a mapped object. Returns results by default as `T`. * *table* `(String)` used to specify the target table name. Returns results by default as `Map`. diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc index 56bbfe92..eb824d08 100644 --- a/src/main/asciidoc/reference/r2dbc-sql.adoc +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -2,7 +2,7 @@ = Executing Statements Running a statement is the basic functionality that is covered by `DatabaseClient`. -The following example shows what you need to include for a minimal but fully functional class that creates a new table: +The following example shows what you need to include for minimal but fully functional code that creates a new table: [source,java] ---- @@ -32,7 +32,7 @@ Mono affectedRows = client.execute() .fetch().rowsUpdated(); ---- -Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`. +Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumed by streaming each `Row`. You might have noticed the use of `fetch()` in the previous example. `fetch()` is a continuation operator that allows you to specify how much data you want to consume. diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java index 40242b82..94fb90e0 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java @@ -33,7 +33,7 @@ import org.springframework.util.Assert; /** - * Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}. Bindings + * Value object representing value and {@code null} bindings for a {@link Statement} using {@link BindMarkers}. Bindings * are typically immutable. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java index c0cb0ce5..dd2abda5 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java @@ -22,7 +22,7 @@ import org.springframework.util.Assert; /** - * Mutable extension to {@link Bindings} for Value and {@code NULL} bindings for a {@link Statement} using + * Mutable extension to {@link Bindings} for Value and {@code null} bindings for a {@link Statement} using * {@link BindMarkers}. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java index 4e4153f1..30fa6a11 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java @@ -46,6 +46,7 @@ private Criteria(String column, Comparator comparator, @Nullable Object value) { private Criteria(@Nullable Criteria previous, Combinator combinator, String column, Comparator comparator, @Nullable Object value) { + this.previous = previous; this.combinator = combinator; this.column = column; @@ -56,7 +57,7 @@ private Criteria(@Nullable Criteria previous, Combinator combinator, String colu /** * Static factory method to create a Criteria using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the first {@link Criteria}. */ public static CriteriaStep where(String column) { @@ -69,7 +70,7 @@ public static CriteriaStep where(String column) { /** * Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. */ public CriteriaStep and(String column) { @@ -87,7 +88,7 @@ protected Criteria createCriteria(Comparator comparator, Object value) { /** * Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. */ public CriteriaStep or(String column) { @@ -179,7 +180,7 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code IN}. * - * @param value + * @param values * @return */ Criteria in(Object... values); @@ -187,7 +188,7 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code IN}. * - * @param value + * @param values * @return */ Criteria in(Collection values); @@ -195,7 +196,7 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param value + * @param values * @return */ Criteria notIn(Object... values); @@ -203,7 +204,7 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param value + * @param values * @return */ Criteria notIn(Collection values); @@ -251,7 +252,6 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code IS NULL}. * - * @param value * @return */ Criteria isNull(); @@ -259,7 +259,6 @@ public interface CriteriaStep { /** * Creates a {@link Criteria} using {@code IS NOT NULL}. * - * @param value * @return */ Criteria isNotNull(); @@ -314,9 +313,9 @@ public Criteria in(Object... values) { return createCriteria(Comparator.IN, Arrays.asList(values)); } - /** - * @param values - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.util.Collection) */ @Override public Criteria in(Collection values) { @@ -343,9 +342,9 @@ public Criteria notIn(Object... values) { return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); } - /** - * @param values - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.util.Collection) */ @Override public Criteria notIn(Collection values) { diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java index 95b06533..18229f36 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java @@ -166,6 +166,7 @@ private Condition getCondition(Criteria criteria, MutableBindings bindings, Tabl typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue); } else { + mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint()); typeHint = actualType.getType(); } @@ -227,6 +228,7 @@ private Condition createCondition(Column column, @Nullable Object mappedValue, C condition = column.in(expressions.toArray(new Expression[0])); } else { + BindMarker bindMarker = bindings.nextMarker(column.getName()); Expression expression = bind(mappedValue, valueType, bindings, bindMarker); diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java index 400a131f..b28a7abf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java @@ -21,6 +21,7 @@ import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; +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;