diff --git a/src/main/asciidoc/reference/r2dbc-fluent.adoc b/src/main/asciidoc/reference/r2dbc-fluent.adoc new file mode 100644 index 0000000..faf9943 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-fluent.adoc @@ -0,0 +1,239 @@ +[[r2dbc.datbaseclient.fluent-api]] += Fluent Data Access API + +You have already seen ``DatabaseClient``s SQL API that offers you maximum flexibility to execute any type of SQL. +`DatabaseClient` provides a more narrow interface for typical ad-hoc use-cases such as querying, inserting, updating, and deleting data. + +The entry points (`insert()`, `select()`, `update()`, and others) follow a natural naming schema based on the operation to be run. Moving on from the entry point, the API is designed to offer only context-dependent methods that lead to a terminating method that creates and runs a SQL statement. Spring Data R2DBC uses a `Dialect` abstraction to determine bind markers, pagination support and data types natively supported by the underlying driver. + +Let's take a look at a simple query: + +==== +[source,java] +---- +Flux people = databaseClient.select() + .from(Person.class) <1> + .fetch() + .all(); <2> +---- +<1> Using `Person` with the `from(…)` method sets the `FROM` table based on mapping metadata. It also maps tabular results on `Person` result objects. +<2> Fetching `all()` rows returns a `Flux` without limiting results. +==== + +The following example declares a more complex query that specifies the table name by name, a `WHERE` condition and `ORDER BY` clause: + +==== +[source,java] +---- +Mono first = databaseClient.select() + .from("legoset") <1> + .matching(where("firstname").is("John") <2> + .and("lastname").in("Doe", "White")) + .orderBy(desc("id")) <3> + .as(Person.class) + .fetch() + .one(); <4> +---- +<1> Selecting from a table by name returns row results as `Map` with case-insensitive column name matching. +<2> The issued query declares a `WHERE` condition on `firstname` and `lastname` columns to filter results. +<3> Results can be ordered by individual column names resulting in an `ORDER BY` clause. +<4> Selecting the one result fetches just a single row. This way of consuming rows expects the query to return exactly a single result. `Mono` emits a `IncorrectResultSizeDataAccessException` if the query yields more than a single result. +==== + +You can consume Query results in three ways: + +* Through object mapping (e.g. `as(Class)`) using Spring Data's mapping-metadata. +* As `Map` where column names are mapped to their value. Column names are looked up case-insensitive. +* By supplying a mapping `BiFunction` for direct access to R2DBC `Row` and `RowMetadata` + +You can switch between retrieving a single entity and retrieving multiple entities as through the terminating methods: + +* `first()`: Consume only the first row returning a `Mono`. The returned `Mono` completes without emitting an object if the query returns no results. +* `one()`: Consume exactly one row returning a `Mono`. The returned `Mono` completes without emitting an object if the query returns no results. If the query returns more than row then `Mono` completes exceptionally emitting `IncorrectResultSizeDataAccessException`. +* `all()`: Consume all returned rows returning a `Flux`. +* `rowsUpdated`: Consume the number of affected rows. Typically used with `INSERT`/`UPDATE`/`DELETE` statements. + +[[r2dbc.datbaseclient.fluent-api.select]] +== Selecting Data + +Use the `select()` entry point to express your `SELECT` queries. +The resulting `SELECT` queries support the commonly used clauses `WHERE`, `ORDER BY` and support pagination. +The fluent API style allows you to chain together multiple methods while having easy-to-understand code. +To improve readability, use static imports that allow you avoid using the 'new' keyword for creating `Criteria` instances. + +[r2dbc.datbaseclient.fluent-api.criteria]] +==== Methods for the Criteria Class + +The `Criteria` class provides the following methods, all of which correspond to SQL operators: + +* `Criteria` *and* `(String column)` Adds a chained `Criteria` with the specified `property` to the current `Criteria` and returns the newly created one. +* `Criteria` *or* `(String column)` Adds a chained `Criteria` with the specified `property` to the current `Criteria` and returns the newly created one. +* `Criteria` *greaterThan* `(Object o)` Creates a criterion using the `>` operator. +* `Criteria` *greaterThanOrEquals* `(Object o)` Creates a criterion using the `>=` operator. +* `Criteria` *in* `(Object... o)` Creates a criterion using the `IN` operator for a varargs argument. +* `Criteria` *in* `(Collection collection)` Creates a criterion using the `IN` operator using a collection. +* `Criteria` *is* `(Object o)` Creates a criterion using column matching (`property = value`). +* `Criteria` *isNull* `()` Creates a criterion using the `IS NULL` operator. +* `Criteria` *isNotNull* `()` Creates a criterion using the `IS NOT NULL` operator. +* `Criteria` *lessThan* `(Object o)` Creates a criterion using the `<` operator. +* `Criteria` *lessThanOrEquals* `(Object o)` Creates a criterion using the `<=` operator. +* `Criteria` *like* `(Object o)` Creates a criterion using the `LIKE` operator without escape character processing. +* `Criteria` *not* `(Object o)` Creates a criterion using the `!=` operator. +* `Criteria` *notIn* `(Object... o)` Creates a criterion using the `NOT IN` operator for a varargs argument. +* `Criteria` *notIn* `(Collection collection)` Creates a criterion using the `NOT IN` operator using a collection. + +You can use `Criteria` with `SELECT`, `UPDATE`, and `DELETE` queries. + +[r2dbc.datbaseclient.fluent-api.select.methods]] +==== Methods for SELECT operations + +The `select()` entry point exposes some additional methods that provide options for the query: + +* *from* `(Class)` used to specify the source table using a mapped object. Returns results by default as `T`. +* *from* `(String)` used to specify the source table name. Returns results by default as `Map`. +* *as* `(Class)` used to map results to `T`. +* *map* `(BiFunction)` used to supply a mapping function to extract results. +* *project* `(String... columns)` used to specify which columns to return. +* *matching* `(Criteria)` used to declare a `WHERE` condition to filter results. +* *orderBy* `(Order)` used to declare a `ORDER BY` clause to sort results. +* *page* `(Page pageable)` used to retrieve a particular page within the result. Limits the size of the returned results and reads from a offset. +* *fetch* `()` transition call declaration to the fetch stage to declare result consumption multiplicity. + +[[r2dbc.datbaseclient.fluent-api.insert]] +== Inserting Data + +Use the `insert()` entry point to insert data. Similar to `select()`, `insert()` allows free-form and mapped object inserts. + +Take a look at a simple typed insert operation: + +==== +[source,java] +---- +Mono insert = databaseClient.insert() + .into(Person.class) <1> + .using(new Person(…)) <2> + .then(); <3> +---- +<1> Using `Person` with the `into(…)` method sets the `INTO` table based on mapping metadata. It also prepares the insert statement to accept `Person` objects for inserting. +<2> Provide a scalar `Person` object. Alternatively, you can supply a `Publisher` to execute a stream of `INSERT` statements. This method extracts all non-``null`` values and inserts these. +<3> Use `then()` to just insert an object without consuming further details. Modifying statements allow consumption of the number of affected rows or tabular results for consuming generated keys. +==== + +Inserts also support untyped operations: + +==== +[source,java] +---- +Mono insert = databaseClient.insert() + .into("person") <1> + .value("firstname", "John") <2> + .nullValue("lastname") <3> + .then(); <4> +---- +<1> Start an insert into the `person` table. +<2> Provide a non-null value for `firstname`. +<3> Set `lastname` to `null`. +<3> Use `then()` to just insert an object without consuming further details. Modifying statements allow consumption of the number of affected rows or tabular results for consuming generated keys. +==== + +[r2dbc.datbaseclient.fluent-api.insert.methods]] +==== Methods for INSERT operations + +The `insert()` entry point exposes some additional methods that provide options for the operation: + +* *into* `(Class)` used to specify the target table using a mapped object. Returns results by default as `T`. +* *into* `(String)` used to specify the target table name. Returns results by default as `Map`. +* *using* `(T)` used to specify the object to insert. +* *using* `(Publisher)` used to accept a stream of objects to insert. +* *table* `(String)` used to override the target table name. +* *value* `(String, Object)` used to provide a column value to insert. +* *nullValue* `(String)` used to provide a null value to insert. +* *map* `(BiFunction)` used to supply a mapping function to extract results. +* *then* `()` execute `INSERT` without consuming any results. +* *fetch* `()` transition call declaration to the fetch stage to declare result consumption multiplicity. + +[[r2dbc.datbaseclient.fluent-api.update]] +== Updating Data + +Use the `update()` entry point to update rows. +Updating data starts with a specification of the table to update accepting `Update` specifying assignments. It also accepts `Criteria` to create a `WHERE` clause. + +Take a look at a simple typed update operation: + +==== +[source,java] +---- +Person modified = … + +Mono update = databaseClient.update() + .table(Person.class) <1> + .using(modified) <2> + .then(); <3> +---- +<1> Using `Person` with the `table(…)` method sets the table to update based on mapping metadata. +<2> Provide a scalar `Person` object value. `using(…)` accepts the modified object and derives primary keys and updates all column values. +<3> Use `then()` to just update rows an object without consuming further details. Modifying statements allow also consumption of the number of affected rows. +==== + +Update also support untyped operations: + +==== +[source,java] +---- +Mono update = databaseClient.update() + .table("person") <1> + .using(Update.update("firstname", "Jane")) <2> + .matching(where("firstname").is("John")) <3> + .then(); <4> +---- +<1> Update table `person`. +<2> Provide a `Update` definition, which columns to update. +<3> The issued query declares a `WHERE` condition on `firstname` columns to filter rows to 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 + +The `delete()` 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`. +* *using* `(T)` used to specify the object to update. Derives criteria itself. +* *using* `(Update)` used to specify the update definition. +* *matching* `(Criteria)` used to declare a `WHERE` condition to rows to update. +* *then* `()` execute `UPDATE` without consuming any results. +* *fetch* `()` transition call declaration to the fetch stage to fetch the number of updated rows. + +[[r2dbc.datbaseclient.fluent-api.delete]] +== Deleting Data + +Use the `delete()` entry point to delete rows. +Removing data starts with a specification of the table to delete from and optionally accepts a `Criteria` to create a `WHERE` clause. + +Take a look at a simple insert operation: + +==== +[source,java] +---- +Mono delete = databaseClient.delete() + .from(Person.class) <1> + .matching(where("firstname").is("John") <2> + .and("lastname").in("Doe", "White")) + .then(); <3> +---- +<1> Using `Person` with the `from(…)` method sets the `FROM` table based on mapping metadata. +<2> The issued query declares a `WHERE` condition on `firstname` and `lastname` columns to filter rows to delete. +<3> Use `then()` to just delete 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 + +The `delete()` entry point exposes some additional methods that provide options for the operation: + +* *from* `(Class)` used to specify the target table using a mapped object. Returns results by default as `T`. +* *from* `(String)` used to specify the target table name. Returns results by default as `Map`. +* *matching* `(Criteria)` used to declare a `WHERE` condition to rows to delete. +* *then* `()` execute `DELETE` without consuming any results. +* *fetch* `()` transition call declaration to the fetch stage to fetch the number of deleted rows. diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc index d71034e..56bbfe9 100644 --- a/src/main/asciidoc/reference/r2dbc-sql.adoc +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -1,5 +1,5 @@ [[r2dbc.datbaseclient.statements]] -= Running Statements += 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: diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc index 2e4fe3b..f85db46 100644 --- a/src/main/asciidoc/reference/r2dbc-transactions.adoc +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -1,5 +1,5 @@ [[r2dbc.datbaseclient.transactions]] -== Transactions += Transactions A common pattern when using relational databases is grouping multiple queries within a unit of work that is guarded by a transaction. Relational databases typically associate a transaction with a single transport connection. diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc index 4c0b55d..27e5f3e 100644 --- a/src/main/asciidoc/reference/r2dbc.adoc +++ b/src/main/asciidoc/reference/r2dbc.adoc @@ -7,4 +7,6 @@ include::r2dbc-databaseclient.adoc[leveloffset=+1] include::r2dbc-sql.adoc[leveloffset=+1] +include::r2dbc-fluent.adoc[leveloffset=+1] + include::r2dbc-transactions.adoc[leveloffset=+1] diff --git a/src/main/java/org/springframework/data/r2dbc/domain/Bindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java similarity index 80% rename from src/main/java/org/springframework/data/r2dbc/domain/Bindings.java rename to src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java index 89c1edf..40242b8 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/Bindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.domain; +package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Statement; @@ -27,19 +27,21 @@ import java.util.Map; import java.util.Spliterator; import java.util.function.Consumer; -import org.springframework.data.r2dbc.dialect.BindMarker; -import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.domain.BindTarget; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}. + * Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}. Bindings + * are typically immutable. * * @author Mark Paluch */ public class Bindings implements Streamable { + private static final Bindings EMPTY = new Bindings(); + private final Map bindings; /** @@ -67,8 +69,17 @@ public class Bindings implements Streamable { this.bindings = bindings; } + /** + * Create a new, empty {@link Bindings} object. + * + * @return a new, empty {@link Bindings} object. + */ + public static Bindings empty() { + return EMPTY; + } + protected Map getBindings() { - return bindings; + return this.bindings; } /** @@ -92,14 +103,24 @@ public class Bindings implements Streamable { } /** - * Apply the bindings to a {@link Statement}. + * Merge this bindings with an other {@link Bindings} object and create a new merged {@link Bindings} object. * - * @param statement the statement to apply to. + * @param other the object to merge with. + * @return a new, merged {@link Bindings} object. */ - public void apply(Statement statement) { + public Bindings and(Bindings other) { + return merge(this, other); + } - Assert.notNull(statement, "Statement must not be null"); - this.bindings.forEach((marker, binding) -> binding.apply(statement)); + /** + * Apply the bindings to a {@link BindTarget}. + * + * @param bindTarget the target to apply bindings to. + */ + public void apply(BindTarget bindTarget) { + + Assert.notNull(bindTarget, "BindTarget must not be null"); + this.bindings.forEach((marker, binding) -> binding.apply(bindTarget)); } /** @@ -146,7 +167,7 @@ public class Bindings implements Streamable { * @return the associated {@link BindMarker}. */ public BindMarker getBindMarker() { - return marker; + return this.marker; } /** @@ -174,11 +195,11 @@ public class Bindings implements Streamable { public abstract Object getValue(); /** - * Applies the binding to a {@link Statement}. + * Applies the binding to a {@link BindTarget}. * - * @param statement the statement to apply to. + * @param bindTarget the target to apply bindings to. */ - public abstract void apply(Statement statement); + public abstract void apply(BindTarget bindTarget); } /** @@ -206,7 +227,7 @@ public class Bindings implements Streamable { * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#getValue() */ public Object getValue() { - return value; + return this.value; } /* @@ -214,8 +235,8 @@ public class Bindings implements Streamable { * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement) */ @Override - public void apply(Statement statement) { - getBindMarker().bind(statement, getValue()); + public void apply(BindTarget bindTarget) { + getBindMarker().bind(bindTarget, getValue()); } } @@ -249,16 +270,16 @@ public class Bindings implements Streamable { } public Class getValueType() { - return valueType; + return this.valueType; } /* * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(io.r2dbc.spi.Statement) + * @see org.springframework.data.r2dbc.function.query.Bindings.Binding#apply(BindTarget) */ @Override - public void apply(Statement statement) { - getBindMarker().bindNull(statement, getValueType()); + public void apply(BindTarget bindTarget) { + getBindMarker().bindNull(bindTarget, getValueType()); } } } diff --git a/src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java rename to src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java index 739aa81..c0cb0ce 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/MutableBindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java @@ -13,14 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.domain; +package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Statement; import java.util.LinkedHashMap; -import org.springframework.data.r2dbc.dialect.BindMarker; -import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.util.Assert; /** 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 4c9a965..39f7c8b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -30,7 +30,9 @@ import org.reactivestreams.Publisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.domain.PreparedOperation; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.query.Criteria; +import org.springframework.data.r2dbc.function.query.Update; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** @@ -59,6 +61,11 @@ public interface DatabaseClient { */ InsertIntoSpec insert(); + /** + * Prepare an SQL UPDATE call. + */ + UpdateTableSpec update(); + /** * Prepare an SQL DELETE call. */ @@ -290,6 +297,29 @@ public interface DatabaseClient { TypedInsertSpec into(Class table); } + /** + * Contract for specifying {@code UPDATE} options leading to the exchange. + */ + interface UpdateTableSpec { + + /** + * Specify the target {@literal table} to update. + * + * @param table must not be {@literal null} or empty. + * @return a {@link GenericUpdateSpec} for further configuration of the update. Guaranteed to be not + * {@literal null}. + */ + GenericUpdateSpec table(String table); + + /** + * Specify the target table to update to using the {@link Class entity class}. + * + * @param table must not be {@literal null}. + * @return a {@link TypedUpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}. + */ + TypedUpdateSpec table(Class table); + } + /** * Contract for specifying {@code DELETE} options leading to the exchange. */ @@ -299,18 +329,18 @@ public interface DatabaseClient { * Specify the source {@literal table} to delete from. * * @param table must not be {@literal null} or empty. - * @return a {@link GenericSelectSpec} for further configuration of the delete. Guaranteed to be not + * @return a {@link DeleteMatchingSpec} for further configuration of the delete. Guaranteed to be not * {@literal null}. */ - DeleteSpec from(String table); + DeleteMatchingSpec from(String table); /** * Specify the source table to delete from to using the {@link Class entity class}. * * @param table must not be {@literal null}. - * @return a {@link DeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}. + * @return a {@link TypedDeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}. */ - DeleteSpec from(Class table); + TypedDeleteSpec from(Class table); } /** @@ -388,7 +418,7 @@ public interface DatabaseClient { * * @param criteria must not be {@literal null}. */ - S where(Criteria criteria); + S matching(Criteria criteria); /** * Configure {@link Sort}. @@ -397,12 +427,21 @@ public interface DatabaseClient { */ S orderBy(Sort sort); + /** + * Configure {@link Sort}. + * + * @param orders must not be {@literal null}. + */ + default S orderBy(Sort.Order... orders) { + return orderBy(Sort.by(orders)); + } + /** * Configure pagination. Overrides {@link Sort} if the {@link Pageable} contains a {@link Sort} object. * - * @param page must not be {@literal null}. + * @param pageable must not be {@literal null}. */ - S page(Pageable page); + S page(Pageable pageable); } /** @@ -425,12 +464,23 @@ public interface DatabaseClient { * * @param field must not be {@literal null} or empty. * @param type must not be {@literal null}. + * @deprecated will be removed soon. Use {@link #nullValue(String)}. */ - GenericInsertSpec nullValue(String field, Class type); + @Deprecated + default GenericInsertSpec nullValue(String field, Class type) { + return value(field, SettableValue.empty(type)); + } + + /** + * Specify a {@literal null} value to insert. + * + * @param field must not be {@literal null} or empty. + */ + GenericInsertSpec nullValue(String field); } /** - * Contract for specifying {@code SELECT} options leading the exchange. + * Contract for specifying {@code INSERT} options leading the exchange. */ interface TypedInsertSpec { @@ -473,7 +523,7 @@ public interface DatabaseClient { /** * Configure a result mapping {@link java.util.function.BiFunction function}. * - * @param mappwingFunction must not be {@literal null}. + * @param mappingFunction must not be {@literal null}. * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ @@ -493,16 +543,110 @@ public interface DatabaseClient { } /** - * Contract for specifying {@code DELETE} options leading to the exchange. + * Contract for specifying {@code UPDATE} options leading to the exchange. */ - interface DeleteSpec { + interface GenericUpdateSpec { + + /** + * Specify an {@link Update} object containing assignments. + * + * @param update must not be {@literal null}. + */ + UpdateMatchingSpec using(Update update); + } + + /** + * Contract for specifying {@code UPDATE} options leading to the exchange. + */ + interface TypedUpdateSpec { + + /** + * Update the given {@code objectToUpdate}. + * + * @param objectToUpdate the object of which the attributes will provide the values for the update and the primary + * key. Must not be {@literal null}. + * @return a {@link UpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}. + */ + UpdateSpec using(T objectToUpdate); + + /** + * Use the given {@code tableName} as update target. + * + * @param tableName must not be {@literal null} or empty. + * @return a {@link TypedUpdateSpec} for further configuration of the update. Guaranteed to be not {@literal null}. + */ + TypedUpdateSpec table(String tableName); + } + + /** + * Contract for specifying {@code UPDATE} options leading to the exchange. + */ + interface UpdateMatchingSpec extends UpdateSpec { /** * Configure a filter {@link Criteria}. * * @param criteria must not be {@literal null}. */ - DeleteSpec where(Criteria criteria); + UpdateSpec matching(Criteria criteria); + } + + /** + * Contract for specifying {@code UPDATE} options leading to the exchange. + */ + interface UpdateSpec { + + /** + * Perform the SQL call and retrieve the result. + */ + UpdatedRowsFetchSpec fetch(); + + /** + * Perform the SQL call and return a {@link Mono} that completes without result on statement completion. + * + * @return a {@link Mono} ignoring its payload (actively dropping). + */ + Mono then(); + } + + /** + * Contract for specifying {@code DELETE} options leading to the exchange. + */ + interface TypedDeleteSpec extends DeleteSpec { + + /** + * Use the given {@code tableName} as delete target. + * + * @param tableName must not be {@literal null} or empty. + * @return a {@link TypedDeleteSpec} for further configuration of the delete. Guaranteed to be not {@literal null}. + */ + TypedDeleteSpec table(String tableName); + + /** + * Configure a filter {@link Criteria}. + * + * @param criteria must not be {@literal null}. + */ + DeleteSpec matching(Criteria criteria); + } + + /** + * Contract for specifying {@code DELETE} options leading to the exchange. + */ + interface DeleteMatchingSpec extends DeleteSpec { + + /** + * Configure a filter {@link Criteria}. + * + * @param criteria must not be {@literal null}. + */ + DeleteSpec matching(Criteria criteria); + } + + /** + * Contract for specifying {@code DELETE} options leading to the exchange. + */ + interface DeleteSpec { /** * Perform the SQL call and retrieve the result. 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 7f99ccf..4db374c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -58,13 +58,9 @@ import org.springframework.data.r2dbc.domain.PreparedOperation; import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; -import org.springframework.data.r2dbc.function.operation.BindableOperation; -import org.springframework.data.r2dbc.function.query.BoundCondition; import org.springframework.data.r2dbc.function.query.Criteria; +import org.springframework.data.r2dbc.function.query.Update; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.relational.core.sql.Delete; -import org.springframework.data.relational.core.sql.Insert; -import org.springframework.data.relational.core.sql.Select; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -101,7 +97,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public Builder mutate() { - return builder; + return this.builder; } @Override @@ -119,6 +115,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return new DefaultInsertIntoSpec(); } + @Override + public UpdateTableSpec update() { + return new DefaultUpdateTableSpec(); + } + @Override public DeleteFromSpec delete() { return new DefaultDeleteFromSpec(); @@ -206,7 +207,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * @throws IllegalStateException in case of no DataSource set */ protected ConnectionFactory obtainConnectionFactory() { - return connector; + return this.connector; } /** @@ -230,7 +231,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { */ protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) { - DataAccessException dae = exceptionTranslator.translate(task, sql, ex); + DataAccessException dae = this.exceptionTranslator.translate(task, sql, ex); return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex)); } @@ -355,9 +356,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(), - new MapBindParameterSource(byName)); + new MapBindParameterSource(this.byName)); - String expanded = operation.toQuery(); + String expanded = getRequiredSql(operation); if (logger.isTraceEnabled()) { logger.trace("Expanded SQL [" + expanded + "]"); } @@ -365,7 +366,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Statement statement = it.createStatement(expanded); BindTarget bindTarget = new StatementWrapper(statement); - byName.forEach((name, o) -> { + this.byName.forEach((name, o) -> { if (o.getValue() != null) { operation.bind(bindTarget, name, o.getValue()); @@ -374,7 +375,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } }); - bindByIndex(statement, byIndex); + bindByIndex(statement, this.byIndex); return statement; }; @@ -570,7 +571,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public FetchSpec fetch() { - return exchange(this.sqlSupplier, mappingFunction); + return exchange(this.sqlSupplier, this.mappingFunction); } @Override @@ -606,7 +607,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedExecuteSpec createInstance(Map byIndex, Map byName, Supplier sqlSupplier) { - return createTypedExecuteSpec(byIndex, byName, sqlSupplier, typeToRead); + return createTypedExecuteSpec(byIndex, byName, sqlSupplier, this.typeToRead); } } @@ -656,37 +657,38 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { projectedFields.addAll(this.projectedFields); projectedFields.addAll(Arrays.asList(selectedFields)); - return createInstance(table, projectedFields, criteria, sort, page); + return createInstance(this.table, projectedFields, this.criteria, this.sort, this.page); } public DefaultSelectSpecSupport where(Criteria whereCriteria) { Assert.notNull(whereCriteria, "Criteria must not be null!"); - return createInstance(table, projectedFields, whereCriteria, sort, page); + return createInstance(this.table, this.projectedFields, whereCriteria, this.sort, this.page); } public DefaultSelectSpecSupport orderBy(Sort sort) { Assert.notNull(sort, "Sort must not be null!"); - return createInstance(table, projectedFields, criteria, sort, page); + return createInstance(this.table, this.projectedFields, this.criteria, sort, this.page); } public DefaultSelectSpecSupport page(Pageable page) { Assert.notNull(page, "Pageable must not be null!"); - return createInstance(table, projectedFields, criteria, sort, page); + return createInstance(this.table, this.projectedFields, this.criteria, this.sort, page); } FetchSpec execute(PreparedOperation preparedOperation, BiFunction mappingFunction) { - Function selectFunction = wrapPreparedOperation(preparedOperation); + String sql = getRequiredSql(preparedOperation); + Function selectFunction = wrapPreparedOperation(sql, preparedOperation); Function> resultFunction = it -> Flux.from(selectFunction.apply(it).execute()); return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - preparedOperation.toQuery(), // + sql, // resultFunction, // it -> Mono.error(new UnsupportedOperationException("Not available for SELECT")), // mappingFunction); @@ -711,8 +713,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(resultType, "Result type must not be null!"); - return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, resultType, - dataAccessStrategy.getRowMapper(resultType)); + return new DefaultTypedSelectSpec<>(this.table, this.projectedFields, this.criteria, this.sort, this.page, + resultType, dataAccessStrategy.getRowMapper(resultType)); } @Override @@ -729,7 +731,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultGenericSelectSpec where(Criteria criteria) { + public DefaultGenericSelectSpec matching(Criteria criteria) { return (DefaultGenericSelectSpec) super.where(criteria); } @@ -739,8 +741,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultGenericSelectSpec page(Pageable page) { - return (DefaultGenericSelectSpec) super.page(page); + public DefaultGenericSelectSpec page(Pageable pageable) { + return (DefaultGenericSelectSpec) super.page(pageable); } @Override @@ -750,19 +752,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private FetchSpec exchange(BiFunction mappingFunction) { - PreparedOperation operation = dataAccessStrategy.getStatements().select(table, columns, - (table, configurer) -> { + StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.table).withProjection(columns) + .withPage(this.page).withSort(this.sort); - Sort sortToUse; - if (this.sort.isSorted()) { - sortToUse = dataAccessStrategy.getMappedSort(this.sort, this.typeToRead); - } else { - sortToUse = this.sort; - } + if (this.criteria != null) { + selectSpec = selectSpec.withCriteria(this.criteria); + } - configurer.withPageRequest(page).withSort(sortToUse); - - if (criteria != null) { - BoundCondition boundCondition = dataAccessStrategy.getMappedCriteria(criteria, table, this.typeToRead); - configurer.withWhere(boundCondition.getCondition()).withBindings(boundCondition.getBindings()); - } - }); + PreparedOperation operation = mapper.getMappedObject(selectSpec); return execute(operation, mappingFunction); } @@ -877,7 +868,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page) { - return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, typeToRead, mappingFunction); + return new DefaultTypedSelectSpec<>(table, projectedFields, criteria, sort, page, this.typeToRead, + this.mappingFunction); } } @@ -915,18 +907,22 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { () -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field)); Map byName = new LinkedHashMap<>(this.byName); - byName.put(field, SettableValue.fromOrEmpty(value, value.getClass())); + if (value instanceof SettableValue) { + byName.put(field, (SettableValue) value); + } else { + byName.put(field, SettableValue.fromOrEmpty(value, value.getClass())); + } return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); } @Override - public GenericInsertSpec nullValue(String field, Class type) { + public GenericInsertSpec nullValue(String field) { Assert.notNull(field, "Field must not be null!"); Map byName = new LinkedHashMap<>(this.byName); - byName.put(field, SettableValue.empty(type)); + byName.put(field, null); return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); } @@ -951,30 +947,19 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private FetchSpec exchange(BiFunction mappingFunction) { - if (byName.isEmpty()) { + if (this.byName.isEmpty()) { throw new IllegalStateException("Insert fields is empty!"); } - PreparedOperation operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(), - it -> { - byName.forEach(it::bind); - }); + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + StatementMapper.InsertSpec insert = mapper.createInsert(this.table); - String sql = getRequiredSql(operation); + for (String column : this.byName.keySet()) { + insert = insert.withColumn(column, this.byName.get(column)); + } - Function> resultFunction = it -> { - - Statement statement = it.createStatement(sql); - operation.bindTo(new StatementWrapper(statement)); - - return Flux.from(statement.execute()); - }; - - return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - operation.toQuery(), // - resultFunction, // - it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // - mappingFunction); + PreparedOperation operation = mapper.getMappedObject(insert); + return exchangeInsert(mappingFunction, operation); } } @@ -1002,7 +987,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.hasText(tableName, "Table name must not be null or empty!"); - return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert, this.mappingFunction); + return new DefaultTypedInsertSpec<>(this.typeToInsert, tableName, this.objectToInsert, this.mappingFunction); } @Override @@ -1010,7 +995,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(objectToInsert, "Object to insert must not be null!"); - return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert), this.mappingFunction); + return new DefaultTypedInsertSpec<>(this.typeToInsert, this.table, Mono.just(objectToInsert), + this.mappingFunction); } @Override @@ -1018,7 +1004,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(objectToInsert, "Publisher to insert must not be null!"); - return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert, this.mappingFunction); + return new DefaultTypedInsertSpec<>(this.typeToInsert, this.table, objectToInsert, this.mappingFunction); } @Override @@ -1036,7 +1022,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public Mono then() { - return Mono.from(objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); + return Mono.from(this.objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); } private FetchSpec exchange(BiFunction mappingFunction) { @@ -1069,34 +1055,167 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(toInsert); - PreparedOperation operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(), - it -> { - outboundRow.forEach((k, v) -> { + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + StatementMapper.InsertSpec insert = mapper.createInsert(this.table); - if (v.hasValue()) { - it.bind(k, v); - } - }); - }); + for (String column : outboundRow.keySet()) { + SettableValue settableValue = outboundRow.get(column); + if (settableValue.hasValue()) { + insert = insert.withColumn(column, settableValue); + } + } - String sql = getRequiredSql(operation); - Function> resultFunction = it -> { + PreparedOperation operation = mapper.getMappedObject(insert); + return exchangeInsert(mappingFunction, operation); + } + } - Statement statement = it.createStatement(sql); - operation.bindTo(new StatementWrapper(statement)); - statement.returnGeneratedValues(); + /** + * Default {@link DatabaseClient.UpdateTableSpec} implementation. + */ + class DefaultUpdateTableSpec implements UpdateTableSpec { - return Flux.from(statement.execute()); - }; + @Override + public GenericUpdateSpec table(String table) { + return new DefaultGenericUpdateSpec(null, table, null, null); + } - return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - operation.toQuery(), // - resultFunction, // - it -> resultFunction // - .apply(it) // - .flatMap(Result::getRowsUpdated) // - .collect(Collectors.summingInt(Integer::intValue)), // - mappingFunction); + @Override + public TypedUpdateSpec table(Class table) { + return new DefaultTypedUpdateSpec<>(table, null, null); + } + } + + @RequiredArgsConstructor + class DefaultGenericUpdateSpec implements GenericUpdateSpec, UpdateMatchingSpec { + + private final @Nullable Class typeToUpdate; + private final @Nullable String table; + private final Update assignments; + private final Criteria where; + + @Override + public UpdateMatchingSpec using(Update update) { + + Assert.notNull(update, "Update must not be null"); + + return new DefaultGenericUpdateSpec(this.typeToUpdate, this.table, update, this.where); + } + + @Override + public UpdateSpec matching(Criteria criteria) { + + Assert.notNull(criteria, "Criteria must not be null"); + + return new DefaultGenericUpdateSpec(this.typeToUpdate, this.table, this.assignments, criteria); + } + + @Override + public UpdatedRowsFetchSpec fetch() { + + String table; + + if (StringUtils.isEmpty(this.table)) { + table = dataAccessStrategy.getTableName(this.typeToUpdate); + } else { + table = this.table; + } + + return exchange(table); + } + + @Override + public Mono then() { + return fetch().rowsUpdated().then(); + } + + private UpdatedRowsFetchSpec exchange(String table) { + + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + + if (this.typeToUpdate != null) { + mapper = mapper.forType(this.typeToUpdate); + } + + StatementMapper.UpdateSpec update = mapper.createUpdate(table, this.assignments); + + if (this.where != null) { + update = update.withCriteria(this.where); + } + + PreparedOperation operation = mapper.getMappedObject(update); + + return exchangeUpdate(operation); + } + } + + @RequiredArgsConstructor + class DefaultTypedUpdateSpec implements TypedUpdateSpec, UpdateSpec { + + private final @Nullable Class typeToUpdate; + private final @Nullable String table; + private final T objectToUpdate; + + @Override + public UpdateSpec using(T objectToUpdate) { + + Assert.notNull(objectToUpdate, "Object to update must not be null"); + + return new DefaultTypedUpdateSpec<>(this.typeToUpdate, this.table, objectToUpdate); + } + + @Override + public TypedUpdateSpec table(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty!"); + + return new DefaultTypedUpdateSpec<>(this.typeToUpdate, tableName, this.objectToUpdate); + } + + @Override + public UpdatedRowsFetchSpec fetch() { + + String table; + + if (StringUtils.isEmpty(this.table)) { + table = dataAccessStrategy.getTableName(this.typeToUpdate); + } else { + table = this.table; + } + + return exchange(table); + } + + @Override + public Mono then() { + return fetch().rowsUpdated().then(); + } + + private UpdatedRowsFetchSpec exchange(String table) { + + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); + Map columns = dataAccessStrategy.getOutboundRow(this.objectToUpdate); + List ids = dataAccessStrategy.getIdentifierColumns(this.typeToUpdate); + + if (ids.isEmpty()) { + throw new IllegalStateException("No identifier columns in " + this.typeToUpdate.getName() + "!"); + } + Object id = columns.remove(ids.get(0)); // do not update the Id column. + + Update update = null; + + for (String column : columns.keySet()) { + if (update == null) { + update = Update.update(column, columns.get(column)); + } else { + update = update.set(column, columns.get(column)); + } + } + + PreparedOperation operation = mapper + .getMappedObject(mapper.createUpdate(table, update).withCriteria(Criteria.where(ids.get(0)).is(id))); + + return exchangeUpdate(operation); } } @@ -1106,13 +1225,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { class DefaultDeleteFromSpec implements DeleteFromSpec { @Override - public DeleteSpec from(String table) { - return new DefaultDeleteSpec(null, table, null); + public DefaultDeleteSpec from(String table) { + return new DefaultDeleteSpec<>(null, table, null); } @Override - public DeleteSpec from(Class table) { - return new DefaultDeleteSpec(table, null, null); + public DefaultDeleteSpec from(Class table) { + return new DefaultDeleteSpec<>(table, null, null); } } @@ -1120,15 +1239,26 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Default implementation of {@link DatabaseClient.TypedInsertSpec}. */ @RequiredArgsConstructor - class DefaultDeleteSpec implements DeleteSpec { + class DefaultDeleteSpec implements DeleteMatchingSpec, TypedDeleteSpec { - private final @Nullable Class typeToDelete; + 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); + public DeleteSpec matching(Criteria criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + + return new DefaultDeleteSpec<>(this.typeToDelete, this.table, criteria); + } + + @Override + public TypedDeleteSpec table(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty!"); + + return new DefaultDeleteSpec<>(this.typeToDelete, tableName, this.where); } @Override @@ -1152,42 +1282,65 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private UpdatedRowsFetchSpec exchange(String table) { - PreparedOperation operation = dataAccessStrategy.getStatements().delete(table, (t, configurer) -> { + StatementMapper mapper = dataAccessStrategy.getStatementMapper(); - if (this.where != null) { + if (this.typeToDelete != null) { + mapper = mapper.forType(this.typeToDelete); + } - BoundCondition condition; - if (this.table != null) { - condition = dataAccessStrategy.getMappedCriteria(this.where, t); - } else { - condition = dataAccessStrategy.getMappedCriteria(this.where, t, this.typeToDelete); - } + StatementMapper.DeleteSpec delete = mapper.createDelete(table); - configurer.withWhere(condition.getCondition()).withBindings(condition.getBindings()); - } - }); + if (this.where != null) { + delete = delete.withCriteria(this.where); + } - Function deleteFunction = wrapPreparedOperation(operation); - Function> resultFunction = it -> Flux.from(deleteFunction.apply(it).execute()); + PreparedOperation operation = mapper.getMappedObject(delete); - return new DefaultSqlResult<>(DefaultDatabaseClient.this, // - operation.toQuery(), // - resultFunction, // - it -> resultFunction // - .apply(it) // - .flatMap(Result::getRowsUpdated) // - .collect(Collectors.summingInt(Integer::intValue)), // - (row, rowMetadata) -> rowMetadata); + return exchangeUpdate(operation); } } - private Function wrapPreparedOperation(PreparedOperation operation) { + private FetchSpec exchangeInsert(BiFunction mappingFunction, + PreparedOperation operation) { + + String sql = getRequiredSql(operation); + Function insertFunction = wrapPreparedOperation(sql, operation) + .andThen(statement -> statement.returnGeneratedValues()); + Function> resultFunction = it -> Flux.from(insertFunction.apply(it).execute()); + + return new DefaultSqlResult<>(this, // + sql, // + resultFunction, // + it -> sumRowsUpdated(resultFunction, it), // + mappingFunction); + } + + private UpdatedRowsFetchSpec exchangeUpdate(PreparedOperation operation) { + + String sql = getRequiredSql(operation); + Function executeFunction = wrapPreparedOperation(sql, operation); + Function> resultFunction = it -> Flux.from(executeFunction.apply(it).execute()); + + return new DefaultSqlResult<>(this, // + sql, // + resultFunction, // + it -> sumRowsUpdated(resultFunction, it), // + (row, rowMetadata) -> rowMetadata); + } + + private static Mono sumRowsUpdated(Function> resultFunction, Connection it) { + + return resultFunction.apply(it) // + .flatMap(Result::getRowsUpdated) // + .collect(Collectors.summingInt(Integer::intValue)); + } + + private Function wrapPreparedOperation(String sql, PreparedOperation operation) { return it -> { - String sql = operation.toQuery(); - if (logger.isDebugEnabled()) { - logger.debug("Executing SQL statement [" + sql + "]"); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Executing SQL statement [" + sql + "]"); } Statement statement = it.createStatement(sql); @@ -1309,7 +1462,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return Mono.defer(() -> { if (compareAndSet(false, true)) { - return Mono.from(closeFunction.apply(connection)); + return Mono.from(this.closeFunction.apply(this.connection)); } return Mono.empty(); 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 6089ce3..a7b7af1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -26,11 +26,8 @@ import java.util.function.Function; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.convert.CustomConversions.StoreConversions; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.ArrayColumns; -import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.domain.OutboundRow; @@ -39,14 +36,11 @@ import org.springframework.data.r2dbc.function.convert.EntityRowMapper; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; -import org.springframework.data.r2dbc.function.query.BoundCondition; -import org.springframework.data.r2dbc.function.query.Criteria; -import org.springframework.data.r2dbc.function.query.CriteriaMapper; +import org.springframework.data.r2dbc.function.query.UpdateMapper; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.Table; import org.springframework.data.relational.core.sql.render.NamingStrategies; import org.springframework.data.relational.core.sql.render.RenderContext; import org.springframework.data.relational.core.sql.render.RenderNamingStrategy; @@ -64,9 +58,9 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final Dialect dialect; private final R2dbcConverter converter; - private final CriteriaMapper criteriaMapper; + private final UpdateMapper updateMapper; private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; - private final StatementFactory statements; + private final StatementMapper statementMapper; /** * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}. @@ -103,7 +97,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra Assert.notNull(converter, "RelationalConverter must not be null"); this.converter = converter; - this.criteriaMapper = new CriteriaMapper(converter); + this.updateMapper = new UpdateMapper(converter); this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) this.converter .getMappingContext(); this.dialect = dialect; @@ -130,17 +124,17 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra } }; - this.statements = new DefaultStatementFactory(this.dialect, renderContext); + this.statementMapper = new DefaultStatementMapper(dialect, renderContext, this.updateMapper, this.mappingContext); } /* * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getAllFields(java.lang.Class) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getAllColumns(java.lang.Class) */ @Override - public List getAllColumns(Class typeToRead) { + public List getAllColumns(Class entityType) { - RelationalPersistentEntity persistentEntity = getPersistentEntity(typeToRead); + RelationalPersistentEntity persistentEntity = getPersistentEntity(entityType); if (persistentEntity == null) { return Collections.singletonList("*"); @@ -154,6 +148,26 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return columnNames; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getIdentifierColumns(java.lang.Class) + */ + @Override + public List getIdentifierColumns(Class entityType) { + + RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(entityType); + + List columnNames = new ArrayList<>(); + for (RelationalPersistentProperty property : persistentEntity) { + + if (property.isIdProperty()) { + columnNames.add(property.getColumnName()); + } + } + + return columnNames; + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getOutboundRow(java.lang.Object) @@ -164,7 +178,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra OutboundRow row = new OutboundRow(); - converter.write(object, row); + this.converter.write(object, row); RelationalPersistentEntity entity = getRequiredPersistentEntity(ClassUtils.getUserClass(object)); @@ -187,77 +201,25 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private SettableValue getArrayValue(SettableValue value, RelationalPersistentProperty property) { - ArrayColumns arrayColumns = dialect.getArraySupport(); + ArrayColumns arrayColumns = this.dialect.getArraySupport(); if (!arrayColumns.isSupported()) { throw new InvalidDataAccessResourceUsageException( - "Dialect " + dialect.getClass().getName() + " does not support array columns"); + "Dialect " + this.dialect.getClass().getName() + " does not support array columns"); } - return SettableValue.fromOrEmpty(converter.getArrayValue(arrayColumns, property, value.getValue()), + return SettableValue.fromOrEmpty(this.converter.getArrayValue(arrayColumns, property, value.getValue()), property.getActualType()); } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedSort(java.lang.Class, org.springframework.data.domain.Sort) - */ - @Override - public Sort getMappedSort(Sort sort, Class typeToRead) { - - RelationalPersistentEntity entity = getPersistentEntity(typeToRead); - if (entity == null) { - return sort; - } - - List mappedOrder = new ArrayList<>(); - - for (Order order : sort) { - - RelationalPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty()); - if (persistentProperty == null) { - mappedOrder.add(order); - } else { - mappedOrder - .add(Order.by(persistentProperty.getColumnName()).with(order.getNullHandling()).with(order.getDirection())); - } - } - - return Sort.by(mappedOrder); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedCriteria(org.springframework.data.r2dbc.function.query.Criteria, org.springframework.data.relational.core.sql.Table) - */ - @Override - public BoundCondition getMappedCriteria(Criteria criteria, Table table) { - return getMappedCriteria(criteria, table, null); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedCriteria(org.springframework.data.r2dbc.function.query.Criteria, org.springframework.data.relational.core.sql.Table, java.lang.Class) - */ - @Override - public BoundCondition getMappedCriteria(Criteria criteria, Table table, @Nullable Class typeToRead) { - - BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create(); - - RelationalPersistentEntity entity = typeToRead != null ? mappingContext.getRequiredPersistentEntity(typeToRead) - : null; - - return criteriaMapper.getMappedObject(bindMarkers, criteria, table, entity); - } - /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getRowMapper(java.lang.Class) */ @Override public BiFunction getRowMapper(Class typeToRead) { - return new EntityRowMapper<>(typeToRead, converter); + return new EntityRowMapper<>(typeToRead, this.converter); } /* @@ -271,11 +233,11 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra /* * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getStatements() + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getStatementMapper() */ @Override - public StatementFactory getStatements() { - return this.statements; + public StatementMapper getStatementMapper() { + return this.statementMapper; } /* @@ -284,7 +246,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra */ @Override public BindMarkersFactory getBindMarkersFactory() { - return dialect.getBindMarkersFactory(); + return this.dialect.getBindMarkersFactory(); } /* @@ -292,19 +254,19 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getConverter() */ public R2dbcConverter getConverter() { - return converter; + return this.converter; } public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { - return mappingContext; + return this.mappingContext; } private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { - return mappingContext.getRequiredPersistentEntity(typeToRead); + return this.mappingContext.getRequiredPersistentEntity(typeToRead); } @Nullable private RelationalPersistentEntity getPersistentEntity(Class typeToRead) { - return mappingContext.getPersistentEntity(typeToRead); + return this.mappingContext.getPersistentEntity(typeToRead); } } 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 bfb4cca..5e713db 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java @@ -15,678 +15,21 @@ */ package org.springframework.data.r2dbc.function; -import io.r2dbc.spi.Statement; -import lombok.Getter; import lombok.RequiredArgsConstructor; -import java.util.ArrayList; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.OptionalLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; -import java.util.function.Consumer; - -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.domain.Sort; -import org.springframework.data.r2dbc.dialect.BindMarker; -import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.Bindings; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.support.StatementRenderUtil; -import org.springframework.data.relational.core.sql.AssignValue; -import org.springframework.data.relational.core.sql.Assignment; -import org.springframework.data.relational.core.sql.Column; -import org.springframework.data.relational.core.sql.Condition; -import org.springframework.data.relational.core.sql.Delete; -import org.springframework.data.relational.core.sql.DeleteBuilder; -import org.springframework.data.relational.core.sql.Expression; -import org.springframework.data.relational.core.sql.Insert; -import org.springframework.data.relational.core.sql.OrderByField; -import org.springframework.data.relational.core.sql.SQL; -import org.springframework.data.relational.core.sql.Select; -import org.springframework.data.relational.core.sql.SelectBuilder; -import org.springframework.data.relational.core.sql.StatementBuilder; -import org.springframework.data.relational.core.sql.Table; -import org.springframework.data.relational.core.sql.Update; -import org.springframework.data.relational.core.sql.UpdateBuilder; import org.springframework.data.relational.core.sql.render.RenderContext; -import org.springframework.data.relational.core.sql.render.SqlRenderer; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; /** * Default {@link StatementFactory} implementation. * * @author Mark Paluch */ +// TODO: Move DefaultPreparedOperation et al to a better place. Probably StatementMapper. @RequiredArgsConstructor -class DefaultStatementFactory implements StatementFactory { +class DefaultStatementFactory { private final Dialect dialect; private final RenderContext renderContext; - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.StatementFactory#select(java.lang.String, java.util.Collection, java.util.function.Consumer) - */ - @Override - public PreparedOperation select(String tableName, Collection columnNames, - BiConsumer configurerConsumer) { - - Assert.hasText(tableName, "Table must not be empty"); - Assert.notEmpty(columnNames, "Columns must not be empty"); - Assert.notNull(configurerConsumer, "Configurer Consumer must not be null"); - - return withDialect((dialect, renderContext) -> { - - DefaultSelectConfigurer configurer = new DefaultSelectConfigurer(dialect.getBindMarkersFactory().create()); - Table table = Table.create(tableName); - configurerConsumer.accept(table, configurer); - - List columns = table.columns(columnNames); - SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table); - - if (configurer.condition != null) { - selectBuilder.where(configurer.condition); - } - - if (configurer.sort != null) { - selectBuilder.orderBy(createOrderByFields(table, configurer.sort)); - } - - Select select = selectBuilder.build(); - return new DefaultPreparedOperation getMappedObject(SelectSpec selectSpec, + @Nullable RelationalPersistentEntity entity) { + + Table table = Table.create(selectSpec.getTable()); + List columns = table.columns(selectSpec.getProjectedFields()); + SelectBuilder.SelectFromAndJoin selectBuilder = StatementBuilder.select(columns).from(table); + + BindMarkers bindMarkers = this.dialect.getBindMarkersFactory().create(); + Bindings bindings = Bindings.empty(); + + if (selectSpec.getCriteria() != null) { + + BoundCondition mappedObject = this.updateMapper.getMappedObject(bindMarkers, selectSpec.getCriteria(), table, + entity); + + bindings = mappedObject.getBindings(); + selectBuilder.where(mappedObject.getCondition()); + } + + if (selectSpec.getSort().isSorted()) { + + Sort mappedSort = this.updateMapper.getMappedObject(selectSpec.getSort(), entity); + selectBuilder.orderBy(createOrderByFields(table, mappedSort)); + } + + OptionalLong limit; + OptionalLong offset; + + if (selectSpec.getPage().isPaged()) { + + Pageable page = selectSpec.getPage(); + limit = OptionalLong.of(page.getPageSize()); + offset = OptionalLong.of(page.getOffset()); + } else { + limit = OptionalLong.empty(); + offset = OptionalLong.empty(); + } + + Select select = selectBuilder.build(); + return new DefaultPreparedOperation select(String tableName, Collection columnNames, - Consumer binderConsumer); - - /** - * Creates a {@link Select} statement. - * - * @param tableName must not be {@literal null} or empty. - * @param columnNames the columns to project, must not be {@literal null} or empty. - * @param configurerConsumer customizer for {@link SelectConfigurer}. - * @return the {@link PreparedOperation} to select the given columns. - */ - PreparedOperation operation = accessStrategy.getStatements().select(entity.getTableName(), columns, - binder -> { - binder.filterBy(idColumnName, SettableValue.from(id)); - }); + StatementMapper mapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType()); + StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.entity.getTableName()) // + .withProjection(columns) // + .withCriteria(Criteria.where(idColumnName).is(id)); - return databaseClient.execute().sql(operation) // - .as(entity.getJavaType()) // + PreparedOperation operation = mapper.getMappedObject(selectSpec); + + return this.databaseClient.execute().sql(operation) // + .as(this.entity.getJavaType()) // .fetch() // .one(); } @@ -151,12 +142,14 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository operation = accessStrategy.getStatements().select(entity.getTableName(), - Collections.singleton(idColumnName), binder -> { - binder.filterBy(idColumnName, SettableValue.from(id)); - }); + StatementMapper mapper = this.accessStrategy.getStatementMapper().forType(this.entity.getJavaType()); + StatementMapper.SelectSpec selectSpec = mapper.createSelect(this.entity.getTableName()) + .withProjection(Collections.singletonList(idColumnName)) // + .withCriteria(Criteria.where(idColumnName).is(id)); - return databaseClient.execute().sql(operation) // + PreparedOperation operation = mapper.getMappedObject(selectSpec); + + return this.databaseClient.execute().sql(operation) // .map((r, md) -> r) // .first() // .hasElement(); @@ -175,7 +168,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository findAll() { - return databaseClient.select().from(entity.getJavaType()).fetch().all(); + return this.databaseClient.select().from(this.entity.getJavaType()).fetch().all(); } /* (non-Javadoc) @@ -203,15 +196,17 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository 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 0000000..bd5ed80 --- /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 c6ac54c..32d190c 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 class CriteriaUnitTests { 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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 class CriteriaUnitTests { @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 e47f9c1..966ff4a 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 @@ package org.springframework.data.r2dbc.function.query; 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.mapping.RelationalMappingContext 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(statementMock); - verify(statementMock).bind(0, "foo"); + 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(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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 class CriteriaMapperUnitTests { @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 0000000..9c23930 --- /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; + } +}