From 8cc1535d4d651606d45d94eb108a600c5647a36c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 17 Oct 2018 11:04:41 +0200 Subject: [PATCH 001/665] #1 - Setup repository --- .gitignore | 10 ++++++++++ .travis.yml | 41 +++++++++++++++++++++++++++++++++++++++++ CODE_OF_CONDUCT.adoc | 27 +++++++++++++++++++++++++++ CONTRIBUTING.adoc | 3 +++ lombok.config | 2 ++ 5 files changed, 83 insertions(+) create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 CODE_OF_CONDUCT.adoc create mode 100644 CONTRIBUTING.adoc create mode 100644 lombok.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..646c021f --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +target/ +.idea/ +.settings/ +*.iml +.project +.classpath +.springBeans +.sonar4clipse +*.sonar4clipseExternals +*.graphml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..736933ca --- /dev/null +++ b/.travis.yml @@ -0,0 +1,41 @@ +language: java + +matrix: + include: + - jdk: oraclejdk8 + env: JDK='Oracle JDK 8' + - jdk: oraclejdk9 + env: JDK='Oracle JDK 9' + - env: + - JDK='Oracle JDK 10' + - NO_JACOCO='true' + before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 10 -L BCL + - env: + - JDK='Oracle JDK 11' + - NO_JACOCO='true' + before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 11 -L BCL + allow_failures: + - env: + - JDK='Oracle JDK 11' + - NO_JACOCO='true' + +addons: + apt: + packages: + - oracle-java8-installer + +cache: + directories: + - $HOME/.m2 + +sudo: false + +services: + - docker + - postgresql + +install: true + +script: + - "mvn -version" + - "mvn clean dependency:list test -Pall-dbs${NO_JACOCO:+',no-jacoco'} -Dsort -U" diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..f64fb1b7 --- /dev/null +++ b/CODE_OF_CONDUCT.adoc @@ -0,0 +1,27 @@ += Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at spring-code-of-conduct@pivotal.io. +All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. +Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +This Code of Conduct is adapted from the http://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]. \ No newline at end of file diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 00000000..f0075914 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,3 @@ += Spring Data contribution guidelines + +You find the contribution guidelines for Spring Data projects https://github.com/spring-projects/spring-data-build/blob/master/CONTRIBUTING.adoc[here]. diff --git a/lombok.config b/lombok.config new file mode 100644 index 00000000..e50c7ea4 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +lombok.nonNull.exceptionType = IllegalArgumentException +lombok.log.fieldName = LOG From c59e9a58eb979dc4d2a8721469dffe057648752a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 4 Jun 2018 16:03:34 +0200 Subject: [PATCH 002/665] #2 - Add DatabaseClient. Provide DatabaseClient and add ExternalDatabase class to provide database connection details/encapsulate a dockerized testcontainer. --- pom.xml | 411 +++++++++ .../core/function/ColumnMapRowMapper.java | 102 +++ .../jdbc/core/function/DatabaseClient.java | 451 ++++++++++ .../core/function/DefaultDatabaseClient.java | 793 ++++++++++++++++++ .../DefaultDatabaseClientBuilder.java | 96 +++ .../DefaultReactiveDataAccessStrategy.java | 75 ++ .../jdbc/core/function/EntityRowMapper.java | 168 ++++ .../jdbc/core/function/IterableUtils.java | 62 ++ .../function/ReactiveDataAccessStrategy.java | 37 + .../data/jdbc/core/function/SqlResult.java | 31 + .../connectionfactory/ConnectionProxy.java | 41 + .../DatabaseClientIntegrationTests.java | 155 ++++ .../jdbc/core/function/ExternalDatabase.java | 126 +++ 13 files changed, 2548 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..90ffd656 --- /dev/null +++ b/pom.xml @@ -0,0 +1,411 @@ + + + + 4.0.0 + + org.springframework.data + spring-data-jdbc + 1.1.0.r2dbc-SNAPSHOT + + Spring Data JDBC + Spring Data module for JDBC repositories. + http://projects.spring.io/spring-data-jdbc + + + org.springframework.data.build + spring-data-parent + 2.2.0.BUILD-SNAPSHOT + + + + + DATAJDBC + + 2.2.0.BUILD-SNAPSHOT + spring.data.jdbc + reuseReports + + 3.6.2 + 0.1.4 + 2.2.8 + 3.4.6 + 1.3.2 + 5.1.41 + 42.0.0 + 2.2.3 + 1.0.0.BUILD-SNAPSHOT + 1.0.0.BUILD-SNAPSHOT + 1.7.3 + + + + 2017 + + + + schauder + Jens Schauder + jschauder(at)pivotal.io + Pivotal Software, Inc. + https://pivotal.io + + Project Lead + + +1 + + + gregturn + Greg L. Turnquist + gturnquist(at)pivotal.io + Pivotal Software, Inc. + https://pivotal.io + + Project Contributor + + -6 + + + + + + release + + + + org.jfrog.buildinfo + artifactory-maven-plugin + false + + + + + + + no-jacoco + + + + org.jacoco + jacoco-maven-plugin + + + jacoco-initialize + none + + + + + + + + + + all-dbs + + + + org.apache.maven.plugins + maven-surefire-plugin + + + mysql-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + mysql + + + + + postgres-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + postgres + + + + + mariadb-test + test + + test + + + + **/*IntegrationTests.java + + + **/*HsqlIntegrationTests.java + + + mariadb + + + + + + + + + + + + + + ${project.groupId} + spring-data-commons + ${springdata.commons} + + + + org.springframework + spring-tx + + + + org.springframework + spring-context + + + + org.springframework + spring-beans + + + + org.springframework + spring-jdbc + + + + org.springframework + spring-core + + + + io.r2dbc + r2dbc-spi + ${r2dbc-spi.version} + true + + + + io.projectreactor + reactor-core + true + + + + org.mybatis + mybatis-spring + ${mybatis-spring.version} + true + + + + org.mybatis + mybatis + ${mybatis.version} + true + + + + org.hsqldb + hsqldb + ${hsqldb.version} + test + + + + org.assertj + assertj-core + ${assertj-core.version} + test + + + + io.projectreactor + reactor-test + test + + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + test + + + + org.postgresql + postgresql + ${postgresql.version} + test + + + + org.mariadb.jdbc + mariadb-java-client + ${mariadb-java-client.version} + test + + + + io.r2dbc + r2dbc-postgresql + ${r2dbc-postgresql.version} + test + + + + de.schauderhaft.degraph + degraph-check + ${degraph-check.version} + test + + + + org.testcontainers + mysql + ${testcontainers.version} + test + + + org.slf4j + jcl-over-slf4j + + + + + + org.testcontainers + postgresql + ${testcontainers.version} + test + + + + org.testcontainers + mariadb + ${testcontainers.version} + test + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.1.0 + + + + + + + + org.jacoco + jacoco-maven-plugin + ${jacoco} + + ${jacoco.destfile} + + + + jacoco-initialize + + prepare-agent + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + default-test + + + **/*Tests.java + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + org.codehaus.mojo + wagon-maven-plugin + + + org.asciidoctor + asciidoctor-maven-plugin + + + + + + + spring-libs-snapshot + https://repo.spring.io/libs-snapshot + + + nebhale-snapshots + https://raw.githubusercontent.com/nebhale/r2dbc/maven/snapshot + + true + + + + nebhale-milestones + https://raw.githubusercontent.com/nebhale/r2dbc/maven/milestone + + false + + + + + + + spring-plugins-snapshot + https://repo.spring.io/plugins-snapshot + + + + diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java new file mode 100644 index 00000000..663d88fb --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java @@ -0,0 +1,102 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.ColumnMetadata; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +import java.util.Collection; +import java.util.Map; +import java.util.function.BiFunction; + +import org.springframework.lang.Nullable; +import org.springframework.util.LinkedCaseInsensitiveMap; + +/** + * {@link RowMapper} implementation that creates a {@link Map} for each row, representing all columns as key-value + * pairs: one entry for each column, with the column name as key. + *

+ * The {@link Map} implementation to use and the key to use for each column in the column Map can be customized through + * overriding {@link #createColumnMap} and {@link #getColumnKey}, respectively. + *

+ * Note: By default, {@link ColumnMapRowMapper} will try to build a linked {@link Map} with case-insensitive + * keys, to preserve column order as well as allow any casing to be used for column names. This requires Commons + * Collections on the classpath (which will be autodetected). Else, the fallback is a standard linked + * {@link java.util.HashMap}, which will still preserve column order but requires the application to specify the column + * names in the same casing as exposed by the driver. + * + * @author Mark Paluch + */ +public class ColumnMapRowMapper implements BiFunction> { + + public final static ColumnMapRowMapper INSTANCE = new ColumnMapRowMapper(); + + @Override + public Map apply(Row row, RowMetadata rowMetadata) { + + Collection columns = IterableUtils.toCollection(rowMetadata.getColumnMetadatas()); + int columnCount = columns.size(); + Map mapOfColValues = createColumnMap(columnCount); + + int index = 0; + for (ColumnMetadata column : columns) { + + String key = getColumnKey(column.getName()); + Object obj = getColumnValue(row, index++); + mapOfColValues.put(key, obj); + } + return mapOfColValues; + } + + /** + * Create a {@link Map} instance to be used as column map. + *

+ * By default, a linked case-insensitive Map will be created. + * + * @param columnCount the column count, to be used as initial capacity for the Map. + * @return the new {@link Map} instance. + * @see LinkedCaseInsensitiveMap + */ + protected Map createColumnMap(int columnCount) { + return new LinkedCaseInsensitiveMap<>(columnCount); + } + + /** + * Determine the key to use for the given column in the column {@link Map}. + * + * @param columnName the column name as returned by the {@link Row}. + * @return the column key to use. + * @see ColumnMetadata#getName() + */ + protected String getColumnKey(String columnName) { + return columnName; + } + + /** + * Retrieve a R2DBC object value for the specified column. + *

+ * The default implementation uses the {@link Row#get(Object, Class)} method. + * + * @param row is the {@link Row} holding the data. + * @param index is the column index. + * @return the Object returned. + */ + @Nullable + protected Object getColumnValue(Row row, int index) { + return row.get(index, Object.class); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java new file mode 100644 index 00000000..59265925 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java @@ -0,0 +1,451 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.jdbc.support.SQLExceptionTranslator; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +/** + * A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides + * a higher level, common API over R2DBC client libraries. + *

+ * Use one of the static factory methods {@link #create(ConnectionFactory)} or obtain a {@link DatabaseClient#builder()} + * to create an instance. + * + * @author Mark Paluch + */ +public interface DatabaseClient { + + /** + * Prepare an SQL call returning a result. + */ + SqlSpec execute(); + + /** + * Prepare an SQL SELECT call. + */ + SelectFromSpec select(); + + /** + * Prepare an SQL INSERT call. + */ + InsertIntoSpec insert(); + + /** + * Return a builder to mutate properties of this database client. + */ + DatabaseClient.Builder mutate(); + + // Static, factory methods + + /** + * A variant of {@link #create()} that accepts a {@link io.r2dbc.spi.ConnectionFactory} + */ + static DatabaseClient create(ConnectionFactory factory) { + return new DefaultDatabaseClientBuilder().connectionFactory(factory).build(); + } + + /** + * Obtain a {@code WebClient} builder. + */ + static DatabaseClient.Builder builder() { + return new DefaultDatabaseClientBuilder(); + } + + /** + * A mutable builder for creating a {@link DatabaseClient}. + */ + interface Builder { + + /** + * Configures the {@link ConnectionFactory R2DBC connector}. + * + * @param factory must not be {@literal null}. + * @return {@code this} {@link Builder}. + */ + Builder connectionFactory(ConnectionFactory factory); + + /** + * Configures a {@link SQLExceptionTranslator}. + * + * @param exceptionTranslator must not be {@literal null}. + * @return {@code this} {@link Builder}. + */ + Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator); + + /** + * Configures a {@link ReactiveDataAccessStrategy}. + * + * @param accessStrategy must not be {@literal null}. + * @return {@code this} {@link Builder}. + */ + Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy); + + /** + * Configures a {@link Consumer} to configure this builder. + * + * @param builderConsumer must not be {@literal null}. + * @return {@code this} {@link Builder}. + */ + Builder apply(Consumer builderConsumer); + + /** + * Builder the {@link DatabaseClient} instance. + */ + DatabaseClient build(); + } + + /** + * Contract for specifying a SQL call along with options leading to the exchange. + */ + interface SqlSpec { + + /** + * Specify a static {@code sql} string to execute. + * + * @param sql must not be {@literal null} or empty. + * @return a new {@link GenericExecuteSpec}. + */ + GenericExecuteSpec sql(String sql); + + /** + * Specify a static {@link Supplier SQL supplier} that provides SQL to execute. + * + * @param sqlSupplier must not be {@literal null}. + * @return a new {@link GenericExecuteSpec}. + */ + GenericExecuteSpec sql(Supplier sqlSupplier); + } + + /** + * Contract for specifying a SQL call along with options leading to the exchange. + */ + interface GenericExecuteSpec extends BindSpec { + + /** + * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion. + * + * @param resultType must not be {@literal null}. + * @param result type. + */ + TypedExecuteSpec as(Class resultType); + + /** + * Perform the SQL call and retrieve the result. + */ + FetchSpec> fetch(); + + /** + * Perform the SQL request and return a {@link SqlResult}. + * + * @return a {@code Mono} for the result + */ + Mono>> exchange(); + } + + /** + * Contract for specifying a SQL call along with options leading to the exchange. + */ + interface TypedExecuteSpec extends BindSpec> { + + /** + * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion. + * + * @param resultType must not be {@literal null}. + * @param result type. + */ + TypedExecuteSpec as(Class resultType); + + /** + * Perform the SQL call and retrieve the result. + */ + FetchSpec fetch(); + + /** + * Perform the SQL request and return a {@link SqlResult}. + * + * @return a {@code Mono} for the result + */ + Mono> exchange(); + } + + /** + * Contract for specifying {@code SELECT} options leading to the exchange. + */ + interface SelectFromSpec { + + GenericSelectSpec from(String table); + + TypedSelectSpec from(Class table); + } + + /** + * Contract for specifying {@code SELECT} options leading to the exchange. + */ + interface InsertIntoSpec { + + /** + * Specify the target {@literal table} to insert into. + * + * @param table must not be {@literal null} or empty. + * @return + */ + GenericInsertSpec into(String table); + + TypedInsertSpec into(Class table); + } + + /** + * Contract for specifying {@code SELECT} options leading to the exchange. + */ + interface GenericSelectSpec extends SelectSpec { + + /** + * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion. + * + * @param resultType must not be {@literal null}. + * @param result type. + */ + TypedSelectSpec as(Class resultType); + + /** + * Perform the SQL call and retrieve the result. + */ + FetchSpec> fetch(); + + /** + * Perform the SQL request and return a {@link SqlResult}. + * + * @return a {@code Mono} for the result + */ + Mono>> exchange(); + } + + /** + * Contract for specifying {@code SELECT} options leading to the exchange. + */ + interface TypedSelectSpec extends SelectSpec> { + + /** + * Define the target type the result should be mapped to.
+ * Skip this step if you are anyway fine with the default conversion. + * + * @param resultType must not be {@literal null}. + * @param result type. + */ + TypedSelectSpec as(Class resultType); + + /** + * Configure a result mapping {@link java.util.function.Function}. + * + * @param mappingFunction must not be {@literal null}. + * @param result type. + * @return + */ + TypedSelectSpec extract(BiFunction mappingFunction); + + /** + * Perform the SQL call and retrieve the result. + */ + FetchSpec fetch(); + + /** + * Perform the SQL request and return a {@link SqlResult}. + * + * @return a {@code Mono} for the result + */ + Mono> exchange(); + } + + /** + * Contract for specifying {@code SELECT} options leading to the exchange. + */ + interface SelectSpec> { + + S project(String... selectedFields); + + S where(Object criteriaDefinition); + + S orderBy(Sort sort); + + S page(Pageable page); + } + + /** + * Contract for specifying {@code INSERT} options leading to the exchange. + */ + interface GenericInsertSpec extends InsertSpec { + + /** + * Specify a field and non-{@literal null} value to insert. + * + * @param field must not be {@literal null} or empty. + * @param value must not be {@literal null} + */ + GenericInsertSpec value(String field, Object value); + + /** + * Specify a {@literal null} value to insert. + * + * @param field must not be {@literal null} or empty. + */ + GenericInsertSpec nullValue(String field); + } + + /** + * Contract for specifying {@code SELECT} options leading the exchange. + */ + interface TypedInsertSpec { + + /** + * Insert the given {@code objectToInsert}. + * + * @param objectToInsert + * @return + */ + InsertSpec using(T objectToInsert); + + /** + * Use the given {@code tableName} as insert target. + * + * @param tableName must not be {@literal null} or empty. + * @return + */ + TypedInsertSpec table(String tableName); + + /** + * Insert the given {@link Publisher} to insert one or more objects. + * + * @param objectToInsert + * @return + */ + InsertSpec using(Publisher objectToInsert); + } + + /** + * Contract for specifying {@code INSERT} options leading to the exchange. + */ + interface InsertSpec { + + /** + * Perform the SQL call. + */ + Mono then(); + + /** + * Perform the SQL request and return a {@link SqlResult}. + * + * @return a {@code Mono} for the result + */ + Mono>> exchange(); + } + + /** + * Contract for specifying parameter bindings. + */ + interface BindSpec> { + + /** + * Bind a non-{@literal null} value to a parameter identified by its {@code index}. + * + * @param index + * @param value must not be {@literal null}. + */ + S bind(int index, Object value); + + /** + * Bind a {@literal null} value to a parameter identified by its {@code index}. + * + * @param index + */ + S bindNull(int index); + + /** + * Bind a non-{@literal null} value to a parameter identified by its {@code name}. + * + * @param name must not be {@literal null} or empty. + * @param value must not be {@literal null}. + */ + S bind(String name, Object value); + + /** + * Bind a {@literal null} value to a parameter identified by its {@code name}. + * + * @param name must not be {@literal null} or empty. + */ + S bindNull(String name); + + /** + * Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names. + * + * @param bean must not be {@literal null}. + */ + S bind(Object bean); + } + + /** + * Contract for fetching results. + */ + interface FetchSpec { + + /** + * Get exactly zero or one result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Mono one(); + + /** + * Get the first or no result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + */ + Mono first(); + + /** + * Get all matching elements. + * + * @return never {@literal null}. + */ + Flux all(); + + /** + * Get the number of updated rows. + * + * @return {@link Mono} emitting the number of updated rows. Never {@literal null}. + */ + Mono rowsUpdated(); + } + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java new file mode 100644 index 00000000..de640cac --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -0,0 +1,793 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import io.r2dbc.spi.Statement; +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.sql.SQLException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy; +import org.springframework.data.util.Pair; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link DatabaseClient}. + * + * @author Mark Paluch + */ +class DefaultDatabaseClient implements DatabaseClient { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private final ConnectionFactory connector; + + private final SQLExceptionTranslator exceptionTranslator; + + private final ReactiveDataAccessStrategy dataAccessStrategy; + + private final DefaultDatabaseClientBuilder builder; + + public DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator, + ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { + + this.connector = connector; + this.exceptionTranslator = exceptionTranslator; + this.dataAccessStrategy = dataAccessStrategy; + this.builder = builder; + } + + @Override + public Builder mutate() { + return builder; + } + + @Override + public SqlSpec execute() { + return new DefaultSqlSpec(); + } + + @Override + public SelectFromSpec select() { + throw new UnsupportedOperationException("Implement me"); + } + + @Override + public InsertIntoSpec insert() { + return new DefaultInsertIntoSpec(); + } + + public Flux execute(Function> action) throws DataAccessException { + + Assert.notNull(action, "Callback object must not be null"); + + Mono connectionMono = Mono.from(obtainConnectionFactory().create()); + // Create close-suppressing Connection proxy, also preparing returned Statements. + + return connectionMono.flatMapMany(connection -> { + + Connection connectionToUse = createConnectionProxy(connection); + + // TODO: Release connection + return doInConnection(action, connectionToUse); + + }).onErrorMap(SQLException.class, ex -> { + + String sql = getSql(action); + return translateException("ConnectionCallback", sql, ex); + }); + } + + /** + * Obtain the {@link ConnectionFactory} for actual use. + * + * @return the ConnectionFactory (never {@code null}) + * @throws IllegalStateException in case of no DataSource set + */ + protected ConnectionFactory obtainConnectionFactory() { + return connector; + } + + /** + * Create a close-suppressing proxy for the given R2DBC Connection. Called by the {@code execute} method. + * + * @param con the R2DBC Connection to create a proxy for + * @return the Connection proxy + */ + protected Connection createConnectionProxy(Connection con) { + return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), + new Class[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(con)); + } + + /** + * Translate the given {@link SQLException} into a generic {@link DataAccessException}. + * + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem (may be {@code null}) + * @param ex the offending {@code SQLException} + * @return a DataAccessException wrapping the {@code SQLException} (never {@code null}) + */ + protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) { + DataAccessException dae = exceptionTranslator.translate(task, sql, ex); + return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex)); + } + + static void doBind(Statement statement, Map> byName, + Map> byIndex) { + + byIndex.forEach((i, o) -> { + + if (o.isPresent()) { + o.ifPresent(v -> statement.bind(i, v)); + } else { + statement.bindNull(i, 0); // TODO: What is type? + } + }); + + byName.forEach((name, o) -> { + + if (o.isPresent()) { + o.ifPresent(v -> statement.bind(name, v)); + } else { + statement.bindNull(name, 0); // TODO: What is type? + } + }); + + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlSpec} implementation. + */ + private class DefaultSqlSpec implements SqlSpec { + + @Override + public GenericExecuteSpec sql(String sql) { + + Assert.hasText(sql, "SQL must not be null or empty!"); + return sql(() -> sql); + } + + @Override + public GenericExecuteSpec sql(Supplier sqlSupplier) { + + Assert.notNull(sqlSupplier, "SQL Supplier must not be null!"); + + return new DefaultGenericExecuteSpec(sqlSupplier); + } + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + */ + @RequiredArgsConstructor + private class GenericExecuteSpecSupport { + + final Map> byIndex; + final Map> byName; + final Supplier sqlSupplier; + + GenericExecuteSpecSupport(Supplier sqlSupplier) { + + this.byIndex = Collections.emptyMap(); + this.byName = Collections.emptyMap(); + this.sqlSupplier = sqlSupplier; + } + + protected String getSql() { + + String sql = sqlSupplier.get(); + Assert.state(sql != null, "SQL supplier returned null!"); + return sql; + } + + Mono> exchange(String sql, BiFunction mappingFunction) { + + return execute(it -> { + + Statement statement = it.createStatement(sql); + doBind(statement, byName, byIndex); + + return Flux + .just((SqlResult) new DefaultSqlResult<>(sql, Flux.from(statement.add().execute()), mappingFunction)); + }).next(); + } + + public GenericExecuteSpecSupport bind(int index, Object value) { + + Map> byIndex = new LinkedHashMap<>(this.byIndex); + byIndex.put(index, Optional.of(value)); + + return createInstance(byIndex, this.byName, this.sqlSupplier); + } + + public GenericExecuteSpecSupport bindNull(int index) { + + Map> byIndex = new LinkedHashMap<>(this.byIndex); + byIndex.put(index, Optional.empty()); + + return createInstance(byIndex, this.byName, this.sqlSupplier); + } + + public GenericExecuteSpecSupport bind(String name, Object value) { + + Assert.hasText(name, "Parameter name must not be null or empty!"); + + Map> byName = new LinkedHashMap<>(this.byName); + byName.put(name, Optional.of(value)); + + return createInstance(this.byIndex, byName, this.sqlSupplier); + } + + public GenericExecuteSpecSupport bindNull(String name) { + + Assert.hasText(name, "Parameter name must not be null or empty!"); + + Map> byName = new LinkedHashMap<>(this.byName); + byName.put(name, Optional.empty()); + + return createInstance(this.byIndex, byName, this.sqlSupplier); + } + + protected GenericExecuteSpecSupport createInstance(Map> byIndex, + Map> byName, Supplier sqlSupplier) { + return new GenericExecuteSpecSupport(byIndex, byName, sqlSupplier); + } + + public GenericExecuteSpecSupport bind(Object bean) { + + Assert.notNull(bean, "Bean must not be null!"); + + throw new UnsupportedOperationException("Implement me!"); + } + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + */ + private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec { + + DefaultGenericExecuteSpec(Map> byIndex, Map> byName, + Supplier sqlSupplier) { + super(byIndex, byName, sqlSupplier); + } + + DefaultGenericExecuteSpec(Supplier sqlSupplier) { + super(sqlSupplier); + } + + @Override + public TypedExecuteSpec as(Class resultType) { + + Assert.notNull(resultType, "Result type must not be null!"); + + return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType); + } + + @Override + public FetchSpec> fetch() { + + String sql = getSql(); + return new DefaultFetchSpec<>(sql, exchange(sql, ColumnMapRowMapper.INSTANCE).flatMapMany(SqlResult::all), + exchange(sql, ColumnMapRowMapper.INSTANCE).flatMap(FetchSpec::rowsUpdated)); + } + + @Override + public Mono>> exchange() { + return exchange(getSql(), ColumnMapRowMapper.INSTANCE); + } + + @Override + public DefaultGenericExecuteSpec bind(int index, Object value) { + return (DefaultGenericExecuteSpec) super.bind(index, value); + } + + @Override + public DefaultGenericExecuteSpec bindNull(int index) { + return (DefaultGenericExecuteSpec) super.bindNull(index); + } + + @Override + public DefaultGenericExecuteSpec bind(String name, Object value) { + return (DefaultGenericExecuteSpec) super.bind(name, value); + } + + @Override + public DefaultGenericExecuteSpec bindNull(String name) { + return (DefaultGenericExecuteSpec) super.bindNull(name); + } + + @Override + public DefaultGenericExecuteSpec bind(Object bean) { + return (DefaultGenericExecuteSpec) super.bind(bean); + } + + @Override + protected GenericExecuteSpecSupport createInstance(Map> byIndex, + Map> byName, Supplier sqlSupplier) { + return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); + } + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + */ + @SuppressWarnings("unchecked") + private class DefaultTypedGenericExecuteSpec extends GenericExecuteSpecSupport implements TypedExecuteSpec { + + private final Class typeToRead; + private final BiFunction mappingFunction; + + DefaultTypedGenericExecuteSpec(Map> byIndex, Map> byName, + Supplier sqlSupplier, Class typeToRead) { + + super(byIndex, byName, sqlSupplier); + + this.typeToRead = typeToRead; + this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); + } + + @Override + public TypedExecuteSpec as(Class resultType) { + + Assert.notNull(resultType, "Result type must not be null!"); + + return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType); + } + + @Override + public FetchSpec fetch() { + String sql = getSql(); + return new DefaultFetchSpec<>(sql, exchange(sql, mappingFunction).flatMapMany(SqlResult::all), + exchange(sql, mappingFunction).flatMap(FetchSpec::rowsUpdated)); + } + + @Override + public Mono> exchange() { + return exchange(getSql(), mappingFunction); + } + + @Override + public DefaultTypedGenericExecuteSpec bind(int index, Object value) { + return (DefaultTypedGenericExecuteSpec) super.bind(index, value); + } + + @Override + public DefaultTypedGenericExecuteSpec bindNull(int index) { + return (DefaultTypedGenericExecuteSpec) super.bindNull(index); + } + + @Override + public DefaultTypedGenericExecuteSpec bind(String name, Object value) { + return (DefaultTypedGenericExecuteSpec) super.bind(name, value); + } + + @Override + public DefaultTypedGenericExecuteSpec bindNull(String name) { + return (DefaultTypedGenericExecuteSpec) super.bindNull(name); + } + + @Override + public DefaultTypedGenericExecuteSpec bind(Object bean) { + return (DefaultTypedGenericExecuteSpec) super.bind(bean); + } + + @Override + protected DefaultTypedGenericExecuteSpec createInstance(Map> byIndex, + Map> byName, Supplier sqlSupplier) { + return new DefaultTypedGenericExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); + } + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation. + */ + class DefaultInsertIntoSpec implements InsertIntoSpec { + + @Override + public GenericInsertSpec into(String table) { + return new DefaultGenericInsertSpec(table, Collections.emptyMap()); + } + + @Override + public TypedInsertSpec into(Class table) { + return new DefaultTypedInsertSpec<>(table); + } + } + + /** + * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericInsertSpec}. + */ + @RequiredArgsConstructor + class DefaultGenericInsertSpec implements GenericInsertSpec { + + private final String table; + private final Map> byName; + + @Override + public GenericInsertSpec value(String field, Object value) { + + Assert.notNull(field, "Field must not be null!"); + + Map> byName = new LinkedHashMap<>(this.byName); + byName.put(field, Optional.of(value)); + + return new DefaultGenericInsertSpec(this.table, byName); + } + + @Override + public GenericInsertSpec nullValue(String field) { + + Assert.notNull(field, "Field must not be null!"); + + Map> byName = new LinkedHashMap<>(this.byName); + byName.put(field, Optional.empty()); + + return new DefaultGenericInsertSpec(this.table, byName); + } + + @Override + public Mono then() { + return exchange().flatMapMany(FetchSpec::all).then(); + } + + @Override + public Mono>> exchange() { + + if (byName.isEmpty()) { + throw new IllegalStateException("Insert fields is empty!"); + } + + StringBuilder builder = new StringBuilder(); + String fieldNames = byName.keySet().stream().collect(Collectors.joining(",")); + String placeholders = IntStream.range(0, byName.size()).mapToObj(i -> "$" + (i + 1)) + .collect(Collectors.joining(",")); + + builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") + .append(placeholders).append(")"); + + return execute(it -> { + + String sql = builder.toString(); + Statement statement = it.createStatement(sql); + + AtomicInteger index = new AtomicInteger(); + for (Optional o : byName.values()) { + + if (o.isPresent()) { + o.ifPresent(v -> statement.bind(index.getAndIncrement(), v)); + } else { + statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? + } + } + + SqlResult> result = new DefaultSqlResult<>(sql, + Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE); + return Flux.just(result); + + }).next(); + } + } + + /** + * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}. + */ + @RequiredArgsConstructor + class DefaultTypedInsertSpec implements TypedInsertSpec, InsertSpec { + + private final Class typeToInsert; + private final String table; + private final Publisher objectToInsert; + + public DefaultTypedInsertSpec(Class typeToInsert) { + + this.typeToInsert = typeToInsert; + this.table = dataAccessStrategy.getTableName(typeToInsert); + this.objectToInsert = Mono.empty(); + } + + @Override + public TypedInsertSpec table(String tableName) { + + Assert.hasText(tableName, "Table name must not be null or empty!"); + + return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert); + } + + @Override + public InsertSpec using(T objectToInsert) { + + Assert.notNull(objectToInsert, "Object to insert must not be null!"); + + return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert)); + } + + @Override + public InsertSpec using(Publisher objectToInsert) { + + Assert.notNull(objectToInsert, "Publisher to insert must not be null!"); + + return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert); + } + + @Override + public Mono then() { + return exchange().flatMapMany(FetchSpec::all).then(); + } + + @Override + public Mono>> exchange() { + + return Mono.from(objectToInsert).flatMap(toInsert -> { + + StringBuilder builder = new StringBuilder(); + + List> insertValues = dataAccessStrategy.getInsert(toInsert); + String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(",")); + String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1)) + .collect(Collectors.joining(",")); + + builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") + .append(placeholders).append(")"); + + return execute(it -> { + + String sql = builder.toString(); + Statement statement = it.createStatement(sql); + + AtomicInteger index = new AtomicInteger(); + + for (Pair pair : insertValues) { + + if (pair.getSecond() != null) { // TODO: Better type to transport null values. + statement.bind(index.getAndIncrement(), pair.getSecond()); + } else { + statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? + } + } + + SqlResult> result = new DefaultSqlResult<>(sql, + Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE); + return Flux.just(result); + + }).next(); + }); + } + } + + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlResult} implementation. + */ + static class DefaultSqlResult implements SqlResult { + + private final String sql; + private final Flux result; + private final FetchSpec fetchSpec; + + DefaultSqlResult(String sql, Flux result, BiFunction mappingFunction) { + + this.sql = sql; + this.result = result; + this.fetchSpec = new DefaultFetchSpec<>(sql, result.flatMap(it -> it.map(mappingFunction)), + result.flatMap(Result::getRowsUpdated).next()); + } + + @Override + public SqlResult extract(BiFunction mappingFunction) { + return new DefaultSqlResult<>(sql, result, mappingFunction); + } + + @Override + public Mono one() { + return fetchSpec.one(); + } + + @Override + public Mono first() { + return fetchSpec.first(); + } + + @Override + public Flux all() { + return fetchSpec.all(); + } + + @Override + public Mono rowsUpdated() { + return fetchSpec.rowsUpdated(); + } + } + + @RequiredArgsConstructor + static class DefaultFetchSpec implements FetchSpec { + + private final String sql; + private final Flux result; + private final Mono updatedRows; + + @Override + public Mono one() { + + return all().buffer(2) // + .flatMap(it -> { + + if (it.isEmpty()) { + return Mono.empty(); + } + + if (it.size() > 1) { + return Mono.error(new IncorrectResultSizeDataAccessException( + String.format("Query [%s] returned non unique result.", this.sql), 1)); + } + + return Mono.just(it.get(0)); + }).next(); + } + + @Override + public Mono first() { + return all().next(); + } + + @Override + public Flux all() { + return result; + } + + @Override + public Mono rowsUpdated() { + return updatedRows; + } + } + + private static Flux doInConnection(Function> action, Connection it) { + + try { + return action.apply(it); + } catch (RuntimeException e) { + + String sql = getSql(action); + return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {}); + } + } + + /** + * Determine SQL from potential provider object. + * + * @param sqlProvider object that's potentially a SqlProvider + * @return the SQL string, or {@code null} + * @see SqlProvider + */ + @Nullable + private static String getSql(Object sqlProvider) { + + if (sqlProvider instanceof SqlProvider) { + return ((SqlProvider) sqlProvider).getSql(); + } else { + return null; + } + } + + /** + * Invocation handler that suppresses close calls on R2DBC Connections. Also prepares returned Statement + * (Prepared/CallbackStatement) objects. + * + * @see Connection#close() + */ + private class CloseSuppressingInvocationHandler implements InvocationHandler { + + private final Connection target; + + CloseSuppressingInvocationHandler(Connection target) { + this.target = target; + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // Invocation on ConnectionProxy interface coming in... + + if (method.getName().equals("equals")) { + // Only consider equal when proxies are identical. + return (proxy == args[0]); + } else if (method.getName().equals("hashCode")) { + // Use hashCode of PersistenceManager proxy. + return System.identityHashCode(proxy); + } else if (method.getName().equals("unwrap")) { + if (((Class) args[0]).isInstance(proxy)) { + return proxy; + } + } else if (method.getName().equals("isWrapperFor")) { + if (((Class) args[0]).isInstance(proxy)) { + return true; + } + } else if (method.getName().equals("close")) { + // Handle close method: suppress, not valid. + return Mono.error(new UnsupportedOperationException("Close is not supported!")); + } else if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return this.target; + } + + // Invoke method on target Connection. + try { + Object retVal = method.invoke(this.target, args); + + return retVal; + } catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + + private static class UncategorizedSQLException extends UncategorizedDataAccessException { + + /** SQL that led to the problem */ + @Nullable private final String sql; + + /** + * Constructor for UncategorizedSQLException. + * + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public UncategorizedSQLException(String task, @Nullable String sql, Exception ex) { + super(String.format("%s; uncategorized SQLException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "", + ex.getMessage()), ex); + this.sql = sql; + } + + /** + * Return the SQL that led to the problem (if known). + */ + @Nullable + public String getSql() { + return this.sql; + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java new file mode 100644 index 00000000..08308598 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java @@ -0,0 +1,96 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.jdbc.core.function; + +import java.util.function.Consumer; + +import org.springframework.data.jdbc.core.function.DatabaseClient.Builder; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +import io.r2dbc.spi.ConnectionFactory; + +/** + * Default implementation of {@link DatabaseClient.Builder}. + * + * @author Mark Paluch + */ +class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { + + private @Nullable ConnectionFactory connector; + private SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(); + private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); + + DefaultDatabaseClientBuilder() {} + + DefaultDatabaseClientBuilder(DefaultDatabaseClientBuilder other) { + + Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!"); + + this.connector = other.connector; + this.exceptionTranslator = exceptionTranslator; + } + + @Override + public Builder connectionFactory(ConnectionFactory factory) { + + Assert.notNull(factory, "ConnectionFactory must not be null!"); + + this.connector = factory; + return this; + } + + @Override + public Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator) { + + Assert.notNull(exceptionTranslator, "SQLExceptionTranslator must not be null!"); + + this.exceptionTranslator = exceptionTranslator; + return this; + } + + @Override + public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) { + + Assert.notNull(accessStrategy, "ReactiveDataAccessStrategy must not be null!"); + + this.accessStrategy = accessStrategy; + return this; + } + + @Override + public DatabaseClient build() { + + return new DefaultDatabaseClient(this.connector, exceptionTranslator, accessStrategy, + new DefaultDatabaseClientBuilder(this)); + } + + @Override + public DatabaseClient.Builder clone() { + return new DefaultDatabaseClientBuilder(this); + } + + @Override + public DatabaseClient.Builder apply(Consumer builderConsumer) { + Assert.notNull(builderConsumer, "BuilderConsumer must not be null"); + + builderConsumer.accept(this); + return this; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java new file mode 100644 index 00000000..38d101e1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; + +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.util.Pair; +import org.springframework.util.ClassUtils; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +/** + * @author Mark Paluch + */ +public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy { + + private final EntityInstantiators instantiators = new EntityInstantiators(); + private final JdbcMappingContext mappingContext = new JdbcMappingContext(); + + @Override + public List> getInsert(Object object) { + + Class userClass = ClassUtils.getUserClass(object); + + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + + List> values = new ArrayList<>(); + + for (JdbcPersistentProperty property : entity) { + + Object value = propertyAccessor.getProperty(property); + + if (value == null) { + continue; + } + + values.add(Pair.of(property.getColumnName(), value)); + } + + return values; + } + + @Override + public BiFunction getRowMapper(Class typeToRead) { + return new EntityRowMapper((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), + instantiators, mappingContext); + } + + @Override + public String getTableName(Class type) { + return mappingContext.getRequiredPersistentEntity(type).getTableName(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java b/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java new file mode 100644 index 00000000..c78d28c7 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java @@ -0,0 +1,168 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.sql.ResultSet; +import java.util.function.BiFunction; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.ClassUtils; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +/** + * Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced. + * + * @author Mark Paluch + * @since 1.0 + */ +public class EntityRowMapper implements BiFunction { + + private final JdbcPersistentEntity entity; + private final EntityInstantiators entityInstantiators; + private final ConversionService conversions; + private final MappingContext, JdbcPersistentProperty> context; + + public EntityRowMapper(JdbcPersistentEntity entity, EntityInstantiators entityInstantiators, + JdbcMappingContext context) { + + this.entity = entity; + this.entityInstantiators = entityInstantiators; + this.conversions = context.getConversions(); + this.context = context; + } + + @Override + public T apply(Row row, RowMetadata metadata) { + + T result = createInstance(row, "", entity); + + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result), + conversions); + + for (JdbcPersistentProperty property : entity) { + + if (property.isCollectionLike()) { + throw new UnsupportedOperationException(); + } else if (property.isMap()) { + + throw new UnsupportedOperationException(); + } else { + propertyAccessor.setProperty(property, readFrom(row, property, "")); + } + } + + return result; + } + + /** + * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. + * + * @param row the {@link Row} to extract the value from. Must not be {@code null}. + * @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@code null}. + * @param prefix to be used for all column names accessed by this method. Must not be {@code null}. + * @return the value read from the {@link ResultSet}. May be {@code null}. + */ + private Object readFrom(Row row, JdbcPersistentProperty property, String prefix) { + + try { + + if (property.isEntity()) { + return readEntityFrom(row, property); + } + + return row.get(prefix + property.getColumnName(), getType(property)); + + } catch (Exception o_O) { + throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); + } + } + + private Class getType(JdbcPersistentProperty property) { + return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType()); + } + + private S readEntityFrom(Row row, PersistentProperty property) { + + String prefix = property.getName() + "_"; + + @SuppressWarnings("unchecked") + JdbcPersistentEntity entity = (JdbcPersistentEntity) context + .getRequiredPersistentEntity(property.getActualType()); + + if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) { + return null; + } + + S instance = createInstance(row, prefix, entity); + + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); + + for (JdbcPersistentProperty p : entity) { + propertyAccessor.setProperty(p, readFrom(row, p, prefix)); + } + + return instance; + } + + private S createInstance(Row row, String prefix, JdbcPersistentEntity entity) { + + return entityInstantiators.getInstantiatorFor(entity).createInstance(entity, + new RowParameterValueProvider(row, entity, conversions, prefix)); + } + + @RequiredArgsConstructor + private static class RowParameterValueProvider implements ParameterValueProvider { + + @NonNull private final Row resultSet; + @NonNull private final JdbcPersistentEntity entity; + @NonNull private final ConversionService conversionService; + @NonNull private final String prefix; + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) + */ + @Override + public T getParameterValue(Parameter parameter) { + + String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName(); + + try { + return conversionService.convert(resultSet.get(column, parameter.getType().getType()), + parameter.getType().getType()); + } catch (Exception o_O) { + throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); + } + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java b/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java new file mode 100644 index 00000000..8b6e5121 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.util.Assert; + +/** + * @author Mark Paluch + */ +class IterableUtils { + + static Collection toCollection(Iterable iterable) { + + Assert.notNull(iterable, "Iterable must not be null!"); + + if (iterable instanceof Collection) { + return (Collection) iterable; + } + + List result = new ArrayList<>(); + + for (T element : iterable) { + result.add(element); + } + + return result; + } + + static List toList(Iterable iterable) { + + Assert.notNull(iterable, "Iterable must not be null!"); + + if (iterable instanceof List) { + return (List) iterable; + } + + List result = new ArrayList<>(); + + for (T element : iterable) { + result.add(element); + } + + return result; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java new file mode 100644 index 00000000..26c9f656 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import java.util.List; +import java.util.function.BiFunction; + +import org.springframework.data.util.Pair; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +/** + * @author Mark Paluch + */ +public interface ReactiveDataAccessStrategy { + + List> getInsert(Object object); + + // TODO: Broaden T to Mono/Flux for reactive relational data access? + BiFunction getRowMapper(Class typeToRead); + + String getTableName(Class type); +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java new file mode 100644 index 00000000..70e746f8 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import java.util.function.BiFunction; + +import org.springframework.data.jdbc.core.function.DatabaseClient.FetchSpec; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +/** + * @author Mark Paluch + */ +public interface SqlResult extends FetchSpec { + + SqlResult extract(BiFunction mappingFunction); +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java new file mode 100644 index 00000000..a473df79 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function.connectionfactory; + +import io.r2dbc.spi.Connection; + +import java.sql.Wrapper; + +/** + * Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target + * Connection. + *

+ * This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. Alternatively, all + * such connections also support JDBC 4.0's {@link Connection#unwrap}. + * + * @author Mark Paluch + */ +public interface ConnectionProxy extends Connection, Wrapper { + + /** + * Return the target Connection of this proxy. + *

+ * This will typically be the native driver Connection or a wrapper from a connection pool. + * + * @return the underlying Connection (never {@code null}) + */ + Connection getTargetConnection(); +} diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java new file mode 100644 index 00000000..53b4bdc6 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java @@ -0,0 +1,155 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; +import io.r2dbc.postgresql.PostgresqlConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; +import lombok.Data; +import reactor.core.publisher.Hooks; +import reactor.test.StepVerifier; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.data.jdbc.core.function.ExternalDatabase.ProvidedDatabase; +import org.springframework.data.jdbc.core.mapping.Table; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for {@link DatabaseClient} against PostgreSQL. + * + * @author Mark Paluch + */ +public class DatabaseClientIntegrationTests { + + /** + * Local test database at {@code postgres:@localhost:5432/postgres}. + */ + @ClassRule public static final ExternalDatabase database = ProvidedDatabase.builder().hostname("localhost").port(5432) + .database("postgres").username("postgres").password("").build(); + + private ConnectionFactory connectionFactory; + + private JdbcTemplate jdbc; + + @Before + public void before() { + + Hooks.onOperatorDebug(); + + connectionFactory = new PostgresqlConnectionFactory( + PostgresqlConnectionConfiguration.builder().host(database.getHostname()).database(database.getDatabase()) + .username(database.getUsername()).password(database.getPassword()).build()); + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + + String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n" + + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n" + + " manual integer NULL\n" + ");"; + + jdbc = new JdbcTemplate(dataSource); + jdbc.execute(tableToCreate); + jdbc.execute("DELETE FROM legoset"); + } + + @Test + public void executeInsert() { + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull("$3") // + .fetch().rowsUpdated() // + .as(StepVerifier::create) // + .expectNext(1) // + .verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + + @Test + public void executeSelect() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + // TODO: Driver/Decode does not support decoding null values? + databaseClient.execute().sql("SELECT id, name, manual FROM legoset") // + .as(LegoSet.class) // + .fetch().all() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + + assertThat(actual.getId()).isEqualTo(42055); + assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER"); + assertThat(actual.getManual()).isEqualTo(12); + }).verifyComplete(); + } + + @Test + public void insert() { + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.insert().into("legoset")// + .value("id", 42055) // + .value("name", "SCHAUFELRADBAGGER") // + .nullValue("manual") // + .exchange() // + .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) // + .expectNext(42055).verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + + @Test + public void insertTypedObject() { + + LegoSet legoSet = new LegoSet(); + legoSet.setId(42055); + legoSet.setName("SCHAUFELRADBAGGER"); + legoSet.setManual(12); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.insert().into(LegoSet.class)// + .using(legoSet).exchange() // + .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) // + .expectNext(42055).verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + + @Data + @Table("legoset") + static class LegoSet { + int id; + String name; + Integer manual; + } +} diff --git a/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java b/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java new file mode 100644 index 00000000..c2e84932 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java @@ -0,0 +1,126 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import lombok.Builder; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.concurrent.TimeUnit; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.ExternalResource; + +/** + * {@link ExternalResource} wrapper to encapsulate {@link ProvidedDatabase} and + * {@link org.testcontainers.containers.PostgreSQLContainer}. + * + * @author Mark Paluch + */ +public abstract class ExternalDatabase extends ExternalResource { + + /** + * @return the post of the database service. + */ + public abstract int getPort(); + + /** + * @return hostname on which the database service runs. + */ + public abstract String getHostname(); + + /** + * @return name of the database. + */ + public abstract String getDatabase(); + + /** + * @return database user name. + */ + public abstract String getUsername(); + + @Override + protected void before() { + + try (Socket socket = new Socket()) { + ; + socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5))); + + } catch (IOException e) { + throw new AssumptionViolatedException( + String.format("Cannot connect to %s:%d. Skiping tests.", getHostname(), getPort())); + } + } + + /** + * @return password for the database user. + */ + public abstract String getPassword(); + + /** + * Provided (unmanaged resource) database connection coordinates. + */ + @Builder + static class ProvidedDatabase extends ExternalDatabase { + + private final int port; + private final String hostname; + private final String database; + private final String username; + private final String password; + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPort() + */ + @Override + public int getPort() { + return port; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname() + */ + @Override + public String getHostname() { + return hostname; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() + */ + @Override + public String getDatabase() { + return database; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getUsername() + */ + @Override + public String getUsername() { + return username; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPassword() + */ + @Override + public String getPassword() { + return password; + } + } +} From f490c1f3d95efaacb8405fd5a3e244374c48e707 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Jun 2018 10:13:27 +0200 Subject: [PATCH 003/665] #2 - Upgrade to Reactor Californium snapshots. --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 90ffd656..d5024f2b 100644 --- a/pom.xml +++ b/pom.xml @@ -37,6 +37,7 @@ 1.0.0.BUILD-SNAPSHOT 1.0.0.BUILD-SNAPSHOT 1.7.3 + Californium-BUILD-SNAPSHOT From b8375f5a96fc75092bea32b26d88eb34f232a0cc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Jun 2018 15:08:11 +0200 Subject: [PATCH 004/665] #2 - Refactor to functional usage of resources. DatabaseClient now creates functions. The actual invocation/execution takes place from DefaultFetchFunctions (FetchSpec) to keep stateful resources in the scope of the execution. execute()/executeMany() use Flux.usingWhen/Mono.usingWhen to release connections after their usage. --- .../core/function/DefaultDatabaseClient.java | 269 ++++++++++++------ .../DatabaseClientIntegrationTests.java | 19 +- 2 files changed, 204 insertions(+), 84 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index de640cac..d4786763 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -102,25 +102,75 @@ class DefaultDatabaseClient implements DatabaseClient { return new DefaultInsertIntoSpec(); } - public Flux execute(Function> action) throws DataAccessException { + /** + * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a + * {@link Flux}. The connection is released after the {@link Flux} terminates (or the subscription is cancelled). + * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get + * defunct. + * + * @param action must not be {@literal null}. + * @return the resulting {@link Flux}. + * @throws DataAccessException + */ + public Flux executeMany(Function> action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); - Mono connectionMono = Mono.from(obtainConnectionFactory().create()); + Mono connectionMono = getConnection(); // Create close-suppressing Connection proxy, also preparing returned Statements. - return connectionMono.flatMapMany(connection -> { + return Flux.usingWhen(connectionMono, it -> { - Connection connectionToUse = createConnectionProxy(connection); + Connection connectionToUse = createConnectionProxy(it); - // TODO: Release connection - return doInConnection(action, connectionToUse); + return doInConnectionMany(connectionToUse, action); + }, this::closeConnection, this::closeConnection, this::closeConnection) // + .onErrorMap(SQLException.class, ex -> translateException("executeMany", getSql(action), ex)); + } - }).onErrorMap(SQLException.class, ex -> { + /** + * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a + * {@link Mono}. The connection is released after the {@link Mono} terminates (or the subscription is cancelled). + * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get + * defunct. + * + * @param action must not be {@literal null}. + * @return the resulting {@link Mono}. + * @throws DataAccessException + */ + public Mono execute(Function> action) throws DataAccessException { - String sql = getSql(action); - return translateException("ConnectionCallback", sql, ex); - }); + Assert.notNull(action, "Callback object must not be null"); + + Mono connectionMono = getConnection(); + // Create close-suppressing Connection proxy, also preparing returned Statements. + + return Mono.usingWhen(connectionMono, it -> { + + Connection connectionToUse = createConnectionProxy(it); + + return doInConnection(connectionToUse, action); + }, this::closeConnection, this::closeConnection, this::closeConnection) // + .onErrorMap(SQLException.class, ex -> translateException("execute", getSql(action), ex)); + } + + /** + * Obtain a {@link Connection}. + * + * @return + */ + protected Mono getConnection() { + return Mono.from(obtainConnectionFactory().create()); + } + + /** + * Release the {@link Connection}. + * + * @param connection + * @return + */ + protected Publisher closeConnection(Connection connection) { + return connection.close(); } /** @@ -153,11 +203,12 @@ class DefaultDatabaseClient implements DatabaseClient { * @return a DataAccessException wrapping the {@code SQLException} (never {@code null}) */ protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) { + DataAccessException dae = exceptionTranslator.translate(task, sql, ex); return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex)); } - static void doBind(Statement statement, Map> byName, + private static void doBind(Statement statement, Map> byName, Map> byIndex) { byIndex.forEach((i, o) -> { @@ -225,16 +276,26 @@ class DefaultDatabaseClient implements DatabaseClient { return sql; } - Mono> exchange(String sql, BiFunction mappingFunction) { + protected SqlResult exchange(String sql, BiFunction mappingFunction) { - return execute(it -> { + Function executeFunction = it -> { + + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } Statement statement = it.createStatement(sql); doBind(statement, byName, byIndex); - return Flux - .just((SqlResult) new DefaultSqlResult<>(sql, Flux.from(statement.add().execute()), mappingFunction)); - }).next(); + return statement; + }; + + Function> resultFunction = it -> Flux.from(executeFunction.apply(it).execute()); + + return new DefaultSqlResultFunctions<>(sql, // + resultFunction, // + it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // + mappingFunction); } public GenericExecuteSpecSupport bind(int index, Object value) { @@ -310,15 +371,12 @@ class DefaultDatabaseClient implements DatabaseClient { @Override public FetchSpec> fetch() { - - String sql = getSql(); - return new DefaultFetchSpec<>(sql, exchange(sql, ColumnMapRowMapper.INSTANCE).flatMapMany(SqlResult::all), - exchange(sql, ColumnMapRowMapper.INSTANCE).flatMap(FetchSpec::rowsUpdated)); + return exchange(getSql(), ColumnMapRowMapper.INSTANCE); } @Override public Mono>> exchange() { - return exchange(getSql(), ColumnMapRowMapper.INSTANCE); + return Mono.just(exchange(getSql(), ColumnMapRowMapper.INSTANCE)); } @Override @@ -381,14 +439,12 @@ class DefaultDatabaseClient implements DatabaseClient { @Override public FetchSpec fetch() { - String sql = getSql(); - return new DefaultFetchSpec<>(sql, exchange(sql, mappingFunction).flatMapMany(SqlResult::all), - exchange(sql, mappingFunction).flatMap(FetchSpec::rowsUpdated)); + return exchange(getSql(), mappingFunction); } @Override public Mono> exchange() { - return exchange(getSql(), mappingFunction); + return Mono.just(exchange(getSql(), mappingFunction)); } @Override @@ -472,11 +528,15 @@ class DefaultDatabaseClient implements DatabaseClient { @Override public Mono then() { - return exchange().flatMapMany(FetchSpec::all).then(); + return exchange((row, md) -> row).all().then(); } @Override public Mono>> exchange() { + return Mono.just(exchange(ColumnMapRowMapper.INSTANCE)); + } + + private SqlResult exchange(BiFunction mappingFunction) { if (byName.isEmpty()) { throw new IllegalStateException("Insert fields is empty!"); @@ -490,26 +550,43 @@ class DefaultDatabaseClient implements DatabaseClient { builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") .append(placeholders).append(")"); - return execute(it -> { + String sql = builder.toString(); + Function insertFunction = it -> { - String sql = builder.toString(); - Statement statement = it.createStatement(sql); - - AtomicInteger index = new AtomicInteger(); - for (Optional o : byName.values()) { - - if (o.isPresent()) { - o.ifPresent(v -> statement.bind(index.getAndIncrement(), v)); - } else { - statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? - } + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); } + Statement statement = it.createStatement(sql); + doBind(statement); + return statement; + }; - SqlResult> result = new DefaultSqlResult<>(sql, - Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE); - return Flux.just(result); + Function> resultFunction = it -> Flux + .from(insertFunction.apply(it).executeReturningGeneratedKeys()); - }).next(); + return new DefaultSqlResultFunctions<>(sql, // + resultFunction, // + it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // + mappingFunction); + } + + /** + * PostgreSQL-specific bind. + * + * @param statement + */ + private void doBind(Statement statement) { + + AtomicInteger index = new AtomicInteger(); + + for (Optional o : byName.values()) { + + if (o.isPresent()) { + o.ifPresent(v -> statement.bind(index.getAndIncrement(), v)); + } else { + statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? + } + } } } @@ -523,7 +600,7 @@ class DefaultDatabaseClient implements DatabaseClient { private final String table; private final Publisher objectToInsert; - public DefaultTypedInsertSpec(Class typeToInsert) { + DefaultTypedInsertSpec(Class typeToInsert) { this.typeToInsert = typeToInsert; this.table = dataAccessStrategy.getTableName(typeToInsert); @@ -556,69 +633,84 @@ class DefaultDatabaseClient implements DatabaseClient { @Override public Mono then() { - return exchange().flatMapMany(FetchSpec::all).then(); + return Mono.from(objectToInsert).map(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); } @Override public Mono>> exchange() { + return Mono.from(objectToInsert).map(toInsert -> exchange(toInsert, ColumnMapRowMapper.INSTANCE)); + } - return Mono.from(objectToInsert).flatMap(toInsert -> { + private SqlResult exchange(Object toInsert, BiFunction mappingFunction) { - StringBuilder builder = new StringBuilder(); + StringBuilder builder = new StringBuilder(); - List> insertValues = dataAccessStrategy.getInsert(toInsert); - String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(",")); - String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1)) - .collect(Collectors.joining(",")); + List> insertValues = dataAccessStrategy.getInsert(toInsert); + String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(",")); + String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1)) + .collect(Collectors.joining(",")); - builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") - .append(placeholders).append(")"); + builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") + .append(placeholders).append(")"); - return execute(it -> { + String sql = builder.toString(); - String sql = builder.toString(); - Statement statement = it.createStatement(sql); + Function insertFunction = it -> { - AtomicInteger index = new AtomicInteger(); + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } - for (Pair pair : insertValues) { + Statement statement = it.createStatement(sql); - if (pair.getSecond() != null) { // TODO: Better type to transport null values. - statement.bind(index.getAndIncrement(), pair.getSecond()); - } else { - statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? - } + AtomicInteger index = new AtomicInteger(); + + for (Pair pair : insertValues) { + + if (pair.getSecond() != null) { // TODO: Better type to transport null values. + statement.bind(index.getAndIncrement(), pair.getSecond()); + } else { + statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? } + } - SqlResult> result = new DefaultSqlResult<>(sql, - Flux.from(statement.executeReturningGeneratedKeys()), ColumnMapRowMapper.INSTANCE); - return Flux.just(result); + return statement; + }; - }).next(); - }); + Function> resultFunction = it -> Flux + .from(insertFunction.apply(it).executeReturningGeneratedKeys()); + + return new DefaultSqlResultFunctions<>(sql, // + resultFunction, // + it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // + mappingFunction); } } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlResult} implementation. + * Default {@link org.springframework.data.jdbc.core.function.SqlResult} implementation. */ - static class DefaultSqlResult implements SqlResult { + class DefaultSqlResultFunctions implements SqlResult { private final String sql; - private final Flux result; + private final Function> resultFunction; + private final Function> updatedRowsFunction; private final FetchSpec fetchSpec; - DefaultSqlResult(String sql, Flux result, BiFunction mappingFunction) { + DefaultSqlResultFunctions(String sql, Function> resultFunction, + Function> updatedRowsFunction, BiFunction mappingFunction) { this.sql = sql; - this.result = result; - this.fetchSpec = new DefaultFetchSpec<>(sql, result.flatMap(it -> it.map(mappingFunction)), - result.flatMap(Result::getRowsUpdated).next()); + this.resultFunction = resultFunction; + this.updatedRowsFunction = updatedRowsFunction; + + this.fetchSpec = new DefaultFetchFunctions<>(sql, + it -> resultFunction.apply(it).flatMap(result -> result.map(mappingFunction)), updatedRowsFunction); } @Override public SqlResult extract(BiFunction mappingFunction) { - return new DefaultSqlResult<>(sql, result, mappingFunction); + return new DefaultSqlResultFunctions<>(sql, resultFunction, updatedRowsFunction, mappingFunction); } @Override @@ -643,11 +735,11 @@ class DefaultDatabaseClient implements DatabaseClient { } @RequiredArgsConstructor - static class DefaultFetchSpec implements FetchSpec { + class DefaultFetchFunctions implements FetchSpec { private final String sql; - private final Flux result; - private final Mono updatedRows; + private final Function> resultFunction; + private final Function> updatedRowsFunction; @Override public Mono one() { @@ -675,19 +767,19 @@ class DefaultDatabaseClient implements DatabaseClient { @Override public Flux all() { - return result; + return executeMany(resultFunction); } @Override public Mono rowsUpdated() { - return updatedRows; + return execute(updatedRowsFunction); } } - private static Flux doInConnection(Function> action, Connection it) { + private static Flux doInConnectionMany(Connection connection, Function> action) { try { - return action.apply(it); + return action.apply(connection); } catch (RuntimeException e) { String sql = getSql(action); @@ -695,6 +787,17 @@ class DefaultDatabaseClient implements DatabaseClient { } } + private static Mono doInConnection(Connection connection, Function> action) { + + try { + return action.apply(connection); + } catch (RuntimeException e) { + + String sql = getSql(action); + return Mono.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {}); + } + } + /** * Determine SQL from potential provider object. * @@ -764,7 +867,7 @@ class DefaultDatabaseClient implements DatabaseClient { } } - private static class UncategorizedSQLException extends UncategorizedDataAccessException { + private static class UncategorizedSQLException extends UncategorizedDataAccessException implements SqlProvider { /** SQL that led to the problem */ @Nullable private final String sql; diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java index 53b4bdc6..ab8a49d7 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java @@ -121,12 +121,29 @@ public class DatabaseClientIntegrationTests { .value("name", "SCHAUFELRADBAGGER") // .nullValue("manual") // .exchange() // - .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()).as(StepVerifier::create) // + .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) // + .as(StepVerifier::create) // .expectNext(42055).verifyComplete(); assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } + @Test + public void insertWithoutResult() { + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.insert().into("legoset")// + .value("id", 42055) // + .value("name", "SCHAUFELRADBAGGER") // + .nullValue("manual") // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + @Test public void insertTypedObject() { From ddc587e898bf592f9f92db83069e2f052fadc6c9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Jun 2018 15:21:27 +0200 Subject: [PATCH 005/665] #2 - Introduce ConnectionAccessor. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Declare interface exposing inConnection(…) methods to encapsulate types implementing functionality that is applies within a connection scope. Move FetchSpec and SqlResult implementations to top-level types. --- .../core/function/ConnectionAccessor.java | 62 +++++++ .../jdbc/core/function/DatabaseClient.java | 44 +---- .../core/function/DefaultDatabaseClient.java | 157 ++++-------------- .../jdbc/core/function/DefaultFetchSpec.java | 85 ++++++++++ .../jdbc/core/function/DefaultSqlResult.java | 92 ++++++++++ .../data/jdbc/core/function/FetchSpec.java | 56 +++++++ .../data/jdbc/core/function/SqlResult.java | 15 +- .../connectionfactory/ConnectionProxy.java | 7 +- 8 files changed, 348 insertions(+), 170 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java b/src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java new file mode 100644 index 00000000..aad998a6 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.Connection; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +import org.springframework.dao.DataAccessException; + +/** + * Interface declaring methods that accept callback {@link Function} to operate within the scope of a + * {@link Connection}. Callback functions operate on a provided connection and must not close the connection as the + * connections may be pooled or be subject to other kinds of resource management. + *

+ * Callback functions are responsible for creating a {@link org.reactivestreams.Publisher} that defines the scope of how + * long the allocated {@link Connection} is valid. Connections are released after the publisher terminates. + * + * @author Mark Paluch + */ +public interface ConnectionAccessor { + + /** + * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a + * {@link Mono}. The connection is released after the {@link Mono} terminates (or the subscription is cancelled). + * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get + * defunct. + * + * @param action must not be {@literal null}. + * @return the resulting {@link Mono}. + * @throws DataAccessException + */ + Mono inConnection(Function> action) throws DataAccessException; + + /** + * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a + * {@link Flux}. The connection is released after the {@link Flux} terminates (or the subscription is cancelled). + * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get + * defunct. + * + * @param action must not be {@literal null}. + * @return the resulting {@link Flux}. + * @throws DataAccessException + */ + Flux inConnectionMany(Function> action) throws DataAccessException; + +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java index 59265925..a17ad27f 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java @@ -15,7 +15,9 @@ */ package org.springframework.data.jdbc.core.function; -import reactor.core.publisher.Flux; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; import reactor.core.publisher.Mono; import java.util.Map; @@ -28,10 +30,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.jdbc.support.SQLExceptionTranslator; -import io.r2dbc.spi.ConnectionFactory; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides * a higher level, common API over R2DBC client libraries. @@ -412,40 +410,4 @@ public interface DatabaseClient { */ S bind(Object bean); } - - /** - * Contract for fetching results. - */ - interface FetchSpec { - - /** - * Get exactly zero or one result. - * - * @return {@link Mono#empty()} if no match found. Never {@literal null}. - * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. - */ - Mono one(); - - /** - * Get the first or no result. - * - * @return {@link Mono#empty()} if no match found. Never {@literal null}. - */ - Mono first(); - - /** - * Get all matching elements. - * - * @return never {@literal null}. - */ - Flux all(); - - /** - * Get the number of updated rows. - * - * @return {@link Mono} emitting the number of updated rows. Never {@literal null}. - */ - Mono rowsUpdated(); - } - } diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index d4786763..e2547354 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -46,7 +46,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.dao.DataAccessException; -import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.dao.UncategorizedDataAccessException; import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy; import org.springframework.data.util.Pair; @@ -60,7 +59,7 @@ import org.springframework.util.Assert; * * @author Mark Paluch */ -class DefaultDatabaseClient implements DatabaseClient { +class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** Logger available to subclasses */ protected final Log logger = LogFactory.getLog(getClass()); @@ -102,32 +101,6 @@ class DefaultDatabaseClient implements DatabaseClient { return new DefaultInsertIntoSpec(); } - /** - * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a - * {@link Flux}. The connection is released after the {@link Flux} terminates (or the subscription is cancelled). - * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get - * defunct. - * - * @param action must not be {@literal null}. - * @return the resulting {@link Flux}. - * @throws DataAccessException - */ - public Flux executeMany(Function> action) throws DataAccessException { - - Assert.notNull(action, "Callback object must not be null"); - - Mono connectionMono = getConnection(); - // Create close-suppressing Connection proxy, also preparing returned Statements. - - return Flux.usingWhen(connectionMono, it -> { - - Connection connectionToUse = createConnectionProxy(it); - - return doInConnectionMany(connectionToUse, action); - }, this::closeConnection, this::closeConnection, this::closeConnection) // - .onErrorMap(SQLException.class, ex -> translateException("executeMany", getSql(action), ex)); - } - /** * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a * {@link Mono}. The connection is released after the {@link Mono} terminates (or the subscription is cancelled). @@ -138,7 +111,8 @@ class DefaultDatabaseClient implements DatabaseClient { * @return the resulting {@link Mono}. * @throws DataAccessException */ - public Mono execute(Function> action) throws DataAccessException { + @Override + public Mono inConnection(Function> action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); @@ -154,6 +128,33 @@ class DefaultDatabaseClient implements DatabaseClient { .onErrorMap(SQLException.class, ex -> translateException("execute", getSql(action), ex)); } + /** + * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a + * {@link Flux}. The connection is released after the {@link Flux} terminates (or the subscription is cancelled). + * Connection resources must not be passed outside of the {@link Function} closure, otherwise resources may get + * defunct. + * + * @param action must not be {@literal null}. + * @return the resulting {@link Flux}. + * @throws DataAccessException + */ + @Override + public Flux inConnectionMany(Function> action) throws DataAccessException { + + Assert.notNull(action, "Callback object must not be null"); + + Mono connectionMono = getConnection(); + // Create close-suppressing Connection proxy, also preparing returned Statements. + + return Flux.usingWhen(connectionMono, it -> { + + Connection connectionToUse = createConnectionProxy(it); + + return doInConnectionMany(connectionToUse, action); + }, this::closeConnection, this::closeConnection, this::closeConnection) // + .onErrorMap(SQLException.class, ex -> translateException("executeMany", getSql(action), ex)); + } + /** * Obtain a {@link Connection}. * @@ -292,7 +293,8 @@ class DefaultDatabaseClient implements DatabaseClient { Function> resultFunction = it -> Flux.from(executeFunction.apply(it).execute()); - return new DefaultSqlResultFunctions<>(sql, // + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + sql, // resultFunction, // it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // mappingFunction); @@ -564,7 +566,8 @@ class DefaultDatabaseClient implements DatabaseClient { Function> resultFunction = it -> Flux .from(insertFunction.apply(it).executeReturningGeneratedKeys()); - return new DefaultSqlResultFunctions<>(sql, // + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + sql, // resultFunction, // it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // mappingFunction); @@ -680,102 +683,14 @@ class DefaultDatabaseClient implements DatabaseClient { Function> resultFunction = it -> Flux .from(insertFunction.apply(it).executeReturningGeneratedKeys()); - return new DefaultSqlResultFunctions<>(sql, // + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + sql, // resultFunction, // it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // mappingFunction); } } - /** - * Default {@link org.springframework.data.jdbc.core.function.SqlResult} implementation. - */ - class DefaultSqlResultFunctions implements SqlResult { - - private final String sql; - private final Function> resultFunction; - private final Function> updatedRowsFunction; - private final FetchSpec fetchSpec; - - DefaultSqlResultFunctions(String sql, Function> resultFunction, - Function> updatedRowsFunction, BiFunction mappingFunction) { - - this.sql = sql; - this.resultFunction = resultFunction; - this.updatedRowsFunction = updatedRowsFunction; - - this.fetchSpec = new DefaultFetchFunctions<>(sql, - it -> resultFunction.apply(it).flatMap(result -> result.map(mappingFunction)), updatedRowsFunction); - } - - @Override - public SqlResult extract(BiFunction mappingFunction) { - return new DefaultSqlResultFunctions<>(sql, resultFunction, updatedRowsFunction, mappingFunction); - } - - @Override - public Mono one() { - return fetchSpec.one(); - } - - @Override - public Mono first() { - return fetchSpec.first(); - } - - @Override - public Flux all() { - return fetchSpec.all(); - } - - @Override - public Mono rowsUpdated() { - return fetchSpec.rowsUpdated(); - } - } - - @RequiredArgsConstructor - class DefaultFetchFunctions implements FetchSpec { - - private final String sql; - private final Function> resultFunction; - private final Function> updatedRowsFunction; - - @Override - public Mono one() { - - return all().buffer(2) // - .flatMap(it -> { - - if (it.isEmpty()) { - return Mono.empty(); - } - - if (it.size() > 1) { - return Mono.error(new IncorrectResultSizeDataAccessException( - String.format("Query [%s] returned non unique result.", this.sql), 1)); - } - - return Mono.just(it.get(0)); - }).next(); - } - - @Override - public Mono first() { - return all().next(); - } - - @Override - public Flux all() { - return executeMany(resultFunction); - } - - @Override - public Mono rowsUpdated() { - return execute(updatedRowsFunction); - } - } - private static Flux doInConnectionMany(Connection connection, Function> action) { try { diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java new file mode 100644 index 00000000..5db2a685 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.Connection; +import lombok.RequiredArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +/** + * Default implementation of {@link FetchSpec}. + * + * @author Mark Paluch + */ +@RequiredArgsConstructor +class DefaultFetchSpec implements FetchSpec { + + private final ConnectionAccessor connectionAccessor; + private final String sql; + private final Function> resultFunction; + private final Function> updatedRowsFunction; + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#one() + */ + @Override + public Mono one() { + + return all().buffer(2) // + .flatMap(it -> { + + if (it.isEmpty()) { + return Mono.empty(); + } + + if (it.size() > 1) { + return Mono.error(new IncorrectResultSizeDataAccessException( + String.format("Query [%s] returned non unique result.", this.sql), 1)); + } + + return Mono.just(it.get(0)); + }).next(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#first() + */ + @Override + public Mono first() { + return all().next(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#all() + */ + @Override + public Flux all() { + return connectionAccessor.inConnectionMany(resultFunction); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#rowsUpdated() + */ + @Override + public Mono rowsUpdated() { + return connectionAccessor.inConnection(updatedRowsFunction); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java new file mode 100644 index 00000000..3cafbb35 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.Result; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Default {@link SqlResult} implementation. + * + * @author Mark Paluch + */ +class DefaultSqlResult implements SqlResult { + + private final ConnectionAccessor connectionAccessor; + private final String sql; + private final Function> resultFunction; + private final Function> updatedRowsFunction; + private final FetchSpec fetchSpec; + + DefaultSqlResult(ConnectionAccessor connectionAccessor, String sql, Function> resultFunction, + Function> updatedRowsFunction, BiFunction mappingFunction) { + + this.sql = sql; + this.connectionAccessor = connectionAccessor; + this.resultFunction = resultFunction; + this.updatedRowsFunction = updatedRowsFunction; + + this.fetchSpec = new DefaultFetchSpec<>(connectionAccessor, sql, + it -> resultFunction.apply(it).flatMap(result -> result.map(mappingFunction)), updatedRowsFunction); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.SqlResult#extract(java.util.function.BiFunction) + */ + @Override + public SqlResult extract(BiFunction mappingFunction) { + return new DefaultSqlResult<>(connectionAccessor, sql, resultFunction, updatedRowsFunction, mappingFunction); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#one() + */ + @Override + public Mono one() { + return fetchSpec.one(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#first() + */ + @Override + public Mono first() { + return fetchSpec.first(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#all() + */ + @Override + public Flux all() { + return fetchSpec.all(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.FetchSpec#rowsUpdated() + */ + @Override + public Mono rowsUpdated() { + return fetchSpec.rowsUpdated(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java b/src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java new file mode 100644 index 00000000..74fca957 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java @@ -0,0 +1,56 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Contract for fetching results. + * + * @author Mark Paluch + */ +public interface FetchSpec { + + /** + * Get exactly zero or one result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Mono one(); + + /** + * Get the first or no result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + */ + Mono first(); + + /** + * Get all matching elements. + * + * @return never {@literal null}. + */ + Flux all(); + + /** + * Get the number of updated rows. + * + * @return {@link Mono} emitting the number of updated rows. Never {@literal null}. + */ + Mono rowsUpdated(); +} diff --git a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java index 70e746f8..b6d3d666 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java @@ -15,17 +15,24 @@ */ package org.springframework.data.jdbc.core.function; -import java.util.function.BiFunction; - -import org.springframework.data.jdbc.core.function.DatabaseClient.FetchSpec; - import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; +import java.util.function.BiFunction; + /** + * Mappable {@link FetchSpec} that accepts a {@link BiFunction mapping function} to map SQL {@link Row}s. + * * @author Mark Paluch */ public interface SqlResult extends FetchSpec { + /** + * Apply a {@link BiFunction mapping function} to the result that emits {@link Row}s. + * + * @param mappingFunction must not be {@literal null}. + * @param + * @return a new {@link SqlResult} with {@link BiFunction mapping function} applied. + */ SqlResult extract(BiFunction mappingFunction); } diff --git a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java index a473df79..6f5891f0 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java @@ -22,9 +22,8 @@ import java.sql.Wrapper; /** * Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target * Connection. - *

- * This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. Alternatively, all - * such connections also support JDBC 4.0's {@link Connection#unwrap}. + *

+ * This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. * * @author Mark Paluch */ @@ -32,7 +31,7 @@ public interface ConnectionProxy extends Connection, Wrapper { /** * Return the target Connection of this proxy. - *

+ *

* This will typically be the native driver Connection or a wrapper from a connection pool. * * @return the underlying Connection (never {@code null}) From 1d5bb962fd76f03b9bdb6d7ba60ff602d186d6e4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Jun 2018 15:28:04 +0200 Subject: [PATCH 006/665] #2 - Polishing. --- .../data/jdbc/core/function/DatabaseClient.java | 8 +++++++- .../data/jdbc/core/function/DefaultDatabaseClient.java | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java index a17ad27f..cf8172bf 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java @@ -71,7 +71,7 @@ public interface DatabaseClient { } /** - * Obtain a {@code WebClient} builder. + * Obtain a {@code DatabaseClient} builder. */ static DatabaseClient.Builder builder() { return new DefaultDatabaseClientBuilder(); @@ -219,6 +219,12 @@ public interface DatabaseClient { */ GenericInsertSpec into(String table); + /** + * Specify the target {@link Class} table to insert to using the {@link Class entity class}. + * + * @param table must not be {@literal null}. + * @return + */ TypedInsertSpec into(Class table); } diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index e2547354..320684b4 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -62,7 +62,7 @@ import org.springframework.util.Assert; class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** Logger available to subclasses */ - protected final Log logger = LogFactory.getLog(getClass()); + private final Log logger = LogFactory.getLog(getClass()); private final ConnectionFactory connector; @@ -72,7 +72,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final DefaultDatabaseClientBuilder builder; - public DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator, + DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator, ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { this.connector = connector; From 4d5d310741f7e8486e1582b2f177ffd1642bef0a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Jun 2018 16:37:14 +0200 Subject: [PATCH 007/665] #2 - Add DatabaseClient.select(). We now allow typesafe and generic selection of rows. --- .../jdbc/core/function/DatabaseClient.java | 30 +- .../core/function/DefaultDatabaseClient.java | 301 +++++++++++++++++- .../DefaultReactiveDataAccessStrategy.java | 49 ++- .../function/ReactiveDataAccessStrategy.java | 11 +- .../DatabaseClientIntegrationTests.java | 77 +++++ 5 files changed, 458 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java index cf8172bf..e94f36ef 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java @@ -201,8 +201,20 @@ public interface DatabaseClient { */ interface SelectFromSpec { + /** + * Specify the source {@literal table} to select from. + * + * @param table must not be {@literal null} or empty. + * @return + */ GenericSelectSpec from(String table); + /** + * Specify the source table to select from to using the {@link Class entity class}. + * + * @param table must not be {@literal null}. + * @return + */ TypedSelectSpec from(Class table); } @@ -220,7 +232,7 @@ public interface DatabaseClient { GenericInsertSpec into(String table); /** - * Specify the target {@link Class} table to insert to using the {@link Class entity class}. + * Specify the target table to insert to using the {@link Class entity class}. * * @param table must not be {@literal null}. * @return @@ -296,12 +308,26 @@ public interface DatabaseClient { */ interface SelectSpec> { + /** + * Configure projected fields. + * + * @param selectedFields must not be {@literal null}. + */ S project(String... selectedFields); - S where(Object criteriaDefinition); + /** + * Configure {@link Sort}. + * + * @param sort must not be {@literal null}. + */ S orderBy(Sort sort); + /** + * Configure pagination. Overrides {@link Sort} if the {@link Pageable} contains a {@link Sort} object. + * + * @param page must not be {@literal null}. + */ S page(Pageable page); } diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index 320684b4..5066cb26 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -30,6 +30,8 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -47,12 +49,17 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.dao.DataAccessException; import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.NullHandling; +import org.springframework.data.domain.Sort.Order; import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy; import org.springframework.data.util.Pair; import org.springframework.jdbc.core.SqlProvider; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Default implementation of {@link DatabaseClient}. @@ -93,7 +100,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public SelectFromSpec select() { - throw new UnsupportedOperationException("Implement me"); + return new DefaultSelectFromSpec(); } @Override @@ -254,7 +261,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} + * implementations. */ @RequiredArgsConstructor private class GenericExecuteSpecSupport { @@ -481,6 +489,295 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } } + /** + * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SelectFromSpec} implementation. + */ + class DefaultSelectFromSpec implements SelectFromSpec { + + @Override + public GenericSelectSpec from(String table) { + return new DefaultGenericSelectSpec(table); + } + + @Override + public TypedSelectSpec from(Class table) { + return new DefaultTypedSelectSpec<>(table); + } + } + + /** + * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} + * implementations. + */ + @RequiredArgsConstructor + private abstract class DefaultSelectSpecSupport { + + final String table; + final List projectedFields; + final Sort sort; + final Pageable page; + + DefaultSelectSpecSupport(String table) { + + Assert.hasText(table, "Table name must not be null!"); + + this.table = table; + this.projectedFields = Collections.emptyList(); + this.sort = Sort.unsorted(); + this.page = Pageable.unpaged(); + } + + public DefaultSelectSpecSupport project(String... selectedFields) { + Assert.notNull(selectedFields, "Projection fields must not be null!"); + + List projectedFields = new ArrayList<>(this.projectedFields.size() + selectedFields.length); + projectedFields.addAll(this.projectedFields); + projectedFields.addAll(Arrays.asList(selectedFields)); + + return createInstance(table, projectedFields, sort, page); + } + + public DefaultSelectSpecSupport orderBy(Sort sort) { + + Assert.notNull(sort, "Sort must not be null!"); + + return createInstance(table, projectedFields, sort, page); + } + + public DefaultSelectSpecSupport page(Pageable page) { + + Assert.notNull(page, "Pageable must not be null!"); + + return createInstance(table, projectedFields, sort, page); + } + + StringBuilder getLimitOffset(Pageable pageable) { + return new StringBuilder().append("LIMIT").append(' ').append(page.getPageSize()) // + .append(' ').append("OFFSET").append(' ').append(page.getOffset()); + } + + StringBuilder getSortClause(Sort sort) { + + StringBuilder sortClause = new StringBuilder(); + + for (Order order : sort) { + + if (sortClause.length() != 0) { + sortClause.append(',').append(' '); + } + + sortClause.append(order.getProperty()).append(' ').append(order.getDirection().isAscending() ? "ASC" : "DESC"); + + if (order.getNullHandling() == NullHandling.NULLS_FIRST) { + sortClause.append(' ').append("NULLS FIRST"); + } + + if (order.getNullHandling() == NullHandling.NULLS_LAST) { + sortClause.append(' ').append("NULLS LAST"); + } + } + return sortClause; + } + + SqlResult execute(String sql, BiFunction mappingFunction) { + + Function selectFunction = it -> { + + if (logger.isDebugEnabled()) { + logger.debug("Executing SQL statement [" + sql + "]"); + } + + return it.createStatement(sql); + }; + + Function> resultFunction = it -> Flux.from(selectFunction.apply(it).execute()); + + return new DefaultSqlResult<>(DefaultDatabaseClient.this, // + sql, // + resultFunction, // + it -> Mono.error(new UnsupportedOperationException("Not available for SELECT")), // + mappingFunction); + } + + protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, Sort sort, + Pageable page); + } + + private class DefaultGenericSelectSpec extends DefaultSelectSpecSupport implements GenericSelectSpec { + + public DefaultGenericSelectSpec(String table, List projectedFields, Sort sort, Pageable page) { + super(table, projectedFields, sort, page); + } + + DefaultGenericSelectSpec(String table) { + super(table); + } + + @Override + public TypedSelectSpec as(Class resultType) { + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, resultType, + dataAccessStrategy.getRowMapper(resultType)); + } + + @Override + public DefaultGenericSelectSpec project(String... selectedFields) { + return (DefaultGenericSelectSpec) super.project(selectedFields); + } + + @Override + public DefaultGenericSelectSpec orderBy(Sort sort) { + return (DefaultGenericSelectSpec) super.orderBy(sort); + } + + @Override + public DefaultGenericSelectSpec page(Pageable page) { + return (DefaultGenericSelectSpec) super.page(page); + } + + @Override + public FetchSpec> fetch() { + return exchange(ColumnMapRowMapper.INSTANCE); + } + + @Override + public Mono>> exchange() { + return Mono.just(exchange(ColumnMapRowMapper.INSTANCE)); + } + + private SqlResult exchange(BiFunction mappingFunction) { + + List projectedFields; + + if (this.projectedFields.isEmpty()) { + projectedFields = Collections.singletonList("*"); + } else { + projectedFields = this.projectedFields; + } + + StringBuilder selectBuilder = new StringBuilder(); + selectBuilder.append("SELECT").append(' ') // + .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') // + .append("FROM").append(' ').append(table); + + if (sort.isSorted()) { + selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(sort)); + } + + if (page.isPaged()) { + selectBuilder.append(' ').append(getLimitOffset(page)); + } + + return execute(selectBuilder.toString(), mappingFunction); + } + + @Override + protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Sort sort, + Pageable page) { + return new DefaultGenericSelectSpec(table, projectedFields, sort, page); + } + } + + /** + * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}. + */ + @SuppressWarnings("unchecked") + private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { + + private final Class typeToRead; + private final BiFunction mappingFunction; + + DefaultTypedSelectSpec(Class typeToRead) { + + super(dataAccessStrategy.getTableName(typeToRead)); + + this.typeToRead = typeToRead; + this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); + } + + DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, + BiFunction mappingFunction) { + super(table, projectedFields, sort, page); + this.typeToRead = typeToRead; + this.mappingFunction = mappingFunction; + } + + @Override + public TypedSelectSpec as(Class resultType) { + + Assert.notNull(resultType, "Result type must not be null!"); + + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, + dataAccessStrategy.getRowMapper(resultType)); + } + + @Override + public TypedSelectSpec extract(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + } + + @Override + public DefaultTypedSelectSpec project(String... selectedFields) { + return (DefaultTypedSelectSpec) super.project(selectedFields); + } + + @Override + public DefaultTypedSelectSpec orderBy(Sort sort) { + return (DefaultTypedSelectSpec) super.orderBy(sort); + } + + @Override + public DefaultTypedSelectSpec page(Pageable page) { + return (DefaultTypedSelectSpec) super.page(page); + } + + @Override + public FetchSpec fetch() { + return exchange(mappingFunction); + } + + @Override + public Mono> exchange() { + return Mono.just(exchange(mappingFunction)); + } + + private SqlResult exchange(BiFunction mappingFunction) { + + List projectedFields; + + if (this.projectedFields.isEmpty()) { + projectedFields = dataAccessStrategy.getAllFields(typeToRead); + } else { + projectedFields = this.projectedFields; + } + + StringBuilder selectBuilder = new StringBuilder(); + selectBuilder.append("SELECT").append(' ') // + .append(StringUtils.collectionToDelimitedString(projectedFields, ", ")).append(' ') // + .append("FROM").append(' ').append(table); + + if (sort.isSorted()) { + + Sort mappedSort = dataAccessStrategy.getMappedSort(typeToRead, sort); + selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(mappedSort)); + } + + if (page.isPaged()) { + selectBuilder.append(' ').append(getLimitOffset(page)); + } + + return execute(selectBuilder.toString(), mappingFunction); + } + + @Override + protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Sort sort, + Pageable page) { + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + } + } + /** * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation. */ diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java index 38d101e1..e30f042d 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java @@ -15,21 +15,26 @@ */ package org.springframework.data.jdbc.core.function; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Order; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.util.Pair; +import org.springframework.data.util.StreamUtils; import org.springframework.util.ClassUtils; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * @author Mark Paluch */ @@ -38,6 +43,20 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final EntityInstantiators instantiators = new EntityInstantiators(); private final JdbcMappingContext mappingContext = new JdbcMappingContext(); + @Override + public List getAllFields(Class typeToRead) { + + JdbcPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeToRead); + + if (persistentEntity == null) { + return Collections.singletonList("*"); + } + + return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) // + .map(JdbcPersistentProperty::getColumnName) // + .collect(Collectors.toList()); + } + @Override public List> getInsert(Object object) { @@ -62,6 +81,30 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return values; } + @Override + public Sort getMappedSort(Class typeToRead, Sort sort) { + + JdbcPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); + if (entity == null) { + return sort; + } + + List mappedOrder = new ArrayList<>(); + + for (Order order : sort) { + + JdbcPersistentProperty 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); + } + @Override public BiFunction getRowMapper(Class typeToRead) { return new EntityRowMapper((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java index 26c9f656..d2c996ce 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java @@ -15,21 +15,26 @@ */ package org.springframework.data.jdbc.core.function; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + import java.util.List; import java.util.function.BiFunction; +import org.springframework.data.domain.Sort; import org.springframework.data.util.Pair; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * @author Mark Paluch */ public interface ReactiveDataAccessStrategy { + List getAllFields(Class typeToRead); + List> getInsert(Object object); + Sort getMappedSort(Class typeToRead, Sort sort); + // TODO: Broaden T to Mono/Flux for reactive relational data access? BiFunction getRowMapper(Class typeToRead); diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java index ab8a49d7..9acba55c 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.core.function; import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Order.*; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; @@ -28,6 +29,8 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.function.ExternalDatabase.ProvidedDatabase; import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; @@ -162,6 +165,80 @@ public class DatabaseClientIntegrationTests { assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } + @Test + public void select() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .project("id", "name", "manual") // + .orderBy(Sort.by("id")) // + .fetch().all() // + .as(StepVerifier::create) // + .assertNext(actual -> { + assertThat(actual.getId()).isEqualTo(42055); + assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER"); + assertThat(actual.getManual()).isEqualTo(12); + }).verifyComplete(); + } + + @Test + public void selectOrderByIdDesc() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .orderBy(Sort.by(desc("id"))) // + .fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42068, 42064, 42055) // + .verifyComplete(); + } + + @Test + public void selectOrderPaged() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .orderBy(Sort.by(desc("id"))) // + .page(PageRequest.of(1, 1)).fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42064) // + .verifyComplete(); + } + + @Test + public void selectTypedLater() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from("legoset") // + .orderBy(Sort.by(desc("id"))) // + .as(LegoSet.class) // + .fetch().all() // + .map(LegoSet::getId) // + .as(StepVerifier::create) // + .expectNext(42068, 42064, 42055) // + .verifyComplete(); + } + @Data @Table("legoset") static class LegoSet { From e8d3e055c676025d2781b8b63fd2953d296a57f8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 11:34:48 +0200 Subject: [PATCH 008/665] #2 - Initial SimpleR2dbcRepository support. --- .../DefaultReactiveDataAccessStrategy.java | 13 +- .../support/SimpleR2dbcRepository.java | 307 ++++++++++++++++++ 2 files changed, 318 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java index e30f042d..b7bd7a95 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java @@ -40,8 +40,17 @@ import org.springframework.util.ClassUtils; */ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy { - private final EntityInstantiators instantiators = new EntityInstantiators(); - private final JdbcMappingContext mappingContext = new JdbcMappingContext(); + private final JdbcMappingContext mappingContext; + private final EntityInstantiators instantiators; + + public DefaultReactiveDataAccessStrategy() { + this(new JdbcMappingContext(), new EntityInstantiators()); + } + + public DefaultReactiveDataAccessStrategy(JdbcMappingContext mappingContext, EntityInstantiators instantiators) { + this.mappingContext = mappingContext; + this.instantiators = instantiators; + } @Override public List getAllFields(Class typeToRead) { diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java new file mode 100644 index 00000000..5c0a016d --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java @@ -0,0 +1,307 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.reactivestreams.Publisher; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; +import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.util.Assert; + +/** + * Simple {@link ReactiveCrudRepository} implementation using R2DBC through {@link DatabaseClient}. + * + * @author Mark Paluch + */ +public class SimpleR2dbcRepository implements ReactiveCrudRepository { + + private final DatabaseClient databaseClient; + private final JdbcPersistentEntity entity; + + /** + * Create a new {@link SimpleR2dbcRepository} given {@link DatabaseClient} and {@link JdbcPersistentEntity}. + * + * @param databaseClient must not be {@literal null}. + * @param entity must not be {@literal null}. + */ + public SimpleR2dbcRepository(DatabaseClient databaseClient, JdbcPersistentEntity entity) { + + Assert.notNull(databaseClient, "DatabaseClient must not be null!"); + Assert.notNull(entity, "PersistentEntity must not be null!"); + + this.databaseClient = databaseClient; + this.entity = entity; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#save(S) + */ + @Override + public Mono save(S objectToSave) { + + Assert.notNull(objectToSave, "Object to save must not be null!"); + + if (entity.isNew(objectToSave)) { + + // TODO populate Id back to model + return databaseClient.insert().into(entity.getType()).using(objectToSave).then().thenReturn(objectToSave); + } + + // TODO update + + return null; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#saveAll(java.lang.Iterable) + */ + @Override + public Flux saveAll(Iterable objectsToSave) { + + Assert.notNull(objectsToSave, "Objects to save must not be null!"); + + return Flux.fromIterable(objectsToSave).flatMap(this::save); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#saveAll(org.reactivestreams.Publisher) + */ + @Override + public Flux saveAll(Publisher objectsToSave) { + + Assert.notNull(objectsToSave, "Object publisher must not be null!"); + + return Flux.from(objectsToSave).flatMap(this::save); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#findById(java.lang.Object) + */ + @Override + public Mono findById(ID id) { + + Assert.notNull(id, "Id must not be null!"); + + // TODO: Generate proper SQL (select, where clause, parameter binding). + return databaseClient.execute() + .sql(String.format("SELECT * FROM %s WHERE %s = $1", entity.getTableName(), getIdColumnName())) // + .bind("$1", id) // + .as(entity.getType()) // + .fetch() // + .one(); + + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#findById(org.reactivestreams.Publisher) + */ + @Override + public Mono findById(Publisher publisher) { + return Mono.from(publisher).flatMap(this::findById); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#existsById(java.lang.Object) + */ + @Override + public Mono existsById(ID id) { + + Assert.notNull(id, "Id must not be null!"); + + // TODO: Generate proper SQL (select, where clause, parameter binding). + return databaseClient.execute() + .sql(String.format("SELECT %s FROM %s WHERE %s = $1 LIMIT 1", getIdColumnName(), entity.getTableName(), + getIdColumnName())) // + .bind("$1", id) // + .exchange() // + .flatMap(it -> it.extract((r, md) -> r).first()).hasElement(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#existsById(org.reactivestreams.Publisher) + */ + @Override + public Mono existsById(Publisher publisher) { + return Mono.from(publisher).flatMap(this::findById).hasElement(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#findAll() + */ + @Override + public Flux findAll() { + return databaseClient.select().from(entity.getType()).fetch().all(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#findAllById(java.lang.Iterable) + */ + @Override + public Flux findAllById(Iterable iterable) { + + Assert.notNull(iterable, "The iterable of Id's must not be null!"); + + return findAllById(Flux.fromIterable(iterable)); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#findAllById(org.reactivestreams.Publisher) + */ + @Override + public Flux findAllById(Publisher idPublisher) { + + Assert.notNull(idPublisher, "The Id Publisher must not be null!"); + + return Flux.from(idPublisher).buffer().filter(ids -> !ids.isEmpty()).flatMap(ids -> { + + String bindings = getInBinding(ids); + + GenericExecuteSpec exec = databaseClient.execute() + .sql(String.format("SELECT * FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings)); + + return bind(ids, exec).as(entity.getType()).fetch().all(); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#count() + */ + @Override + public Mono count() { + + return databaseClient.execute() + .sql(String.format("SELECT COUNT(%s) FROM %s", getIdColumnName(), entity.getTableName())) // + .exchange() // + .flatMap(it -> it.extract((r, md) -> r.get(0, Long.class)).first()) // + .defaultIfEmpty(0L); + + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(java.lang.Object) + */ + @Override + public Mono deleteById(ID id) { + + Assert.notNull(id, "Id must not be null!"); + + return databaseClient.execute() + .sql(String.format("DELETE FROM %s WHERE %s = $1", entity.getTableName(), getIdColumnName())) // + .bind("$1", id) // + .fetch() // + .rowsUpdated() // + .then(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteById(org.reactivestreams.Publisher) + */ + @Override + public Mono deleteById(Publisher idPublisher) { + + Assert.notNull(idPublisher, "The Id Publisher must not be null!"); + + return Flux.from(idPublisher).buffer().filter(ids -> !ids.isEmpty()).flatMap(ids -> { + + String bindings = getInBinding(ids); + + GenericExecuteSpec exec = databaseClient.execute() + .sql(String.format("DELETE FROM %s WHERE %s IN (%s)", entity.getTableName(), getIdColumnName(), bindings)); + + return bind(ids, exec).as(entity.getType()).fetch().rowsUpdated(); + }).then(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#delete(java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono delete(T objectToDelete) { + + Assert.notNull(objectToDelete, "Object to delete must not be null!"); + + IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToDelete); + + return deleteById((ID) identifierAccessor.getRequiredIdentifier()); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(java.lang.Iterable) + */ + @Override + public Mono deleteAll(Iterable iterable) { + + Assert.notNull(iterable, "The iterable of Id's must not be null!"); + + return deleteAll(Flux.fromIterable(iterable)); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll(org.reactivestreams.Publisher) + */ + @Override + @SuppressWarnings("unchecked") + public Mono deleteAll(Publisher objectPublisher) { + + Assert.notNull(objectPublisher, "The Object Publisher must not be null!"); + + Flux idPublisher = Flux.from(objectPublisher) // + .map(entity::getIdentifierAccessor) // + .map(identifierAccessor -> (ID) identifierAccessor.getRequiredIdentifier()); + + return deleteById(idPublisher); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#deleteAll() + */ + @Override + public Mono deleteAll() { + + return databaseClient.execute().sql(String.format("DELETE FROM %s", entity.getTableName())) // + .exchange() // + .then(); + } + + private String getInBinding(List ids) { + return IntStream.range(1, ids.size() + 1).mapToObj(i -> "$" + i).collect(Collectors.joining(", ")); + } + + @SuppressWarnings("unchecked") + private > S bind(List it, S bindSpec) { + + for (int i = 0; i < it.size(); i++) { + bindSpec = (S) bindSpec.bind(i, it.get(i)); + } + + return bindSpec; + } + + private String getIdColumnName() { + return entity.getRequiredIdProperty().getColumnName(); + } +} From 893149df8c99f713a83eafb9ef9793fc8ba93466 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 11:35:37 +0200 Subject: [PATCH 009/665] #2 - Fix DefaultTypedInsertSpec.then(). --- .../data/jdbc/core/function/DefaultDatabaseClient.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index 5066cb26..1385ff60 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -933,7 +933,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public Mono then() { - return Mono.from(objectToInsert).map(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); + return Mono.from(objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); } @Override @@ -977,8 +977,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return statement; }; - Function> resultFunction = it -> Flux - .from(insertFunction.apply(it).executeReturningGeneratedKeys()); + Function> resultFunction = it -> { + return Flux.from(insertFunction.apply(it).executeReturningGeneratedKeys()); + }; return new DefaultSqlResult<>(DefaultDatabaseClient.this, // sql, // From ee5e3a72e8e86cf58bb9f958875f366e31f3ed85 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 11:35:59 +0200 Subject: [PATCH 010/665] #2 - Refactor R2dbc test support code into R2dbcIntegrationTestSupport. --- .../DatabaseClientIntegrationTests.java | 27 ++----- .../ExternalDatabase.java | 4 +- .../testing/R2dbcIntegrationTestSupport.java | 70 +++++++++++++++++++ 3 files changed, 76 insertions(+), 25 deletions(-) rename src/test/java/org/springframework/data/jdbc/{core/function => testing}/ExternalDatabase.java (96%) create mode 100644 src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java index 9acba55c..bcd0c499 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java @@ -18,21 +18,17 @@ package org.springframework.data.jdbc.core.function; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Sort.Order.*; -import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; -import io.r2dbc.postgresql.PostgresqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; import lombok.Data; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; import org.junit.Before; -import org.junit.ClassRule; import org.junit.Test; -import org.postgresql.ds.PGSimpleDataSource; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.function.ExternalDatabase.ProvidedDatabase; import org.springframework.data.jdbc.core.mapping.Table; +import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; /** @@ -40,13 +36,7 @@ import org.springframework.jdbc.core.JdbcTemplate; * * @author Mark Paluch */ -public class DatabaseClientIntegrationTests { - - /** - * Local test database at {@code postgres:@localhost:5432/postgres}. - */ - @ClassRule public static final ExternalDatabase database = ProvidedDatabase.builder().hostname("localhost").port(5432) - .database("postgres").username("postgres").password("").build(); +public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport { private ConnectionFactory connectionFactory; @@ -57,22 +47,13 @@ public class DatabaseClientIntegrationTests { Hooks.onOperatorDebug(); - connectionFactory = new PostgresqlConnectionFactory( - PostgresqlConnectionConfiguration.builder().host(database.getHostname()).database(database.getDatabase()) - .username(database.getUsername()).password(database.getPassword()).build()); - - PGSimpleDataSource dataSource = new PGSimpleDataSource(); - dataSource.setUser(database.getUsername()); - dataSource.setPassword(database.getPassword()); - dataSource.setDatabaseName(database.getDatabase()); - dataSource.setServerName(database.getHostname()); - dataSource.setPortNumber(database.getPort()); + connectionFactory = createConnectionFactory(); String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n" + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");"; - jdbc = new JdbcTemplate(dataSource); + jdbc = createJdbcTemplate(createDataSource()); jdbc.execute(tableToCreate); jdbc.execute("DELETE FROM legoset"); } diff --git a/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java b/src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java similarity index 96% rename from src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java rename to src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java index c2e84932..2c6ebe39 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.jdbc.testing; import lombok.Builder; @@ -75,7 +75,7 @@ public abstract class ExternalDatabase extends ExternalResource { * Provided (unmanaged resource) database connection coordinates. */ @Builder - static class ProvidedDatabase extends ExternalDatabase { + public static class ProvidedDatabase extends ExternalDatabase { private final int port; private final String hostname; diff --git a/src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java b/src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java new file mode 100644 index 00000000..ea99ac4c --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.testing; + +import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; +import io.r2dbc.postgresql.PostgresqlConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.postgresql.ds.PGSimpleDataSource; +import org.springframework.data.jdbc.testing.ExternalDatabase.ProvidedDatabase; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Base class for R2DBC integration tests. + * + * @author Mark Paluch + */ +public abstract class R2dbcIntegrationTestSupport { + + /** + * Local test database at {@code postgres:@localhost:5432/postgres}. + */ + @ClassRule public static final ExternalDatabase database = ProvidedDatabase.builder().hostname("localhost").port(5432) + .database("postgres").username("postgres").password("").build(); + + /** + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + */ + protected static ConnectionFactory createConnectionFactory() { + return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder().host(database.getHostname()) + .database(database.getDatabase()).username(database.getUsername()).password(database.getPassword()).build()); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + protected static DataSource createDataSource() { + + PGSimpleDataSource dataSource = new PGSimpleDataSource(); + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + return dataSource; + } + + /** + * Creates a new {@link JdbcTemplate} for a {@link DataSource}. + */ + protected JdbcTemplate createJdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } +} From 590df39bc851fb09f81b8aaae3304cb6049a9817 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 11:36:11 +0200 Subject: [PATCH 011/665] #2 - Add tests for SimpleR2dbcRepository. --- ...SimpleR2dbcRepositoryIntegrationTests.java | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..4daa5ec2 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.Table; +import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for {@link SimpleR2dbcRepository}. + * + * @author Mark Paluch + */ +public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { + + private static JdbcMappingContext mappingContext = new JdbcMappingContext(); + + private ConnectionFactory connectionFactory; + private DatabaseClient databaseClient; + private SimpleR2dbcRepository repository; + private JdbcTemplate jdbc; + + @Before + public void before() { + + Hooks.onOperatorDebug(); + + this.connectionFactory = createConnectionFactory(); + this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + this.repository = new SimpleR2dbcRepository<>(databaseClient, + (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); + + this.jdbc = createJdbcTemplate(createDataSource()); + + String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n" + + " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");"; + + this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset"); + this.jdbc.execute(tableToCreate); + } + + @Test + public void shouldSaveNewObject() { + + LegoSet legoSet = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + + repository.save(legoSet) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); + assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 12).containsKey("id"); + } + + @Test + @Ignore("Implement me") + public void shouldUpdateObject() { + + LegoSet legoSet = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + + repository.save(legoSet) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + legoSet.setManual(14); + + repository.save(legoSet) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); + assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 14).containsKey("id"); + } + + @Test + public void shouldSaveObjectsUsingIterable() { + + LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); + + repository.saveAll(Arrays.asList(legoSet1, legoSet2)) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 2L); + } + + @Test + public void shouldSaveObjectsUsingPublisher() { + + LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); + + repository.saveAll(Flux.just(legoSet1, legoSet2)) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 2L); + } + + @Test + public void shouldFindById() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.findById(42055) // + .as(StepVerifier::create) // + .assertNext(actual -> { + + assertThat(actual.getId()).isEqualTo(42055); + assertThat(actual.getName()).isEqualTo("SCHAUFELRADBAGGER"); + assertThat(actual.getManual()).isEqualTo(12); + }).verifyComplete(); + } + + @Test + public void shouldExistsById() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.existsById(42055) // + .as(StepVerifier::create) // + .expectNext(true)// + .verifyComplete(); + + repository.existsById(42) // + .as(StepVerifier::create) // + .expectNext(false)// + .verifyComplete(); + } + + @Test + public void shouldExistsByIdPublisher() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.existsById(Mono.just(42055)) // + .as(StepVerifier::create) // + .expectNext(true)// + .verifyComplete(); + + repository.existsById(Mono.just(42)) // + .as(StepVerifier::create) // + .expectNext(false)// + .verifyComplete(); + } + + @Test + public void shouldFindByAll() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + repository.findAll() // + .map(LegoSet::getName) // + .collectList() // + .as(StepVerifier::create) // + .assertNext(actual -> { + + assertThat(actual).hasSize(2).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"); + }).verifyComplete(); + } + + @Test + public void shouldFindAllByIdUsingIterable() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + repository.findAllById(Arrays.asList(42055, 42064)) // + .map(LegoSet::getName) // + .collectList() // + .as(StepVerifier::create) // + .assertNext(actual -> { + + assertThat(actual).hasSize(2).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"); + }).verifyComplete(); + } + + @Test + public void shouldFindAllByIdUsingPublisher() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + repository.findAllById(Flux.just(42055, 42064)) // + .map(LegoSet::getName) // + .collectList() // + .as(StepVerifier::create) // + .assertNext(actual -> { + + assertThat(actual).hasSize(2).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"); + }).verifyComplete(); + } + + @Test + public void shouldCount() { + + repository.count() // + .as(StepVerifier::create) // + .expectNext(0L) // + .verifyComplete(); + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + repository.count() // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + } + + @Test + public void shouldDeleteById() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.deleteById(42055) // + .as(StepVerifier::create) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 0L); + } + + @Test + public void shouldDeleteByIdPublisher() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.deleteById(Mono.just(42055)) // + .as(StepVerifier::create) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 0L); + } + + @Test + public void shouldDelete() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + + repository.delete(legoSet) // + .as(StepVerifier::create) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 0L); + } + + @Test + public void shouldDeleteAllUsingIterable() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + + repository.deleteAll(Collections.singletonList(legoSet)) // + .as(StepVerifier::create) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 0L); + } + + @Test + public void shouldDeleteAllUsingPublisher() { + + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); + + repository.deleteAll(Mono.just(legoSet)) // + .as(StepVerifier::create) // + .verifyComplete(); + + Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); + assertThat(map).containsEntry("count", 0L); + } + + @Data + @Table("repo_legoset") + @AllArgsConstructor + @NoArgsConstructor + static class LegoSet { + @Id Integer id; + String name; + Integer manual; + } +} From 28dee1274ffe9bc4b370476bed586ee6dfeb21f5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 12:36:01 +0200 Subject: [PATCH 012/665] #2 - Add updates to SimpleR2dbcRepository. --- .../core/function/MappingR2dbcConverter.java | 100 ++++++++++++++++++ .../support/SimpleR2dbcRepository.java | 63 ++++++++++- ...SimpleR2dbcRepositoryIntegrationTests.java | 17 ++- 3 files changed, 166 insertions(+), 14 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java diff --git a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java new file mode 100644 index 00000000..81a90f45 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.core.function; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; + +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Converter for R2DBC. + * + * @author Mark Paluch + */ +public class MappingR2dbcConverter { + + private final MappingContext, JdbcPersistentProperty> mappingContext; + + public MappingR2dbcConverter(MappingContext, JdbcPersistentProperty> mappingContext) { + this.mappingContext = mappingContext; + } + + /** + * Returns a {@link Map} that maps column names to an {@link Optional} value. Used {@link Optional#empty()} if the + * underlying property is {@literal null}. + * + * @param object must not be {@literal null}. + * @return + */ + public Map> getFieldsToUpdate(Object object) { + + Assert.notNull(object, "Entity object must not be null!"); + + Class userClass = ClassUtils.getUserClass(object); + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + + Map> update = new LinkedHashMap<>(); + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + + for (JdbcPersistentProperty property : entity) { + update.put(property.getColumnName(), Optional.ofNullable(propertyAccessor.getProperty(property))); + } + + return update; + } + + /** + * Returns a {@link java.util.function.Function} that populates the id property of the {@code object} from a + * {@link Row}. + * + * @param object must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public BiFunction populateIdIfNecessary(T object) { + + Assert.notNull(object, "Entity object must not be null!"); + + Class userClass = ClassUtils.getUserClass(object); + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + + return (row, metadata) -> { + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + JdbcPersistentProperty idProperty = entity.getRequiredIdProperty(); + + if (propertyAccessor.getProperty(idProperty) == null) { + + propertyAccessor.setProperty(idProperty, row.get(idProperty.getColumnName(), idProperty.getColumnType())); + return (T) propertyAccessor.getBean(); + } + + return object; + }; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java index 5c0a016d..955087ee 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java @@ -19,6 +19,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -26,6 +28,8 @@ import org.reactivestreams.Publisher; import org.springframework.data.jdbc.core.function.DatabaseClient; import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.jdbc.core.function.FetchSpec; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.mapping.IdentifierAccessor; import org.springframework.data.repository.reactive.ReactiveCrudRepository; @@ -39,17 +43,22 @@ import org.springframework.util.Assert; public class SimpleR2dbcRepository implements ReactiveCrudRepository { private final DatabaseClient databaseClient; + private final MappingR2dbcConverter converter; private final JdbcPersistentEntity entity; /** * Create a new {@link SimpleR2dbcRepository} given {@link DatabaseClient} and {@link JdbcPersistentEntity}. * * @param databaseClient must not be {@literal null}. + * @param converter must not be {@literal null}. * @param entity must not be {@literal null}. */ - public SimpleR2dbcRepository(DatabaseClient databaseClient, JdbcPersistentEntity entity) { + public SimpleR2dbcRepository(DatabaseClient databaseClient, MappingR2dbcConverter converter, + JdbcPersistentEntity entity) { + this.converter = converter; Assert.notNull(databaseClient, "DatabaseClient must not be null!"); + Assert.notNull(converter, "MappingR2dbcConverter must not be null!"); Assert.notNull(entity, "PersistentEntity must not be null!"); this.databaseClient = databaseClient; @@ -66,13 +75,57 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract(converter.populateIdIfNecessary(objectToSave)).one()); } - // TODO update + // TODO: Extract in some kind of SQL generator + IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToSave); + Object id = identifierAccessor.getRequiredIdentifier(); - return null; + Map> fields = converter.getFieldsToUpdate(objectToSave); + + String setClause = getSetClause(fields); + + GenericExecuteSpec exec = databaseClient.execute() + .sql(String.format("UPDATE %s SET %s WHERE %s = $1", entity.getTableName(), setClause, getIdColumnName())) // + .bind(0, id); + + int index = 1; + for (Optional setValue : fields.values()) { + + Object value = setValue.orElse(null); + if (value != null) { + exec = exec.bind(index++, value); + } else { + exec = exec.bindNull(index++); + } + } + + return exec.as(entity.getType()) // + .exchange() // + .flatMap(FetchSpec::rowsUpdated) // + .thenReturn(objectToSave); + } + + private static String getSetClause(Map> fields) { + + StringBuilder setClause = new StringBuilder(); + + int index = 2; + for (String field : fields.keySet()) { + + if (setClause.length() != 0) { + setClause.append(", "); + } + + setClause.append(field).append('=').append('$').append(index++); + } + + return setClause.toString(); } /* (non-Javadoc) diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index 4daa5ec2..ffe4f8ca 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -31,12 +31,12 @@ import java.util.Collections; import java.util.Map; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.core.function.DatabaseClient; import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.Table; @@ -66,6 +66,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); this.repository = new SimpleR2dbcRepository<>(databaseClient, + new MappingR2dbcConverter(mappingContext), (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.jdbc = createJdbcTemplate(createDataSource()); @@ -84,7 +85,10 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS repository.save(legoSet) // .as(StepVerifier::create) // - .expectNextCount(1) // + .consumeNextWith(actual -> { + + assertThat(actual.getId()).isNotNull(); + }) .verifyComplete(); Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); @@ -92,16 +96,11 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS } @Test - @Ignore("Implement me") public void shouldUpdateObject() { - LegoSet legoSet = new LegoSet(null, "SCHAUFELRADBAGGER", 12); - - repository.save(legoSet) // - .as(StepVerifier::create) // - .expectNextCount(1) // - .verifyComplete(); + jdbc.execute("INSERT INTO repo_legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + LegoSet legoSet = new LegoSet(42055, "SCHAUFELRADBAGGER", 12); legoSet.setManual(14); repository.save(legoSet) // From f794bfc3ab8796cb262bcd7f2e5c580b50c61c2d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 16:54:27 +0200 Subject: [PATCH 013/665] #2 - Add R2dbcRepositoryFactory and simple query subsystem. --- .../core/function/MappingR2dbcConverter.java | 9 +- .../data/jdbc/repository/R2dbcRepository.java | 27 +++ .../repository/query/AbstractR2dbcQuery.java | 147 ++++++++++++ .../jdbc/repository/query/BindableQuery.java | 36 +++ .../query/DtoInstantiatingConverter.java | 108 +++++++++ .../query/JdbcEntityInformation.java | 33 +++ .../repository/query/JdbcEntityMetadata.java | 41 ++++ .../query/JdbcParameterAccessor.java | 31 +++ .../jdbc/repository/query/JdbcParameters.java | 80 ++++++ .../JdbcParametersParameterAccessor.java | 52 ++++ .../query/R2dbcParameterAccessor.java | 99 ++++++++ .../repository/query/R2dbcQueryExecution.java | 87 +++++++ .../repository/query/R2dbcQueryMethod.java | 227 ++++++++++++++++++ .../query/SimpleJdbcEntityMetadata.java | 62 +++++ .../query/StringBasedR2dbcQuery.java | 110 +++++++++ .../support/MappingJdbcEntityInformation.java | 111 +++++++++ .../support/R2dbcRepositoryFactory.java | 159 ++++++++++++ .../support/SimpleR2dbcRepository.java | 56 ++--- .../data/jdbc/degraph/DependencyTests.java | 67 ++++++ .../R2dbcRepositoryIntegrationTests.java | 157 ++++++++++++ .../query/R2dbcQueryMethodUnitTests.java | 140 +++++++++++ .../query/StringBasedR2dbcQueryUnitTests.java | 105 ++++++++ .../R2dbcRepositoryFactoryUnitTests.java | 73 ++++++ ...SimpleR2dbcRepositoryIntegrationTests.java | 8 +- 24 files changed, 1983 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java create mode 100644 src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java create mode 100644 src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java create mode 100644 src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java diff --git a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java index 81a90f45..7ab046f3 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java @@ -37,9 +37,10 @@ import org.springframework.util.ClassUtils; */ public class MappingR2dbcConverter { - private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingContext, JdbcPersistentProperty> mappingContext; - public MappingR2dbcConverter(MappingContext, JdbcPersistentProperty> mappingContext) { + public MappingR2dbcConverter( + MappingContext, JdbcPersistentProperty> mappingContext) { this.mappingContext = mappingContext; } @@ -97,4 +98,8 @@ public class MappingR2dbcConverter { return object; }; } + + public MappingContext, JdbcPersistentProperty> getMappingContext() { + return mappingContext; + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java new file mode 100644 index 00000000..d797a64b --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java @@ -0,0 +1,27 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +/** + * R2DBC specific {@link org.springframework.data.repository.Repository} interface with reactive support. + * + * @author Mark Paluch + */ +@NoRepositoryBean +public interface R2dbcRepository extends ReactiveCrudRepository {} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java new file mode 100644 index 00000000..2e64068c --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.jdbc.core.function.FetchSpec; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; +import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter; +import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.Assert; + +/** + * Base class for reactive {@link RepositoryQuery} implementations for R2DBC. + * + * @author Mark Paluch + */ +public abstract class AbstractR2dbcQuery implements RepositoryQuery { + + private final R2dbcQueryMethod method; + private final DatabaseClient databaseClient; + private final MappingR2dbcConverter converter; + private final EntityInstantiators instantiators; + + /** + * Creates a new {@link AbstractR2dbcQuery} from the given {@link R2dbcQueryMethod} and {@link DatabaseClient}. + * + * @param method must not be {@literal null}. + * @param databaseClient must not be {@literal null}. + * @param converter must not be {@literal null}. + */ + public AbstractR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, MappingR2dbcConverter converter) { + + Assert.notNull(method, "R2dbcQueryMethod must not be null!"); + Assert.notNull(databaseClient, "DatabaseClient must not be null!"); + Assert.notNull(converter, "MappingR2dbcConverter must not be null!"); + + this.method = method; + this.databaseClient = databaseClient; + this.converter = converter; + this.instantiators = new EntityInstantiators(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#getQueryMethod() + */ + public R2dbcQueryMethod getQueryMethod() { + return method; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.RepositoryQuery#execute(java.lang.Object[]) + */ + public Object execute(Object[] parameters) { + + return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) + : execute(new JdbcParametersParameterAccessor(method, parameters)); + } + + @SuppressWarnings("unchecked") + private Object executeDeferred(Object[] parameters) { + + R2dbcParameterAccessor parameterAccessor = new R2dbcParameterAccessor(method, parameters); + + if (getQueryMethod().isCollectionQuery()) { + return Flux.defer(() -> (Publisher) execute(parameterAccessor)); + } + + return Mono.defer(() -> (Mono) execute(parameterAccessor)); + } + + private Object execute(JdbcParameterAccessor parameterAccessor) { + + // TODO: ConvertingParameterAccessor + BindableQuery query = createQuery(parameterAccessor); + + ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor); + GenericExecuteSpec boundQuery = query.bind(databaseClient.execute().sql(query)); + FetchSpec fetchSpec = boundQuery.as(resolveResultType(processor)).fetch(); + + String tableName = method.getEntityInformation().getTableName(); + + R2dbcQueryExecution execution = getExecution( + new ResultProcessingConverter(processor, converter.getMappingContext(), instantiators)); + + return execution.execute(fetchSpec, processor.getReturnedType().getDomainType(), tableName); + } + + private Class resolveResultType(ResultProcessor resultProcessor) { + + ReturnedType returnedType = resultProcessor.getReturnedType(); + + return returnedType.isProjecting() ? returnedType.getDomainType() : returnedType.getReturnedType(); + } + + /** + * Returns the execution instance to use. + * + * @param resultProcessing must not be {@literal null}. + * @return + */ + private R2dbcQueryExecution getExecution(Converter resultProcessing) { + return new ResultProcessingExecution(getExecutionToWrap(), resultProcessing); + } + + private R2dbcQueryExecution getExecutionToWrap() { + + if (method.isCollectionQuery()) { + return (q, t, c) -> q.all(); + } + + return (q, t, c) -> q.one(); + } + + /** + * Creates a {@link BindableQuery} instance using the given {@link ParameterAccessor} + * + * @param accessor must not be {@literal null}. + * @return + */ + protected abstract BindableQuery createQuery(JdbcParameterAccessor accessor); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java new file mode 100644 index 00000000..d8361d7e --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java @@ -0,0 +1,36 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import java.util.function.Supplier; + +import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; + +/** + * Interface declaring a query that supplies SQL and can bind parameters to a {@link BindSpec}. + * + * @author Mark Paluch + */ +public interface BindableQuery extends Supplier { + + /** + * Bind parameters to the {@link BindSpec query}. + * + * @param bindSpec must not be {@literal null}. + * @return the bound query object. + */ + > T bind(T bindSpec); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java new file mode 100644 index 00000000..ceedda03 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiator; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.PersistentEntity; +import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PreferredConstructor.Parameter; +import org.springframework.data.mapping.SimplePropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.util.Assert; + +/** + * {@link Converter} to instantiate DTOs from fully equipped domain objects. + * + * @author Mark Paluch + */ +class DtoInstantiatingConverter implements Converter { + + private final Class targetType; + private final MappingContext, ? extends PersistentProperty> context; + private final EntityInstantiator instantiator; + + /** + * Creates a new {@link Converter} to instantiate DTOs. + * + * @param dtoType must not be {@literal null}. + * @param context must not be {@literal null}. + * @param instantiators must not be {@literal null}. + */ + public DtoInstantiatingConverter(Class dtoType, + MappingContext, JdbcPersistentProperty> context, + EntityInstantiators instantiator) { + + Assert.notNull(dtoType, "DTO type must not be null!"); + Assert.notNull(context, "MappingContext must not be null!"); + Assert.notNull(instantiator, "EntityInstantiators must not be null!"); + + this.targetType = dtoType; + this.context = context; + this.instantiator = instantiator.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (targetType.isInterface()) { + return source; + } + + final PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); + final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); + final PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); + final PreferredConstructor> constructor = targetEntity + .getPersistenceConstructor(); + + @SuppressWarnings({ "rawtypes", "unchecked" }) + Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { + + @Override + public Object getParameterValue(Parameter parameter) { + return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); + } + }); + + final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); + + targetEntity.doWithProperties(new SimplePropertyHandler() { + + @Override + public void doWithPersistentProperty(PersistentProperty property) { + + if (constructor.isConstructorParameter(property)) { + return; + } + + dtoAccessor.setProperty(property, + sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); + } + }); + + return dto; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java new file mode 100644 index 00000000..6ce86dae --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.data.repository.core.EntityInformation; + +/** + * JDBC specific {@link EntityInformation}. + * + * @author Mark Paluch + */ +public interface JdbcEntityInformation extends EntityInformation { + + /** + * Returns the name of the table the entity shall be persisted to. + * + * @return + */ + String getTableName(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java new file mode 100644 index 00000000..278cc000 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java @@ -0,0 +1,41 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.repository.core.EntityMetadata; + +/** + * Extension of {@link EntityMetadata} to additionally expose the collection name an entity shall be persisted to. + * + * @author Mark Paluch + */ +public interface JdbcEntityMetadata extends EntityMetadata { + + /** + * Returns the name of the table the entity shall be persisted to. + * + * @return + */ + String getTableName(); + + /** + * Returns the {@link JdbcPersistentEntity} that supposed to determine the table to be queried. + * + * @return + */ + JdbcPersistentEntity getTableEntity(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java new file mode 100644 index 00000000..b7c57f1f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.data.repository.query.ParameterAccessor; + +/** + * JDBC-specific {@link ParameterAccessor}. + * + * @author Mark Paluch + */ +public interface JdbcParameterAccessor extends ParameterAccessor { + + /** + * Returns the raw parameter values of the underlying query method. + */ + Object[] getValues(); +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java new file mode 100644 index 00000000..d199225f --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import java.lang.reflect.Method; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; + +/** + * Custom extension of {@link Parameters}. + * + * @author Mark Paluch + */ +public class JdbcParameters extends Parameters { + + /** + * Creates a new {@link JdbcParameters} instance from the given {@link Method}. + * + * @param method must not be {@literal null}. + */ + public JdbcParameters(Method method) { + super(method); + } + + private JdbcParameters(List parameters) { + super(parameters); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter) + */ + @Override + protected JdbcParameter createParameter(MethodParameter parameter) { + return new JdbcParameter(parameter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) + */ + @Override + protected JdbcParameters createFrom(List parameters) { + return new JdbcParameters(parameters); + } + + /** + * Custom {@link Parameter} implementation. + * + * @author Mark Paluch + */ + class JdbcParameter extends Parameter { + + /** + * Creates a new {@link JdbcParameter}. + * + * @param parameter must not be {@literal null}. + */ + JdbcParameter(MethodParameter parameter) { + super(parameter); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java new file mode 100644 index 00000000..52c8c58d --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import java.util.Arrays; +import java.util.List; + +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; + +/** + * JDBC-specific {@link ParametersParameterAccessor}. + * + * @author Mark Paluch + */ +public class JdbcParametersParameterAccessor extends ParametersParameterAccessor implements JdbcParameterAccessor { + + private final List values; + + /** + * Creates a new {@link JdbcParametersParameterAccessor}. + * + * @param method must not be {@literal null}. + * @param values must not be {@literal null}. + */ + public JdbcParametersParameterAccessor(QueryMethod method, Object[] values) { + + super(method.getParameters(), values); + this.values = Arrays.asList(values); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.JdbcParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + return values.toArray(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java new file mode 100644 index 00000000..8a1c83c1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.repository.util.ReactiveWrappers; + +/** + * Reactive {@link org.springframework.data.repository.query.ParametersParameterAccessor} implementation that subscribes + * to reactive parameter wrapper types upon creation. This class performs synchronization when accessing parameters. + * + * @author Mark Paluch + */ +class R2dbcParameterAccessor extends JdbcParametersParameterAccessor { + + private final Object[] values; + private final List> subscriptions; + + /** + * Creates a new {@link R2dbcParameterAccessor}. + */ + public R2dbcParameterAccessor(R2dbcQueryMethod method, Object... values) { + + super(method, values); + + this.values = values; + this.subscriptions = new ArrayList<>(values.length); + + for (int i = 0; i < values.length; i++) { + + Object value = values[i]; + + if (value == null || !ReactiveWrappers.supports(value.getClass())) { + subscriptions.add(null); + continue; + } + + if (ReactiveWrappers.isSingleValueType(value.getClass())) { + subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Mono.class).toProcessor()); + } else { + subscriptions.add(ReactiveWrapperConverters.toWrapper(value, Flux.class).collectList().toProcessor()); + } + } + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParametersParameterAccessor#getValue(int) + */ + @SuppressWarnings("unchecked") + @Override + protected T getValue(int index) { + + if (subscriptions.get(index) != null) { + return (T) subscriptions.get(index).block(); + } + + return super.getValue(index); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.JdbcParametersParameterAccessor#getValues() + */ + @Override + public Object[] getValues() { + + Object[] result = new Object[values.length]; + for (int i = 0; i < result.length; i++) { + result[i] = getValue(i); + } + return result; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.ParametersParameterAccessor#getBindableValue(int) + */ + public Object getBindableValue(int index) { + return getValue(getParameters().getBindableParameter(index).getIndex()); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java new file mode 100644 index 00000000..eba2ab21 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.function.FetchSpec; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; +import org.springframework.util.ClassUtils; + +/** + * Set of classes to contain query execution strategies. Depending (mostly) on the return type of a + * {@link org.springframework.data.repository.query.QueryMethod}. + * + * @author Mark Paluch + */ +interface R2dbcQueryExecution { + + Object execute(FetchSpec query, Class type, String tableName); + + /** + * An {@link R2dbcQueryExecution} that wraps the results of the given delegate with the given result processing. + */ + @RequiredArgsConstructor + final class ResultProcessingExecution implements R2dbcQueryExecution { + + private final @NonNull R2dbcQueryExecution delegate; + private final @NonNull Converter converter; + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.R2dbcQueryExecution#execute(org.springframework.data.jdbc.core.function.FetchSpec, java.lang.Class, java.lang.String) + */ + @Override + public Object execute(FetchSpec query, Class type, String tableName) { + return converter.convert(delegate.execute(query, type, tableName)); + } + } + + /** + * A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}. + */ + @RequiredArgsConstructor + final class ResultProcessingConverter implements Converter { + + private final @NonNull ResultProcessor processor; + private final @NonNull MappingContext, JdbcPersistentProperty> mappingContext; + private final @NonNull EntityInstantiators instantiators; + + /* (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + ReturnedType returnedType = processor.getReturnedType(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) { + return source; + } + + Converter converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), + mappingContext, instantiators); + + return processor.processResult(source, converter); + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java new file mode 100644 index 00000000..1e223dec --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java @@ -0,0 +1,227 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import static org.springframework.data.repository.util.ClassUtils.*; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.util.ReactiveWrapperConverters; +import org.springframework.data.repository.util.ReactiveWrappers; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Reactive specific implementation of {@link QueryMethod}. + * + * @author Mark Paluch + */ +public class R2dbcQueryMethod extends QueryMethod { + + private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class); + private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); + + private final Method method; + private final MappingContext, JdbcPersistentProperty> mappingContext; + private final Optional query; + + private @Nullable JdbcEntityMetadata metadata; + + /** + * Creates a new {@link R2dbcQueryMethod} from the given {@link Method}. + * + * @param method must not be {@literal null}. + * @param metadata must not be {@literal null}. + * @param projectionFactory must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, + MappingContext, JdbcPersistentProperty> mappingContext) { + + super(method, metadata, projectionFactory); + + Assert.notNull(mappingContext, "MappingContext must not be null!"); + + this.mappingContext = mappingContext; + + if (hasParameterOfType(method, Pageable.class)) { + + TypeInformation returnType = ClassTypeInformation.fromReturnTypeOf(method); + + boolean multiWrapper = ReactiveWrappers.isMultiValueType(returnType.getType()); + boolean singleWrapperWithWrappedPageableResult = ReactiveWrappers.isSingleValueType(returnType.getType()) + && (PAGE_TYPE.isAssignableFrom(returnType.getRequiredComponentType()) + || SLICE_TYPE.isAssignableFrom(returnType.getRequiredComponentType())); + + if (singleWrapperWithWrappedPageableResult) { + throw new InvalidDataAccessApiUsageException( + String.format("'%s.%s' must not use sliced or paged execution. Please use Flux.buffer(size, skip).", + ClassUtils.getShortName(method.getDeclaringClass()), method.getName())); + } + + if (!multiWrapper) { + throw new IllegalStateException(String.format( + "Method has to use a either multi-item reactive wrapper return type or a wrapped Page/Slice type. Offending method: %s", + method.toString())); + } + + if (hasParameterOfType(method, Sort.class)) { + throw new IllegalStateException(String.format("Method must not have Pageable *and* Sort parameter. " + + "Use sorting capabilities on Pageble instead! Offending method: %s", method.toString())); + } + } + + this.method = method; + this.query = Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(method, Query.class)); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) + */ + @Override + protected JdbcParameters createParameters(Method method) { + return new JdbcParameters(method); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isCollectionQuery() + */ + @Override + public boolean isCollectionQuery() { + return !(isPageQuery() || isSliceQuery()) && ReactiveWrappers.isMultiValueType(method.getReturnType()); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isModifyingQuery() + */ + @Override + public boolean isModifyingQuery() { + return super.isModifyingQuery(); + } + + /* + * All reactive query methods are streaming queries. + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#isStreamQuery() + */ + @Override + public boolean isStreamQuery() { + return true; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getEntityInformation() + */ + @Override + @SuppressWarnings("unchecked") + public JdbcEntityMetadata getEntityInformation() { + + if (metadata == null) { + + Class returnedObjectType = getReturnedObjectType(); + Class domainClass = getDomainClass(); + + if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) { + + this.metadata = new SimpleJdbcEntityMetadata<>((Class) domainClass, + mappingContext.getRequiredPersistentEntity(domainClass)); + + } else { + + JdbcPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); + JdbcPersistentEntity managedEntity = mappingContext.getRequiredPersistentEntity(domainClass); + returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity + : returnedEntity; + JdbcPersistentEntity tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity + : managedEntity; + + this.metadata = new SimpleJdbcEntityMetadata<>((Class) returnedEntity.getType(), tableEntity); + } + } + + return this.metadata; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryMethod#getParameters() + */ + @Override + public JdbcParameters getParameters() { + return (JdbcParameters) super.getParameters(); + } + + /** + * Check if the given {@link org.springframework.data.repository.query.QueryMethod} receives a reactive parameter + * wrapper as one of its parameters. + * + * @return {@literal true} if the given {@link org.springframework.data.repository.query.QueryMethod} receives a + * reactive parameter wrapper as one of its parameters. + */ + public boolean hasReactiveWrapperParameter() { + + for (JdbcParameter parameter : getParameters()) { + if (ReactiveWrapperConverters.supports(parameter.getType())) { + return true; + } + } + return false; + } + + /** + * Returns the required query string declared in a {@link Query} annotation or throws {@link IllegalStateException} if + * neither the annotation found nor the attribute was specified. + * + * @return the query string. + * @throws IllegalStateException in case query method has no annotated query. + */ + public String getRequiredAnnotatedQuery() { + return this.query.map(Query::value) + .orElseThrow(() -> new IllegalStateException("Query method " + this + " has no annotated query")); + } + + /** + * Returns the {@link Query} annotation that is applied to the method or {@literal null} if none available. + * + * @return the optional query annotation. + */ + Optional getQueryAnnotation() { + return this.query; + } + + /** + * @return {@literal true} if the {@link Method} is annotated with {@link Query}. + */ + public boolean hasAnnotatedQuery() { + return getQueryAnnotation().isPresent(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java b/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java new file mode 100644 index 00000000..ea4dc5ed --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java @@ -0,0 +1,62 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import lombok.Getter; + +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link JdbcEntityMetadata}. + * + * @author Mark Paluch + */ +class SimpleJdbcEntityMetadata implements JdbcEntityMetadata { + + private final Class type; + private final @Getter JdbcPersistentEntity tableEntity; + + /** + * Creates a new {@link SimpleJdbcEntityMetadata} using the given type and {@link JdbcPersistentEntity} to use for + * table lookups. + * + * @param type must not be {@literal null}. + * @param tableEntity must not be {@literal null}. + */ + SimpleJdbcEntityMetadata(Class type, JdbcPersistentEntity tableEntity) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(tableEntity, "Table entity must not be null!"); + + this.type = type; + this.tableEntity = tableEntity; + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.EntityMetadata#getJavaType() + */ + public Class getJavaType() { + return type; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.JdbcEntityMetadata#getTableName() + */ + public String getTableName() { + return tableEntity.getTableName(); + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java new file mode 100644 index 00000000..319b25f1 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java @@ -0,0 +1,110 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.Assert; + +/** + * String-based {@link StringBasedR2dbcQuery} implementation. + *

+ * A {@link StringBasedR2dbcQuery} expects a query method to be annotated with {@link Query} with a SQL query. + * + * @author Mark Paluch + */ +public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { + + private final String sql; + + /** + * Creates a new {@link StringBasedR2dbcQuery} for the given {@link StringBasedR2dbcQuery}, {@link DatabaseClient}, + * {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * + * @param queryMethod must not be {@literal null}. + * @param databaseClient must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, DatabaseClient databaseClient, + MappingR2dbcConverter converter, SpelExpressionParser expressionParser, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, databaseClient, converter, expressionParser, + evaluationContextProvider); + } + + /** + * Create a new {@link StringBasedR2dbcQuery} for the given {@code query}, {@link R2dbcQueryMethod}, + * {@link DatabaseClient}, {@link SpelExpressionParser}, and {@link QueryMethodEvaluationContextProvider}. + * + * @param method must not be {@literal null}. + * @param databaseClient must not be {@literal null}. + * @param converter must not be {@literal null}. + * @param expressionParser must not be {@literal null}. + * @param evaluationContextProvider must not be {@literal null}. + */ + public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, DatabaseClient databaseClient, + MappingR2dbcConverter converter, SpelExpressionParser expressionParser, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + + super(method, databaseClient, converter); + + Assert.hasText(query, "Query must not be empty"); + + this.sql = query; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.AbstractR2dbcQuery#createQuery(org.springframework.data.jdbc.repository.query.JdbcParameterAccessor) + */ + @Override + protected BindableQuery createQuery(JdbcParameterAccessor accessor) { + + return new BindableQuery() { + + @Override + public > T bind(T bindSpec) { + + T bindSpecToUse = bindSpec; + + // TODO: Encapsulate PostgreSQL-specific bindings + int index = 1; + for (Object value : accessor.getValues()) { + + if (value == null) { + if (accessor.hasBindableNullValue()) { + bindSpecToUse = bindSpecToUse.bindNull("$" + (index++)); + } + } else { + bindSpecToUse = bindSpecToUse.bind("$" + (index++), value); + } + } + + return bindSpecToUse; + } + + @Override + public String get() { + return sql; + } + }; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java b/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java new file mode 100644 index 00000000..b590ad77 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; +import org.springframework.data.repository.core.support.PersistentEntityInformation; +import org.springframework.lang.Nullable; + +import com.sun.corba.se.spi.ior.ObjectId; + +/** + * {@link JdbcEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the necessary + * information. Can be configured with a custom table name. + * + * @author Mark Paluch + */ +public class MappingJdbcEntityInformation extends PersistentEntityInformation + implements JdbcEntityInformation { + + private final JdbcPersistentEntity entityMetadata; + private final @Nullable String customTableName; + private final Class fallbackIdType; + + /** + * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}. + * + * @param entity must not be {@literal null}. + */ + public MappingJdbcEntityInformation(JdbcPersistentEntity entity) { + this(entity, null, null); + } + + /** + * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and fallback + * identifier type. + * + * @param entity must not be {@literal null}. + * @param fallbackIdType can be {@literal null}. + */ + public MappingJdbcEntityInformation(JdbcPersistentEntity entity, @Nullable Class fallbackIdType) { + this(entity, null, fallbackIdType); + } + + /** + * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and custom table + * name. + * + * @param entity must not be {@literal null}. + * @param customTableName can be {@literal null}. + */ + public MappingJdbcEntityInformation(JdbcPersistentEntity entity, String customTableName) { + this(entity, customTableName, null); + } + + /** + * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}, collection name and + * identifier type. + * + * @param entity must not be {@literal null}. + * @param customTableName can be {@literal null}. + * @param idType can be {@literal null}. + */ + @SuppressWarnings("unchecked") + private MappingJdbcEntityInformation(JdbcPersistentEntity entity, @Nullable String customTableName, + @Nullable Class idType) { + + super(entity); + + this.entityMetadata = entity; + this.customTableName = customTableName; + this.fallbackIdType = idType != null ? idType : (Class) ObjectId.class; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.JdbcEntityInformation#getTableName() + */ + public String getTableName() { + return customTableName == null ? entityMetadata.getTableName() : customTableName; + } + + public String getIdAttribute() { + return entityMetadata.getRequiredIdProperty().getName(); + } + + /* (non-Javadoc) + * @see org.springframework.data.repository.core.support.PersistentEntityInformation#getIdType() + */ + @Override + public Class getIdType() { + + if (this.entityMetadata.hasIdProperty()) { + return super.getIdType(); + } + + return fallbackIdType; + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java new file mode 100644 index 00000000..6fba57a3 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java @@ -0,0 +1,159 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.lang.reflect.Method; +import java.util.Optional; + +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; +import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; +import org.springframework.data.jdbc.repository.query.R2dbcQueryMethod; +import org.springframework.data.jdbc.repository.query.StringBasedR2dbcQuery; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.NamedQueries; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; +import org.springframework.data.repository.query.QueryLookupStrategy; +import org.springframework.data.repository.query.QueryLookupStrategy.Key; +import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.RepositoryQuery; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Factory to create {@link org.springframework.data.jdbc.repository.R2dbcRepository} instances. + * + * @author Mark Paluch + */ +public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { + + private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + private final DatabaseClient databaseClient; + private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingR2dbcConverter converter; + + /** + * Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}. + * + * @param databaseClient must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + */ + public R2dbcRepositoryFactory(DatabaseClient databaseClient, + MappingContext, JdbcPersistentProperty> mappingContext) { + + Assert.notNull(databaseClient, "DatabaseClient must not be null!"); + Assert.notNull(mappingContext, "MappingContext must not be null!"); + + this.databaseClient = databaseClient; + this.mappingContext = mappingContext; + this.converter = new MappingR2dbcConverter(mappingContext); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getRepositoryBaseClass(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { + return SimpleR2dbcRepository.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getTargetRepository(org.springframework.data.repository.core.RepositoryInformation) + */ + @Override + protected Object getTargetRepository(RepositoryInformation information) { + + JdbcEntityInformation entityInformation = getEntityInformation(information.getDomainType(), information); + + return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider) + */ + @Override + protected Optional getQueryLookupStrategy(@Nullable Key key, + QueryMethodEvaluationContextProvider evaluationContextProvider) { + return Optional.of(new R2dbcQueryLookupStrategy(databaseClient, evaluationContextProvider, converter)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) + */ + public JdbcEntityInformation getEntityInformation(Class domainClass) { + return getEntityInformation(domainClass, null); + } + + @SuppressWarnings("unchecked") + private JdbcEntityInformation getEntityInformation(Class domainClass, + @Nullable RepositoryInformation information) { + + JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + + return new MappingJdbcEntityInformation<>((JdbcPersistentEntity) entity); + } + + /** + * {@link QueryLookupStrategy} to create R2DBC queries.. + * + * @author Mark Paluch + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + private static class R2dbcQueryLookupStrategy implements QueryLookupStrategy { + + private final DatabaseClient databaseClient; + private final QueryMethodEvaluationContextProvider evaluationContextProvider; + private final MappingR2dbcConverter converter; + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.projection.ProjectionFactory, org.springframework.data.repository.core.NamedQueries) + */ + @Override + public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, + NamedQueries namedQueries) { + + R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext()); + String namedQueryName = queryMethod.getNamedQueryName(); + + if (namedQueries.hasQuery(namedQueryName)) { + String namedQuery = namedQueries.getQuery(namedQueryName); + return new StringBasedR2dbcQuery(namedQuery, queryMethod, databaseClient, converter, EXPRESSION_PARSER, + evaluationContextProvider); + } else if (queryMethod.hasAnnotatedQuery()) { + return new StringBasedR2dbcQuery(queryMethod, databaseClient, converter, EXPRESSION_PARSER, + evaluationContextProvider); + } + + throw new UnsupportedOperationException("Query derivation not yet supported!"); + + } + } +} diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java index 955087ee..66c09d87 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java @@ -15,6 +15,8 @@ */ package org.springframework.data.jdbc.repository.support; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,8 +32,7 @@ import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.jdbc.core.function.FetchSpec; import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.Assert; @@ -40,30 +41,12 @@ import org.springframework.util.Assert; * * @author Mark Paluch */ +@RequiredArgsConstructor public class SimpleR2dbcRepository implements ReactiveCrudRepository { - private final DatabaseClient databaseClient; - private final MappingR2dbcConverter converter; - private final JdbcPersistentEntity entity; - - /** - * Create a new {@link SimpleR2dbcRepository} given {@link DatabaseClient} and {@link JdbcPersistentEntity}. - * - * @param databaseClient must not be {@literal null}. - * @param converter must not be {@literal null}. - * @param entity must not be {@literal null}. - */ - public SimpleR2dbcRepository(DatabaseClient databaseClient, MappingR2dbcConverter converter, - JdbcPersistentEntity entity) { - this.converter = converter; - - Assert.notNull(databaseClient, "DatabaseClient must not be null!"); - Assert.notNull(converter, "MappingR2dbcConverter must not be null!"); - Assert.notNull(entity, "PersistentEntity must not be null!"); - - this.databaseClient = databaseClient; - this.entity = entity; - } + private final @NonNull JdbcEntityInformation entity; + private final @NonNull DatabaseClient databaseClient; + private final @NonNull MappingR2dbcConverter converter; /* (non-Javadoc) * @see org.springframework.data.repository.reactive.ReactiveCrudRepository#save(S) @@ -76,15 +59,14 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract(converter.populateIdIfNecessary(objectToSave)).one()); } // TODO: Extract in some kind of SQL generator - IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(objectToSave); - Object id = identifierAccessor.getRequiredIdentifier(); + Object id = entity.getRequiredId(objectToSave); Map> fields = converter.getFieldsToUpdate(objectToSave); @@ -105,7 +87,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository findAll() { - return databaseClient.select().from(entity.getType()).fetch().all(); + return databaseClient.select().from(entity.getJavaType()).fetch().all(); } /* (non-Javadoc) @@ -235,7 +217,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository idPublisher = Flux.from(objectPublisher) // - .map(entity::getIdentifierAccessor) // - .map(identifierAccessor -> (ID) identifierAccessor.getRequiredIdentifier()); + .map(entity::getRequiredId); return deleteById(idPublisher); } @@ -355,6 +334,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository() { + @Override + public Object apply(String s) { // + // only the current module + commons + return s.endsWith("target/classes") || s.contains("spring-data-commons"); + } + }) // exclude test code + .withSlicing("sub-modules", // sub-modules are defined by any of the following pattern. + "org.springframework.data.jdbc.(**).*", // + "org.springframework.data.(**).*") // + .printTo("degraph-across-modules.graphml"), // writes a graphml to this location + JCheck.violationFree()); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..34b2ff71 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.Arrays; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.EntityInstantiators; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.Table; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.jdbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}. + * + * @author Mark Paluch + */ +public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { + + private static JdbcMappingContext mappingContext = new JdbcMappingContext(); + + private ConnectionFactory connectionFactory; + private DatabaseClient databaseClient; + private LegoSetRepository repository; + private JdbcTemplate jdbc; + + @Before + public void before() { + + Hooks.onOperatorDebug(); + + this.connectionFactory = createConnectionFactory(); + this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + + this.jdbc = createJdbcTemplate(createDataSource()); + + String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n" + + " name varchar(255) NOT NULL,\n" + " manual integer NULL\n" + ");"; + + this.jdbc.execute("DROP TABLE IF EXISTS repo_legoset"); + this.jdbc.execute(tableToCreate); + + this.repository = new R2dbcRepositoryFactory(databaseClient, mappingContext).getRepository(LegoSetRepository.class); + } + + @Test + public void shouldInsertNewItems() { + + LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); + + repository.saveAll(Arrays.asList(legoSet1, legoSet2)) // + .as(StepVerifier::create) // + .expectNextCount(2) // + .verifyComplete(); + } + + @Test + public void shouldFindItemsByManual() { + + shouldInsertNewItems(); + + repository.findByManual(13) // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getName()).isEqualTo("FORSCHUNGSSCHIFF"); + }) // + .verifyComplete(); + } + + @Test + public void shouldFindItemsByNameLike() { + + shouldInsertNewItems(); + + repository.findByNameContains("%F%") // + .map(LegoSet::getName) // + .collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"); + }).verifyComplete(); + } + + @Test + public void shouldFindApplyingProjection() { + + shouldInsertNewItems(); + + repository.findAsProjection() // + .map(Named::getName) // + .collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).contains("SCHAUFELRADBAGGER", "FORSCHUNGSSCHIFF"); + }).verifyComplete(); + } + + interface LegoSetRepository extends ReactiveCrudRepository { + + @Query("SELECT * FROM repo_legoset WHERE name like $1") + Flux findByNameContains(String name); + + @Query("SELECT * FROM repo_legoset") + Flux findAsProjection(); + + @Query("SELECT * FROM repo_legoset WHERE manual = $1") + Mono findByManual(int manual); + } + + @Data + @Table("repo_legoset") + @AllArgsConstructor + @NoArgsConstructor + static class LegoSet { + @Id Integer id; + String name; + Integer manual; + } + + interface Named { + String getName(); + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java new file mode 100644 index 00000000..487cf7f0 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Mono; + +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; + +/** + * Unit test for {@link R2dbcQueryMethod}. + * + * @author Mark Paluch + */ +public class R2dbcQueryMethodUnitTests { + + JdbcMappingContext context; + + @Before + public void setUp() { + context = new JdbcMappingContext(); + } + + @Test + public void detectsCollectionFromReturnTypeIfReturnTypeAssignable() throws Exception { + + R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); + JdbcEntityMetadata metadata = queryMethod.getEntityInformation(); + + assertThat(metadata.getJavaType()).isAssignableFrom(Contact.class); + assertThat(metadata.getTableName()).isEqualTo("contact"); + } + + @Test + public void detectsTableNameFromRepoTypeIfReturnTypeNotAssignable() throws Exception { + + R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "differentTable"); + JdbcEntityMetadata metadata = queryMethod.getEntityInformation(); + + assertThat(metadata.getJavaType()).isAssignableFrom(Address.class); + assertThat(metadata.getTableName()).isEqualTo("contact"); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullMappingContext() throws Exception { + + Method method = PersonRepository.class.getMethod("findMonoByLastname", String.class, Pageable.class); + + new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(PersonRepository.class), + new SpelAwareProxyProjectionFactory(), null); + } + + @Test(expected = IllegalStateException.class) + public void rejectsMonoPageableResult() throws Exception { + queryMethod(PersonRepository.class, "findMonoByLastname", String.class, Pageable.class); + } + + @Test + public void createsQueryMethodObjectForMethodReturningAnInterface() throws Exception { + queryMethod(SampleRepository.class, "methodReturningAnInterface"); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void throwsExceptionOnWrappedPage() throws Exception { + queryMethod(PersonRepository.class, "findMonoPageByLastname", String.class, Pageable.class); + } + + @Test(expected = InvalidDataAccessApiUsageException.class) + public void throwsExceptionOnWrappedSlice() throws Exception { + queryMethod(PersonRepository.class, "findMonoSliceByLastname", String.class, Pageable.class); + } + + @Test + public void fallsBackToRepositoryDomainTypeIfMethodDoesNotReturnADomainType() throws Exception { + + R2dbcQueryMethod method = queryMethod(PersonRepository.class, "deleteByUserName", String.class); + + assertThat(method.getEntityInformation().getJavaType()).isAssignableFrom(Contact.class); + } + + private R2dbcQueryMethod queryMethod(Class repository, String name, Class... parameters) throws Exception { + + Method method = repository.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + return new R2dbcQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, context); + } + + interface PersonRepository extends Repository { + + Mono findMonoByLastname(String lastname, Pageable pageRequest); + + Mono> findMonoPageByLastname(String lastname, Pageable pageRequest); + + Mono> findMonoSliceByLastname(String lastname, Pageable pageRequest); + + void deleteByUserName(String userName); + } + + interface SampleRepository extends Repository { + + List method(); + + List

differentTable(); + + Customer methodReturningAnInterface(); + } + + interface Customer {} + + static class Contact {} + + static class Address {} +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java new file mode 100644 index 00000000..a32de2ac --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; +import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.ReflectionUtils; + +/** + * Unit tests for {@link StringBasedR2dbcQuery}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class StringBasedR2dbcQueryUnitTests { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + + @Mock private DatabaseClient databaseClient; + @Mock private GenericExecuteSpec bindSpec; + + private JdbcMappingContext mappingContext; + private MappingR2dbcConverter converter; + private ProjectionFactory factory; + private RepositoryMetadata metadata; + + @Before + @SuppressWarnings("unchecked") + public void setUp() { + + this.mappingContext = new JdbcMappingContext(); + this.converter = new MappingR2dbcConverter(this.mappingContext); + this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); + this.factory = new SpelAwareProxyProjectionFactory(); + + when(bindSpec.bind(anyString(), any())).thenReturn(bindSpec); + } + + @Test + public void bindsSimplePropertyCorrectly() { + + StringBasedR2dbcQuery query = getQueryMethod("findByLastname", String.class); + R2dbcParameterAccessor accessor = new R2dbcParameterAccessor(query.getQueryMethod(), "White"); + + BindableQuery stringQuery = query.createQuery(accessor); + + assertThat(stringQuery.get()).isEqualTo("SELECT * FROM person WHERE lastname = $1"); + assertThat(stringQuery.bind(bindSpec)).isNotNull(); + + verify(bindSpec).bind("$1", "White"); + } + + private StringBasedR2dbcQuery getQueryMethod(String name, Class... args) { + + Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args); + + R2dbcQueryMethod queryMethod = new R2dbcQueryMethod(method, metadata, factory, converter.getMappingContext()); + + return new StringBasedR2dbcQuery(queryMethod, databaseClient, converter, PARSER, + ExtensionAwareQueryMethodEvaluationContextProvider.DEFAULT); + } + + @SuppressWarnings("unused") + private interface SampleRepository extends Repository { + + @Query("SELECT * FROM person WHERE lastname = $1") + Person findByLastname(String lastname); + } + + static class Person { + + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java new file mode 100644 index 00000000..d040daad --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.jdbc.core.function.DatabaseClient; +import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.repository.Repository; + +/** + * Unit test for {@link R2dbcRepositoryFactory}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class R2dbcRepositoryFactoryUnitTests { + + @Mock DatabaseClient databaseClient; + @Mock @SuppressWarnings("rawtypes") MappingContext mappingContext; + @Mock @SuppressWarnings("rawtypes") JdbcPersistentEntity entity; + + @Before + @SuppressWarnings("unchecked") + public void before() { + when(mappingContext.getRequiredPersistentEntity(Person.class)).thenReturn(entity); + } + + @Test + @SuppressWarnings("unchecked") + public void usesMappingJdbcEntityInformationIfMappingContextSet() { + + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); + JdbcEntityInformation entityInformation = factory.getEntityInformation(Person.class); + + assertThat(entityInformation).isInstanceOf(MappingJdbcEntityInformation.class); + } + + @Test + @SuppressWarnings("unchecked") + public void createsRepositoryWithIdTypeLong() { + + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); + MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); + + assertThat(repository).isNotNull(); + } + + interface MyPersonRepository extends Repository {} + + static class Person {} +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index ffe4f8ca..7e358ac9 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -40,6 +40,7 @@ import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.Table; +import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; @@ -65,10 +66,13 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS this.connectionFactory = createConnectionFactory(); this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); - this.repository = new SimpleR2dbcRepository<>(databaseClient, - new MappingR2dbcConverter(mappingContext), + + JdbcEntityInformation entityInformation = new MappingJdbcEntityInformation<>( (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); + this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, + new MappingR2dbcConverter(mappingContext)); + this.jdbc = createJdbcTemplate(createDataSource()); String tableToCreate = "CREATE TABLE IF NOT EXISTS repo_legoset (\n" + " id SERIAL PRIMARY KEY,\n" From 9d9c665f7633edbff89de253494b6fc4aa98137a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 16:54:35 +0200 Subject: [PATCH 014/665] #2 - Polishing. --- .../data/jdbc/core/function/DefaultDatabaseClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java index 1385ff60..5146746b 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java @@ -996,7 +996,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (RuntimeException e) { String sql = getSql(action); - return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {}); + return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("doInConnectionMany", sql, e) {}); } } @@ -1007,7 +1007,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (RuntimeException e) { String sql = getSql(action); - return Mono.error(new DefaultDatabaseClient.UncategorizedSQLException("ConnectionCallback", sql, e) {}); + return Mono.error(new DefaultDatabaseClient.UncategorizedSQLException("doInConnection", sql, e) {}); } } From cee7479cd09c52a7f81640b7453b08a65b5bfdf2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Jun 2018 16:59:45 +0200 Subject: [PATCH 015/665] #2 - Exclude reactive repositories from JdbcRepositoryConfigExtension. --- .../config/JdbcRepositoryConfigExtension.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java new file mode 100644 index 00000000..fcac3166 --- /dev/null +++ b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.jdbc.repository.config; + +import java.util.Locale; + +import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; +import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; +import org.springframework.data.repository.core.RepositoryMetadata; + +/** + * {@link org.springframework.data.repository.config.RepositoryConfigurationExtension} extending the repository + * registration process by registering JDBC repositories. + * + * @author Jens Schauder + * @author Mark Paluch + */ +public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() + */ + @Override + public String getModuleName() { + return "JDBC"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getRepositoryFactoryBeanClassName() + */ + @Override + public String getRepositoryFactoryBeanClassName() { + return JdbcRepositoryFactoryBean.class.getName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() + */ + @Override + protected String getModulePrefix() { + return getModuleName().toLowerCase(Locale.US); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#useRepositoryConfiguration(org.springframework.data.repository.core.RepositoryMetadata) + */ + @Override + protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { + return !metadata.isReactiveRepository(); + } +} From 8b006abbe58a7a02aeaa50dd0839bd587280357c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 21 Jun 2018 08:44:03 +0200 Subject: [PATCH 016/665] #2 - Create relational and r2dbc packages. Move types into relational and r2dbc packages in preparation for a later module separation. --- .../function/ConnectionAccessor.java | 2 +- .../function/DatabaseClient.java | 2 +- .../function/DefaultDatabaseClient.java | 27 ++++++++-------- .../DefaultDatabaseClientBuilder.java | 8 ++--- .../function/DefaultFetchSpec.java | 2 +- .../DefaultReactiveDataAccessStrategy.java | 3 +- .../function/DefaultSqlResult.java | 2 +- .../core => r2dbc}/function/FetchSpec.java | 2 +- .../function/ReactiveDataAccessStrategy.java | 2 +- .../core => r2dbc}/function/SqlResult.java | 2 +- .../connectionfactory/ConnectionProxy.java | 2 +- .../function/convert}/ColumnMapRowMapper.java | 2 +- .../function/convert}/EntityRowMapper.java | 7 ++-- .../function/convert}/IterableUtils.java | 2 +- .../convert}/MappingR2dbcConverter.java | 2 +- .../repository/R2dbcRepository.java | 2 +- .../data/r2dbc/repository/package-info.java | 7 ++++ .../repository/query/AbstractR2dbcQuery.java | 22 +++++++------ .../repository/query/BindableQuery.java | 8 ++--- .../query/R2dbcParameterAccessor.java | 5 +-- .../repository/query/R2dbcQueryExecution.java | 5 +-- .../repository/query/R2dbcQueryMethod.java | 26 ++++++++------- .../query/StringBasedR2dbcQuery.java | 12 ++++--- .../r2dbc/repository/query/package-info.java | 9 ++++++ .../support/R2dbcRepositoryFactory.java | 25 ++++++++------- .../support/SimpleR2dbcRepository.java | 16 +++++----- .../repository/support/package-info.java | 7 ++++ .../query/DtoInstantiatingConverter.java | 4 +-- .../query/RelationalEntityInformation.java} | 4 +-- .../query/RelationalEntityMetadata.java} | 4 +-- .../query/RelationalParameterAccessor.java} | 4 +-- .../query/RelationalParameters.java} | 26 +++++++-------- ...elationalParametersParameterAccessor.java} | 9 +++--- .../SimpleRelationalEntityMetadata.java} | 12 +++---- .../repository/query/package-info.java | 7 ++++ .../MappingRelationalEntityInformation.java} | 32 +++++++++---------- .../repository/support/package-info.java | 7 ++++ .../DatabaseClientIntegrationTests.java | 2 +- .../R2dbcRepositoryIntegrationTests.java | 8 ++--- .../query/R2dbcQueryMethodUnitTests.java | 7 ++-- .../query/StringBasedR2dbcQueryUnitTests.java | 9 +++--- .../R2dbcRepositoryFactoryUnitTests.java | 11 ++++--- ...SimpleR2dbcRepositoryIntegrationTests.java | 13 ++++---- 43 files changed, 212 insertions(+), 158 deletions(-) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/ConnectionAccessor.java (98%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DatabaseClient.java (99%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DefaultDatabaseClient.java (96%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DefaultDatabaseClientBuilder.java (95%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DefaultFetchSpec.java (97%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DefaultReactiveDataAccessStrategy.java (97%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/DefaultSqlResult.java (98%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/FetchSpec.java (96%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/ReactiveDataAccessStrategy.java (95%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/SqlResult.java (95%) rename src/main/java/org/springframework/data/{jdbc/core => r2dbc}/function/connectionfactory/ConnectionProxy.java (94%) rename src/main/java/org/springframework/data/{jdbc/core/function => r2dbc/function/convert}/ColumnMapRowMapper.java (98%) rename src/main/java/org/springframework/data/{jdbc/core/function => r2dbc/function/convert}/EntityRowMapper.java (99%) rename src/main/java/org/springframework/data/{jdbc/core/function => r2dbc/function/convert}/IterableUtils.java (96%) rename src/main/java/org/springframework/data/{jdbc/core/function => r2dbc/function/convert}/MappingR2dbcConverter.java (98%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/R2dbcRepository.java (95%) create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/package-info.java rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/AbstractR2dbcQuery.java (82%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/BindableQuery.java (88%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/R2dbcParameterAccessor.java (92%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/R2dbcQueryExecution.java (94%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/R2dbcQueryMethod.java (88%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/query/StringBasedR2dbcQuery.java (88%) create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/query/package-info.java rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/support/R2dbcRepositoryFactory.java (85%) rename src/main/java/org/springframework/data/{jdbc => r2dbc}/repository/support/SimpleR2dbcRepository.java (94%) create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/support/package-info.java rename src/main/java/org/springframework/data/{jdbc => relational}/repository/query/DtoInstantiatingConverter.java (96%) rename src/main/java/org/springframework/data/{jdbc/repository/query/JdbcEntityInformation.java => relational/repository/query/RelationalEntityInformation.java} (85%) rename src/main/java/org/springframework/data/{jdbc/repository/query/JdbcEntityMetadata.java => relational/repository/query/RelationalEntityMetadata.java} (89%) rename src/main/java/org/springframework/data/{jdbc/repository/query/JdbcParameterAccessor.java => relational/repository/query/RelationalParameterAccessor.java} (86%) rename src/main/java/org/springframework/data/{jdbc/repository/query/JdbcParameters.java => relational/repository/query/RelationalParameters.java} (63%) rename src/main/java/org/springframework/data/{jdbc/repository/query/JdbcParametersParameterAccessor.java => relational/repository/query/RelationalParametersParameterAccessor.java} (79%) rename src/main/java/org/springframework/data/{jdbc/repository/query/SimpleJdbcEntityMetadata.java => relational/repository/query/SimpleRelationalEntityMetadata.java} (77%) create mode 100644 src/main/java/org/springframework/data/relational/repository/query/package-info.java rename src/main/java/org/springframework/data/{jdbc/repository/support/MappingJdbcEntityInformation.java => relational/repository/support/MappingRelationalEntityInformation.java} (64%) create mode 100644 src/main/java/org/springframework/data/relational/repository/support/package-info.java rename src/test/java/org/springframework/data/{jdbc/core => r2dbc}/function/DatabaseClientIntegrationTests.java (99%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/repository/R2dbcRepositoryIntegrationTests.java (94%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/repository/query/R2dbcQueryMethodUnitTests.java (93%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/repository/query/StringBasedR2dbcQueryUnitTests.java (91%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/repository/support/R2dbcRepositoryFactoryUnitTests.java (80%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/repository/support/SimpleR2dbcRepositoryIntegrationTests.java (94%) diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java similarity index 98% rename from src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java rename to src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java index aad998a6..6f9b4b48 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/ConnectionAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Connection; import reactor.core.publisher.Flux; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java similarity index 99% rename from src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java index e94f36ef..986200c7 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Row; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java similarity index 96% rename from src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java index 5146746b..1b4378d0 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -53,7 +53,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; -import org.springframework.data.jdbc.core.function.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; import org.springframework.data.util.Pair; import org.springframework.jdbc.core.SqlProvider; import org.springframework.jdbc.support.SQLExceptionTranslator; @@ -240,7 +241,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SqlSpec} implementation. + * Default {@link DatabaseClient.SqlSpec} implementation. */ private class DefaultSqlSpec implements SqlSpec { @@ -261,8 +262,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} - * implementations. + * Base class for {@link DatabaseClient.GenericExecuteSpec} implementations. */ @RequiredArgsConstructor private class GenericExecuteSpecSupport { @@ -358,7 +358,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + * Default {@link DatabaseClient.GenericExecuteSpec} implementation. */ private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec { @@ -422,7 +422,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} implementation. + * Default {@link DatabaseClient.GenericExecuteSpec} implementation. */ @SuppressWarnings("unchecked") private class DefaultTypedGenericExecuteSpec extends GenericExecuteSpecSupport implements TypedExecuteSpec { @@ -490,7 +490,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.SelectFromSpec} implementation. + * Default {@link DatabaseClient.SelectFromSpec} implementation. */ class DefaultSelectFromSpec implements SelectFromSpec { @@ -506,8 +506,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Base class for {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec} - * implementations. + * Base class for {@link DatabaseClient.GenericExecuteSpec} implementations. */ @RequiredArgsConstructor private abstract class DefaultSelectSpecSupport { @@ -678,7 +677,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}. + * Default implementation of {@link DatabaseClient.TypedInsertSpec}. */ @SuppressWarnings("unchecked") private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { @@ -779,7 +778,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default {@link org.springframework.data.jdbc.core.function.DatabaseClient.InsertIntoSpec} implementation. + * Default {@link DatabaseClient.InsertIntoSpec} implementation. */ class DefaultInsertIntoSpec implements InsertIntoSpec { @@ -795,7 +794,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.GenericInsertSpec}. + * Default implementation of {@link DatabaseClient.GenericInsertSpec}. */ @RequiredArgsConstructor class DefaultGenericInsertSpec implements GenericInsertSpec { @@ -891,7 +890,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Default implementation of {@link org.springframework.data.jdbc.core.function.DatabaseClient.TypedInsertSpec}. + * Default implementation of {@link DatabaseClient.TypedInsertSpec}. */ @RequiredArgsConstructor class DefaultTypedInsertSpec implements TypedInsertSpec, InsertSpec { diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java similarity index 95% rename from src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java rename to src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 08308598..8b6b1de7 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -14,18 +14,18 @@ * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; import java.util.function.Consumer; -import org.springframework.data.jdbc.core.function.DatabaseClient.Builder; +import org.springframework.data.r2dbc.function.DatabaseClient.Builder; import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import io.r2dbc.spi.ConnectionFactory; - /** * Default implementation of {@link DatabaseClient.Builder}. * diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java similarity index 97% rename from src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java index 5db2a685..c51bac8a 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Connection; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java similarity index 97% rename from src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java rename to src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java index b7bd7a95..8a5b9bd1 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -31,6 +31,7 @@ import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.r2dbc.function.convert.EntityRowMapper; import org.springframework.data.util.Pair; import org.springframework.data.util.StreamUtils; import org.springframework.util.ClassUtils; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java similarity index 98% rename from src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java rename to src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index 3cafbb35..c8262c62 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Connection; import io.r2dbc.spi.Result; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java similarity index 96% rename from src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java index 74fca957..f7e8f2da 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/FetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java similarity index 95% rename from src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java rename to src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index d2c996ce..566c2ae5 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java similarity index 95% rename from src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java rename to src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index b6d3d666..eabf54a6 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java similarity index 94% rename from src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java rename to src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index 6f5891f0..09b16ac1 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function.connectionfactory; +package org.springframework.data.r2dbc.function.connectionfactory; import io.r2dbc.spi.Connection; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java similarity index 98% rename from src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java rename to src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java index 663d88fb..acfa37d1 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/ColumnMapRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function.convert; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Row; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java similarity index 99% rename from src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java rename to src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index c78d28c7..34984097 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function.convert; +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -35,9 +37,6 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.util.ClassUtils; -import io.r2dbc.spi.Row; -import io.r2dbc.spi.RowMetadata; - /** * Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced. * diff --git a/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java similarity index 96% rename from src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java rename to src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java index 8b6e5121..c65010e0 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/IterableUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function.convert; import java.util.ArrayList; import java.util.Collection; diff --git a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java similarity index 98% rename from src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java rename to src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 7ab046f3..fae18435 100644 --- a/src/main/java/org/springframework/data/jdbc/core/function/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function.convert; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; diff --git a/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java similarity index 95% rename from src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java rename to src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java index d797a64b..eba2d5bf 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/R2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.r2dbc.repository; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.reactive.ReactiveCrudRepository; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/package-info.java b/src/main/java/org/springframework/data/r2dbc/repository/package-info.java new file mode 100644 index 00000000..a39ae503 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/package-info.java @@ -0,0 +1,7 @@ +/** + * R2DBC-specific repository implementation. + */ +@NonNullApi +package org.springframework.data.r2dbc.repository; + +import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java similarity index 82% rename from src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java index 2e64068c..3fbeeb6b 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/AbstractR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -21,12 +21,14 @@ import reactor.core.publisher.Mono; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.jdbc.core.function.FetchSpec; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; -import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter; -import org.springframework.data.jdbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter; +import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution; +import org.springframework.data.relational.repository.query.RelationalParameterAccessor; +import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; @@ -79,7 +81,7 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { public Object execute(Object[] parameters) { return method.hasReactiveWrapperParameter() ? executeDeferred(parameters) - : execute(new JdbcParametersParameterAccessor(method, parameters)); + : execute(new RelationalParametersParameterAccessor(method, parameters)); } @SuppressWarnings("unchecked") @@ -94,7 +96,7 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { return Mono.defer(() -> (Mono) execute(parameterAccessor)); } - private Object execute(JdbcParameterAccessor parameterAccessor) { + private Object execute(RelationalParameterAccessor parameterAccessor) { // TODO: ConvertingParameterAccessor BindableQuery query = createQuery(parameterAccessor); @@ -143,5 +145,5 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { * @param accessor must not be {@literal null}. * @return */ - protected abstract BindableQuery createQuery(JdbcParameterAccessor accessor); + protected abstract BindableQuery createQuery(RelationalParameterAccessor accessor); } diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java similarity index 88% rename from src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java index d8361d7e..dc2957ee 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/BindableQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java @@ -13,22 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import java.util.function.Supplier; -import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; +import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; /** * Interface declaring a query that supplies SQL and can bind parameters to a {@link BindSpec}. - * + * * @author Mark Paluch */ public interface BindableQuery extends Supplier { /** * Bind parameters to the {@link BindSpec query}. - * + * * @param bindSpec must not be {@literal null}. * @return the bound query object. */ diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java similarity index 92% rename from src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index 8a1c83c1..c859d7a5 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcParameterAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -22,6 +22,7 @@ import reactor.core.publisher.MonoProcessor; import java.util.ArrayList; import java.util.List; +import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.repository.util.ReactiveWrappers; @@ -31,7 +32,7 @@ import org.springframework.data.repository.util.ReactiveWrappers; * * @author Mark Paluch */ -class R2dbcParameterAccessor extends JdbcParametersParameterAccessor { +class R2dbcParameterAccessor extends RelationalParametersParameterAccessor { private final Object[] values; private final List> subscriptions; diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java similarity index 94% rename from src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index eba2ab21..6c846eff 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -13,17 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import lombok.NonNull; import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.function.FetchSpec; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.relational.repository.query.DtoInstantiatingConverter; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.util.ClassUtils; diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java similarity index 88% rename from src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index 1e223dec..55249bca 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import static org.springframework.data.repository.util.ClassUtils.*; @@ -28,10 +28,14 @@ import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; -import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; +import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.repository.query.RelationalEntityMetadata; +import org.springframework.data.relational.repository.query.RelationalParameters; +import org.springframework.data.relational.repository.query.SimpleRelationalEntityMetadata; import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.repository.util.ReactiveWrappers; @@ -55,7 +59,7 @@ public class R2dbcQueryMethod extends QueryMethod { private final MappingContext, JdbcPersistentProperty> mappingContext; private final Optional query; - private @Nullable JdbcEntityMetadata metadata; + private @Nullable RelationalEntityMetadata metadata; /** * Creates a new {@link R2dbcQueryMethod} from the given {@link Method}. @@ -109,8 +113,8 @@ public class R2dbcQueryMethod extends QueryMethod { * @see org.springframework.data.repository.query.QueryMethod#createParameters(java.lang.reflect.Method) */ @Override - protected JdbcParameters createParameters(Method method) { - return new JdbcParameters(method); + protected RelationalParameters createParameters(Method method) { + return new RelationalParameters(method); } /* (non-Javadoc) @@ -144,7 +148,7 @@ public class R2dbcQueryMethod extends QueryMethod { */ @Override @SuppressWarnings("unchecked") - public JdbcEntityMetadata getEntityInformation() { + public RelationalEntityMetadata getEntityInformation() { if (metadata == null) { @@ -153,7 +157,7 @@ public class R2dbcQueryMethod extends QueryMethod { if (ClassUtils.isPrimitiveOrWrapper(returnedObjectType)) { - this.metadata = new SimpleJdbcEntityMetadata<>((Class) domainClass, + this.metadata = new SimpleRelationalEntityMetadata<>((Class) domainClass, mappingContext.getRequiredPersistentEntity(domainClass)); } else { @@ -165,7 +169,7 @@ public class R2dbcQueryMethod extends QueryMethod { JdbcPersistentEntity tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity : managedEntity; - this.metadata = new SimpleJdbcEntityMetadata<>((Class) returnedEntity.getType(), tableEntity); + this.metadata = new SimpleRelationalEntityMetadata<>((Class) returnedEntity.getType(), tableEntity); } } @@ -176,8 +180,8 @@ public class R2dbcQueryMethod extends QueryMethod { * @see org.springframework.data.repository.query.QueryMethod#getParameters() */ @Override - public JdbcParameters getParameters() { - return (JdbcParameters) super.getParameters(); + public RelationalParameters getParameters() { + return (RelationalParameters) super.getParameters(); } /** @@ -189,7 +193,7 @@ public class R2dbcQueryMethod extends QueryMethod { */ public boolean hasReactiveWrapperParameter() { - for (JdbcParameter parameter : getParameters()) { + for (Parameter parameter : getParameters()) { if (ReactiveWrapperConverters.supports(parameter.getType())) { return true; } diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java similarity index 88% rename from src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 319b25f1..ff388dcc 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -76,7 +78,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { * @see org.springframework.data.jdbc.repository.query.AbstractR2dbcQuery#createQuery(org.springframework.data.jdbc.repository.query.JdbcParameterAccessor) */ @Override - protected BindableQuery createQuery(JdbcParameterAccessor accessor) { + protected BindableQuery createQuery(RelationalParameterAccessor accessor) { return new BindableQuery() { diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/package-info.java b/src/main/java/org/springframework/data/r2dbc/repository/query/package-info.java new file mode 100644 index 00000000..0ab85fa3 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/package-info.java @@ -0,0 +1,9 @@ +/** + * Query support for R2DBC repositories. + */ +@NonNullApi +@NonNullFields +package org.springframework.data.r2dbc.repository.query; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java similarity index 85% rename from src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java rename to src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 6fba57a3..a4482bec 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.r2dbc.repository.support; import lombok.AccessLevel; import lombok.RequiredArgsConstructor; @@ -21,15 +21,17 @@ import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import java.util.Optional; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; -import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; -import org.springframework.data.jdbc.repository.query.R2dbcQueryMethod; -import org.springframework.data.jdbc.repository.query.StringBasedR2dbcQuery; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.repository.R2dbcRepository; +import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod; +import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery; +import org.springframework.data.relational.repository.query.RelationalEntityInformation; +import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -43,7 +45,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Factory to create {@link org.springframework.data.jdbc.repository.R2dbcRepository} instances. + * Factory to create {@link R2dbcRepository} instances. * * @author Mark Paluch */ @@ -88,7 +90,8 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { @Override protected Object getTargetRepository(RepositoryInformation information) { - JdbcEntityInformation entityInformation = getEntityInformation(information.getDomainType(), information); + RelationalEntityInformation entityInformation = getEntityInformation(information.getDomainType(), + information); return getTargetRepositoryViaReflection(information, entityInformation, databaseClient, converter); } @@ -107,17 +110,17 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { * (non-Javadoc) * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#getEntityInformation(java.lang.Class) */ - public JdbcEntityInformation getEntityInformation(Class domainClass) { + public RelationalEntityInformation getEntityInformation(Class domainClass) { return getEntityInformation(domainClass, null); } @SuppressWarnings("unchecked") - private JdbcEntityInformation getEntityInformation(Class domainClass, + private RelationalEntityInformation getEntityInformation(Class domainClass, @Nullable RepositoryInformation information) { JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); - return new MappingJdbcEntityInformation<>((JdbcPersistentEntity) entity); + return new MappingRelationalEntityInformation<>((JdbcPersistentEntity) entity); } /** diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java similarity index 94% rename from src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java rename to src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 66c09d87..943c43af 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.r2dbc.repository.support; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -27,12 +27,12 @@ import java.util.stream.Collectors; import java.util.stream.IntStream; import org.reactivestreams.Publisher; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DatabaseClient.BindSpec; -import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.jdbc.core.function.FetchSpec; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; -import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; +import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.Assert; @@ -44,7 +44,7 @@ import org.springframework.util.Assert; @RequiredArgsConstructor public class SimpleR2dbcRepository implements ReactiveCrudRepository { - private final @NonNull JdbcEntityInformation entity; + private final @NonNull RelationalEntityInformation entity; private final @NonNull DatabaseClient databaseClient; private final @NonNull MappingR2dbcConverter converter; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/package-info.java b/src/main/java/org/springframework/data/r2dbc/repository/support/package-info.java new file mode 100644 index 00000000..5fc32c6d --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/package-info.java @@ -0,0 +1,7 @@ +/** + * Support infrastructure for query derivation of R2DBC-specific repositories. + */ +@NonNullApi +package org.springframework.data.r2dbc.repository.support; + +import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java similarity index 96% rename from src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java rename to src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java index ceedda03..0d8f1ebc 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiator; @@ -35,7 +35,7 @@ import org.springframework.util.Assert; * * @author Mark Paluch */ -class DtoInstantiatingConverter implements Converter { +public class DtoInstantiatingConverter implements Converter { private final Class targetType; private final MappingContext, ? extends PersistentProperty> context; diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java similarity index 85% rename from src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java rename to src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java index 6ce86dae..c331c0a8 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityInformation.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import org.springframework.data.repository.core.EntityInformation; @@ -22,7 +22,7 @@ import org.springframework.data.repository.core.EntityInformation; * * @author Mark Paluch */ -public interface JdbcEntityInformation extends EntityInformation { +public interface RelationalEntityInformation extends EntityInformation { /** * Returns the name of the table the entity shall be persisted to. diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java similarity index 89% rename from src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java rename to src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java index 278cc000..92fff24f 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcEntityMetadata.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.repository.core.EntityMetadata; @@ -23,7 +23,7 @@ import org.springframework.data.repository.core.EntityMetadata; * * @author Mark Paluch */ -public interface JdbcEntityMetadata extends EntityMetadata { +public interface RelationalEntityMetadata extends EntityMetadata { /** * Returns the name of the table the entity shall be persisted to. diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java similarity index 86% rename from src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java rename to src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java index b7c57f1f..50f8aae0 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameterAccessor.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import org.springframework.data.repository.query.ParameterAccessor; @@ -22,7 +22,7 @@ import org.springframework.data.repository.query.ParameterAccessor; * * @author Mark Paluch */ -public interface JdbcParameterAccessor extends ParameterAccessor { +public interface RelationalParameterAccessor extends ParameterAccessor { /** * Returns the raw parameter values of the underlying query method. diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java similarity index 63% rename from src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java rename to src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java index d199225f..8af173e4 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParameters.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import java.lang.reflect.Method; import java.util.List; import org.springframework.core.MethodParameter; -import org.springframework.data.jdbc.repository.query.JdbcParameters.JdbcParameter; +import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -28,18 +28,18 @@ import org.springframework.data.repository.query.Parameters; * * @author Mark Paluch */ -public class JdbcParameters extends Parameters { +public class RelationalParameters extends Parameters { /** - * Creates a new {@link JdbcParameters} instance from the given {@link Method}. + * Creates a new {@link RelationalParameters} instance from the given {@link Method}. * * @param method must not be {@literal null}. */ - public JdbcParameters(Method method) { + public RelationalParameters(Method method) { super(method); } - private JdbcParameters(List parameters) { + private RelationalParameters(List parameters) { super(parameters); } @@ -48,8 +48,8 @@ public class JdbcParameters extends Parameters { * @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter) */ @Override - protected JdbcParameter createParameter(MethodParameter parameter) { - return new JdbcParameter(parameter); + protected RelationalParameter createParameter(MethodParameter parameter) { + return new RelationalParameter(parameter); } /* @@ -57,8 +57,8 @@ public class JdbcParameters extends Parameters { * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) */ @Override - protected JdbcParameters createFrom(List parameters) { - return new JdbcParameters(parameters); + protected RelationalParameters createFrom(List parameters) { + return new RelationalParameters(parameters); } /** @@ -66,14 +66,14 @@ public class JdbcParameters extends Parameters { * * @author Mark Paluch */ - class JdbcParameter extends Parameter { + public static class RelationalParameter extends Parameter { /** - * Creates a new {@link JdbcParameter}. + * Creates a new {@link RelationalParameter}. * * @param parameter must not be {@literal null}. */ - JdbcParameter(MethodParameter parameter) { + RelationalParameter(MethodParameter parameter) { super(parameter); } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java similarity index 79% rename from src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java rename to src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java index 52c8c58d..62bfc09f 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/JdbcParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import java.util.Arrays; import java.util.List; @@ -26,17 +26,18 @@ import org.springframework.data.repository.query.QueryMethod; * * @author Mark Paluch */ -public class JdbcParametersParameterAccessor extends ParametersParameterAccessor implements JdbcParameterAccessor { +public class RelationalParametersParameterAccessor extends ParametersParameterAccessor + implements RelationalParameterAccessor { private final List values; /** - * Creates a new {@link JdbcParametersParameterAccessor}. + * Creates a new {@link RelationalParametersParameterAccessor}. * * @param method must not be {@literal null}. * @param values must not be {@literal null}. */ - public JdbcParametersParameterAccessor(QueryMethod method, Object[] values) { + public RelationalParametersParameterAccessor(QueryMethod method, Object[] values) { super(method.getParameters(), values); this.values = Arrays.asList(values); diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java similarity index 77% rename from src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java rename to src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java index ea4dc5ed..9e650912 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/SimpleJdbcEntityMetadata.java +++ b/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.relational.repository.query; import lombok.Getter; @@ -21,23 +21,23 @@ import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.util.Assert; /** - * Default implementation of {@link JdbcEntityMetadata}. + * Default implementation of {@link RelationalEntityMetadata}. * * @author Mark Paluch */ -class SimpleJdbcEntityMetadata implements JdbcEntityMetadata { +public class SimpleRelationalEntityMetadata implements RelationalEntityMetadata { private final Class type; private final @Getter JdbcPersistentEntity tableEntity; /** - * Creates a new {@link SimpleJdbcEntityMetadata} using the given type and {@link JdbcPersistentEntity} to use for - * table lookups. + * Creates a new {@link SimpleRelationalEntityMetadata} using the given type and {@link JdbcPersistentEntity} to use + * for table lookups. * * @param type must not be {@literal null}. * @param tableEntity must not be {@literal null}. */ - SimpleJdbcEntityMetadata(Class type, JdbcPersistentEntity tableEntity) { + public SimpleRelationalEntityMetadata(Class type, JdbcPersistentEntity tableEntity) { Assert.notNull(type, "Type must not be null!"); Assert.notNull(tableEntity, "Table entity must not be null!"); diff --git a/src/main/java/org/springframework/data/relational/repository/query/package-info.java b/src/main/java/org/springframework/data/relational/repository/query/package-info.java new file mode 100644 index 00000000..ccd616a6 --- /dev/null +++ b/src/main/java/org/springframework/data/relational/repository/query/package-info.java @@ -0,0 +1,7 @@ +/** + * Query support for relational database repositories. + */ +@NonNullApi +package org.springframework.data.relational.repository.query; + +import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java similarity index 64% rename from src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java rename to src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java index b590ad77..6e77a52e 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/MappingJdbcEntityInformation.java +++ b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java @@ -13,69 +13,69 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.relational.repository.support; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; +import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.lang.Nullable; import com.sun.corba.se.spi.ior.ObjectId; /** - * {@link JdbcEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the necessary - * information. Can be configured with a custom table name. + * {@link RelationalEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the + * necessary information. Can be configured with a custom table name. * * @author Mark Paluch */ -public class MappingJdbcEntityInformation extends PersistentEntityInformation - implements JdbcEntityInformation { +public class MappingRelationalEntityInformation extends PersistentEntityInformation + implements RelationalEntityInformation { private final JdbcPersistentEntity entityMetadata; private final @Nullable String customTableName; private final Class fallbackIdType; /** - * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity}. * * @param entity must not be {@literal null}. */ - public MappingJdbcEntityInformation(JdbcPersistentEntity entity) { + public MappingRelationalEntityInformation(JdbcPersistentEntity entity) { this(entity, null, null); } /** - * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and fallback + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity} and fallback * identifier type. * * @param entity must not be {@literal null}. * @param fallbackIdType can be {@literal null}. */ - public MappingJdbcEntityInformation(JdbcPersistentEntity entity, @Nullable Class fallbackIdType) { + public MappingRelationalEntityInformation(JdbcPersistentEntity entity, @Nullable Class fallbackIdType) { this(entity, null, fallbackIdType); } /** - * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity} and custom table - * name. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity} and custom + * table name. * * @param entity must not be {@literal null}. * @param customTableName can be {@literal null}. */ - public MappingJdbcEntityInformation(JdbcPersistentEntity entity, String customTableName) { + public MappingRelationalEntityInformation(JdbcPersistentEntity entity, String customTableName) { this(entity, customTableName, null); } /** - * Creates a new {@link MappingJdbcEntityInformation} for the given {@link JdbcPersistentEntity}, collection name and - * identifier type. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity}, collection + * name and identifier type. * * @param entity must not be {@literal null}. * @param customTableName can be {@literal null}. * @param idType can be {@literal null}. */ @SuppressWarnings("unchecked") - private MappingJdbcEntityInformation(JdbcPersistentEntity entity, @Nullable String customTableName, + private MappingRelationalEntityInformation(JdbcPersistentEntity entity, @Nullable String customTableName, @Nullable Class idType) { super(entity); diff --git a/src/main/java/org/springframework/data/relational/repository/support/package-info.java b/src/main/java/org/springframework/data/relational/repository/support/package-info.java new file mode 100644 index 00000000..28aeb251 --- /dev/null +++ b/src/main/java/org/springframework/data/relational/repository/support/package-info.java @@ -0,0 +1,7 @@ +/** + * Support infrastructure for query derivation of relational database repositories. + */ +@NonNullApi +package org.springframework.data.relational.repository.support; + +import org.springframework.lang.NonNullApi; diff --git a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java similarity index 99% rename from src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java index bcd0c499..3f06dfdb 100644 --- a/src/test/java/org/springframework/data/jdbc/core/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.core.function; +package org.springframework.data.r2dbc.function; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Sort.Order.*; diff --git a/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java similarity index 94% rename from src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java index 34b2ff71..d18fdc5c 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository; +package org.springframework.data.r2dbc.repository; import static org.assertj.core.api.Assertions.*; @@ -32,13 +32,13 @@ import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.data.jdbc.repository.query.Query; -import org.springframework.data.jdbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java similarity index 93% rename from src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java index 487cf7f0..1dcb41cd 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/query/R2dbcQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import static org.assertj.core.api.Assertions.*; @@ -31,6 +31,7 @@ import org.springframework.data.domain.Slice; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -52,7 +53,7 @@ public class R2dbcQueryMethodUnitTests { public void detectsCollectionFromReturnTypeIfReturnTypeAssignable() throws Exception { R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "method"); - JdbcEntityMetadata metadata = queryMethod.getEntityInformation(); + RelationalEntityMetadata metadata = queryMethod.getEntityInformation(); assertThat(metadata.getJavaType()).isAssignableFrom(Contact.class); assertThat(metadata.getTableName()).isEqualTo("contact"); @@ -62,7 +63,7 @@ public class R2dbcQueryMethodUnitTests { public void detectsTableNameFromRepoTypeIfReturnTypeNotAssignable() throws Exception { R2dbcQueryMethod queryMethod = queryMethod(SampleRepository.class, "differentTable"); - JdbcEntityMetadata metadata = queryMethod.getEntityInformation(); + RelationalEntityMetadata metadata = queryMethod.getEntityInformation(); assertThat(metadata.getJavaType()).isAssignableFrom(Address.class); assertThat(metadata.getTableName()).isEqualTo("contact"); diff --git a/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java similarity index 91% rename from src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index a32de2ac..9ddbbe1c 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.query; +package org.springframework.data.r2dbc.repository.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -25,12 +25,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java similarity index 80% rename from src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index d040daad..5448c977 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.r2dbc.repository.support; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -23,10 +23,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jdbc.core.function.DatabaseClient; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.relational.repository.query.RelationalEntityInformation; +import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.data.repository.Repository; /** @@ -52,9 +53,9 @@ public class R2dbcRepositoryFactoryUnitTests { public void usesMappingJdbcEntityInformationIfMappingContextSet() { R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); - JdbcEntityInformation entityInformation = factory.getEntityInformation(Person.class); + RelationalEntityInformation entityInformation = factory.getEntityInformation(Person.class); - assertThat(entityInformation).isInstanceOf(MappingJdbcEntityInformation.class); + assertThat(entityInformation).isInstanceOf(MappingRelationalEntityInformation.class); } @Test diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java similarity index 94% rename from src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index 7e358ac9..6a42ce25 100644 --- a/src/test/java/org/springframework/data/jdbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.repository.support; +package org.springframework.data.r2dbc.repository.support; import static org.assertj.core.api.Assertions.*; @@ -34,14 +34,15 @@ import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.function.DatabaseClient; -import org.springframework.data.jdbc.core.function.DefaultReactiveDataAccessStrategy; -import org.springframework.data.jdbc.core.function.MappingR2dbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.jdbc.core.mapping.Table; -import org.springframework.data.jdbc.repository.query.JdbcEntityInformation; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.repository.query.RelationalEntityInformation; +import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.jdbc.core.JdbcTemplate; /** @@ -67,7 +68,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); - JdbcEntityInformation entityInformation = new MappingJdbcEntityInformation<>( + RelationalEntityInformation entityInformation = new MappingRelationalEntityInformation<>( (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, From c59ba067cc4851a6cb02a8bcf5f03f2e02926e7f Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Thu, 21 Jun 2018 14:55:46 +0200 Subject: [PATCH 017/665] #2 - EntityRowMapper now prevents constructor argument properties from being read twice. --- .../function/convert/EntityRowMapper.java | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index 34984097..bcf32bde 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -69,6 +69,10 @@ public class EntityRowMapper implements BiFunction { for (JdbcPersistentProperty property : entity) { + if (entity.isConstructorArgument(property)) { + continue; + } + if (property.isCollectionLike()) { throw new UnsupportedOperationException(); } else if (property.isMap()) { @@ -105,7 +109,7 @@ public class EntityRowMapper implements BiFunction { } } - private Class getType(JdbcPersistentProperty property) { + private static Class getType(JdbcPersistentProperty property) { return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType()); } @@ -127,7 +131,9 @@ public class EntityRowMapper implements BiFunction { ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); for (JdbcPersistentProperty p : entity) { - propertyAccessor.setProperty(p, readFrom(row, p, prefix)); + if (!entity.isConstructorArgument(property)) { + propertyAccessor.setProperty(p, readFrom(row, p, prefix)); + } } return instance; @@ -142,10 +148,10 @@ public class EntityRowMapper implements BiFunction { @RequiredArgsConstructor private static class RowParameterValueProvider implements ParameterValueProvider { - @NonNull private final Row resultSet; - @NonNull private final JdbcPersistentEntity entity; - @NonNull private final ConversionService conversionService; - @NonNull private final String prefix; + private final @NonNull Row resultSet; + private final @NonNull JdbcPersistentEntity entity; + private final @NonNull ConversionService conversionService; + private final @NonNull String prefix; /* * (non-Javadoc) From 368835c12861e64c73cd578aed270fc08fc793ae Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Jun 2018 09:43:00 +0200 Subject: [PATCH 018/665] #2 - Remove intermediate repositories for GitHub-hosted Maven artifacts. --- pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/pom.xml b/pom.xml index d5024f2b..a4f7036b 100644 --- a/pom.xml +++ b/pom.xml @@ -386,20 +386,6 @@ spring-libs-snapshot https://repo.spring.io/libs-snapshot - - nebhale-snapshots - https://raw.githubusercontent.com/nebhale/r2dbc/maven/snapshot - - true - - - - nebhale-milestones - https://raw.githubusercontent.com/nebhale/r2dbc/maven/milestone - - false - - From 831e753658124ae8ed168180009eb304fb0ccf69 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Jun 2018 11:12:00 +0200 Subject: [PATCH 019/665] #2 - Introduce R2DBC exception translation. We now provide exception translation for R2DBC exceptions based on Spring JDBC's SQLErrorCodes. --- .../data/r2dbc/BadSqlGrammarException.java | 63 ++++ .../r2dbc/InvalidResultAccessException.java | 80 +++++ .../r2dbc/UncategorizedR2dbcException.java | 65 +++++ .../data/r2dbc/function/DatabaseClient.java | 6 +- .../r2dbc/function/DefaultDatabaseClient.java | 67 ++--- .../DefaultDatabaseClientBuilder.java | 16 +- .../data/r2dbc/function/DefaultSqlResult.java | 33 ++- ...tractFallbackR2dbcExceptionTranslator.java | 115 ++++++++ .../support/R2dbcExceptionTranslator.java | 57 ++++ .../SqlErrorCodeR2dbcExceptionTranslator.java | 273 ++++++++++++++++++ .../SqlStateR2dbcExceptionTranslator.java | 144 +++++++++ .../DatabaseClientIntegrationTests.java | 22 ++ ...CodeR2dbcExceptionTranslatorUnitTests.java | 158 ++++++++++ ...tateR2dbcExceptionTranslatorUnitTests.java | 85 ++++++ 14 files changed, 1128 insertions(+), 56 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java create mode 100644 src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java create mode 100644 src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java create mode 100644 src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java create mode 100644 src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java create mode 100644 src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java create mode 100644 src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java create mode 100644 src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java new file mode 100644 index 00000000..f1dd079f --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; + +/** + * Exception thrown when SQL specified is invalid. Such exceptions always have a {@link io.r2dbc.spi.R2dbcException} + * root cause. + *

+ * It would be possible to have subclasses for no such table, no such column etc. A custom + * {@link org.springframework.data.r2dbc.support.R2dbcExceptionTranslator} could create such more specific exceptions, + * without affecting code using this class. + * + * @author Mark Paluch + */ +public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException { + + private final String sql; + + /** + * Creates a new {@link BadSqlGrammarException}. + * + * @param task name of current task. + * @param sql the offending SQL statement. + * @param ex the root cause. + */ + public BadSqlGrammarException(String task, String sql, R2dbcException ex) { + + super(task + "; bad SQL grammar [" + sql + "]", ex); + + this.sql = sql; + } + + /** + * Return the wrapped {@link R2dbcException}. + */ + public R2dbcException getR2dbcException() { + return (R2dbcException) getCause(); + } + + /** + * Return the SQL that caused the problem. + */ + public String getSql() { + return this.sql; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java new file mode 100644 index 00000000..3dd808ae --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -0,0 +1,80 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when a {@link io.r2dbc.spi.Result} has been accessed in an invalid fashion. Such exceptions always + * have a {@link io.r2dbc.spi.R2dbcException} root cause. + *

+ * This typically happens when an invalid {@link Result} column index or name has been specified. + * + * @author Mark Paluch + * @see BadSqlGrammarException + */ +@SuppressWarnings("serial") +public class InvalidResultAccessException extends InvalidDataAccessResourceUsageException { + + private final @Nullable String sql; + + /** + * Creates a new {@link InvalidResultAccessException}. + * + * @param task name of current task. + * @param sql the offending SQL statement. + * @param ex the root cause. + */ + public InvalidResultAccessException(String task, String sql, R2dbcException ex) { + + super(task + "; invalid Result access for SQL [" + sql + "]", ex); + + this.sql = sql; + } + + /** + * Creates a new {@link InvalidResultAccessException}. + * + * @param ex the root cause. + */ + public InvalidResultAccessException(R2dbcException ex) { + + super(ex.getMessage(), ex); + + this.sql = null; + } + + /** + * Return the wrapped {@link R2dbcException}. + */ + public R2dbcException getR2dbcException() { + return (R2dbcException) getCause(); + } + + /** + * Return the SQL that caused the problem. + * + * @return the offending SQL, if known. + */ + @Nullable + public String getSql() { + return this.sql; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java new file mode 100644 index 00000000..da20c40c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.UncategorizedDataAccessException; +import org.springframework.lang.Nullable; + +/** + * Exception thrown when we can't classify a {@link R2dbcException} into one of our generic data access exceptions. + * + * @author Mark Paluch + */ +public class UncategorizedR2dbcException extends UncategorizedDataAccessException { + + /** + * SQL that led to the problem + */ + private final @Nullable String sql; + + /** + * Creates a new {@link UncategorizedR2dbcException}. + * + * @param task name of current task + * @param sql the offending SQL statement + * @param ex the root cause + */ + public UncategorizedR2dbcException(String task, @Nullable String sql, R2dbcException ex) { + + super(String.format("%s; uncategorized R2dbcException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "", + ex.getMessage()), ex); + this.sql = sql; + } + + /** + * Returns the original {@link R2dbcException}. + * + * @return the original {@link R2dbcException}. + */ + public R2dbcException getR2dbcException() { + return (R2dbcException) getCause(); + } + + /** + * Return the SQL that led to the problem (if known). + */ + @Nullable + public String getSql() { + return this.sql; + } +} 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 986200c7..31161176 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -28,7 +28,7 @@ import java.util.function.Supplier; import org.reactivestreams.Publisher; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** * A non-blocking, reactive client for performing database calls requests with Reactive Streams back pressure. Provides @@ -91,12 +91,12 @@ public interface DatabaseClient { Builder connectionFactory(ConnectionFactory factory); /** - * Configures a {@link SQLExceptionTranslator}. + * Configures a {@link R2dbcExceptionTranslator}. * * @param exceptionTranslator must not be {@literal null}. * @return {@code this} {@link Builder}. */ - Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator); + Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator); /** * Configures a {@link ReactiveDataAccessStrategy}. 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 1b4378d0..5c5adc44 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -17,6 +17,7 @@ package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.Result; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -29,7 +30,6 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; -import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -48,16 +48,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.dao.DataAccessException; -import org.springframework.dao.UncategorizedDataAccessException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.data.util.Pair; import org.springframework.jdbc.core.SqlProvider; -import org.springframework.jdbc.support.SQLExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -74,13 +74,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final ConnectionFactory connector; - private final SQLExceptionTranslator exceptionTranslator; + private final R2dbcExceptionTranslator exceptionTranslator; private final ReactiveDataAccessStrategy dataAccessStrategy; private final DefaultDatabaseClientBuilder builder; - DefaultDatabaseClient(ConnectionFactory connector, SQLExceptionTranslator exceptionTranslator, + DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { this.connector = connector; @@ -133,7 +133,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return doInConnection(connectionToUse, action); }, this::closeConnection, this::closeConnection, this::closeConnection) // - .onErrorMap(SQLException.class, ex -> translateException("execute", getSql(action), ex)); + .onErrorMap(R2dbcException.class, ex -> translateException("execute", getSql(action), ex)); } /** @@ -160,7 +160,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return doInConnectionMany(connectionToUse, action); }, this::closeConnection, this::closeConnection, this::closeConnection) // - .onErrorMap(SQLException.class, ex -> translateException("executeMany", getSql(action), ex)); + .onErrorMap(R2dbcException.class, ex -> translateException("executeMany", getSql(action), ex)); } /** @@ -185,7 +185,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** * Obtain the {@link ConnectionFactory} for actual use. * - * @return the ConnectionFactory (never {@code null}) + * @return the ConnectionFactory (never {@literal null}) * @throws IllegalStateException in case of no DataSource set */ protected ConnectionFactory obtainConnectionFactory() { @@ -204,17 +204,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } /** - * Translate the given {@link SQLException} into a generic {@link DataAccessException}. + * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}. * - * @param task readable text describing the task being attempted - * @param sql SQL query or update that caused the problem (may be {@code null}) - * @param ex the offending {@code SQLException} - * @return a DataAccessException wrapping the {@code SQLException} (never {@code null}) + * @param task readable text describing the task being attempted. + * @param sql SQL query or update that caused the problem (may be {@literal null}). + * @param ex the offending {@link R2dbcException}. + * @return a DataAccessException wrapping the {@link R2dbcException} (never {@literal null}). */ - protected DataAccessException translateException(String task, @Nullable String sql, SQLException ex) { + protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) { DataAccessException dae = exceptionTranslator.translate(task, sql, ex); - return (dae != null ? dae : new UncategorizedSQLException(task, sql, ex)); + return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex)); } private static void doBind(Statement statement, Map> byName, @@ -992,10 +992,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { try { return action.apply(connection); - } catch (RuntimeException e) { + } catch (R2dbcException e) { String sql = getSql(action); - return Flux.error(new DefaultDatabaseClient.UncategorizedSQLException("doInConnectionMany", sql, e) {}); + return Flux.error(new UncategorizedR2dbcException("doInConnectionMany", sql, e) {}); } } @@ -1003,10 +1003,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { try { return action.apply(connection); - } catch (RuntimeException e) { + } catch (R2dbcException e) { String sql = getSql(action); - return Mono.error(new DefaultDatabaseClient.UncategorizedSQLException("doInConnection", sql, e) {}); + return Mono.error(new UncategorizedR2dbcException("doInConnection", sql, e) {}); } } @@ -1014,7 +1014,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Determine SQL from potential provider object. * * @param sqlProvider object that's potentially a SqlProvider - * @return the SQL string, or {@code null} + * @return the SQL string, or {@literal null} * @see SqlProvider */ @Nullable @@ -1078,31 +1078,4 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } } } - - private static class UncategorizedSQLException extends UncategorizedDataAccessException implements SqlProvider { - - /** SQL that led to the problem */ - @Nullable private final String sql; - - /** - * Constructor for UncategorizedSQLException. - * - * @param task name of current task - * @param sql the offending SQL statement - * @param ex the root cause - */ - public UncategorizedSQLException(String task, @Nullable String sql, Exception ex) { - super(String.format("%s; uncategorized SQLException%s; %s", task, sql != null ? " for SQL [" + sql + "]" : "", - ex.getMessage()), ex); - this.sql = sql; - } - - /** - * Return the SQL that led to the problem (if known). - */ - @Nullable - public String getSql() { - return this.sql; - } - } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 8b6b1de7..8ae87bf7 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -21,8 +21,8 @@ import io.r2dbc.spi.ConnectionFactory; import java.util.function.Consumer; import org.springframework.data.r2dbc.function.DatabaseClient.Builder; -import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; -import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,7 +34,7 @@ import org.springframework.util.Assert; class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { private @Nullable ConnectionFactory connector; - private SQLExceptionTranslator exceptionTranslator = new SQLErrorCodeSQLExceptionTranslator(); + private @Nullable R2dbcExceptionTranslator exceptionTranslator; private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); DefaultDatabaseClientBuilder() {} @@ -57,9 +57,9 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { } @Override - public Builder exceptionTranslator(SQLExceptionTranslator exceptionTranslator) { + public Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) { - Assert.notNull(exceptionTranslator, "SQLExceptionTranslator must not be null!"); + Assert.notNull(exceptionTranslator, "R2dbcExceptionTranslator must not be null!"); this.exceptionTranslator = exceptionTranslator; return this; @@ -77,6 +77,12 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { @Override public DatabaseClient build() { + R2dbcExceptionTranslator exceptionTranslator = this.exceptionTranslator; + + if (exceptionTranslator == null) { + exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connector); + } + return new DefaultDatabaseClient(this.connector, exceptionTranslator, accessStrategy, new DefaultDatabaseClientBuilder(this)); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index c8262c62..ec38a79a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -25,6 +25,8 @@ import reactor.core.publisher.Mono; import java.util.function.BiFunction; import java.util.function.Function; +import org.springframework.jdbc.core.SqlProvider; + /** * Default {@link SqlResult} implementation. * @@ -47,7 +49,27 @@ class DefaultSqlResult implements SqlResult { this.updatedRowsFunction = updatedRowsFunction; this.fetchSpec = new DefaultFetchSpec<>(connectionAccessor, sql, - it -> resultFunction.apply(it).flatMap(result -> result.map(mappingFunction)), updatedRowsFunction); + new SqlFunction>() { + @Override + public Flux apply(Connection connection) { + return resultFunction.apply(connection).flatMap(result -> result.map(mappingFunction)); + } + + @Override + public String getSql() { + return sql; + } + }, new SqlFunction>() { + @Override + public Mono apply(Connection connection) { + return updatedRowsFunction.apply(connection); + } + + @Override + public String getSql() { + return sql; + } + }); } /* (non-Javadoc) @@ -89,4 +111,13 @@ class DefaultSqlResult implements SqlResult { public Mono rowsUpdated() { return fetchSpec.rowsUpdated(); } + + /** + * Union type combining {@link Function} and {@link SqlProvider} to expose the SQL that is related to the underlying + * action. + * + * @param the type of the input to the function. + * @param the type of the result of the function. + */ + interface SqlFunction extends Function, SqlProvider {} } diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java new file mode 100644 index 00000000..dded5da8 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -0,0 +1,115 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import io.r2dbc.spi.R2dbcException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataAccessException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Base class for {@link R2dbcExceptionTranslator} implementations that allow for fallback to some other + * {@link R2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcExceptionTranslator { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + @Nullable private R2dbcExceptionTranslator fallbackTranslator; + + /** + * Override the default SQL state fallback translator (typically a {@link R2dbcExceptionTranslator}). + */ + public void setFallbackTranslator(@Nullable R2dbcExceptionTranslator fallback) { + this.fallbackTranslator = fallback; + } + + /** + * Return the fallback exception translator, if any. + */ + @Nullable + public R2dbcExceptionTranslator getFallbackTranslator() { + return this.fallbackTranslator; + } + + /** + * Pre-checks the arguments, calls {@link #doTranslate}, and invokes the {@link #getFallbackTranslator() fallback + * translator} if necessary. + */ + @Override + @NonNull + public DataAccessException translate(String task, @Nullable String sql, R2dbcException ex) { + + Assert.notNull(ex, "Cannot translate a null R2dbcException"); + + DataAccessException dae = doTranslate(task, sql, ex); + if (dae != null) { + // Specific exception match found. + return dae; + } + + // Looking for a fallback... + R2dbcExceptionTranslator fallback = getFallbackTranslator(); + if (fallback != null) { + dae = fallback.translate(task, sql, ex); + if (dae != null) { + // Fallback exception match found. + return dae; + } + } + + // We couldn't identify it more precisely. + return new UncategorizedR2dbcException(task, sql, ex); + } + + /** + * Template method for actually translating the given exception. + *

+ * The passed-in arguments will have been pre-checked. Furthermore, this method is allowed to return {@literal null} + * to indicate that no exception match has been found and that fallback translation should kick in. + * + * @param task readable text describing the task being attempted. + * @param sql SQL query or update that caused the problem (if known). + * @param ex the offending {@link R2dbcException}. + * @return the DataAccessException, wrapping the {@link R2dbcException}; or {@literal null} if no exception match + * found. + */ + @Nullable + protected abstract DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex); + + /** + * Build a message {@code String} for the given {@link java.sql.R2dbcException}. + *

+ * To be called by translator subclasses when creating an instance of a generic + * {@link org.springframework.dao.DataAccessException} class. + * + * @param task readable text describing the task being attempted. + * @param sql the SQL statement that caused the problem. + * @param ex the offending {@link R2dbcException}. + * @return the message {@code String} to use. + */ + protected String buildMessage(String task, @Nullable String sql, R2dbcException ex) { + return task + "; " + (sql != null ? "SQL [" + sql : "]; " + "") + ex.getMessage(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java new file mode 100644 index 00000000..5bbcd4a2 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import io.r2dbc.spi.R2dbcException; + +import org.springframework.dao.DataAccessException; +import org.springframework.lang.Nullable; + +/** + * Strategy interface for translating between {@link io.r2dbc.spi.R2dbcException R2dbcExceptions} and Spring's data + * access strategy-agnostic {@link DataAccessException} hierarchy. + *

+ * Implementations can be generic (for example, using {@link io.r2dbc.spi.R2dbcException#getSqlState() SQLState} codes + * for R2DBC) or wholly proprietary (for example, using Oracle error codes) for greater precision. + * + * @author Mark Paluch + * @see org.springframework.dao.DataAccessException + * @see SqlStateR2dbcExceptionTranslator + * @see SqlErrorCodeR2dbcExceptionTranslator + */ +@FunctionalInterface +public interface R2dbcExceptionTranslator { + + /** + * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}. + *

+ * The returned DataAccessException is supposed to contain the original {@link R2dbcException} as root cause. However, + * client code may not generally rely on this due to DataAccessExceptions possibly being caused by other resource APIs + * as well. That said, a {@code getRootCause() instanceof R2dbcException} check (and subsequent cast) is considered + * reliable when expecting R2DBC-based access to have happened. + * + * @param task readable text describing the task being attempted. + * @param sql SQL query or update that caused the problem (if known). + * @param ex the offending {@link R2dbcException}. + * @return the DataAccessException wrapping the {@code R2dbcException}, or {@literal null} if no translation could be + * applied (in a custom translator; the default translators always throw an + * {@link org.springframework.data.r2dbc.UncategorizedR2dbcException} in such a case). + * @see org.springframework.dao.DataAccessException#getRootCause() + */ + @Nullable + DataAccessException translate(String task, @Nullable String sql, R2dbcException ex); + +} diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java new file mode 100644 index 00000000..87046a1b --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java @@ -0,0 +1,273 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.R2dbcException; + +import java.sql.SQLException; +import java.util.Arrays; + +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.CannotSerializeTransactionException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.InvalidResultAccessException; +import org.springframework.jdbc.support.SQLErrorCodes; +import org.springframework.jdbc.support.SQLErrorCodesFactory; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.lang.Nullable; + +/** + * Implementation of {@link R2dbcExceptionTranslator} that analyzes vendor-specific error codes. More precise than an + * implementation based on SQL state, but heavily vendor-specific. + *

+ * This class applies the following matching rules: + *

    + *
  • Try custom translation implemented by any subclass. Note that this class is concrete and is typically used + * itself, in which case this rule doesn't apply. + *
  • Apply error code matching. Error codes are obtained from the SQLErrorCodesFactory by default. This factory loads + * a "sql-error-codes.xml" file from the class path, defining error code mappings for database names from database + * meta-data. + *
  • Fallback to a fallback translator. {@link SqlStateR2dbcExceptionTranslator} is the default fallback translator, + * analyzing the exception's SQL state only. + *
+ *

+ * The configuration file named "sql-error-codes.xml" is by default read from the + * {@code org.springframework.jdbc.support} package. It can be overridden through a file of the same name in the root of + * the class path (e.g. in the "/WEB-INF/classes" directory), as long as the Spring JDBC package is loaded from the same + * ClassLoader. + * + * @author Mark Paluch + * @see SQLErrorCodesFactory + * @see SqlStateR2dbcExceptionTranslator + */ +public class SqlErrorCodeR2dbcExceptionTranslator extends AbstractFallbackR2dbcExceptionTranslator { + + /** Error codes used by this translator */ + @Nullable private SQLErrorCodes sqlErrorCodes; + + /** + * Creates a new {@link SqlErrorCodeR2dbcExceptionTranslator}. The {@link SQLErrorCodes} or + * {@link io.r2dbc.spi.ConnectionFactory} property must be set. + */ + public SqlErrorCodeR2dbcExceptionTranslator() {} + + /** + * Create a SQL error code translator for the given DataSource. Invoking this constructor will cause a Connection to + * be obtained from the DataSource to get the meta-data. + * + * @param connectionFactory {@link ConnectionFactory} to use to find meta-data and establish which error codes are + * usable. + * @see SQLErrorCodesFactory + */ + public SqlErrorCodeR2dbcExceptionTranslator(ConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + } + + /** + * Create a SQL error code translator for the given database product name. Invoking this constructor will avoid + * obtaining a Connection from the DataSource to get the meta-data. + * + * @param dbName the database product name that identifies the error codes entry + * @see SQLErrorCodesFactory + * @see java.sql.DatabaseMetaData#getDatabaseProductName() + */ + public SqlErrorCodeR2dbcExceptionTranslator(String dbName) { + this(); + setDatabaseProductName(dbName); + } + + /** + * Create a SQLErrorCode translator given these error codes. Does not require a database meta-data lookup to be + * performed using a connection. + * + * @param sec error codes + */ + public SqlErrorCodeR2dbcExceptionTranslator(SQLErrorCodes sec) { + this(); + this.sqlErrorCodes = sec; + } + + /** + * Set the DataSource for this translator. + *

+ * Setting this property will cause a Connection to be obtained from the DataSource to get the meta-data. + * + * @param connectionFactory {@link ConnectionFactory} to use to find meta-data and establish which error codes are + * usable. + * @see SQLErrorCodesFactory#getErrorCodes(String) + * @see io.r2dbc.spi.ConnectionFactoryMetadata#getName() + */ + public void setConnectionFactory(ConnectionFactory connectionFactory) { + this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(connectionFactory.getMetadata().getName()); + } + + /** + * Set the database product name for this translator. + *

+ * Setting this property will avoid obtaining a Connection from the DataSource to get the meta-data. + * + * @param dbName the database product name that identifies the error codes entry. + * @see SQLErrorCodesFactory#getErrorCodes(String) + * @see io.r2dbc.spi.ConnectionFactoryMetadata#getName() + */ + public void setDatabaseProductName(String dbName) { + this.sqlErrorCodes = SQLErrorCodesFactory.getInstance().getErrorCodes(dbName); + } + + /** + * Set custom error codes to be used for translation. + * + * @param sec custom error codes to use. + */ + public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) { + this.sqlErrorCodes = sec; + } + + /** + * Return the error codes used by this translator. Usually determined via a DataSource. + * + * @see #setConnectionFactory + */ + @Nullable + public SQLErrorCodes getSqlErrorCodes() { + return this.sqlErrorCodes; + } + + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) { + + R2dbcException translated = ex; + + // First, try custom translation from overridden method. + DataAccessException dex = customTranslate(task, sql, translated); + if (dex != null) { + return dex; + } + + // Next, try the custom SQLExceptionTranslator, if available. + if (this.sqlErrorCodes != null) { + SQLExceptionTranslator customTranslator = this.sqlErrorCodes.getCustomSqlExceptionTranslator(); + if (customTranslator != null) { + DataAccessException customDex = customTranslator.translate(task, sql, + new SQLException(ex.getMessage(), ex.getSqlState(), ex)); + if (customDex != null) { + return customDex; + } + } + } + + // Check SQLErrorCodes with corresponding error code, if available. + if (this.sqlErrorCodes != null) { + String errorCode; + if (this.sqlErrorCodes.isUseSqlStateForTranslation()) { + errorCode = translated.getSqlState(); + } else { + // Try to find R2dbcException with actual error code, looping through the causes. + R2dbcException current = translated; + while (current.getErrorCode() == 0 && current.getCause() instanceof R2dbcException) { + current = (R2dbcException) current.getCause(); + } + errorCode = Integer.toString(current.getErrorCode()); + } + + if (errorCode != null) { + // Look for grouped error codes. + if (Arrays.binarySearch(this.sqlErrorCodes.getBadSqlGrammarCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new BadSqlGrammarException(task, (sql != null ? sql : ""), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getInvalidResultSetAccessCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new InvalidResultAccessException(task, (sql != null ? sql : ""), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getDuplicateKeyCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new DuplicateKeyException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getDataIntegrityViolationCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new DataIntegrityViolationException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getPermissionDeniedCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new PermissionDeniedDataAccessException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getDataAccessResourceFailureCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new DataAccessResourceFailureException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getTransientDataAccessResourceCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new TransientDataAccessResourceException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotAcquireLockCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new CannotAcquireLockException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getDeadlockLoserCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new DeadlockLoserDataAccessException(buildMessage(task, sql, translated), translated); + } else if (Arrays.binarySearch(this.sqlErrorCodes.getCannotSerializeTransactionCodes(), errorCode) >= 0) { + logTranslation(task, sql, translated); + return new CannotSerializeTransactionException(buildMessage(task, sql, translated), translated); + } + } + } + + // We couldn't identify it more precisely - let's hand it over to the SQLState fallback translator. + if (logger.isDebugEnabled()) { + String codes; + if (this.sqlErrorCodes != null && this.sqlErrorCodes.isUseSqlStateForTranslation()) { + codes = "SQL state '" + translated.getSqlState() + "', error code '" + translated.getErrorCode(); + } else { + codes = "Error code '" + translated.getErrorCode() + "'"; + } + logger.debug("Unable to translate R2dbcException with " + codes + ", will now try the fallback translator"); + } + + return null; + } + + /** + * Subclasses can override this method to attempt a custom mapping from {@link R2dbcException} to + * {@link DataAccessException}. + * + * @param task readable text describing the task being attempted + * @param sql SQL query or update that caused the problem. May be {@literal null}. + * @param ex the offending {@link R2dbcException}. + * @return null if no custom translation was possible, otherwise a {@link DataAccessException} resulting from custom + * translation. This exception should include the {@link R2dbcException} parameter as a nested root cause. + * This implementation always returns null, meaning that the translator always falls back to the default error + * codes. + */ + @Nullable + protected DataAccessException customTranslate(String task, @Nullable String sql, R2dbcException ex) { + return null; + } + + private void logTranslation(String task, @Nullable String sql, R2dbcException exception) { + + if (logger.isDebugEnabled()) { + + String intro = "Translating"; + logger.debug(intro + " R2dbcException with SQL state '" + exception.getSqlState() + "', error code '" + + exception.getErrorCode() + "', message [" + exception.getMessage() + "]" + + (sql != null ? "; SQL was [" + sql + "]" : "") + " for task [" + task + "]"); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java new file mode 100644 index 00000000..97eb4e62 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java @@ -0,0 +1,144 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import io.r2dbc.spi.R2dbcException; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link R2dbcExceptionTranslator} implementation that analyzes the SQL state in the {@link R2dbcException} based on + * the first two digits (the SQL state "class"). Detects standard SQL state values and well-known vendor-specific SQL + * states. + *

+ * Not able to diagnose all problems, but is portable between databases and does not require special initialization (no + * database vendor detection, etc.). For more precise translation, consider + * {@link SqlErrorCodeR2dbcExceptionTranslator}. + * + * @author Mark Paluch + * @see io.r2dbc.spi.R2dbcException#getSqlState() + * @see SqlErrorCodeR2dbcExceptionTranslator + */ +public class SqlStateR2dbcExceptionTranslator extends AbstractFallbackR2dbcExceptionTranslator { + + private static final Set BAD_SQL_GRAMMAR_CODES = new HashSet<>(8); + private static final Set DATA_INTEGRITY_VIOLATION_CODES = new HashSet<>(8); + private static final Set DATA_ACCESS_RESOURCE_FAILURE_CODES = new HashSet<>(8); + private static final Set TRANSIENT_DATA_ACCESS_RESOURCE_CODES = new HashSet<>(8); + private static final Set CONCURRENCY_FAILURE_CODES = new HashSet<>(4); + + static { + BAD_SQL_GRAMMAR_CODES.add("07"); // Dynamic SQL error + BAD_SQL_GRAMMAR_CODES.add("21"); // Cardinality violation + BAD_SQL_GRAMMAR_CODES.add("2A"); // Syntax error direct SQL + BAD_SQL_GRAMMAR_CODES.add("37"); // Syntax error dynamic SQL + BAD_SQL_GRAMMAR_CODES.add("42"); // General SQL syntax error + BAD_SQL_GRAMMAR_CODES.add("65"); // Oracle: unknown identifier + + DATA_INTEGRITY_VIOLATION_CODES.add("01"); // Data truncation + DATA_INTEGRITY_VIOLATION_CODES.add("02"); // No data found + DATA_INTEGRITY_VIOLATION_CODES.add("22"); // Value out of range + DATA_INTEGRITY_VIOLATION_CODES.add("23"); // Integrity constraint violation + DATA_INTEGRITY_VIOLATION_CODES.add("27"); // Triggered data change violation + DATA_INTEGRITY_VIOLATION_CODES.add("44"); // With check violation + + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("08"); // Connection exception + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("53"); // PostgreSQL: insufficient resources (e.g. disk full) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("54"); // PostgreSQL: program limit exceeded (e.g. statement too complex) + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("57"); // DB2: out-of-memory exception / database not started + DATA_ACCESS_RESOURCE_FAILURE_CODES.add("58"); // DB2: unexpected system error + + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JW"); // Sybase: internal I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("JZ"); // Sybase: unexpected I/O error + TRANSIENT_DATA_ACCESS_RESOURCE_CODES.add("S1"); // DB2: communication failure + + CONCURRENCY_FAILURE_CODES.add("40"); // Transaction rollback + CONCURRENCY_FAILURE_CODES.add("61"); // Oracle: deadlock + } + + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) { + + // First, the getSQLState check... + String sqlState = getSqlState(ex); + if (sqlState != null && sqlState.length() >= 2) { + + String classCode = sqlState.substring(0, 2); + + if (logger.isDebugEnabled()) { + logger.debug("Extracted SQL state class '" + classCode + "' from value '" + sqlState + "'"); + } + + if (BAD_SQL_GRAMMAR_CODES.contains(classCode)) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } else if (DATA_INTEGRITY_VIOLATION_CODES.contains(classCode)) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } else if (DATA_ACCESS_RESOURCE_FAILURE_CODES.contains(classCode)) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } else if (TRANSIENT_DATA_ACCESS_RESOURCE_CODES.contains(classCode)) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } else if (CONCURRENCY_FAILURE_CODES.contains(classCode)) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + } + + // Couldn't resolve anything proper - resort to UncategorizedR2dbcException. + return null; + } + + /** + * Gets the SQL state code from the supplied {@link R2dbcException exception}. + *

+ * Some R2DBC drivers nest the actual exception from a batched update, so we might need to dig down into the nested + * exception. + * + * @param ex the exception from which the {@link R2dbcException#getSqlState() SQL state} is to be extracted. + * @return the SQL state code. + */ + @Nullable + private String getSqlState(R2dbcException ex) { + + String sqlState = ex.getSqlState(); + + if (sqlState == null) { + + for (Throwable throwable : ex.getSuppressed()) { + + if (!(throwable instanceof R2dbcException)) { + continue; + } + + R2dbcException r2dbcException = (R2dbcException) throwable; + if (r2dbcException.getSqlState() != null) { + sqlState = r2dbcException.getSqlState(); + break; + } + } + } + + return sqlState; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java index 3f06dfdb..2ce51549 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java @@ -25,6 +25,7 @@ import reactor.test.StepVerifier; import org.junit.Before; import org.junit.Test; +import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.jdbc.core.mapping.Table; @@ -75,6 +76,27 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } + @Test + public void shouldTranslateDuplicateKeyException() { + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + executeInsert(); + + databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull("$3") // + .fetch().rowsUpdated() // + .as(StepVerifier::create) // + .expectErrorSatisfies(exception -> { + + assertThat(exception).isInstanceOf(DuplicateKeyException.class) + .hasMessageContaining("execute; SQL [INSERT INTO legoset"); + }) // + .verify(); + } + @Test public void executeSelect() { diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java new file mode 100644 index 00000000..3f3fb136 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.R2dbcException; + +import org.junit.Test; +import org.springframework.dao.CannotAcquireLockException; +import org.springframework.dao.CannotSerializeTransactionException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.DeadlockLoserDataAccessException; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.InvalidResultAccessException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.jdbc.support.SQLErrorCodes; + +/** + * Unit tests for {@link SqlErrorCodeR2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public class SqlErrorCodeR2dbcExceptionTranslatorUnitTests { + + private static SQLErrorCodes ERROR_CODES = new SQLErrorCodes(); + static { + ERROR_CODES.setBadSqlGrammarCodes("1", "2"); + ERROR_CODES.setInvalidResultSetAccessCodes("3", "4"); + ERROR_CODES.setDuplicateKeyCodes("10"); + ERROR_CODES.setDataAccessResourceFailureCodes("5"); + ERROR_CODES.setDataIntegrityViolationCodes("6"); + ERROR_CODES.setCannotAcquireLockCodes("7"); + ERROR_CODES.setDeadlockLoserCodes("8"); + ERROR_CODES.setCannotSerializeTransactionCodes("9"); + } + + @Test + public void shouldTranslateToBadGrammarException() { + + R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES); + + R2dbcException cause = new MyR2dbcException("", "", 1); + BadSqlGrammarException exception = (BadSqlGrammarException) sut.translate("task", "SQL", cause); + + assertThat(exception.getSql()).isEqualTo("SQL"); + assertThat(exception.getR2dbcException()).isEqualTo(cause); + } + + @Test + public void shouldTranslateToResultException() { + + R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES); + + R2dbcException cause = new MyR2dbcException("", "", 4); + InvalidResultAccessException exception = (InvalidResultAccessException) sut.translate("task", "SQL", cause); + + assertThat(exception.getSql()).isEqualTo("SQL"); + assertThat(exception.getR2dbcException()).isEqualTo(cause); + } + + @Test + public void shouldFallbackToUncategorized() { + + R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES); + + // Test fallback. We assume that no database will ever return this error code, + // but 07xxx will be bad grammar picked up by the fallback SQLState translator + R2dbcException cause = new MyR2dbcException("", "07xxx", 666666666); + UncategorizedR2dbcException exception = (UncategorizedR2dbcException) sut.translate("task", "SQL2", cause); + + assertThat(exception.getSql()).isEqualTo("SQL2"); + assertThat(exception.getR2dbcException()).isEqualTo(cause); + } + + @Test + public void shouldTranslateDataIntegrityViolationException() { + + R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES); + + R2dbcException cause = new MyR2dbcException("", "", 10); + DataAccessException exception = sut.translate("task", "SQL", cause); + + assertThat(exception).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test + public void errorCodeTranslation() { + + R2dbcExceptionTranslator sut = new SqlErrorCodeR2dbcExceptionTranslator(ERROR_CODES); + + checkTranslation(sut, 5, DataAccessResourceFailureException.class); + checkTranslation(sut, 6, DataIntegrityViolationException.class); + checkTranslation(sut, 7, CannotAcquireLockException.class); + checkTranslation(sut, 8, DeadlockLoserDataAccessException.class); + checkTranslation(sut, 9, CannotSerializeTransactionException.class); + checkTranslation(sut, 10, DuplicateKeyException.class); + } + + private static void checkTranslation(R2dbcExceptionTranslator sext, int errorCode, Class exClass) { + + R2dbcException cause = new MyR2dbcException("", "", errorCode); + DataAccessException exception = sext.translate("", "", cause); + + assertThat(exception).isInstanceOf(exClass).hasCause(cause); + } + + @Test + public void shouldApplyCustomTranslation() { + + String TASK = "TASK"; + String SQL = "SQL SELECT *"; + DataAccessException custom = new DataAccessException("") {}; + + R2dbcException cause = new MyR2dbcException("", "", 1); + R2dbcException intVioEx = new MyR2dbcException("", "", 6); + + SqlErrorCodeR2dbcExceptionTranslator translator = new SqlErrorCodeR2dbcExceptionTranslator() { + @Override + protected DataAccessException customTranslate(String task, String sql, R2dbcException sqlex) { + + assertThat(task).isEqualTo(TASK); + assertThat(sql).isEqualTo(SQL); + return (sqlex == cause) ? custom : null; + } + }; + translator.setSqlErrorCodes(ERROR_CODES); + + // Shouldn't custom translate this + assertThat(translator.translate(TASK, SQL, cause)).isEqualTo(custom); + + DataIntegrityViolationException diex = (DataIntegrityViolationException) translator.translate(TASK, SQL, intVioEx); + assertThat(diex).hasCause(intVioEx); + } + + static class MyR2dbcException extends R2dbcException { + + MyR2dbcException(String reason, String sqlState, int errorCode) { + super(reason, sqlState, errorCode); + } + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java new file mode 100644 index 00000000..39b6f8f5 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.R2dbcException; + +import org.junit.Test; +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; + +/** + * Unit tests for {@link SqlStateR2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public class SqlStateR2dbcExceptionTranslatorUnitTests { + + private static final String REASON = "The game is afoot!"; + private static final String TASK = "Counting sheep... yawn."; + private static final String SQL = "select count(0) from t_sheep where over_fence = ... yawn... 1"; + + @Test(expected = IllegalArgumentException.class) + public void testTranslateNullException() { + new SqlStateR2dbcExceptionTranslator().translate("", "", null); + } + + @Test + public void testTranslateBadSqlGrammar() { + doTest("07", BadSqlGrammarException.class); + } + + @Test + public void testTranslateDataIntegrityViolation() { + doTest("23", DataIntegrityViolationException.class); + } + + @Test + public void testTranslateDataAccessResourceFailure() { + doTest("53", DataAccessResourceFailureException.class); + } + + @Test + public void testTranslateTransientDataAccessResourceFailure() { + doTest("S1", TransientDataAccessResourceException.class); + } + + @Test + public void testTranslateConcurrencyFailure() { + doTest("40", ConcurrencyFailureException.class); + } + + @Test + public void testTranslateUncategorized() { + doTest("00000000", UncategorizedR2dbcException.class); + } + + private static void doTest(String sqlState, Class dataAccessExceptionType) { + + R2dbcException ex = new R2dbcException(REASON, sqlState) {}; + SqlStateR2dbcExceptionTranslator translator = new SqlStateR2dbcExceptionTranslator(); + DataAccessException dax = translator.translate(TASK, SQL, ex); + + assertThat(dax).isNotNull().isInstanceOf(dataAccessExceptionType).hasCause(ex); + } +} From cccc3808aaa8d051905ae08144d4787f6cd218c5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Jun 2018 11:12:21 +0200 Subject: [PATCH 020/665] #2 - Polishing. --- .../r2dbc/function/connectionfactory/ConnectionProxy.java | 2 +- .../data/r2dbc/function/convert/EntityRowMapper.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index 09b16ac1..2bb0ff29 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -34,7 +34,7 @@ public interface ConnectionProxy extends Connection, Wrapper { *

* This will typically be the native driver Connection or a wrapper from a connection pool. * - * @return the underlying Connection (never {@code null}) + * @return the underlying Connection (never {@literal null}) */ Connection getTargetConnection(); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index bcf32bde..7d21d345 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -89,10 +89,10 @@ public class EntityRowMapper implements BiFunction { /** * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. * - * @param row the {@link Row} to extract the value from. Must not be {@code null}. - * @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@code null}. - * @param prefix to be used for all column names accessed by this method. Must not be {@code null}. - * @return the value read from the {@link ResultSet}. May be {@code null}. + * @param row the {@link Row} to extract the value from. Must not be {@literal null}. + * @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@literal null}. + * @param prefix to be used for all column names accessed by this method. Must not be {@literal null}. + * @return the value read from the {@link ResultSet}. May be {@literal null}. */ private Object readFrom(Row row, JdbcPersistentProperty property, String prefix) { From 900918fa5404a73d7c95dc4dbb48a1358e45301b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 22 Jun 2018 11:45:10 +0200 Subject: [PATCH 021/665] #2 - Use java.lang.Long as fallback Id type. We now use java.lang.Long as fallback Id type for entities that do not declare an explicit Id property. Previously we used ObjectId which was a left-over from the initial draft of MappingRelationalEntityInformation. --- .../repository/query/RelationalEntityInformation.java | 2 +- .../support/MappingRelationalEntityInformation.java | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java index c331c0a8..c03f202e 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java @@ -18,7 +18,7 @@ package org.springframework.data.relational.repository.query; import org.springframework.data.repository.core.EntityInformation; /** - * JDBC specific {@link EntityInformation}. + * Relational database-specific {@link EntityInformation}. * * @author Mark Paluch */ diff --git a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java index 6e77a52e..3f75d91b 100644 --- a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java +++ b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java @@ -20,11 +20,11 @@ import org.springframework.data.relational.repository.query.RelationalEntityInfo import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.lang.Nullable; -import com.sun.corba.se.spi.ior.ObjectId; - /** * {@link RelationalEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the * necessary information. Can be configured with a custom table name. + *

+ * Entity types that do not declare an explicit Id type fall back to {@link Long} as Id type. * * @author Mark Paluch */ @@ -82,11 +82,11 @@ public class MappingRelationalEntityInformation extends PersistentEntityI this.entityMetadata = entity; this.customTableName = customTableName; - this.fallbackIdType = idType != null ? idType : (Class) ObjectId.class; + this.fallbackIdType = idType != null ? idType : (Class) Long.class; } /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.JdbcEntityInformation#getTableName() + * @see org.springframework.data.relational.repository.query.RelationalEntityInformation#getTableName() */ public String getTableName() { return customTableName == null ? entityMetadata.getTableName() : customTableName; From bea95010ae90a0f86940422a302fb188e9b580ca Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Fri, 22 Jun 2018 15:16:30 +0200 Subject: [PATCH 022/665] #2 - Fixes and improves the Dependency test. Now considers the new packages appropriately as well. consideres the `repository` package to be part of the sub-module `repository.reactive`. --- .../data/jdbc/degraph/DependencyTests.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java b/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java index 97a3cefa..4e6a4a01 100644 --- a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java +++ b/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java @@ -19,9 +19,9 @@ import static de.schauderhaft.degraph.check.JCheck.*; import static org.junit.Assert.*; import de.schauderhaft.degraph.check.JCheck; +import de.schauderhaft.degraph.configuration.NamedPattern; import scala.runtime.AbstractFunction1; -import org.junit.Ignore; import org.junit.Test; /** @@ -38,13 +38,14 @@ public class DependencyTests { classpath() // .noJars() // .including("org.springframework.data.jdbc.**") // + .including("org.springframework.data.relational.**") // + .including("org.springframework.data.r2dbc.**") // .filterClasspath("*target/classes") // exclude test code - .printOnFailure("degraph.graphml"), + .withSlicing("modules", "org.springframework.data.(*).**").printOnFailure("degraph.graphml"), JCheck.violationFree()); } @Test // DATAJDBC-220 - @Ignore("I don't understand why this fails after adding reactive repos - mp911de") public void acrossModules() { assertThat( // @@ -60,8 +61,11 @@ public class DependencyTests { }) // exclude test code .withSlicing("sub-modules", // sub-modules are defined by any of the following pattern. "org.springframework.data.jdbc.(**).*", // + "org.springframework.data.relational.(**).*", // + new NamedPattern("org.springframework.data.r2dbc.**", "repository.reactive"), // "org.springframework.data.(**).*") // .printTo("degraph-across-modules.graphml"), // writes a graphml to this location JCheck.violationFree()); } + } From 853b3fb449445bfd01bbcf4ec75caa65af2ef960 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 22 Jun 2018 15:58:28 +0200 Subject: [PATCH 023/665] #2 - Adapt code to API changes. --- .../DefaultReactiveDataAccessStrategy.java | 26 +++++++------- .../function/convert/EntityRowMapper.java | 35 ++++++++++--------- .../convert/MappingR2dbcConverter.java | 18 +++++----- .../repository/query/R2dbcQueryExecution.java | 6 ++-- .../repository/query/R2dbcQueryMethod.java | 14 ++++---- .../support/R2dbcRepositoryFactory.java | 12 +++---- .../query/DtoInstantiatingConverter.java | 6 ++-- .../query/RelationalEntityMetadata.java | 6 ++-- .../query/SimpleRelationalEntityMetadata.java | 12 +++---- .../MappingRelationalEntityInformation.java | 28 +++++++-------- .../DatabaseClientIntegrationTests.java | 2 +- .../R2dbcRepositoryIntegrationTests.java | 6 ++-- .../query/R2dbcQueryMethodUnitTests.java | 6 ++-- .../query/StringBasedR2dbcQueryUnitTests.java | 7 ++-- .../R2dbcRepositoryFactoryUnitTests.java | 6 ++-- ...SimpleR2dbcRepositoryIntegrationTests.java | 13 ++++--- 16 files changed, 102 insertions(+), 101 deletions(-) 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 8a5b9bd1..c6d52cab 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -27,11 +27,11 @@ import java.util.stream.Collectors; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.r2dbc.function.convert.EntityRowMapper; +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.util.Pair; import org.springframework.data.util.StreamUtils; import org.springframework.util.ClassUtils; @@ -41,14 +41,14 @@ import org.springframework.util.ClassUtils; */ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy { - private final JdbcMappingContext mappingContext; + private final RelationalMappingContext mappingContext; private final EntityInstantiators instantiators; public DefaultReactiveDataAccessStrategy() { - this(new JdbcMappingContext(), new EntityInstantiators()); + this(new RelationalMappingContext(), new EntityInstantiators()); } - public DefaultReactiveDataAccessStrategy(JdbcMappingContext mappingContext, EntityInstantiators instantiators) { + public DefaultReactiveDataAccessStrategy(RelationalMappingContext mappingContext, EntityInstantiators instantiators) { this.mappingContext = mappingContext; this.instantiators = instantiators; } @@ -56,14 +56,14 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra @Override public List getAllFields(Class typeToRead) { - JdbcPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeToRead); + RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeToRead); if (persistentEntity == null) { return Collections.singletonList("*"); } return StreamUtils.createStreamFromIterator(persistentEntity.iterator()) // - .map(JdbcPersistentProperty::getColumnName) // + .map(RelationalPersistentProperty::getColumnName) // .collect(Collectors.toList()); } @@ -72,12 +72,12 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra Class userClass = ClassUtils.getUserClass(object); - JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); List> values = new ArrayList<>(); - for (JdbcPersistentProperty property : entity) { + for (RelationalPersistentProperty property : entity) { Object value = propertyAccessor.getProperty(property); @@ -94,7 +94,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra @Override public Sort getMappedSort(Class typeToRead, Sort sort) { - JdbcPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); + RelationalPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); if (entity == null) { return sort; } @@ -103,7 +103,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra for (Order order : sort) { - JdbcPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty()); + RelationalPersistentProperty persistentProperty = entity.getPersistentProperty(order.getProperty()); if (persistentProperty == null) { mappedOrder.add(order); } else { @@ -117,7 +117,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra @Override public BiFunction getRowMapper(Class typeToRead) { - return new EntityRowMapper((JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), + return new EntityRowMapper((RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), instantiators, mappingContext); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index 7d21d345..22f45c63 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -25,9 +25,6 @@ import java.util.function.BiFunction; import org.springframework.core.convert.ConversionService; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -35,6 +32,9 @@ import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.ClassUtils; /** @@ -45,13 +45,13 @@ import org.springframework.util.ClassUtils; */ public class EntityRowMapper implements BiFunction { - private final JdbcPersistentEntity entity; + private final RelationalPersistentEntity entity; private final EntityInstantiators entityInstantiators; private final ConversionService conversions; - private final MappingContext, JdbcPersistentProperty> context; + private final MappingContext, RelationalPersistentProperty> context; - public EntityRowMapper(JdbcPersistentEntity entity, EntityInstantiators entityInstantiators, - JdbcMappingContext context) { + public EntityRowMapper(RelationalPersistentEntity entity, EntityInstantiators entityInstantiators, + RelationalMappingContext context) { this.entity = entity; this.entityInstantiators = entityInstantiators; @@ -67,7 +67,7 @@ public class EntityRowMapper implements BiFunction { ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result), conversions); - for (JdbcPersistentProperty property : entity) { + for (RelationalPersistentProperty property : entity) { if (entity.isConstructorArgument(property)) { continue; @@ -90,11 +90,12 @@ public class EntityRowMapper implements BiFunction { * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument. * * @param row the {@link Row} to extract the value from. Must not be {@literal null}. - * @param property the {@link JdbcPersistentProperty} for which the value is intended. Must not be {@literal null}. + * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be + * {@literal null}. * @param prefix to be used for all column names accessed by this method. Must not be {@literal null}. * @return the value read from the {@link ResultSet}. May be {@literal null}. */ - private Object readFrom(Row row, JdbcPersistentProperty property, String prefix) { + private Object readFrom(Row row, RelationalPersistentProperty property, String prefix) { try { @@ -109,7 +110,7 @@ public class EntityRowMapper implements BiFunction { } } - private static Class getType(JdbcPersistentProperty property) { + private static Class getType(RelationalPersistentProperty property) { return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType()); } @@ -118,7 +119,7 @@ public class EntityRowMapper implements BiFunction { String prefix = property.getName() + "_"; @SuppressWarnings("unchecked") - JdbcPersistentEntity entity = (JdbcPersistentEntity) context + RelationalPersistentEntity entity = (RelationalPersistentEntity) context .getRequiredPersistentEntity(property.getActualType()); if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) { @@ -130,7 +131,7 @@ public class EntityRowMapper implements BiFunction { PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); - for (JdbcPersistentProperty p : entity) { + for (RelationalPersistentProperty p : entity) { if (!entity.isConstructorArgument(property)) { propertyAccessor.setProperty(p, readFrom(row, p, prefix)); } @@ -139,17 +140,17 @@ public class EntityRowMapper implements BiFunction { return instance; } - private S createInstance(Row row, String prefix, JdbcPersistentEntity entity) { + private S createInstance(Row row, String prefix, RelationalPersistentEntity entity) { return entityInstantiators.getInstantiatorFor(entity).createInstance(entity, new RowParameterValueProvider(row, entity, conversions, prefix)); } @RequiredArgsConstructor - private static class RowParameterValueProvider implements ParameterValueProvider { + private static class RowParameterValueProvider implements ParameterValueProvider { private final @NonNull Row resultSet; - private final @NonNull JdbcPersistentEntity entity; + private final @NonNull RelationalPersistentEntity entity; private final @NonNull ConversionService conversionService; private final @NonNull String prefix; @@ -158,7 +159,7 @@ public class EntityRowMapper implements BiFunction { * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter) */ @Override - public T getParameterValue(Parameter parameter) { + public T getParameterValue(Parameter parameter) { String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index fae18435..6f712d0e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -23,10 +23,10 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -37,10 +37,10 @@ import org.springframework.util.ClassUtils; */ public class MappingR2dbcConverter { - private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingContext, RelationalPersistentProperty> mappingContext; public MappingR2dbcConverter( - MappingContext, JdbcPersistentProperty> mappingContext) { + MappingContext, RelationalPersistentProperty> mappingContext) { this.mappingContext = mappingContext; } @@ -56,13 +56,13 @@ public class MappingR2dbcConverter { Assert.notNull(object, "Entity object must not be null!"); Class userClass = ClassUtils.getUserClass(object); - JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); Map> update = new LinkedHashMap<>(); PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); - for (JdbcPersistentProperty property : entity) { + for (RelationalPersistentProperty property : entity) { update.put(property.getColumnName(), Optional.ofNullable(propertyAccessor.getProperty(property))); } @@ -82,12 +82,12 @@ public class MappingR2dbcConverter { Assert.notNull(object, "Entity object must not be null!"); Class userClass = ClassUtils.getUserClass(object); - JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); return (row, metadata) -> { PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); - JdbcPersistentProperty idProperty = entity.getRequiredIdProperty(); + RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); if (propertyAccessor.getProperty(idProperty) == null) { @@ -99,7 +99,7 @@ public class MappingR2dbcConverter { }; } - public MappingContext, JdbcPersistentProperty> getMappingContext() { + public MappingContext, RelationalPersistentProperty> getMappingContext() { return mappingContext; } } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index 6c846eff..f0f45e90 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -20,10 +20,10 @@ import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.DtoInstantiatingConverter; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -64,7 +64,7 @@ interface R2dbcQueryExecution { final class ResultProcessingConverter implements Converter { private final @NonNull ResultProcessor processor; - private final @NonNull MappingContext, JdbcPersistentProperty> mappingContext; + private final @NonNull MappingContext, RelationalPersistentProperty> mappingContext; private final @NonNull EntityInstantiators instantiators; /* (non-Javadoc) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index 55249bca..3ebf2014 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -26,11 +26,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.relational.repository.query.RelationalParameters; import org.springframework.data.relational.repository.query.SimpleRelationalEntityMetadata; @@ -56,7 +56,7 @@ public class R2dbcQueryMethod extends QueryMethod { private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); private final Method method; - private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingContext, RelationalPersistentProperty> mappingContext; private final Optional query; private @Nullable RelationalEntityMetadata metadata; @@ -70,7 +70,7 @@ public class R2dbcQueryMethod extends QueryMethod { * @param mappingContext must not be {@literal null}. */ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, - MappingContext, JdbcPersistentProperty> mappingContext) { + MappingContext, RelationalPersistentProperty> mappingContext) { super(method, metadata, projectionFactory); @@ -162,11 +162,11 @@ public class R2dbcQueryMethod extends QueryMethod { } else { - JdbcPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); - JdbcPersistentEntity managedEntity = mappingContext.getRequiredPersistentEntity(domainClass); + RelationalPersistentEntity returnedEntity = mappingContext.getPersistentEntity(returnedObjectType); + RelationalPersistentEntity managedEntity = mappingContext.getRequiredPersistentEntity(domainClass); returnedEntity = returnedEntity == null || returnedEntity.getType().isInterface() ? managedEntity : returnedEntity; - JdbcPersistentEntity tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity + RelationalPersistentEntity tableEntity = domainClass.isAssignableFrom(returnedObjectType) ? returnedEntity : managedEntity; this.metadata = new SimpleRelationalEntityMetadata<>((Class) returnedEntity.getType(), tableEntity); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index a4482bec..35b5fd2a 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -21,8 +21,6 @@ import lombok.RequiredArgsConstructor; import java.lang.reflect.Method; import java.util.Optional; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; @@ -30,6 +28,8 @@ import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod; import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.data.repository.core.NamedQueries; @@ -54,7 +54,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private final DatabaseClient databaseClient; - private final MappingContext, JdbcPersistentProperty> mappingContext; + private final MappingContext, RelationalPersistentProperty> mappingContext; private final MappingR2dbcConverter converter; /** @@ -64,7 +64,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { * @param mappingContext must not be {@literal null}. */ public R2dbcRepositoryFactory(DatabaseClient databaseClient, - MappingContext, JdbcPersistentProperty> mappingContext) { + MappingContext, RelationalPersistentProperty> mappingContext) { Assert.notNull(databaseClient, "DatabaseClient must not be null!"); Assert.notNull(mappingContext, "MappingContext must not be null!"); @@ -118,9 +118,9 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { private RelationalEntityInformation getEntityInformation(Class domainClass, @Nullable RepositoryInformation information) { - JdbcPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); - return new MappingRelationalEntityInformation<>((JdbcPersistentEntity) entity); + return new MappingRelationalEntityInformation<>((RelationalPersistentEntity) entity); } /** diff --git a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java index 0d8f1ebc..a40eedae 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java @@ -18,8 +18,6 @@ package org.springframework.data.relational.repository.query; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentProperty; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -28,6 +26,8 @@ import org.springframework.data.mapping.PreferredConstructor.Parameter; import org.springframework.data.mapping.SimplePropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ParameterValueProvider; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; /** @@ -49,7 +49,7 @@ public class DtoInstantiatingConverter implements Converter { * @param instantiators must not be {@literal null}. */ public DtoInstantiatingConverter(Class dtoType, - MappingContext, JdbcPersistentProperty> context, + MappingContext, RelationalPersistentProperty> context, EntityInstantiators instantiator) { Assert.notNull(dtoType, "DTO type must not be null!"); diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java index 92fff24f..8691cb5a 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java @@ -15,7 +15,7 @@ */ package org.springframework.data.relational.repository.query; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.repository.core.EntityMetadata; /** @@ -33,9 +33,9 @@ public interface RelationalEntityMetadata extends EntityMetadata { String getTableName(); /** - * Returns the {@link JdbcPersistentEntity} that supposed to determine the table to be queried. + * Returns the {@link RelationalPersistentEntity} that supposed to determine the table to be queried. * * @return */ - JdbcPersistentEntity getTableEntity(); + RelationalPersistentEntity getTableEntity(); } diff --git a/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java index 9e650912..f64c7f51 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java +++ b/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java @@ -17,7 +17,7 @@ package org.springframework.data.relational.repository.query; import lombok.Getter; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.util.Assert; /** @@ -28,16 +28,16 @@ import org.springframework.util.Assert; public class SimpleRelationalEntityMetadata implements RelationalEntityMetadata { private final Class type; - private final @Getter JdbcPersistentEntity tableEntity; + private final @Getter RelationalPersistentEntity tableEntity; /** - * Creates a new {@link SimpleRelationalEntityMetadata} using the given type and {@link JdbcPersistentEntity} to use - * for table lookups. + * Creates a new {@link SimpleRelationalEntityMetadata} using the given type and {@link RelationalPersistentEntity} to + * use for table lookups. * * @param type must not be {@literal null}. * @param tableEntity must not be {@literal null}. */ - public SimpleRelationalEntityMetadata(Class type, JdbcPersistentEntity tableEntity) { + public SimpleRelationalEntityMetadata(Class type, RelationalPersistentEntity tableEntity) { Assert.notNull(type, "Type must not be null!"); Assert.notNull(tableEntity, "Table entity must not be null!"); @@ -54,7 +54,7 @@ public class SimpleRelationalEntityMetadata implements RelationalEntityMetada } /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.JdbcEntityMetadata#getTableName() + * @see org.springframework.data.jdbc.repository.query.RelationalEntityMetadata#getTableName() */ public String getTableName() { return tableEntity.getTableName(); diff --git a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java index 3f75d91b..6231de0d 100644 --- a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java +++ b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java @@ -15,13 +15,13 @@ */ package org.springframework.data.relational.repository.support; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; import org.springframework.lang.Nullable; /** - * {@link RelationalEntityInformation} implementation using a {@link JdbcPersistentEntity} instance to lookup the + * {@link RelationalEntityInformation} implementation using a {@link RelationalPersistentEntity} instance to lookup the * necessary information. Can be configured with a custom table name. *

* Entity types that do not declare an explicit Id type fall back to {@link Long} as Id type. @@ -31,51 +31,51 @@ import org.springframework.lang.Nullable; public class MappingRelationalEntityInformation extends PersistentEntityInformation implements RelationalEntityInformation { - private final JdbcPersistentEntity entityMetadata; + private final RelationalPersistentEntity entityMetadata; private final @Nullable String customTableName; private final Class fallbackIdType; /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity}. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity}. * * @param entity must not be {@literal null}. */ - public MappingRelationalEntityInformation(JdbcPersistentEntity entity) { + public MappingRelationalEntityInformation(RelationalPersistentEntity entity) { this(entity, null, null); } /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity} and fallback - * identifier type. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity} and + * fallback identifier type. * * @param entity must not be {@literal null}. * @param fallbackIdType can be {@literal null}. */ - public MappingRelationalEntityInformation(JdbcPersistentEntity entity, @Nullable Class fallbackIdType) { + public MappingRelationalEntityInformation(RelationalPersistentEntity entity, @Nullable Class fallbackIdType) { this(entity, null, fallbackIdType); } /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity} and custom - * table name. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity} and + * custom table name. * * @param entity must not be {@literal null}. * @param customTableName can be {@literal null}. */ - public MappingRelationalEntityInformation(JdbcPersistentEntity entity, String customTableName) { + public MappingRelationalEntityInformation(RelationalPersistentEntity entity, String customTableName) { this(entity, customTableName, null); } /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link JdbcPersistentEntity}, collection - * name and identifier type. + * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity}, + * collection name and identifier type. * * @param entity must not be {@literal null}. * @param customTableName can be {@literal null}. * @param idType can be {@literal null}. */ @SuppressWarnings("unchecked") - private MappingRelationalEntityInformation(JdbcPersistentEntity entity, @Nullable String customTableName, + private MappingRelationalEntityInformation(RelationalPersistentEntity entity, @Nullable String customTableName, @Nullable Class idType) { super(entity); diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java index 2ce51549..77cdac61 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java @@ -28,8 +28,8 @@ import org.junit.Test; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; /** diff --git a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java index d18fdc5c..5a6679cd 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java @@ -32,13 +32,13 @@ import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.jdbc.core.JdbcTemplate; @@ -49,7 +49,7 @@ import org.springframework.jdbc.core.JdbcTemplate; */ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { - private static JdbcMappingContext mappingContext = new JdbcMappingContext(); + private static RelationalMappingContext mappingContext = new RelationalMappingContext(); private ConnectionFactory connectionFactory; private DatabaseClient databaseClient; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java index 1dcb41cd..415fcc2d 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -28,9 +28,9 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.repository.query.RelationalEntityMetadata; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; @@ -42,11 +42,11 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadat */ public class R2dbcQueryMethodUnitTests { - JdbcMappingContext context; + RelationalMappingContext context; @Before public void setUp() { - context = new JdbcMappingContext(); + this.context = new RelationalMappingContext(); } @Test diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index 9ddbbe1c..afcebfa9 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.r2dbc.repository.query; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -25,13 +26,13 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; @@ -52,7 +53,7 @@ public class StringBasedR2dbcQueryUnitTests { @Mock private DatabaseClient databaseClient; @Mock private GenericExecuteSpec bindSpec; - private JdbcMappingContext mappingContext; + private RelationalMappingContext mappingContext; private MappingR2dbcConverter converter; private ProjectionFactory factory; private RepositoryMetadata metadata; @@ -61,7 +62,7 @@ public class StringBasedR2dbcQueryUnitTests { @SuppressWarnings("unchecked") public void setUp() { - this.mappingContext = new JdbcMappingContext(); + this.mappingContext = new RelationalMappingContext(); this.converter = new MappingR2dbcConverter(this.mappingContext); this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.factory = new SpelAwareProxyProjectionFactory(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 5448c977..30f91bbe 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -23,9 +23,9 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.data.repository.Repository; @@ -40,7 +40,7 @@ public class R2dbcRepositoryFactoryUnitTests { @Mock DatabaseClient databaseClient; @Mock @SuppressWarnings("rawtypes") MappingContext mappingContext; - @Mock @SuppressWarnings("rawtypes") JdbcPersistentEntity entity; + @Mock @SuppressWarnings("rawtypes") RelationalPersistentEntity entity; @Before @SuppressWarnings("unchecked") @@ -50,7 +50,7 @@ public class R2dbcRepositoryFactoryUnitTests { @Test @SuppressWarnings("unchecked") - public void usesMappingJdbcEntityInformationIfMappingContextSet() { + public void usesMappingRelationalEntityInformationIfMappingContextSet() { R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext); RelationalEntityInformation entityInformation = factory.getEntityInformation(Person.class); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index 6a42ce25..8d9b49d7 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -34,13 +34,13 @@ import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; -import org.springframework.data.jdbc.core.mapping.JdbcPersistentEntity; -import org.springframework.data.jdbc.core.mapping.Table; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.jdbc.core.JdbcTemplate; @@ -52,7 +52,7 @@ import org.springframework.jdbc.core.JdbcTemplate; */ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport { - private static JdbcMappingContext mappingContext = new JdbcMappingContext(); + private static RelationalMappingContext mappingContext = new RelationalMappingContext(); private ConnectionFactory connectionFactory; private DatabaseClient databaseClient; @@ -69,7 +69,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); RelationalEntityInformation entityInformation = new MappingRelationalEntityInformation<>( - (JdbcPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); + (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, new MappingR2dbcConverter(mappingContext)); @@ -93,8 +93,7 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS .consumeNextWith(actual -> { assertThat(actual.getId()).isNotNull(); - }) - .verifyComplete(); + }).verifyComplete(); Map map = jdbc.queryForMap("SELECT * FROM repo_legoset"); assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 12).containsKey("id"); From 1464df99d428efdabdc6f8670513ff50709808f8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 26 Jun 2018 16:26:00 +0200 Subject: [PATCH 024/665] #2 - Add transactional support. We now support transaction hosting and transaction management via TransactionalDatabaseClient. TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); Flux integerFlux = databaseClient.inTransaction(db -> { return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // .bindNull("$3") // .fetch().rowsUpdated(); }); --- .../DefaultDatabaseClientBuilder.java | 16 +- .../DefaultTransactionalDatabaseClient.java | 167 ++++++++++++ ...ultTransactionalDatabaseClientBuilder.java | 99 +++++++ .../function/TransactionalDatabaseClient.java | 202 ++++++++++++++ .../ConnectionFactoryUtils.java | 249 ++++++++++++++++++ .../DefaultTransactionResources.java | 51 ++++ .../ReactiveTransactionSynchronization.java | 87 ++++++ .../SingletonConnectionFactory.java | 85 ++++++ .../SmartConnectionFactory.java | 44 ++++ .../TransactionResources.java | 58 ++++ ...ctionalDatabaseClientIntegrationTests.java | 181 +++++++++++++ .../ConnectionFactoryUtilsUnitTests.java | 93 +++++++ .../R2dbcRepositoryIntegrationTests.java | 31 +++ 13 files changed, 1357 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 8ae87bf7..57c64fcd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -33,9 +33,9 @@ import org.springframework.util.Assert; */ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { - private @Nullable ConnectionFactory connector; - private @Nullable R2dbcExceptionTranslator exceptionTranslator; - private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); + @Nullable ConnectionFactory connector; + @Nullable R2dbcExceptionTranslator exceptionTranslator; + ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); DefaultDatabaseClientBuilder() {} @@ -44,7 +44,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!"); this.connector = other.connector; - this.exceptionTranslator = exceptionTranslator; + this.exceptionTranslator = other.exceptionTranslator; } @Override @@ -83,8 +83,12 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connector); } - return new DefaultDatabaseClient(this.connector, exceptionTranslator, accessStrategy, - new DefaultDatabaseClientBuilder(this)); + return doBuild(this.connector, exceptionTranslator, this.accessStrategy, new DefaultDatabaseClientBuilder(this)); + } + + protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, + ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) { + return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, builder); } @Override diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java new file mode 100644 index 00000000..9206c244 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java @@ -0,0 +1,167 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; +import reactor.util.function.Tuple2; + +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils; +import org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization; +import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.transaction.NoTransactionException; + +/** + * Default implementation of a {@link TransactionalDatabaseClient}. + * + * @author Mark Paluch + */ +class DefaultTransactionalDatabaseClient extends DefaultDatabaseClient implements TransactionalDatabaseClient { + + DefaultTransactionalDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, + ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { + super(connector, exceptionTranslator, dataAccessStrategy, builder); + } + + @Override + public TransactionalDatabaseClient.Builder mutate() { + return (TransactionalDatabaseClient.Builder) super.mutate(); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.TransactionalDatabaseClient#beginTransaction() + */ + @Override + public Mono beginTransaction() { + + Mono transactional = ConnectionFactoryUtils.currentReactiveTransactionSynchronization() // + .map(synchronization -> { + + TransactionResources transactionResources = TransactionResources.create(); + // TODO: This Tx management code creating a TransactionContext. Find a better place. + synchronization.registerTransaction(transactionResources); + return transactionResources; + }); + + return transactional.flatMap(it -> { + return ConnectionFactoryUtils.doGetConnection(obtainConnectionFactory()); + }).flatMap(it -> Mono.from(it.getT1().beginTransaction())); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.TransactionalDatabaseClient#commitTransaction() + */ + @Override + public Mono commitTransaction() { + return cleanup(Connection::commitTransaction); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.TransactionalDatabaseClient#rollbackTransaction() + */ + @Override + public Mono rollbackTransaction() { + return cleanup(Connection::rollbackTransaction); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.TransactionalDatabaseClient#inTransaction(java.util.function.Function) + */ + @Override + public Flux inTransaction(Function> callback) { + + return Flux.usingWhen(beginTransaction().thenReturn(this), callback, // + DefaultTransactionalDatabaseClient::commitTransaction, // + DefaultTransactionalDatabaseClient::rollbackTransaction, // + DefaultTransactionalDatabaseClient::rollbackTransaction) // + .subscriberContext(DefaultTransactionalDatabaseClient::withTransactionSynchronization); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClient#getConnection() + */ + @Override + protected Mono getConnection() { + return ConnectionFactoryUtils.getConnection(obtainConnectionFactory()).map(Tuple2::getT1); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClient#closeConnection(io.r2dbc.spi.Connection) + */ + @Override + protected Publisher closeConnection(Connection connection) { + + return Mono.subscriberContext().flatMap(context -> { + + if (context.hasKey(ReactiveTransactionSynchronization.class)) { + + return ConnectionFactoryUtils.currentConnectionFactory() + .flatMap(it -> ConnectionFactoryUtils.releaseConnection(connection, it)); + } + + return Mono.from(connection.close()); + }); + } + + /** + * Execute a transactional cleanup. Also, deregister the current {@link TransactionResources synchronization} element. + */ + private static Mono cleanup(Function> callback) { + + return ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() // + .flatMap(synchronization -> { + + TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); + + ConnectionFactory connectionFactory = currentSynchronization.getResource(ConnectionFactory.class); + + if (connectionFactory == null) { + throw new NoTransactionException("No ConnectionFactory attached"); + } + + return Mono.from(connectionFactory.create()) + .flatMap(connection -> Mono.from(callback.apply(connection)) + .then(ConnectionFactoryUtils.releaseConnection(connection, connectionFactory)) + .then(ConnectionFactoryUtils.closeConnection(connection, connectionFactory))) // TODO: Is this rather + // related to + // TransactionContext + // cleanup? + .doFinally(s -> synchronization.unregisterTransaction(currentSynchronization)); + }); + } + + /** + * Potentially register a {@link ReactiveTransactionSynchronization} in the {@link Context} if no synchronization + * object is registered. + * + * @param context the subscriber context. + * @return subscriber context with a registered synchronization. + */ + static Context withTransactionSynchronization(Context context) { + + // associate synchronizer object to host transactional resources. + // TODO: Should be moved to a better place. + return context.put(ReactiveTransactionSynchronization.class, + context.getOrDefault(ReactiveTransactionSynchronization.class, new ReactiveTransactionSynchronization())); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java new file mode 100644 index 00000000..7e322e05 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java @@ -0,0 +1,99 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import java.util.function.Consumer; + +import org.springframework.data.r2dbc.function.DatabaseClient.Builder; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.util.Assert; + +/** + * @author Mark Paluch + */ +class DefaultTransactionalDatabaseClientBuilder extends DefaultDatabaseClientBuilder + implements TransactionalDatabaseClient.Builder { + + DefaultTransactionalDatabaseClientBuilder() {} + + DefaultTransactionalDatabaseClientBuilder(DefaultDatabaseClientBuilder other) { + + Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!"); + + this.connector = other.connector; + this.exceptionTranslator = other.exceptionTranslator; + } + + @Override + public DatabaseClient.Builder clone() { + return new DefaultTransactionalDatabaseClientBuilder(this); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#connectionFactory(io.r2dbc.spi.ConnectionFactory) + */ + @Override + public TransactionalDatabaseClient.Builder connectionFactory(ConnectionFactory factory) { + super.connectionFactory(factory); + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#exceptionTranslator(org.springframework.data.r2dbc.support.R2dbcExceptionTranslator) + */ + @Override + public TransactionalDatabaseClient.Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) { + super.exceptionTranslator(exceptionTranslator); + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#dataAccessStrategy(org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy) + */ + @Override + public TransactionalDatabaseClient.Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) { + super.dataAccessStrategy(accessStrategy); + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#apply(java.util.function.Consumer) + */ + @Override + public TransactionalDatabaseClient.Builder apply(Consumer builderConsumer) { + super.apply(builderConsumer); + return this; + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#build() + */ + @Override + public TransactionalDatabaseClient build() { + return (TransactionalDatabaseClient) super.build(); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#doBuild(io.r2dbc.spi.ConnectionFactory, org.springframework.data.r2dbc.support.R2dbcExceptionTranslator, org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy, org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder) + */ + @Override + protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, + ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) { + return new DefaultTransactionalDatabaseClient(connector, exceptionTranslator, accessStrategy, builder); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java new file mode 100644 index 00000000..a747032c --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java @@ -0,0 +1,202 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Consumer; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.util.Assert; + +/** + * {@link DatabaseClient} that participates in an ongoing transaction if the subscription happens within a hosted + * transaction. Alternatively, transactions can be started and cleaned up using {@link #beginTransaction()} and + * {@link #commitTransaction()}. + *

+ * Transactional resources are bound to {@link ReactiveTransactionSynchronization} through nested + * {@link TransactionContext} enabling nested (parallel) transactions. The simplemost approach to use transactions is by + * using {@link #inTransaction(Function)} which will start a transaction and commit it on successful termination. The + * callback allows execution of multiple statements within the same transaction. + * + *

+ * Flux transactionalFlux = databaseClient.inTransaction(db -> {
+ *
+ * 	return db.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES($1, $2, $3)") //
+ * 			.bind(0, 1) //
+ * 			.bind(1, "Walter") //
+ * 			.bind(2, "White") //
+ * 			.fetch().rowsUpdated();
+ * });
+ * 
+ * + * Alternatively, transactions can be controlled by using {@link #beginTransaction()} and {@link #commitTransaction()} + * methods. This approach requires {@link #enableTransactionSynchronization(Publisher) enabling of transaction + * synchronization} for the transactional operation. + * + *
+ * Mono mono = databaseClient.beginTransaction()
+ * 		.then(databaseClient.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES($1, $2, $3)") //
+ * 				.bind(0, 1) //
+ * 				.bind(1, "Walter") //
+ * 				.bind(2, "White") //
+ * 				.fetch().rowsUpdated())
+ * 		.then(databaseClient.commitTransaction());
+ *
+ * Mono transactionalMono = databaseClient.enableTransactionSynchronization(mono);
+ * 
+ *

+ * This {@link DatabaseClient} can be safely used without transaction synchronization to invoke database functionality + * in auto-commit transactions. + * + * @author Mark Paluch + * @see #inTransaction(Function) + * @see #enableTransactionSynchronization(Publisher) + * @see #beginTransaction() + * @see #commitTransaction() + * @see #rollbackTransaction() + * @see org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization + * @see TransactionResources + * @see org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils + */ +public interface TransactionalDatabaseClient extends DatabaseClient { + + /** + * Start a transaction and bind connection resources to the subscriber context. + * + * @return + */ + Mono beginTransaction(); + + /** + * Commit a transaction and unbind connection resources from the subscriber context. + * + * @return + * @throws org.springframework.transaction.NoTransactionException if no transaction is ongoing. + */ + Mono commitTransaction(); + + /** + * Rollback a transaction and unbind connection resources from the subscriber context. + * + * @return + * @throws org.springframework.transaction.NoTransactionException if no transaction is ongoing. + */ + Mono rollbackTransaction(); + + /** + * Execute a {@link Function} accepting a {@link DatabaseClient} within a managed transaction. {@link Exception Error + * signals} cause the transaction to be rolled back. + * + * @param callback + * @return the callback result. + */ + Flux inTransaction(Function> callback); + + /** + * Enable transaction management so that connections can be bound to the subscription. + * + * @param publisher must not be {@literal null}. + * @return the Transaction-enabled {@link Mono}. + */ + default Mono enableTransactionSynchronization(Mono publisher) { + + Assert.notNull(publisher, "Publisher must not be null!"); + + return publisher.subscriberContext(DefaultTransactionalDatabaseClient::withTransactionSynchronization); + } + + /** + * Enable transaction management so that connections can be bound to the subscription. + * + * @param publisher must not be {@literal null}. + * @return the Transaction-enabled {@link Flux}. + */ + default Flux enableTransactionSynchronization(Publisher publisher) { + + Assert.notNull(publisher, "Publisher must not be null!"); + + return Flux.from(publisher).subscriberContext(DefaultTransactionalDatabaseClient::withTransactionSynchronization); + } + + /** + * Return a builder to mutate properties of this database client. + */ + TransactionalDatabaseClient.Builder mutate(); + + // Static, factory methods + + /** + * A variant of {@link #create(ConnectionFactory)} that accepts a {@link io.r2dbc.spi.ConnectionFactory}. + */ + static TransactionalDatabaseClient create(ConnectionFactory factory) { + return (TransactionalDatabaseClient) new DefaultTransactionalDatabaseClientBuilder().connectionFactory(factory) + .build(); + } + + /** + * Obtain a {@code DatabaseClient} builder. + */ + static TransactionalDatabaseClient.Builder builder() { + return new DefaultTransactionalDatabaseClientBuilder(); + } + + /** + * A mutable builder for creating a {@link TransactionalDatabaseClient}. + */ + interface Builder extends DatabaseClient.Builder { + + /** + * Configures the {@link ConnectionFactory R2DBC connector}. + * + * @param factory must not be {@literal null}. + * @return {@code this} {@link DatabaseClient.Builder}. + */ + Builder connectionFactory(ConnectionFactory factory); + + /** + * Configures a {@link R2dbcExceptionTranslator}. + * + * @param exceptionTranslator must not be {@literal null}. + * @return {@code this} {@link DatabaseClient.Builder}. + */ + Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator); + + /** + * Configures a {@link ReactiveDataAccessStrategy}. + * + * @param accessStrategy must not be {@literal null}. + * @return {@code this} {@link DatabaseClient.Builder}. + */ + Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy); + + /** + * Configures a {@link Consumer} to configure this builder. + * + * @param builderConsumer must not be {@literal null}. + * @return {@code this} {@link DatabaseClient.Builder}. + */ + Builder apply(Consumer builderConsumer); + + @Override + TransactionalDatabaseClient build(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java new file mode 100644 index 00000000..d2e60f96 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java @@ -0,0 +1,249 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.lang.Nullable; +import org.springframework.transaction.NoTransactionException; +import org.springframework.util.Assert; + +/** + * Helper class that provides static methods for obtaining R2DBC Connections from a + * {@link io.r2dbc.spi.ConnectionFactory}. + *

+ * Used internally by Spring's {@link org.springframework.data.r2dbc.function.DatabaseClient}, Spring's R2DBC operation + * objects. Can also be used directly in application code. + * + * @author Mark Paluch + */ +public class ConnectionFactoryUtils { + + private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class); + + /** + * Obtain a {@link io.r2dbc.spi.Connection} from the given {@link io.r2dbc.spi.ConnectionFactory}. Translates + * exceptions into the Spring hierarchy of unchecked generic data access exceptions, simplifying calling code and + * making any exception that is thrown more meaningful. + *

+ * Is aware of a corresponding Connection bound to the current {@link reactor.util.context.Context}. Will bind a + * Connection to the {@link reactor.util.context.Context} if transaction synchronization is active. + * + * @param connectionFactory the {@link io.r2dbc.spi.ConnectionFactory} to obtain Connections from + * @return a R2DBC Connection from the given {@link io.r2dbc.spi.ConnectionFactory}. + * @throws DataAccessResourceFailureException if the attempt to get a {@link io.r2dbc.spi.Connection} failed + * @see #releaseConnection + */ + public static Mono> getConnection(ConnectionFactory connectionFactory) { + return doGetConnection(connectionFactory) + .onErrorMap(e -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", e)); + } + + /** + * Actually obtain a R2DBC Connection from the given {@link ConnectionFactory}. Same as {@link #getConnection}, but + * preserving the original exceptions. + *

+ * Is aware of a corresponding Connection bound to the current {@link reactor.util.context.Context}. Will bind a + * Connection to the {@link reactor.util.context.Context} if transaction synchronization is active. + * + * @param connectionFactory the {@link ConnectionFactory} to obtain Connections from. + * @return a R2DBC {@link io.r2dbc.spi.Connection} from the given {@link ConnectionFactory}. + */ + public static Mono> doGetConnection(ConnectionFactory connectionFactory) { + + Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); + + return Mono.subscriberContext().flatMap(it -> { + + if (it.hasKey(ReactiveTransactionSynchronization.class)) { + + ReactiveTransactionSynchronization synchronization = it.get(ReactiveTransactionSynchronization.class); + + return obtainConnection(synchronization, connectionFactory); + } + return Mono.empty(); + }).switchIfEmpty(Mono.defer(() -> { + return Mono.from(connectionFactory.create()).map(it -> Tuples.of(it, connectionFactory)); + })); + } + + private static Mono> obtainConnection( + ReactiveTransactionSynchronization synchronization, ConnectionFactory connectionFactory) { + + if (synchronization.isSynchronizationActive()) { + + logger.debug("Registering transaction synchronization for R2DBC Connection"); + + TransactionResources txContext = synchronization.getCurrentTransaction(); + ConnectionFactory resource = txContext.getResource(ConnectionFactory.class); + + Mono> attachNewConnection = Mono + .defer(() -> Mono.from(connectionFactory.create()).map(it -> { + + logger.debug("Fetching new R2DBC Connection from ConnectionFactory"); + + SingletonConnectionFactory s = new SingletonConnectionFactory(connectionFactory.getMetadata(), it); + txContext.registerResource(ConnectionFactory.class, s); + + return Tuples.of(it, connectionFactory); + })); + + return Mono.justOrEmpty(resource).flatMap(factory -> { + + logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); + + return Mono.from(factory.create()) + .map(connection -> Tuples. of(connection, factory)); + + }).switchIfEmpty(attachNewConnection); + } + + return Mono.empty(); + } + + /** + * Close the given {@link io.r2dbc.spi.Connection}, obtained from the given {@link ConnectionFactory}, if it is not + * managed externally (that is, not bound to the thread). + * + * @param con the {@link io.r2dbc.spi.Connection} to close if necessary. + * @param connectionFactory the {@link ConnectionFactory} that the Connection was obtained from (may be + * {@literal null}). + * @see #getConnection + */ + public static Mono releaseConnection(@Nullable io.r2dbc.spi.Connection con, + @Nullable ConnectionFactory connectionFactory) { + + return doReleaseConnection(con, connectionFactory) + .onErrorMap(e -> new DataAccessResourceFailureException("Failed to close R2DBC Connection", e)); + } + + /** + * Actually close the given {@link io.r2dbc.spi.Connection}, obtained from the given {@link ConnectionFactory}. Same + * as {@link #releaseConnection}, but preserving the original exception. + * + * @param con the {@link io.r2dbc.spi.Connection} to close if necessary. + * @param connectionFactory the {@link ConnectionFactory} that the Connection was obtained from (may be + * {@literal null}). + * @see #doGetConnection + */ + public static Mono doReleaseConnection(@Nullable io.r2dbc.spi.Connection con, + @Nullable ConnectionFactory connectionFactory) { + + if (connectionFactory instanceof SingletonConnectionFactory) { + + SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory; + + logger.debug("Releasing R2DBC Connection"); + + return factory.close(con); + } + + logger.debug("Closing R2DBC Connection"); + + return Mono.from(con.close()); + } + + /** + * Close the {@link io.r2dbc.spi.Connection}. Translates exceptions into the Spring hierarchy of unchecked generic + * data access exceptions, simplifying calling code and making any exception that is thrown more meaningful. + * + * @param connectionFactory the {@link io.r2dbc.spi.ConnectionFactory} to obtain Connections from + * @return a R2DBC Connection from the given {@link io.r2dbc.spi.ConnectionFactory}. + * @throws DataAccessResourceFailureException if the attempt to get a {@link io.r2dbc.spi.Connection} failed + */ + public static Mono closeConnection(Connection connection, ConnectionFactory connectionFactory) { + return doCloseConnection(connection, connectionFactory) + .onErrorMap(e -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", e)); + } + + /** + * Close the {@link io.r2dbc.spi.Connection}, unless a {@link SmartConnectionFactory} doesn't want us to. + * + * @param connection the {@link io.r2dbc.spi.Connection} to close if necessary. + * @param connectionFactory the {@link ConnectionFactory} that the Connection was obtained from. + * @see Connection#close() + * @see SmartConnectionFactory#shouldClose(Connection) + */ + public static Mono doCloseConnection(Connection connection, @Nullable ConnectionFactory connectionFactory) { + + if (!(connectionFactory instanceof SingletonConnectionFactory) + || ((SingletonConnectionFactory) connectionFactory).shouldClose(connection)) { + + SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory; + return factory.close(connection).then(Mono.from(connection.close())); + } + + return Mono.empty(); + } + + /** + * Obtain the currently {@link ReactiveTransactionSynchronization} from the current subscriber + * {@link reactor.util.context.Context}. + * + * @see Mono#subscriberContext() + * @see ReactiveTransactionSynchronization + * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the + * current subscription. + */ + public static Mono currentReactiveTransactionSynchronization() { + + return Mono.subscriberContext().filter(it -> it.hasKey(ReactiveTransactionSynchronization.class)) // + .switchIfEmpty(Mono.error(new NoTransactionException( + "Transaction management is not enabled. Make sure to register ReactiveTransactionSynchronization in the subscriber Context!"))) // + .map(it -> it.get(ReactiveTransactionSynchronization.class)); + } + + /** + * Obtain the currently active {@link ReactiveTransactionSynchronization} from the current subscriber + * {@link reactor.util.context.Context}. + * + * @see Mono#subscriberContext() + * @see ReactiveTransactionSynchronization + * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the + * current subscription. + */ + public static Mono currentActiveReactiveTransactionSynchronization() { + + return currentReactiveTransactionSynchronization() + .filter(ReactiveTransactionSynchronization::isSynchronizationActive) // + .switchIfEmpty(Mono.error(new NoTransactionException("ReactiveTransactionSynchronization not active!"))); + } + + /** + * Obtain the {@link io.r2dbc.spi.ConnectionFactory} from the current subscriber {@link reactor.util.context.Context}. + * + * @see Mono#subscriberContext() + * @see ReactiveTransactionSynchronization + * @see TransactionResources + */ + public static Mono currentConnectionFactory() { + + return currentActiveReactiveTransactionSynchronization() // + .map(synchronization -> { + + TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); + return currentSynchronization.getResource(ConnectionFactory.class); + }).switchIfEmpty(Mono.error(new DataAccessResourceFailureException( + "Cannot extract ConnectionFactory from current TransactionContext!"))); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java new file mode 100644 index 00000000..853448dc --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java @@ -0,0 +1,51 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.util.Assert; + +/** + * Default implementation of {@link TransactionResources}. + * + * @author Mark Paluch + */ +class DefaultTransactionResources implements TransactionResources { + + private Map, Object> items = new ConcurrentHashMap<>(); + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#registerResource(java.lang.Class, java.lang.Object) + */ + @Override + public void registerResource(Class key, T value) { + + Assert.state(!items.containsKey(key), () -> String.format("Resource for %s is already bound", key)); + + items.put(key, value); + } + + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#getResource(java.lang.Class) + */ + @SuppressWarnings("unchecked") + @Override + public T getResource(Class key) { + return (T) items.get(key); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java new file mode 100644 index 00000000..af2ab934 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java @@ -0,0 +1,87 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import java.util.Stack; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Central delegate that manages transactional resources. To be used by resource management code but not by typical + * application code. + *

+ * Supports a list of transactional resources if synchronization is active. + *

+ * Resource management code should check for subscriber {@link reactor.util.context.Context}-bound resources, e.g. R2DBC + * Connections using {@link TransactionResources#getResource(Class)}. Such code is normally not supposed to bind + * resources, as this is the responsibility of transaction managers. A further option is to lazily bind on first use if + * transaction synchronization is active, for performing transactions that span an arbitrary number of resources. + *

+ * Transaction synchronization must be activated and deactivated by a transaction manager by registering + * {@link ReactiveTransactionSynchronization} in the {@link reactor.util.context.Context subscriber context}. + * + * @author Mark Paluch + */ +public class ReactiveTransactionSynchronization { + + private Stack resources = new Stack<>(); + + /** + * Return if transaction synchronization is active for the current {@link reactor.util.context.Context}. Can be called + * before register to avoid unnecessary instance creation. + */ + public boolean isSynchronizationActive() { + return !resources.isEmpty(); + } + + /** + * Create a new transaction span and register a {@link TransactionResources} instance. + * + * @param transactionResources must not be {@literal null}. + */ + public void registerTransaction(TransactionResources transactionResources) { + + Assert.notNull(transactionResources, "TransactionContext must not be null!"); + + resources.push(transactionResources); + } + + /** + * Unregister a transaction span and by removing {@link TransactionResources} instance. + * + * @param transactionResources must not be {@literal null}. + */ + public void unregisterTransaction(TransactionResources transactionResources) { + + Assert.notNull(transactionResources, "TransactionContext must not be null!"); + + resources.remove(transactionResources); + } + + /** + * @return obtain the current {@link TransactionResources} or {@literal null} if none is present. + */ + @Nullable + public TransactionResources getCurrentTransaction() { + + if (!resources.isEmpty()) { + return resources.peek(); + } + + return null; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java new file mode 100644 index 00000000..5610bf79 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import reactor.core.publisher.Mono; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.reactivestreams.Publisher; + +/** + * Connection holder, wrapping a R2DBC Connection. + * {@link org.springframework.data.r2dbc.function.TransactionalDatabaseClient} binds instances of this class to the + * {@link TransactionResources} for a specific subscription. + * + * @author Mark Paluch + */ +class SingletonConnectionFactory implements SmartConnectionFactory { + + private final ConnectionFactoryMetadata metadata; + private final Connection connection; + private final Mono connectionMono; + private final AtomicInteger refCount = new AtomicInteger(); + + SingletonConnectionFactory(ConnectionFactoryMetadata metadata, Connection connection) { + + this.metadata = metadata; + this.connection = connection; + this.connectionMono = Mono.just(connection); + } + + /* (non-Javadoc) + * @see io.r2dbc.spi.ConnectionFactory#create() + */ + @Override + public Publisher create() { + + if (refCount.get() == -1) { + throw new IllegalStateException("Connection is closed!"); + } + + return connectionMono.doOnSubscribe(s -> refCount.incrementAndGet()); + } + + /* (non-Javadoc) + * @see io.r2dbc.spi.ConnectionFactory#getMetadata() + */ + @Override + public ConnectionFactoryMetadata getMetadata() { + return metadata; + } + + private boolean connectionEquals(Connection connection) { + return this.connection == connection; + } + + @Override + public boolean shouldClose(Connection connection) { + return refCount.get() == 1; + } + + Mono close(Connection connection) { + + if (connectionEquals(connection)) { + return Mono. empty().doOnSubscribe(s -> refCount.decrementAndGet()); + } + + throw new IllegalArgumentException("Connection is not associated with this connection factory"); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java new file mode 100644 index 00000000..221cdc54 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; + +/** + * Extension of the {@code io.r2dbc.spi.ConnectionFactory} interface, to be implemented by special connection factories + * that return R2DBC Connections in an unwrapped fashion. + *

+ * Classes using this interface can query whether or not the {@link Connection} should be closed after an operation. + * Spring's {@link ConnectionFactoryUtils} automatically perform such a check. + * + * @author Mark Paluch + * @see ConnectionFactoryUtils#closeConnection + */ +public interface SmartConnectionFactory extends ConnectionFactory { + + /** + * Should we close this {@link io.r2dbc.spi.Connection}, obtained from this {@code io.r2dbc.spi.ConnectionFactory}? + *

+ * Code that uses Connections from a SmartConnectionFactory should always perform a check via this method before + * invoking {@code close()}. + * + * @param connection the {@link io.r2dbc.spi.Connection} to check. + * @return whether the given {@link Connection} should be closed. + * @see io.r2dbc.spi.Connection#close() + */ + boolean shouldClose(Connection connection); +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java new file mode 100644 index 00000000..119c6ca8 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import reactor.core.publisher.Mono; + +/** + * Transaction context for an ongoing transaction synchronization allowing to register transactional resources. + *

+ * Supports one resource per key without overwriting, that is, a resource needs to be removed before a new one can be + * set for the same key. + *

+ * Primarily used by {@link ConnectionFactoryUtils} but can be also used by application code to register resources that + * should be bound to a transaction. + * + * @author Mark Paluch + */ +public interface TransactionResources { + + /** + * Creates a new empty {@link TransactionResources}. + * + * @return the empty {@link TransactionResources}. + */ + static TransactionResources create() { + return new DefaultTransactionResources(); + } + + /** + * Retrieve a resource from this context identified by {@code key}. + * + * @param key the resource key. + * @return the resource emitted through {@link Mono} or {@link Mono#empty()} if the resource was not found. + */ + T getResource(Class key); + + /** + * Register a resource in this context. + * + * @param key the resource key. + * @param value can be a subclass of the {@code key} type. + * @throws IllegalStateException if a resource is already bound under {@code key}. + */ + void registerResource(Class key, T value); +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..ade4922c --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,181 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.NoTransactionException; + +/** + * Integration tests for {@link TransactionalDatabaseClient}. + * + * @author Mark Paluch + */ +public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport { + + private ConnectionFactory connectionFactory; + + private JdbcTemplate jdbc; + + @Before + public void before() { + + Hooks.onOperatorDebug(); + + connectionFactory = createConnectionFactory(); + + String tableToCreate = "CREATE TABLE IF NOT EXISTS legoset (\n" + + " id integer CONSTRAINT id PRIMARY KEY,\n" + " name varchar(255) NOT NULL,\n" + + " manual integer NULL\n" + ");"; + + jdbc = createJdbcTemplate(createDataSource()); + jdbc.execute(tableToCreate); + jdbc.execute("DELETE FROM legoset"); + } + + @Test + public void executeInsertInManagedTransaction() { + + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Flux integerFlux = databaseClient.inTransaction(db -> { + + return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull("$3") // + .fetch().rowsUpdated(); + }); + + integerFlux.as(StepVerifier::create) // + .expectNext(1) // + .verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + + @Test + public void executeInsertInAutoCommitTransaction() { + + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Mono integerFlux = databaseClient.execute() + .sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull("$3") // + .fetch().rowsUpdated(); + + integerFlux.as(StepVerifier::create) // + .expectNext(1) // + .verifyComplete(); + + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); + } + + @Test + public void shouldManageUserTransaction() { + + Queue transactionIds = new ArrayBlockingQueue<>(5); + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Flux txId = databaseClient.execute().sql("SELECT txid_current();").exchange() + .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + + Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() // + .thenMany(txId.concatWith(txId).doOnNext(transactionIds::add)) // + .then(databaseClient.rollbackTransaction())); + + then.as(StepVerifier::create) // + .verifyComplete(); + + List listOfTxIds = new ArrayList<>(transactionIds); + assertThat(listOfTxIds).hasSize(2); + assertThat(listOfTxIds).containsExactly(listOfTxIds.get(1), listOfTxIds.get(0)); + } + + @Test + public void userTransactionManagementShouldFailWithoutSynchronizer() { + + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Mono then = databaseClient.beginTransaction().then(databaseClient.rollbackTransaction()); + + then.as(StepVerifier::create) // + .consumeErrorWith(exception -> { + + assertThat(exception).isInstanceOf(NoTransactionException.class) + .hasMessageContaining("Transaction management is not enabled"); + }).verify(); + } + + @Test + public void shouldRollbackTransaction() { + + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Flux integerFlux = databaseClient.inTransaction(db -> { + + return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull("$3") // + .fetch().rowsUpdated().then(Mono.error(new IllegalStateException("failed"))); + }); + + integerFlux.as(StepVerifier::create) // + .expectError(IllegalStateException.class) // + .verify(); + + assertThat(jdbc.queryForMap("SELECT count(*) FROM legoset")).containsEntry("count", 0L); + } + + @Test + public void emitTransactionIds() { + + TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + + Flux transactionIds = databaseClient.inTransaction(db -> { + + Flux txId = db.execute().sql("SELECT txid_current();").exchange() + .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + return txId.concatWith(txId); + }); + + transactionIds.collectList().as(StepVerifier::create) // + .consumeNextWith(actual -> { + + assertThat(actual).hasSize(2); + assertThat(actual).containsExactly(actual.get(1), actual.get(0)); + }) // + .verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java new file mode 100644 index 00000000..75634058 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.test.StepVerifier; + +import org.junit.Test; +import org.springframework.transaction.NoTransactionException; + +/** + * Unit tests for {@link ConnectionFactoryUtils}. + * + * @author Mark Paluch + */ +public class ConnectionFactoryUtilsUnitTests { + + @Test + public void currentReactiveTransactionSynchronizationShouldReportSynchronization() { + + ConnectionFactoryUtils.currentReactiveTransactionSynchronization() // + .subscriberContext( + it -> it.put(ReactiveTransactionSynchronization.class, new ReactiveTransactionSynchronization())) + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + } + + @Test + public void currentReactiveTransactionSynchronizationShouldFailWithoutTxMgmt() { + + ConnectionFactoryUtils.currentReactiveTransactionSynchronization() // + .as(StepVerifier::create) // + .expectError(NoTransactionException.class) // + .verify(); + } + + @Test + public void currentActiveReactiveTransactionSynchronizationShouldReportSynchronization() { + + ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() // + .subscriberContext(it -> { + ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization(); + sync.registerTransaction(TransactionResources.create()); + return it.put(ReactiveTransactionSynchronization.class, sync); + }).as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + } + + @Test + public void currentActiveReactiveTransactionSynchronization() { + + ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() // + .subscriberContext( + it -> it.put(ReactiveTransactionSynchronization.class, new ReactiveTransactionSynchronization())) + .as(StepVerifier::create) // + .expectError(NoTransactionException.class) // + .verify(); + } + + @Test + public void currentConnectionFactoryShouldReportConnectionFactory() { + + ConnectionFactory factoryMock = mock(ConnectionFactory.class); + + ConnectionFactoryUtils.currentConnectionFactory() // + .subscriberContext(it -> { + ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization(); + TransactionResources resources = TransactionResources.create(); + resources.registerResource(ConnectionFactory.class, factoryMock); + sync.registerTransaction(resources); + return it.put(ReactiveTransactionSynchronization.class, sync); + }).as(StepVerifier::create) // + .expectNext(factoryMock) // + .verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java index 5a6679cd..cc22129b 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java @@ -27,6 +27,8 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.util.Arrays; +import java.util.Collections; +import java.util.Map; import org.junit.Before; import org.junit.Test; @@ -36,6 +38,7 @@ import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; @@ -129,6 +132,34 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport }).verifyComplete(); } + @Test + public void shouldInsertItemsTransactional() { + + TransactionalDatabaseClient client = TransactionalDatabaseClient.builder().connectionFactory(connectionFactory) + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + + LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext) + .getRepository(LegoSetRepository.class); + + LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); + LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); + + Flux> transactional = client.inTransaction(db -> { + + return transactionalRepository.save(legoSet1) // + .map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset")); + }); + + Mono> nonTransactional = transactionalRepository.save(legoSet2) // + .map(it -> jdbc.queryForMap("SELECT count(*) FROM repo_legoset")); + + transactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 0L)).verifyComplete(); + nonTransactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 2L)).verifyComplete(); + + Map count = jdbc.queryForMap("SELECT count(*) FROM repo_legoset"); + assertThat(count).containsEntry("count", 2L); + } + interface LegoSetRepository extends ReactiveCrudRepository { @Query("SELECT * FROM repo_legoset WHERE name like $1") From f223475112c27d31de286ed802edcdc0ccd27b15 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 17 Aug 2018 16:24:56 +0200 Subject: [PATCH 025/665] #2 - Adapt to R2DBC API changes. --- .../data/r2dbc/function/DatabaseClient.java | 9 +- .../r2dbc/function/DefaultDatabaseClient.java | 108 +++++++++--------- .../DefaultReactiveDataAccessStrategy.java | 42 ++++--- .../function/ReactiveDataAccessStrategy.java | 55 ++++++++- .../function/convert/ColumnMapRowMapper.java | 4 +- .../function/convert/EntityRowMapper.java | 37 +++--- .../convert/MappingR2dbcConverter.java | 32 ++++-- .../repository/query/R2dbcQueryExecution.java | 2 +- .../repository/query/R2dbcQueryMethod.java | 4 +- .../query/StringBasedR2dbcQuery.java | 9 +- .../support/R2dbcRepositoryFactory.java | 3 +- .../support/SimpleR2dbcRepository.java | 12 +- .../query/DtoInstantiatingConverter.java | 2 +- .../query/RelationalParameterAccessor.java | 6 + ...RelationalParametersParameterAccessor.java | 9 ++ .../DatabaseClientIntegrationTests.java | 8 +- ...ctionalDatabaseClientIntegrationTests.java | 6 +- .../R2dbcRepositoryIntegrationTests.java | 8 +- .../query/StringBasedR2dbcQueryUnitTests.java | 6 +- ...SimpleR2dbcRepositoryIntegrationTests.java | 7 +- 20 files changed, 229 insertions(+), 140 deletions(-) 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 31161176..e587d072 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -348,8 +348,9 @@ public interface DatabaseClient { * Specify a {@literal null} value to insert. * * @param field must not be {@literal null} or empty. + * @param type must not be {@literal null}. */ - GenericInsertSpec nullValue(String field); + GenericInsertSpec nullValue(String field, Class type); } /** @@ -417,8 +418,9 @@ public interface DatabaseClient { * Bind a {@literal null} value to a parameter identified by its {@code index}. * * @param index + * @param type must not be {@literal null}. */ - S bindNull(int index); + S bindNull(int index, Class type); /** * Bind a non-{@literal null} value to a parameter identified by its {@code name}. @@ -432,8 +434,9 @@ public interface DatabaseClient { * Bind a {@literal null} value to a parameter identified by its {@code name}. * * @param name must not be {@literal null} or empty. + * @param type must not be {@literal null}. */ - S bindNull(String name); + S bindNull(String name, Class type); /** * Bind a bean according to Java {@link java.beans.BeanInfo Beans} using property names. 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 5c5adc44..0b4ce1e5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -36,7 +36,6 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import java.util.function.Function; @@ -53,10 +52,10 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.util.Pair; import org.springframework.jdbc.core.SqlProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -217,24 +216,24 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex)); } - private static void doBind(Statement statement, Map> byName, - Map> byIndex) { + private static void doBind(Statement statement, Map byName, + Map byIndex) { byIndex.forEach((i, o) -> { - if (o.isPresent()) { - o.ifPresent(v -> statement.bind(i, v)); + if (o.getValue() != null) { + statement.bind(i, o.getValue()); } else { - statement.bindNull(i, 0); // TODO: What is type? + statement.bindNull(i, o.getType()); } }); byName.forEach((name, o) -> { - if (o.isPresent()) { - o.ifPresent(v -> statement.bind(name, v)); + if (o.getValue() != null) { + statement.bind(name, o.getValue()); } else { - statement.bindNull(name, 0); // TODO: What is type? + statement.bindNull(name, o.getType()); } }); @@ -267,8 +266,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @RequiredArgsConstructor private class GenericExecuteSpecSupport { - final Map> byIndex; - final Map> byName; + final Map byIndex; + final Map byName; final Supplier sqlSupplier; GenericExecuteSpecSupport(Supplier sqlSupplier) { @@ -310,16 +309,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public GenericExecuteSpecSupport bind(int index, Object value) { - Map> byIndex = new LinkedHashMap<>(this.byIndex); - byIndex.put(index, Optional.of(value)); + Map byIndex = new LinkedHashMap<>(this.byIndex); + byIndex.put(index, new SettableValue(index, value, null)); return createInstance(byIndex, this.byName, this.sqlSupplier); } - public GenericExecuteSpecSupport bindNull(int index) { + public GenericExecuteSpecSupport bindNull(int index, Class type) { - Map> byIndex = new LinkedHashMap<>(this.byIndex); - byIndex.put(index, Optional.empty()); + Map byIndex = new LinkedHashMap<>(this.byIndex); + byIndex.put(index, new SettableValue(index, null, type)); return createInstance(byIndex, this.byName, this.sqlSupplier); } @@ -328,24 +327,24 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.hasText(name, "Parameter name must not be null or empty!"); - Map> byName = new LinkedHashMap<>(this.byName); - byName.put(name, Optional.of(value)); + Map byName = new LinkedHashMap<>(this.byName); + byName.put(name, new SettableValue(name, value, null)); return createInstance(this.byIndex, byName, this.sqlSupplier); } - public GenericExecuteSpecSupport bindNull(String name) { + public GenericExecuteSpecSupport bindNull(String name, Class type) { Assert.hasText(name, "Parameter name must not be null or empty!"); - Map> byName = new LinkedHashMap<>(this.byName); - byName.put(name, Optional.empty()); + Map byName = new LinkedHashMap<>(this.byName); + byName.put(name, new SettableValue(name, null, type)); return createInstance(this.byIndex, byName, this.sqlSupplier); } - protected GenericExecuteSpecSupport createInstance(Map> byIndex, - Map> byName, Supplier sqlSupplier) { + protected GenericExecuteSpecSupport createInstance(Map byIndex, + Map byName, Supplier sqlSupplier) { return new GenericExecuteSpecSupport(byIndex, byName, sqlSupplier); } @@ -362,7 +361,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { */ private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec { - DefaultGenericExecuteSpec(Map> byIndex, Map> byName, + DefaultGenericExecuteSpec(Map byIndex, Map byName, Supplier sqlSupplier) { super(byIndex, byName, sqlSupplier); } @@ -395,8 +394,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultGenericExecuteSpec bindNull(int index) { - return (DefaultGenericExecuteSpec) super.bindNull(index); + public DefaultGenericExecuteSpec bindNull(int index, Class type) { + return (DefaultGenericExecuteSpec) super.bindNull(index, type); } @Override @@ -405,8 +404,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultGenericExecuteSpec bindNull(String name) { - return (DefaultGenericExecuteSpec) super.bindNull(name); + public DefaultGenericExecuteSpec bindNull(String name, Class type) { + return (DefaultGenericExecuteSpec) super.bindNull(name, type); } @Override @@ -415,8 +414,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - protected GenericExecuteSpecSupport createInstance(Map> byIndex, - Map> byName, Supplier sqlSupplier) { + protected GenericExecuteSpecSupport createInstance(Map byIndex, + Map byName, Supplier sqlSupplier) { return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); } } @@ -430,7 +429,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final Class typeToRead; private final BiFunction mappingFunction; - DefaultTypedGenericExecuteSpec(Map> byIndex, Map> byName, + DefaultTypedGenericExecuteSpec(Map byIndex, Map byName, Supplier sqlSupplier, Class typeToRead) { super(byIndex, byName, sqlSupplier); @@ -463,8 +462,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultTypedGenericExecuteSpec bindNull(int index) { - return (DefaultTypedGenericExecuteSpec) super.bindNull(index); + public DefaultTypedGenericExecuteSpec bindNull(int index, Class type) { + return (DefaultTypedGenericExecuteSpec) super.bindNull(index, type); } @Override @@ -473,8 +472,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultTypedGenericExecuteSpec bindNull(String name) { - return (DefaultTypedGenericExecuteSpec) super.bindNull(name); + public DefaultTypedGenericExecuteSpec bindNull(String name, Class type) { + return (DefaultTypedGenericExecuteSpec) super.bindNull(name, type); } @Override @@ -483,8 +482,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - protected DefaultTypedGenericExecuteSpec createInstance(Map> byIndex, - Map> byName, Supplier sqlSupplier) { + protected DefaultTypedGenericExecuteSpec createInstance(Map byIndex, + Map byName, Supplier sqlSupplier) { return new DefaultTypedGenericExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); } } @@ -800,26 +799,26 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { class DefaultGenericInsertSpec implements GenericInsertSpec { private final String table; - private final Map> byName; + private final Map byName; @Override public GenericInsertSpec value(String field, Object value) { Assert.notNull(field, "Field must not be null!"); - Map> byName = new LinkedHashMap<>(this.byName); - byName.put(field, Optional.of(value)); + Map byName = new LinkedHashMap<>(this.byName); + byName.put(field, new SettableValue(field, value, null)); return new DefaultGenericInsertSpec(this.table, byName); } @Override - public GenericInsertSpec nullValue(String field) { + public GenericInsertSpec nullValue(String field, Class type) { Assert.notNull(field, "Field must not be null!"); - Map> byName = new LinkedHashMap<>(this.byName); - byName.put(field, Optional.empty()); + Map byName = new LinkedHashMap<>(this.byName); + byName.put(field, new SettableValue(field, null, type)); return new DefaultGenericInsertSpec(this.table, byName); } @@ -878,12 +877,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { AtomicInteger index = new AtomicInteger(); - for (Optional o : byName.values()) { + for (SettableValue value : byName.values()) { - if (o.isPresent()) { - o.ifPresent(v -> statement.bind(index.getAndIncrement(), v)); + if (value.getValue() != null) { + statement.bind(index.getAndIncrement(), value.getValue()); } else { - statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? + statement.bindNull("$" + (index.getAndIncrement() + 1), value.getType()); } } } @@ -944,8 +943,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { StringBuilder builder = new StringBuilder(); - List> insertValues = dataAccessStrategy.getInsert(toInsert); - String fieldNames = insertValues.stream().map(Pair::getFirst).collect(Collectors.joining(",")); + List insertValues = dataAccessStrategy.getInsert(toInsert); + String fieldNames = insertValues.stream().map(SettableValue::getIdentifier).map(Object::toString) + .collect(Collectors.joining(",")); String placeholders = IntStream.range(0, insertValues.size()).mapToObj(i -> "$" + (i + 1)) .collect(Collectors.joining(",")); @@ -964,12 +964,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { AtomicInteger index = new AtomicInteger(); - for (Pair pair : insertValues) { + for (SettableValue settable : insertValues) { - if (pair.getSecond() != null) { // TODO: Better type to transport null values. - statement.bind(index.getAndIncrement(), pair.getSecond()); + if (settable.getValue() != null) { + statement.bind(index.getAndIncrement(), settable.getValue()); } else { - statement.bindNull("$" + (index.getAndIncrement() + 1), 0); // TODO: What is type? + statement.bindNull("$" + (index.getAndIncrement() + 1), settable.getType()); } } 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 c6d52cab..332bcb49 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -24,16 +24,17 @@ import java.util.List; import java.util.function.BiFunction; import java.util.stream.Collectors; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.r2dbc.function.convert.EntityRowMapper; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; +import org.springframework.data.relational.core.conversion.RelationalConverter; 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.util.Pair; import org.springframework.data.util.StreamUtils; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -41,22 +42,20 @@ import org.springframework.util.ClassUtils; */ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy { - private final RelationalMappingContext mappingContext; - private final EntityInstantiators instantiators; + private final RelationalConverter relationalConverter; public DefaultReactiveDataAccessStrategy() { - this(new RelationalMappingContext(), new EntityInstantiators()); + this(new BasicRelationalConverter(new RelationalMappingContext())); } - public DefaultReactiveDataAccessStrategy(RelationalMappingContext mappingContext, EntityInstantiators instantiators) { - this.mappingContext = mappingContext; - this.instantiators = instantiators; + public DefaultReactiveDataAccessStrategy(RelationalConverter converter) { + this.relationalConverter = converter; } @Override public List getAllFields(Class typeToRead) { - RelationalPersistentEntity persistentEntity = mappingContext.getPersistentEntity(typeToRead); + RelationalPersistentEntity persistentEntity = getPersistentEntity(typeToRead); if (persistentEntity == null) { return Collections.singletonList("*"); @@ -68,14 +67,14 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra } @Override - public List> getInsert(Object object) { + public List getInsert(Object object) { Class userClass = ClassUtils.getUserClass(object); - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass); PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); - List> values = new ArrayList<>(); + List values = new ArrayList<>(); for (RelationalPersistentProperty property : entity) { @@ -85,7 +84,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra continue; } - values.add(Pair.of(property.getColumnName(), value)); + values.add(new SettableValue(property.getColumnName(), value, property.getType())); } return values; @@ -94,7 +93,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra @Override public Sort getMappedSort(Class typeToRead, Sort sort) { - RelationalPersistentEntity entity = mappingContext.getPersistentEntity(typeToRead); + RelationalPersistentEntity entity = getPersistentEntity(typeToRead); if (entity == null) { return sort; } @@ -117,12 +116,21 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra @Override public BiFunction getRowMapper(Class typeToRead) { - return new EntityRowMapper((RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(typeToRead), - instantiators, mappingContext); + return new EntityRowMapper((RelationalPersistentEntity) getRequiredPersistentEntity(typeToRead), + relationalConverter); } @Override public String getTableName(Class type) { - return mappingContext.getRequiredPersistentEntity(type).getTableName(); + return getRequiredPersistentEntity(type).getTableName(); + } + + private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { + return relationalConverter.getMappingContext().getRequiredPersistentEntity(typeToRead); + } + + @Nullable + private RelationalPersistentEntity getPersistentEntity(Class typeToRead) { + return relationalConverter.getMappingContext().getPersistentEntity(typeToRead); } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 566c2ae5..76711a5c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.function.BiFunction; import org.springframework.data.domain.Sort; -import org.springframework.data.util.Pair; +import org.springframework.lang.Nullable; /** * @author Mark Paluch @@ -31,7 +31,7 @@ public interface ReactiveDataAccessStrategy { List getAllFields(Class typeToRead); - List> getInsert(Object object); + List getInsert(Object object); Sort getMappedSort(Class typeToRead, Sort sort); @@ -39,4 +39,55 @@ public interface ReactiveDataAccessStrategy { BiFunction getRowMapper(Class typeToRead); String getTableName(Class type); + + /** + * A database value that can be set in a statement. + */ + class SettableValue { + + private final Object identifier; + private final @Nullable Object value; + private final Class type; + + /** + * Create a {@link SettableValue} using an integer index. + * + * @param index + * @param value + * @param type + */ + public SettableValue(int index, @Nullable Object value, Class type) { + + this.identifier = index; + this.value = value; + this.type = type; + } + + /** + * Create a {@link SettableValue} using a {@link String} identifier. + * + * @param identifier + * @param value + * @param type + */ + public SettableValue(String identifier, @Nullable Object value, Class type) { + + this.identifier = identifier; + this.value = value; + this.type = type; + } + + public Object getIdentifier() { + return identifier; + } + + @Nullable + public Object getValue() { + return value; + } + + public Class getType() { + return type; + } + } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java index acfa37d1..d7acd3c4 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java @@ -89,7 +89,7 @@ public class ColumnMapRowMapper implements BiFunction - * The default implementation uses the {@link Row#get(Object, Class)} method. + * The default implementation uses the {@link Row#get(Object)} method. * * @param row is the {@link Row} holding the data. * @param index is the column index. @@ -97,6 +97,6 @@ public class ColumnMapRowMapper implements BiFunction implements BiFunction { private final RelationalPersistentEntity entity; - private final EntityInstantiators entityInstantiators; - private final ConversionService conversions; - private final MappingContext, RelationalPersistentProperty> context; + private final RelationalConverter converter; - public EntityRowMapper(RelationalPersistentEntity entity, EntityInstantiators entityInstantiators, - RelationalMappingContext context) { + public EntityRowMapper(RelationalPersistentEntity entity, RelationalConverter converter) { this.entity = entity; - this.entityInstantiators = entityInstantiators; - this.conversions = context.getConversions(); - this.context = context; + this.converter = converter; } @Override @@ -65,7 +57,7 @@ public class EntityRowMapper implements BiFunction { T result = createInstance(row, "", entity); ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result), - conversions); + converter.getConversionService()); for (RelationalPersistentProperty property : entity) { @@ -103,23 +95,19 @@ public class EntityRowMapper implements BiFunction { return readEntityFrom(row, property); } - return row.get(prefix + property.getColumnName(), getType(property)); + return row.get(prefix + property.getColumnName()); } catch (Exception o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); } } - private static Class getType(RelationalPersistentProperty property) { - return ClassUtils.resolvePrimitiveIfNecessary(property.getActualType()); - } - private S readEntityFrom(Row row, PersistentProperty property) { String prefix = property.getName() + "_"; @SuppressWarnings("unchecked") - RelationalPersistentEntity entity = (RelationalPersistentEntity) context + RelationalPersistentEntity entity = (RelationalPersistentEntity) converter.getMappingContext() .getRequiredPersistentEntity(property.getActualType()); if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) { @@ -129,7 +117,8 @@ public class EntityRowMapper implements BiFunction { S instance = createInstance(row, prefix, entity); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, conversions); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, + converter.getConversionService()); for (RelationalPersistentProperty p : entity) { if (!entity.isConstructorArgument(property)) { @@ -142,8 +131,10 @@ public class EntityRowMapper implements BiFunction { private S createInstance(Row row, String prefix, RelationalPersistentEntity entity) { - return entityInstantiators.getInstantiatorFor(entity).createInstance(entity, - new RowParameterValueProvider(row, entity, conversions, prefix)); + RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, entity, + converter.getConversionService(), prefix); + + return converter.createInstance(entity, rowParameterValueProvider::getParameterValue); } @RequiredArgsConstructor @@ -164,7 +155,7 @@ public class EntityRowMapper implements BiFunction { String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName(); try { - return conversionService.convert(resultSet.get(column, parameter.getType().getType()), + return conversionService.convert(resultSet.get(column), parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 6f712d0e..21fb76cf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -23,8 +23,11 @@ import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; +import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; +import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.util.Assert; @@ -37,11 +40,10 @@ import org.springframework.util.ClassUtils; */ public class MappingR2dbcConverter { - private final MappingContext, RelationalPersistentProperty> mappingContext; + private final RelationalConverter relationalConverter; - public MappingR2dbcConverter( - MappingContext, RelationalPersistentProperty> mappingContext) { - this.mappingContext = mappingContext; + public MappingR2dbcConverter(RelationalConverter converter) { + this.relationalConverter = converter; } /** @@ -51,19 +53,20 @@ public class MappingR2dbcConverter { * @param object must not be {@literal null}. * @return */ - public Map> getFieldsToUpdate(Object object) { + public Map getFieldsToUpdate(Object object) { Assert.notNull(object, "Entity object must not be null!"); Class userClass = ClassUtils.getUserClass(object); - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(userClass); - Map> update = new LinkedHashMap<>(); + Map update = new LinkedHashMap<>(); PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); for (RelationalPersistentProperty property : entity) { - update.put(property.getColumnName(), Optional.ofNullable(propertyAccessor.getProperty(property))); + update.put(property.getColumnName(), + new SettableValue(property.getColumnName(), propertyAccessor.getProperty(property), property.getType())); } return update; @@ -82,7 +85,7 @@ public class MappingR2dbcConverter { Assert.notNull(object, "Entity object must not be null!"); Class userClass = ClassUtils.getUserClass(object); - RelationalPersistentEntity entity = mappingContext.getRequiredPersistentEntity(userClass); + RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(userClass); return (row, metadata) -> { @@ -91,7 +94,11 @@ public class MappingR2dbcConverter { if (propertyAccessor.getProperty(idProperty) == null) { - propertyAccessor.setProperty(idProperty, row.get(idProperty.getColumnName(), idProperty.getColumnType())); + ConversionService conversionService = relationalConverter.getConversionService(); + Object value = row.get(idProperty.getColumnName()); + + propertyAccessor.setProperty(idProperty, conversionService.convert(value, idProperty.getType())); + return (T) propertyAccessor.getBean(); } @@ -99,7 +106,8 @@ public class MappingR2dbcConverter { }; } - public MappingContext, RelationalPersistentProperty> getMappingContext() { - return mappingContext; + public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { + return relationalConverter.getMappingContext(); } } + diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index f0f45e90..41c53396 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -64,7 +64,7 @@ interface R2dbcQueryExecution { final class ResultProcessingConverter implements Converter { private final @NonNull ResultProcessor processor; - private final @NonNull MappingContext, RelationalPersistentProperty> mappingContext; + private final @NonNull MappingContext, ? extends RelationalPersistentProperty> mappingContext; private final @NonNull EntityInstantiators instantiators; /* (non-Javadoc) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index 3ebf2014..48f7fd5e 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -56,7 +56,7 @@ public class R2dbcQueryMethod extends QueryMethod { private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); private final Method method; - private final MappingContext, RelationalPersistentProperty> mappingContext; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; private final Optional query; private @Nullable RelationalEntityMetadata metadata; @@ -70,7 +70,7 @@ public class R2dbcQueryMethod extends QueryMethod { * @param mappingContext must not be {@literal null}. */ public R2dbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory projectionFactory, - MappingContext, RelationalPersistentProperty> mappingContext) { + MappingContext, ? extends RelationalPersistentProperty> mappingContext) { super(method, metadata, projectionFactory); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index ff388dcc..3ff5291b 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -20,6 +20,8 @@ import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; +import org.springframework.data.repository.query.Parameter; +import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -88,12 +90,17 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { T bindSpecToUse = bindSpec; // TODO: Encapsulate PostgreSQL-specific bindings + + Parameters bindableParameters = accessor.getBindableParameters(); + int index = 1; for (Object value : accessor.getValues()) { + Parameter bindableParameter = bindableParameters.getBindableParameter(index - 1); + if (value == null) { if (accessor.hasBindableNullValue()) { - bindSpecToUse = bindSpecToUse.bindNull("$" + (index++)); + bindSpecToUse = bindSpecToUse.bindNull("$" + (index++), bindableParameter.getType()); } } else { bindSpecToUse = bindSpecToUse.bind("$" + (index++), value); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 35b5fd2a..4d605451 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -28,6 +28,7 @@ import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod; import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.RelationalEntityInformation; @@ -71,7 +72,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { this.databaseClient = databaseClient; this.mappingContext = mappingContext; - this.converter = new MappingR2dbcConverter(mappingContext); + this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext)); } /* diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 943c43af..03537b9e 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -22,7 +22,6 @@ import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -31,6 +30,7 @@ import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; @@ -68,7 +68,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository> fields = converter.getFieldsToUpdate(objectToSave); + Map fields = converter.getFieldsToUpdate(objectToSave); String setClause = getSetClause(fields); @@ -77,13 +77,13 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository setValue : fields.values()) { + for (SettableValue setValue : fields.values()) { - Object value = setValue.orElse(null); + Object value = setValue.getValue(); if (value != null) { exec = exec.bind(index++, value); } else { - exec = exec.bindNull(index++); + exec = exec.bindNull(index++, setValue.getType()); } } @@ -93,7 +93,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository> fields) { + private static String getSetClause(Map fields) { StringBuilder setClause = new StringBuilder(); diff --git a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java index a40eedae..6299e38a 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java +++ b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java @@ -49,7 +49,7 @@ public class DtoInstantiatingConverter implements Converter { * @param instantiators must not be {@literal null}. */ public DtoInstantiatingConverter(Class dtoType, - MappingContext, RelationalPersistentProperty> context, + MappingContext, ? extends RelationalPersistentProperty> context, EntityInstantiators instantiator) { Assert.notNull(dtoType, "DTO type must not be null!"); diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java index 50f8aae0..52123569 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java @@ -16,6 +16,7 @@ package org.springframework.data.relational.repository.query; import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.Parameters; /** * JDBC-specific {@link ParameterAccessor}. @@ -28,4 +29,9 @@ public interface RelationalParameterAccessor extends ParameterAccessor { * Returns the raw parameter values of the underlying query method. */ Object[] getValues(); + + /** + * @return the bindable parameters. + */ + Parameters getBindableParameters(); } diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java index 62bfc09f..c62c7cb2 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java +++ b/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java @@ -18,6 +18,7 @@ package org.springframework.data.relational.repository.query; import java.util.Arrays; import java.util.List; +import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; @@ -50,4 +51,12 @@ public class RelationalParametersParameterAccessor extends ParametersParameterAc public Object[] getValues() { return values.toArray(); } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.repository.query.RelationalParameterAccessor#getBindableParameters() + */ + @Override + public Parameters getBindableParameters() { + return getParameters().getBindableParameters(); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java index 77cdac61..8ab0fda1 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java @@ -67,7 +67,7 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // - .bindNull("$3") // + .bindNull("$3", Integer.class) // .fetch().rowsUpdated() // .as(StepVerifier::create) // .expectNext(1) // @@ -86,7 +86,7 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport databaseClient.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // - .bindNull("$3") // + .bindNull("$3", Integer.class) // .fetch().rowsUpdated() // .as(StepVerifier::create) // .expectErrorSatisfies(exception -> { @@ -125,7 +125,7 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport databaseClient.insert().into("legoset")// .value("id", 42055) // .value("name", "SCHAUFELRADBAGGER") // - .nullValue("manual") // + .nullValue("manual", Integer.class) // .exchange() // .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) // .as(StepVerifier::create) // @@ -142,7 +142,7 @@ public class DatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport databaseClient.insert().into("legoset")// .value("id", 42055) // .value("name", "SCHAUFELRADBAGGER") // - .nullValue("manual") // + .nullValue("manual", Integer.class) // .then() // .as(StepVerifier::create) // .verifyComplete(); diff --git a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java index ade4922c..6a3d19cd 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java @@ -71,7 +71,7 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // - .bindNull("$3") // + .bindNull("$3", Integer.class) // .fetch().rowsUpdated(); }); @@ -91,7 +91,7 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio .sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // - .bindNull("$3") // + .bindNull("$3", Integer.class) // .fetch().rowsUpdated(); integerFlux.as(StepVerifier::create) // @@ -147,7 +147,7 @@ public class TransactionalDatabaseClientIntegrationTests extends R2dbcIntegratio return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // - .bindNull("$3") // + .bindNull("$3", Integer.class) // .fetch().rowsUpdated().then(Mono.error(new IllegalStateException("failed"))); }); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java index cc22129b..4dde9d5d 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java @@ -33,13 +33,13 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.repository.reactive.ReactiveCrudRepository; @@ -66,7 +66,8 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport this.connectionFactory = createConnectionFactory(); this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) - .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(new BasicRelationalConverter(mappingContext))) + .build(); this.jdbc = createJdbcTemplate(createDataSource()); @@ -136,7 +137,8 @@ public class R2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestSupport public void shouldInsertItemsTransactional() { TransactionalDatabaseClient client = TransactionalDatabaseClient.builder().connectionFactory(connectionFactory) - .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(new BasicRelationalConverter(mappingContext))) + .build(); LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext) .getRepository(LegoSetRepository.class); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index afcebfa9..c697b43f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -16,7 +16,8 @@ package org.springframework.data.r2dbc.repository.query; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -32,6 +33,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -63,7 +65,7 @@ public class StringBasedR2dbcQueryUnitTests { public void setUp() { this.mappingContext = new RelationalMappingContext(); - this.converter = new MappingR2dbcConverter(this.mappingContext); + this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(this.mappingContext)); this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.factory = new SpelAwareProxyProjectionFactory(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index 8d9b49d7..b5e2dfd9 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -33,11 +33,11 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.Table; @@ -66,13 +66,14 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS this.connectionFactory = createConnectionFactory(); this.databaseClient = DatabaseClient.builder().connectionFactory(connectionFactory) - .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(mappingContext, new EntityInstantiators())).build(); + .dataAccessStrategy(new DefaultReactiveDataAccessStrategy(new BasicRelationalConverter(mappingContext))) + .build(); RelationalEntityInformation entityInformation = new MappingRelationalEntityInformation<>( (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, - new MappingR2dbcConverter(mappingContext)); + new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext))); this.jdbc = createJdbcTemplate(createDataSource()); From 20eb2c1a6d9f56abc93d5fb767f910b75a92fc67 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Sep 2018 14:28:45 +0200 Subject: [PATCH 026/665] #2 - Resolve package cycle between r2dbc.function and r2dbc.function.convert. --- .../r2dbc/function/DefaultDatabaseClient.java | 2 +- .../DefaultReactiveDataAccessStrategy.java | 1 + .../function/ReactiveDataAccessStrategy.java | 70 +++++------------- .../convert/MappingR2dbcConverter.java | 1 - .../r2dbc/function/convert/SettableValue.java | 71 +++++++++++++++++++ .../support/SimpleR2dbcRepository.java | 2 +- 6 files changed, 93 insertions(+), 54 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java 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 0b4ce1e5..7aac79b8 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -52,9 +52,9 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.NullHandling; import org.springframework.data.domain.Sort.Order; import org.springframework.data.r2dbc.UncategorizedR2dbcException; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; +import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.jdbc.core.SqlProvider; import org.springframework.lang.Nullable; 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 332bcb49..7a4a2823 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -28,6 +28,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.r2dbc.function.convert.EntityRowMapper; +import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 76711a5c..20024a2b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -22,72 +22,40 @@ import java.util.List; import java.util.function.BiFunction; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; +import org.springframework.data.r2dbc.function.convert.SettableValue; /** * @author Mark Paluch */ public interface ReactiveDataAccessStrategy { + /** + * @param typeToRead + * @return all field names for a specific type. + */ List getAllFields(Class typeToRead); + /** + * @param object + * @return {@link SettableValue} that represent an {@code INSERT} of {@code object}. + */ List getInsert(Object object); + /** + * Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}. + * + * @param typeToRead + * @param sort + * @return + */ Sort getMappedSort(Class typeToRead, Sort sort); // TODO: Broaden T to Mono/Flux for reactive relational data access? BiFunction getRowMapper(Class typeToRead); - String getTableName(Class type); - /** - * A database value that can be set in a statement. + * @param type + * @return the table name for the {@link Class entity type}. */ - class SettableValue { - - private final Object identifier; - private final @Nullable Object value; - private final Class type; - - /** - * Create a {@link SettableValue} using an integer index. - * - * @param index - * @param value - * @param type - */ - public SettableValue(int index, @Nullable Object value, Class type) { - - this.identifier = index; - this.value = value; - this.type = type; - } - - /** - * Create a {@link SettableValue} using a {@link String} identifier. - * - * @param identifier - * @param value - * @param type - */ - public SettableValue(String identifier, @Nullable Object value, Class type) { - - this.identifier = identifier; - this.value = value; - this.type = type; - } - - public Object getIdentifier() { - return identifier; - } - - @Nullable - public Object getValue() { - return value; - } - - public Class getType() { - return type; - } - } + String getTableName(Class type); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 21fb76cf..b187e679 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -26,7 +26,6 @@ import java.util.function.BiFunction; import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java new file mode 100644 index 00000000..8338b169 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java @@ -0,0 +1,71 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.convert; + +import org.springframework.lang.Nullable; + +/** + * A database value that can be set in a statement. + * + * @author Mark Paluch + */ +public class SettableValue { + + private final Object identifier; + private final @Nullable Object value; + private final Class type; + + /** + * Create a {@link SettableValue} using an integer index. + * + * @param index + * @param value + * @param type + */ + public SettableValue(int index, @Nullable Object value, Class type) { + + this.identifier = index; + this.value = value; + this.type = type; + } + + /** + * Create a {@link SettableValue} using a {@link String} identifier. + * + * @param identifier + * @param value + * @param type + */ + public SettableValue(String identifier, @Nullable Object value, Class type) { + + this.identifier = identifier; + this.value = value; + this.type = type; + } + + public Object getIdentifier() { + return identifier; + } + + @Nullable + public Object getValue() { + return value; + } + + public Class getType() { + return type; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 03537b9e..b72b0169 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -30,8 +30,8 @@ import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.FetchSpec; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy.SettableValue; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.Assert; From a5e2a8e61f79e3a3622ae955a638bf703a23d8ca Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Sep 2018 14:50:42 +0200 Subject: [PATCH 027/665] #2 - Polishing. Introduce customization hook methods. Rename DefaultTypedGenericExecuteSpec to DefaultTypedExecuteSpec and GenericExecuteSpecSupport to ExecuteSpecSupport as types are not tied to generic execution. Encapsulate fields in DatabaseClient builders. --- .../r2dbc/function/DefaultDatabaseClient.java | 92 ++++++++++++------- .../DefaultDatabaseClientBuilder.java | 7 +- ...ultTransactionalDatabaseClientBuilder.java | 4 +- 3 files changed, 62 insertions(+), 41 deletions(-) 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 7aac79b8..18503add 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -216,6 +216,29 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex)); } + /** + * Customization hook. + */ + protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, + Map byName, Supplier sqlSupplier, Class typeToRead) { + return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); + } + + /** + * Customization hook. + */ + protected ExecuteSpecSupport createGenericExecuteSpec(Map byIndex, + Map byName, Supplier sqlSupplier) { + return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); + } + + /** + * Customization hook. + */ + protected DefaultGenericExecuteSpec createGenericExecuteSpec(Supplier sqlSupplier) { + return new DefaultGenericExecuteSpec(sqlSupplier); + } + private static void doBind(Statement statement, Map byName, Map byIndex) { @@ -236,7 +259,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { statement.bindNull(name, o.getType()); } }); - } /** @@ -256,7 +278,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(sqlSupplier, "SQL Supplier must not be null!"); - return new DefaultGenericExecuteSpec(sqlSupplier); + return createGenericExecuteSpec(sqlSupplier); } } @@ -264,13 +286,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Base class for {@link DatabaseClient.GenericExecuteSpec} implementations. */ @RequiredArgsConstructor - private class GenericExecuteSpecSupport { + class ExecuteSpecSupport { final Map byIndex; final Map byName; final Supplier sqlSupplier; - GenericExecuteSpecSupport(Supplier sqlSupplier) { + ExecuteSpecSupport(Supplier sqlSupplier) { this.byIndex = Collections.emptyMap(); this.byName = Collections.emptyMap(); @@ -284,7 +306,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return sql; } - protected SqlResult exchange(String sql, BiFunction mappingFunction) { + SqlResult exchange(String sql, BiFunction mappingFunction) { Function executeFunction = it -> { @@ -307,7 +329,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { mappingFunction); } - public GenericExecuteSpecSupport bind(int index, Object value) { + public ExecuteSpecSupport bind(int index, Object value) { Map byIndex = new LinkedHashMap<>(this.byIndex); byIndex.put(index, new SettableValue(index, value, null)); @@ -315,7 +337,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(byIndex, this.byName, this.sqlSupplier); } - public GenericExecuteSpecSupport bindNull(int index, Class type) { + public ExecuteSpecSupport bindNull(int index, Class type) { Map byIndex = new LinkedHashMap<>(this.byIndex); byIndex.put(index, new SettableValue(index, null, type)); @@ -323,7 +345,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(byIndex, this.byName, this.sqlSupplier); } - public GenericExecuteSpecSupport bind(String name, Object value) { + public ExecuteSpecSupport bind(String name, Object value) { Assert.hasText(name, "Parameter name must not be null or empty!"); @@ -333,7 +355,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(this.byIndex, byName, this.sqlSupplier); } - public GenericExecuteSpecSupport bindNull(String name, Class type) { + public ExecuteSpecSupport bindNull(String name, Class type) { Assert.hasText(name, "Parameter name must not be null or empty!"); @@ -343,12 +365,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(this.byIndex, byName, this.sqlSupplier); } - protected GenericExecuteSpecSupport createInstance(Map byIndex, - Map byName, Supplier sqlSupplier) { - return new GenericExecuteSpecSupport(byIndex, byName, sqlSupplier); + protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, + Supplier sqlSupplier) { + return new ExecuteSpecSupport(byIndex, byName, sqlSupplier); } - public GenericExecuteSpecSupport bind(Object bean) { + public ExecuteSpecSupport bind(Object bean) { Assert.notNull(bean, "Bean must not be null!"); @@ -359,7 +381,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** * Default {@link DatabaseClient.GenericExecuteSpec} implementation. */ - private class DefaultGenericExecuteSpec extends GenericExecuteSpecSupport implements GenericExecuteSpec { + protected class DefaultGenericExecuteSpec extends ExecuteSpecSupport implements GenericExecuteSpec { DefaultGenericExecuteSpec(Map byIndex, Map byName, Supplier sqlSupplier) { @@ -375,7 +397,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(resultType, "Result type must not be null!"); - return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType); + return createTypedExecuteSpec(this.byIndex, this.byName, this.sqlSupplier, resultType); } @Override @@ -414,9 +436,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - protected GenericExecuteSpecSupport createInstance(Map byIndex, - Map byName, Supplier sqlSupplier) { - return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); + protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, + Supplier sqlSupplier) { + return createGenericExecuteSpec(byIndex, byName, sqlSupplier); } } @@ -424,12 +446,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Default {@link DatabaseClient.GenericExecuteSpec} implementation. */ @SuppressWarnings("unchecked") - private class DefaultTypedGenericExecuteSpec extends GenericExecuteSpecSupport implements TypedExecuteSpec { + protected class DefaultTypedExecuteSpec extends ExecuteSpecSupport implements TypedExecuteSpec { private final Class typeToRead; private final BiFunction mappingFunction; - DefaultTypedGenericExecuteSpec(Map byIndex, Map byName, + DefaultTypedExecuteSpec(Map byIndex, Map byName, Supplier sqlSupplier, Class typeToRead) { super(byIndex, byName, sqlSupplier); @@ -443,7 +465,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(resultType, "Result type must not be null!"); - return new DefaultTypedGenericExecuteSpec<>(this.byIndex, this.byName, this.sqlSupplier, resultType); + return createTypedExecuteSpec(this.byIndex, this.byName, this.sqlSupplier, resultType); } @Override @@ -457,34 +479,34 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public DefaultTypedGenericExecuteSpec bind(int index, Object value) { - return (DefaultTypedGenericExecuteSpec) super.bind(index, value); + public DefaultTypedExecuteSpec bind(int index, Object value) { + return (DefaultTypedExecuteSpec) super.bind(index, value); } @Override - public DefaultTypedGenericExecuteSpec bindNull(int index, Class type) { - return (DefaultTypedGenericExecuteSpec) super.bindNull(index, type); + public DefaultTypedExecuteSpec bindNull(int index, Class type) { + return (DefaultTypedExecuteSpec) super.bindNull(index, type); } @Override - public DefaultTypedGenericExecuteSpec bind(String name, Object value) { - return (DefaultTypedGenericExecuteSpec) super.bind(name, value); + public DefaultTypedExecuteSpec bind(String name, Object value) { + return (DefaultTypedExecuteSpec) super.bind(name, value); } @Override - public DefaultTypedGenericExecuteSpec bindNull(String name, Class type) { - return (DefaultTypedGenericExecuteSpec) super.bindNull(name, type); + public DefaultTypedExecuteSpec bindNull(String name, Class type) { + return (DefaultTypedExecuteSpec) super.bindNull(name, type); } @Override - public DefaultTypedGenericExecuteSpec bind(Object bean) { - return (DefaultTypedGenericExecuteSpec) super.bind(bean); + public DefaultTypedExecuteSpec bind(Object bean) { + return (DefaultTypedExecuteSpec) super.bind(bean); } @Override - protected DefaultTypedGenericExecuteSpec createInstance(Map byIndex, + protected DefaultTypedExecuteSpec createInstance(Map byIndex, Map byName, Supplier sqlSupplier) { - return new DefaultTypedGenericExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); + return createTypedExecuteSpec(byIndex, byName, sqlSupplier, typeToRead); } } @@ -550,8 +572,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } StringBuilder getLimitOffset(Pageable pageable) { - return new StringBuilder().append("LIMIT").append(' ').append(page.getPageSize()) // - .append(' ').append("OFFSET").append(' ').append(page.getOffset()); + return new StringBuilder().append("LIMIT").append(' ').append(pageable.getPageSize()) // + .append(' ').append("OFFSET").append(' ').append(pageable.getOffset()); } StringBuilder getSortClause(Sort sort) { diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 57c64fcd..a4e4e4e8 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -33,9 +33,9 @@ import org.springframework.util.Assert; */ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { - @Nullable ConnectionFactory connector; - @Nullable R2dbcExceptionTranslator exceptionTranslator; - ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); + private @Nullable ConnectionFactory connector; + private @Nullable R2dbcExceptionTranslator exceptionTranslator; + private ReactiveDataAccessStrategy accessStrategy = new DefaultReactiveDataAccessStrategy(); DefaultDatabaseClientBuilder() {} @@ -45,6 +45,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { this.connector = other.connector; this.exceptionTranslator = other.exceptionTranslator; + this.accessStrategy = other.accessStrategy; } @Override diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java index 7e322e05..5dbf6fa7 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java @@ -33,10 +33,8 @@ class DefaultTransactionalDatabaseClientBuilder extends DefaultDatabaseClientBui DefaultTransactionalDatabaseClientBuilder(DefaultDatabaseClientBuilder other) { + super(other); Assert.notNull(other, "DefaultDatabaseClientBuilder must not be null!"); - - this.connector = other.connector; - this.exceptionTranslator = other.exceptionTranslator; } @Override From b6a8c8128948374dd8b505e95cd46530fa02995d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Sep 2018 15:36:30 +0200 Subject: [PATCH 028/665] #2 - Document R2DBC capabilities. --- README.adoc | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 README.adoc diff --git a/README.adoc b/README.adoc new file mode 100644 index 00000000..31d15d4d --- /dev/null +++ b/README.adoc @@ -0,0 +1,88 @@ +image:https://spring.io/badges/spring-data-jdbc/ga.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] +image:https://spring.io/badges/spring-data-jdbc/snapshot.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] + += Spring Data JDBC + +The primary goal of the http://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use data access technologies. *Spring Data JDBC* offers the popular Repository abstraction based on JDBC. + +It aims at being conceptually easy. +In order to achieve this it does NOT offer caching, lazy loading, write behind or many other features of JPA. +This makes Spring Data JDBC a simple, limited, opinionated ORM. + +== Features + +* Implementation of CRUD methods for Aggregates. +* `@Query` annotation +* Support for transparent auditing (created, last changed) +* Events for persistence events +* Possibility to integrate custom repository code +* JavaConfig based repository configuration by introducing `EnableJdbcRepository` +* Integration with MyBatis + +== Getting Help + +If you are new to Spring Data JDBC read the following two articles https://spring.io/blog/2018/09/17/introducing-spring-data-jdbc["Introducing Spring Data JDBC"] and https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates["Spring Data JDBC, References, and Aggregates"] + +There are also examples in the https://github.com/spring-projects/spring-data-examples/tree/master/jdbc[Spring Data Examples] project. + +A very good source of information is the source code in this repository. +Especially the integration tests (if you are reading this on github, type `t` and then `IntegrationTests.java`) + +We are keeping an eye on the (soon to be created) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow]. + +If you think you found a bug, or have a feature request please https://jira.spring.io/browse/DATAJDBC/?selectedTab=com.atlassian.jira.jira-projects-plugin:summary-panel[create a ticket in our issue tracker]. + +== Execute Tests + +=== Fast running tests + +Fast running tests can be executed with a simple + +[source] +---- +mvn test +---- + +This will execute unit tests and integration tests using an in-memory database. + +=== Running tests with a real database + +In order to run the integration tests against a specific database you need to have a local Docker installation available, and then execute. + +[source] +---- +mvn test -Dspring.profiles.active= +---- + +This will also execute the unit tests. + +Currently the following _databasetypes_ are available: + +* hsql (default, does not require a running database) +* mysql +* postgres +* mariadb + +=== Run tests with all databases + +[source] +---- +mvn test -Pall-dbs +---- + +This will execute the unit tests, and all the integration tests with all the databases we currently support for testing. Running the integration-tests depends on Docker. + +== Contributing to Spring Data JDBC + +Here are some ways for you to get involved in the community: + +* Get involved with the Spring community by helping out on http://stackoverflow.com/questions/tagged/spring-data-jdbc[stackoverflow] by responding to questions and joining the debate. +* Create https://jira.spring.io/browse/DATAJDBC[JIRA] tickets for bugs and new features and comment and vote on the ones that you are interested in. +* Github is for social coding: if you want to write code, we encourage contributions through pull requests from http://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing. +* Watch for upcoming articles on Spring by http://spring.io/blog[subscribing] to spring.io. + +Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement]. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you'll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests. + +== License + +link:src/main/resources/license.txt[The license und which Spring Data JDBC is published can be found here]. From d60d8b04a088dcb78f5a8b84bf1dc9dd5d323bd7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Sep 2018 15:43:28 +0200 Subject: [PATCH 029/665] #2 - Switch dependency test to assumption. --- .../org/springframework/data/jdbc/degraph/DependencyTests.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java b/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java index 4e6a4a01..fe474423 100644 --- a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java +++ b/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java @@ -22,6 +22,7 @@ import de.schauderhaft.degraph.check.JCheck; import de.schauderhaft.degraph.configuration.NamedPattern; import scala.runtime.AbstractFunction1; +import org.junit.Assume; import org.junit.Test; /** @@ -34,7 +35,7 @@ public class DependencyTests { @Test // DATAJDBC-114 public void cycleFree() { - assertThat( // + Assume.assumeThat( // classpath() // .noJars() // .including("org.springframework.data.jdbc.**") // From ce0a7075004bd411fe5a3a3295db383401d325ff Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 24 Sep 2018 10:37:48 -0400 Subject: [PATCH 030/665] #2 - Upgrade to R2DBC 1.0M5. --- pom.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a4f7036b..e1873853 100644 --- a/pom.xml +++ b/pom.xml @@ -34,10 +34,9 @@ 5.1.41 42.0.0 2.2.3 - 1.0.0.BUILD-SNAPSHOT - 1.0.0.BUILD-SNAPSHOT + 1.0.0.M5 + 1.0.0.M5 1.7.3 - Californium-BUILD-SNAPSHOT From 12dc98bcbdc15e499ee828643afe3edec6a6bd6d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 17 Oct 2018 11:04:34 +0200 Subject: [PATCH 031/665] #2 - Polishing. --- README.adoc | 224 +++++++++++++----- pom.xml | 90 ++----- .../config/JdbcRepositoryConfigExtension.java | 68 ------ .../data/r2dbc/function/DatabaseClient.java | 1 - .../data/r2dbc/function/DefaultSqlResult.java | 39 ++- .../function/convert/EntityRowMapper.java | 3 +- .../convert/MappingR2dbcConverter.java | 1 - .../repository/query/Query.java} | 30 ++- .../repository/query/R2dbcQueryMethod.java | 1 - .../query/StringBasedR2dbcQuery.java | 1 - .../query/DtoInstantiatingConverter.java | 108 --------- .../query/RelationalEntityInformation.java | 33 --- .../query/RelationalEntityMetadata.java | 41 ---- .../query/RelationalParameters.java | 80 ------- ...RelationalParametersParameterAccessor.java | 62 ----- .../query/SimpleRelationalEntityMetadata.java | 62 ----- .../repository/query/package-info.java | 7 - .../MappingRelationalEntityInformation.java | 111 --------- .../repository/support/package-info.java | 7 - .../degraph => r2dbc}/DependencyTests.java | 4 +- .../DatabaseClientIntegrationTests.java | 2 +- ...ctionalDatabaseClientIntegrationTests.java | 2 +- .../R2dbcRepositoryIntegrationTests.java | 4 +- .../query/StringBasedR2dbcQueryUnitTests.java | 1 - ...SimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../testing/ExternalDatabase.java | 2 +- .../testing/R2dbcIntegrationTestSupport.java | 4 +- src/test/resources/logback.xml | 17 ++ 28 files changed, 254 insertions(+), 753 deletions(-) delete mode 100644 src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java rename src/main/java/org/springframework/data/{relational/repository/query/RelationalParameterAccessor.java => r2dbc/repository/query/Query.java} (51%) delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/query/package-info.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java delete mode 100644 src/main/java/org/springframework/data/relational/repository/support/package-info.java rename src/test/java/org/springframework/data/{jdbc/degraph => r2dbc}/DependencyTests.java (95%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/testing/ExternalDatabase.java (98%) rename src/test/java/org/springframework/data/{jdbc => r2dbc}/testing/R2dbcIntegrationTestSupport.java (94%) create mode 100644 src/test/resources/logback.xml diff --git a/README.adoc b/README.adoc index 31d15d4d..1c32284e 100644 --- a/README.adoc +++ b/README.adoc @@ -1,88 +1,196 @@ -image:https://spring.io/badges/spring-data-jdbc/ga.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] -image:https://spring.io/badges/spring-data-jdbc/snapshot.svg["Spring Data JDBC", link="https://spring.io/projects/spring-data-jdbc#learn"] += Spring Data R2DBC -= Spring Data JDBC +The primary goal of the http://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use data access technologies. *Spring Data R2DBC* offers the popular Repository abstraction based on https://r2dbc.io[R2DBC]. -The primary goal of the http://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use data access technologies. *Spring Data JDBC* offers the popular Repository abstraction based on JDBC. +R2DBC is the abbreviation for https://github.com/r2dbc/[Reactive Relational Database Connectivity], an incubator to integrate relational databases using a reactive driver. -It aims at being conceptually easy. -In order to achieve this it does NOT offer caching, lazy loading, write behind or many other features of JPA. -This makes Spring Data JDBC a simple, limited, opinionated ORM. +The state of R2DBC is incubating to evaluate how an reactive integration could look like. To get started, you need a R2DBC driver first. -== Features +== This is NOT an ORM -* Implementation of CRUD methods for Aggregates. -* `@Query` annotation -* Support for transparent auditing (created, last changed) -* Events for persistence events -* Possibility to integrate custom repository code -* JavaConfig based repository configuration by introducing `EnableJdbcRepository` -* Integration with MyBatis +Spring Data R2DBC does not try to be an ORM. +Instead it is more of a construction kit for your personal reactive relational data access component that you can define the way you like or need it. -== Getting Help +== Maven Coordinates -If you are new to Spring Data JDBC read the following two articles https://spring.io/blog/2018/09/17/introducing-spring-data-jdbc["Introducing Spring Data JDBC"] and https://spring.io/blog/2018/09/24/spring-data-jdbc-references-and-aggregates["Spring Data JDBC, References, and Aggregates"] - -There are also examples in the https://github.com/spring-projects/spring-data-examples/tree/master/jdbc[Spring Data Examples] project. - -A very good source of information is the source code in this repository. -Especially the integration tests (if you are reading this on github, type `t` and then `IntegrationTests.java`) - -We are keeping an eye on the (soon to be created) https://stackoverflow.com/questions/tagged/spring-data-jdbc[spring-data-jdbc tag on stackoverflow]. - -If you think you found a bug, or have a feature request please https://jira.spring.io/browse/DATAJDBC/?selectedTab=com.atlassian.jira.jira-projects-plugin:summary-panel[create a ticket in our issue tracker]. - -== Execute Tests - -=== Fast running tests - -Fast running tests can be executed with a simple - -[source] +[source,xml] ---- -mvn test + + org.springframework.data + spring-data-r2dbc + 1.0.0.BUILD-SNAPSHOT + ---- -This will execute unit tests and integration tests using an in-memory database. -=== Running tests with a real database +== DatabaseClient -In order to run the integration tests against a specific database you need to have a local Docker installation available, and then execute. +All functionality is encapsulated in `DatabaseClient` which is the entry point for applications that wish to integrate with relational databases using reactive drivers: -[source] +[source,java] ---- -mvn test -Dspring.profiles.active= +PostgresqlConnectionFactory connectionFactory = new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() + .host(…) + .database(…) + .username(…) + .password(…).build()); + +DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); ---- -This will also execute the unit tests. +The client API provides covers the following features: -Currently the following _databasetypes_ are available: +* Execution of generic SQL and consumption of update count/row results. +* Generic `SELECT` with paging and ordering. +* `SELECT` of mapped objects with paging and ordering. +* Generic `INSERT` with parameter binding. +* `INSERT` of mapped objects. +* Parameter binding using the native syntax. +* Result consumption: Update count, unmapped (`Map`), mapped to entities, extraction function. +* Reactive repositories using `@Query` annotated methods. +* Transaction Management. -* hsql (default, does not require a running database) -* mysql -* postgres -* mariadb +=== Examples executing generic SQL statements -=== Run tests with all databases - -[source] +[source,java] ---- -mvn test -Pall-dbs +Mono count = databaseClient.execute() + .sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") + .bind("$1", 42055) + .bind("$2", "Description") + .bindNull("$3", Integer.class) + .fetch() + .rowsUpdated(); + +Flux> rows = databaseClient.execute() + .sql("SELECT id, name, manual FROM legoset") + .fetch().all(); + +Flux result = db.execute() + .sql("SELECT txid_current();") + .exchange() + .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); ---- -This will execute the unit tests, and all the integration tests with all the databases we currently support for testing. Running the integration-tests depends on Docker. +=== Examples selecting data -== Contributing to Spring Data JDBC +[source,java] +---- + +Flux> rows = databaseClient.select() + .from("legoset") + .orderBy(Sort.by(desc("id"))) + .fetch() + .all(); + +Flux rows = databaseClient.select() + .from("legoset") + .orderBy(Sort.by(desc("id"))) + .as(LegoSet.class) + .fetch() + .all(); +---- + +=== Examples inserting data + +[source,java] +---- +Flux ids = databaseClient.insert() + .into("legoset") + .value("id", 42055) + .value("name", "Description") + .nullValue("manual", Integer.class) + .exchange() // + .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) + +Flux ids = databaseClient.insert() + .into(LegoSet.class) + .using(legoSet) + .exchange() + .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) +---- + +=== Examples using reactive repositories + +[source,java] +---- +interface LegoSetRepository extends ReactiveCrudRepository { + + @Query("SELECT * FROM legoset WHERE name like $1") + Flux findByNameContains(String name); + + @Query("SELECT * FROM legoset WHERE manual = $1") + Mono findByManual(int manual); +} +---- + +=== Examples using transaction control + +All examples above run with auto-committed transactions. To get group multiple statements within the same transaction or +control the transaction yourself, you need to use `TransactionalDatabaseClient`: + +[source,java] +---- +TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); +---- + +`TransactionalDatabaseClient` allows multiple flavors of transaction management: + +* Participate in ongoing transactions and fall-back to auto-commit mode if there's no active transaction (default). +* Group multiple statements in a managed transaction using `TransactionalDatabaseClient.inTransaction(…)`. +* Application-controlled transaction management using `TransactionalDatabaseClient.beginTransaction()`/`commitTransaction()`/`rollbackTransaction()`. + +Participating in ongoing transactions does not require changes to your application code. Instead, a managed transaction must be hosted by your application container. Transaction control needs to happen there, as well. + +**Statement grouping** + +[source,java] +---- +Flux rowsUpdated = databaseClient.inTransaction(db -> { + + return db.execute().sql("INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)") // + .bind(0, 42055) // + .bind(1, "Description") // + .bindNull("$3", Integer.class) // + .fetch() + .rowsUpdated(); +}); +---- + +**Application-controlled transaction management** + +[source,java] +---- +Flux txId = databaseClient.execute().sql("SELECT txid_current();").exchange() + .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + +Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() // + .thenMany(txId)) // + .then(databaseClient.rollbackTransaction())); +---- + +NOTE: Application-controlled transactions must be enabled with `enableTransactionSynchronization(…)`. + +== Building from Source + +You don't need to build from source to use Spring Data R2DBC (binaries in https://repo.spring.io[repo.spring.io]), but if you want to try out the latest and greatest, Spring Data R2DBC can be easily built with the https://github.com/takari/maven-wrapper[maven wrapper]. You also need JDK 1.8. + +[indent=0] +---- + $ ./mvnw clean install +---- + +If you want to build with the regular `mvn` command, you will need https://maven.apache.org/run-maven/index.html[Maven v3.5.0 or above]. + +_Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull requests, and in particular please fill out the https://cla.pivotal.io/[Contributor's Agreement] before your first change._ + +== Contributing to Spring Data R2DBC Here are some ways for you to get involved in the community: -* Get involved with the Spring community by helping out on http://stackoverflow.com/questions/tagged/spring-data-jdbc[stackoverflow] by responding to questions and joining the debate. -* Create https://jira.spring.io/browse/DATAJDBC[JIRA] tickets for bugs and new features and comment and vote on the ones that you are interested in. +* Get involved with the Spring community by helping out on http://stackoverflow.com/questions/tagged/spring-data-r2dbc[Stackoverflow] by responding to questions and joining the debate. +* Create https://github.com/spring-data/spring-data-r2dbc[GitHub] tickets for bugs and new features and comment and vote on the ones that you are interested in. * Github is for social coding: if you want to write code, we encourage contributions through pull requests from http://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing. * Watch for upcoming articles on Spring by http://spring.io/blog[subscribing] to spring.io. Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement]. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you'll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests. - -== License - -link:src/main/resources/license.txt[The license und which Spring Data JDBC is published can be found here]. diff --git a/pom.xml b/pom.xml index e1873853..73ed1365 100644 --- a/pom.xml +++ b/pom.xml @@ -5,12 +5,12 @@ 4.0.0 org.springframework.data - spring-data-jdbc - 1.1.0.r2dbc-SNAPSHOT + spring-data-r2dbc + 1.0.0.BUILD-SNAPSHOT - Spring Data JDBC - Spring Data module for JDBC repositories. - http://projects.spring.io/spring-data-jdbc + Spring Data R2DBC + Spring Data module for R2DBC. + http://projects.spring.io/spring-data-r2dbc org.springframework.data.build @@ -20,33 +20,29 @@ - DATAJDBC + DATAR2DBC 2.2.0.BUILD-SNAPSHOT - spring.data.jdbc + 1.1.0.BUILD-SNAPSHOT + spring.data.r2dbc reuseReports - 3.6.2 0.1.4 2.2.8 - 3.4.6 - 1.3.2 - 5.1.41 42.0.0 - 2.2.3 1.0.0.M5 1.0.0.M5 1.7.3 - 2017 + 2018 - schauder - Jens Schauder - jschauder(at)pivotal.io + mpaluch + Mark Paluch + mpaluch(at)pivotal.io Pivotal Software, Inc. https://pivotal.io @@ -55,15 +51,15 @@ +1 - gregturn - Greg L. Turnquist - gturnquist(at)pivotal.io + ogierke + Oliver Gierke + ogierke(at)pivotal.io Pivotal Software, Inc. https://pivotal.io - Project Contributor + Project Lead - -6 + +1 @@ -177,6 +173,12 @@ ${springdata.commons} + + ${project.groupId} + spring-data-relational + ${springdata.relational} + + org.springframework spring-tx @@ -206,40 +208,17 @@ io.r2dbc r2dbc-spi ${r2dbc-spi.version} - true io.projectreactor reactor-core - true - - - - org.mybatis - mybatis-spring - ${mybatis-spring.version} - true - - - - org.mybatis - mybatis - ${mybatis.version} - true - - - - org.hsqldb - hsqldb - ${hsqldb.version} - test org.assertj assertj-core - ${assertj-core.version} + ${assertj} test @@ -249,13 +228,6 @@ test - - mysql - mysql-connector-java - ${mysql-connector-java.version} - test - - org.postgresql postgresql @@ -263,13 +235,6 @@ test - - org.mariadb.jdbc - mariadb-java-client - ${mariadb-java-client.version} - test - - io.r2dbc r2dbc-postgresql @@ -304,13 +269,6 @@ test - - org.testcontainers - mariadb - ${testcontainers.version} - test - - diff --git a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java deleted file mode 100644 index fcac3166..00000000 --- a/src/main/java/org/springframework/data/jdbc/repository/config/JdbcRepositoryConfigExtension.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2017-2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.jdbc.repository.config; - -import java.util.Locale; - -import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactoryBean; -import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; -import org.springframework.data.repository.core.RepositoryMetadata; - -/** - * {@link org.springframework.data.repository.config.RepositoryConfigurationExtension} extending the repository - * registration process by registering JDBC repositories. - * - * @author Jens Schauder - * @author Mark Paluch - */ -public class JdbcRepositoryConfigExtension extends RepositoryConfigurationExtensionSupport { - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtension#getModuleName() - */ - @Override - public String getModuleName() { - return "JDBC"; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getRepositoryFactoryBeanClassName() - */ - @Override - public String getRepositoryFactoryBeanClassName() { - return JdbcRepositoryFactoryBean.class.getName(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getModulePrefix() - */ - @Override - protected String getModulePrefix() { - return getModuleName().toLowerCase(Locale.US); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#useRepositoryConfiguration(org.springframework.data.repository.core.RepositoryMetadata) - */ - @Override - protected boolean useRepositoryConfiguration(RepositoryMetadata metadata) { - return !metadata.isReactiveRepository(); - } -} 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 e587d072..4e49dcbe 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -315,7 +315,6 @@ public interface DatabaseClient { */ S project(String... selectedFields); - /** * Configure {@link Sort}. * diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index ec38a79a..378b4731 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -48,28 +48,27 @@ class DefaultSqlResult implements SqlResult { this.resultFunction = resultFunction; this.updatedRowsFunction = updatedRowsFunction; - this.fetchSpec = new DefaultFetchSpec<>(connectionAccessor, sql, - new SqlFunction>() { - @Override - public Flux apply(Connection connection) { - return resultFunction.apply(connection).flatMap(result -> result.map(mappingFunction)); - } + this.fetchSpec = new DefaultFetchSpec<>(connectionAccessor, sql, new SqlFunction>() { + @Override + public Flux apply(Connection connection) { + return resultFunction.apply(connection).flatMap(result -> result.map(mappingFunction)); + } - @Override - public String getSql() { - return sql; - } - }, new SqlFunction>() { - @Override - public Mono apply(Connection connection) { - return updatedRowsFunction.apply(connection); - } + @Override + public String getSql() { + return sql; + } + }, new SqlFunction>() { + @Override + public Mono apply(Connection connection) { + return updatedRowsFunction.apply(connection); + } - @Override - public String getSql() { - return sql; - } - }); + @Override + public String getSql() { + return sql; + } + }); } /* (non-Javadoc) diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index edf84d93..0aa86bee 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -155,8 +155,7 @@ public class EntityRowMapper implements BiFunction { String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName(); try { - return conversionService.convert(resultSet.get(column), - parameter.getType().getType()); + return conversionService.convert(resultSet.get(column), parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index b187e679..49fb815d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -109,4 +109,3 @@ public class MappingR2dbcConverter { return relationalConverter.getMappingContext(); } } - diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java similarity index 51% rename from src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java rename to src/main/java/org/springframework/data/r2dbc/repository/query/Query.java index 52123569..43b451da 100644 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameterAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java @@ -13,25 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.relational.repository.query; +package org.springframework.data.r2dbc.repository.query; -import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.Parameters; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.data.annotation.QueryAnnotation; /** - * JDBC-specific {@link ParameterAccessor}. - * + * Annotation to provide SQL statements that will get used for executing the method. + * * @author Mark Paluch */ -public interface RelationalParameterAccessor extends ParameterAccessor { +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@QueryAnnotation +@Documented +public @interface Query { /** - * Returns the raw parameter values of the underlying query method. + * The SQL statement to execute when the annotated method gets invoked. */ - Object[] getValues(); - - /** - * @return the bindable parameters. - */ - Parameters getBindableParameters(); + String value(); } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index 48f7fd5e..63adcae1 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -26,7 +26,6 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 3ff5291b..d3fb559f 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -15,7 +15,6 @@ */ package org.springframework.data.r2dbc.repository.query; -import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; diff --git a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java b/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java deleted file mode 100644 index 6299e38a..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/DtoInstantiatingConverter.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.convert.EntityInstantiator; -import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PreferredConstructor; -import org.springframework.data.mapping.PreferredConstructor.Parameter; -import org.springframework.data.mapping.SimplePropertyHandler; -import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; -import org.springframework.util.Assert; - -/** - * {@link Converter} to instantiate DTOs from fully equipped domain objects. - * - * @author Mark Paluch - */ -public class DtoInstantiatingConverter implements Converter { - - private final Class targetType; - private final MappingContext, ? extends PersistentProperty> context; - private final EntityInstantiator instantiator; - - /** - * Creates a new {@link Converter} to instantiate DTOs. - * - * @param dtoType must not be {@literal null}. - * @param context must not be {@literal null}. - * @param instantiators must not be {@literal null}. - */ - public DtoInstantiatingConverter(Class dtoType, - MappingContext, ? extends RelationalPersistentProperty> context, - EntityInstantiators instantiator) { - - Assert.notNull(dtoType, "DTO type must not be null!"); - Assert.notNull(context, "MappingContext must not be null!"); - Assert.notNull(instantiator, "EntityInstantiators must not be null!"); - - this.targetType = dtoType; - this.context = context; - this.instantiator = instantiator.getInstantiatorFor(context.getRequiredPersistentEntity(dtoType)); - } - - /* - * (non-Javadoc) - * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) - */ - @Override - public Object convert(Object source) { - - if (targetType.isInterface()) { - return source; - } - - final PersistentEntity sourceEntity = context.getRequiredPersistentEntity(source.getClass()); - final PersistentPropertyAccessor sourceAccessor = sourceEntity.getPropertyAccessor(source); - final PersistentEntity targetEntity = context.getRequiredPersistentEntity(targetType); - final PreferredConstructor> constructor = targetEntity - .getPersistenceConstructor(); - - @SuppressWarnings({ "rawtypes", "unchecked" }) - Object dto = instantiator.createInstance(targetEntity, new ParameterValueProvider() { - - @Override - public Object getParameterValue(Parameter parameter) { - return sourceAccessor.getProperty(sourceEntity.getPersistentProperty(parameter.getName())); - } - }); - - final PersistentPropertyAccessor dtoAccessor = targetEntity.getPropertyAccessor(dto); - - targetEntity.doWithProperties(new SimplePropertyHandler() { - - @Override - public void doWithPersistentProperty(PersistentProperty property) { - - if (constructor.isConstructorParameter(property)) { - return; - } - - dtoAccessor.setProperty(property, - sourceAccessor.getProperty(sourceEntity.getPersistentProperty(property.getName()))); - } - }); - - return dto; - } -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java deleted file mode 100644 index c03f202e..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityInformation.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import org.springframework.data.repository.core.EntityInformation; - -/** - * Relational database-specific {@link EntityInformation}. - * - * @author Mark Paluch - */ -public interface RelationalEntityInformation extends EntityInformation { - - /** - * Returns the name of the table the entity shall be persisted to. - * - * @return - */ - String getTableName(); -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java deleted file mode 100644 index 8691cb5a..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalEntityMetadata.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.repository.core.EntityMetadata; - -/** - * Extension of {@link EntityMetadata} to additionally expose the collection name an entity shall be persisted to. - * - * @author Mark Paluch - */ -public interface RelationalEntityMetadata extends EntityMetadata { - - /** - * Returns the name of the table the entity shall be persisted to. - * - * @return - */ - String getTableName(); - - /** - * Returns the {@link RelationalPersistentEntity} that supposed to determine the table to be queried. - * - * @return - */ - RelationalPersistentEntity getTableEntity(); -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java deleted file mode 100644 index 8af173e4..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import java.lang.reflect.Method; -import java.util.List; - -import org.springframework.core.MethodParameter; -import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter; -import org.springframework.data.repository.query.Parameter; -import org.springframework.data.repository.query.Parameters; - -/** - * Custom extension of {@link Parameters}. - * - * @author Mark Paluch - */ -public class RelationalParameters extends Parameters { - - /** - * Creates a new {@link RelationalParameters} instance from the given {@link Method}. - * - * @param method must not be {@literal null}. - */ - public RelationalParameters(Method method) { - super(method); - } - - private RelationalParameters(List parameters) { - super(parameters); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.Parameters#createParameter(org.springframework.core.MethodParameter) - */ - @Override - protected RelationalParameter createParameter(MethodParameter parameter) { - return new RelationalParameter(parameter); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) - */ - @Override - protected RelationalParameters createFrom(List parameters) { - return new RelationalParameters(parameters); - } - - /** - * Custom {@link Parameter} implementation. - * - * @author Mark Paluch - */ - public static class RelationalParameter extends Parameter { - - /** - * Creates a new {@link RelationalParameter}. - * - * @param parameter must not be {@literal null}. - */ - RelationalParameter(MethodParameter parameter) { - super(parameter); - } - } -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java b/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java deleted file mode 100644 index c62c7cb2..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/RelationalParametersParameterAccessor.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import java.util.Arrays; -import java.util.List; - -import org.springframework.data.repository.query.Parameters; -import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.QueryMethod; - -/** - * JDBC-specific {@link ParametersParameterAccessor}. - * - * @author Mark Paluch - */ -public class RelationalParametersParameterAccessor extends ParametersParameterAccessor - implements RelationalParameterAccessor { - - private final List values; - - /** - * Creates a new {@link RelationalParametersParameterAccessor}. - * - * @param method must not be {@literal null}. - * @param values must not be {@literal null}. - */ - public RelationalParametersParameterAccessor(QueryMethod method, Object[] values) { - - super(method.getParameters(), values); - this.values = Arrays.asList(values); - } - - /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.JdbcParameterAccessor#getValues() - */ - @Override - public Object[] getValues() { - return values.toArray(); - } - - /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.RelationalParameterAccessor#getBindableParameters() - */ - @Override - public Parameters getBindableParameters() { - return getParameters().getBindableParameters(); - } -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java b/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java deleted file mode 100644 index f64c7f51..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/SimpleRelationalEntityMetadata.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.query; - -import lombok.Getter; - -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.util.Assert; - -/** - * Default implementation of {@link RelationalEntityMetadata}. - * - * @author Mark Paluch - */ -public class SimpleRelationalEntityMetadata implements RelationalEntityMetadata { - - private final Class type; - private final @Getter RelationalPersistentEntity tableEntity; - - /** - * Creates a new {@link SimpleRelationalEntityMetadata} using the given type and {@link RelationalPersistentEntity} to - * use for table lookups. - * - * @param type must not be {@literal null}. - * @param tableEntity must not be {@literal null}. - */ - public SimpleRelationalEntityMetadata(Class type, RelationalPersistentEntity tableEntity) { - - Assert.notNull(type, "Type must not be null!"); - Assert.notNull(tableEntity, "Table entity must not be null!"); - - this.type = type; - this.tableEntity = tableEntity; - } - - /* (non-Javadoc) - * @see org.springframework.data.repository.core.EntityMetadata#getJavaType() - */ - public Class getJavaType() { - return type; - } - - /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.RelationalEntityMetadata#getTableName() - */ - public String getTableName() { - return tableEntity.getTableName(); - } -} diff --git a/src/main/java/org/springframework/data/relational/repository/query/package-info.java b/src/main/java/org/springframework/data/relational/repository/query/package-info.java deleted file mode 100644 index ccd616a6..00000000 --- a/src/main/java/org/springframework/data/relational/repository/query/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Query support for relational database repositories. - */ -@NonNullApi -package org.springframework.data.relational.repository.query; - -import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java b/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java deleted file mode 100644 index 6231de0d..00000000 --- a/src/main/java/org/springframework/data/relational/repository/support/MappingRelationalEntityInformation.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2018 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.relational.repository.support; - -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; -import org.springframework.data.relational.repository.query.RelationalEntityInformation; -import org.springframework.data.repository.core.support.PersistentEntityInformation; -import org.springframework.lang.Nullable; - -/** - * {@link RelationalEntityInformation} implementation using a {@link RelationalPersistentEntity} instance to lookup the - * necessary information. Can be configured with a custom table name. - *

- * Entity types that do not declare an explicit Id type fall back to {@link Long} as Id type. - * - * @author Mark Paluch - */ -public class MappingRelationalEntityInformation extends PersistentEntityInformation - implements RelationalEntityInformation { - - private final RelationalPersistentEntity entityMetadata; - private final @Nullable String customTableName; - private final Class fallbackIdType; - - /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity}. - * - * @param entity must not be {@literal null}. - */ - public MappingRelationalEntityInformation(RelationalPersistentEntity entity) { - this(entity, null, null); - } - - /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity} and - * fallback identifier type. - * - * @param entity must not be {@literal null}. - * @param fallbackIdType can be {@literal null}. - */ - public MappingRelationalEntityInformation(RelationalPersistentEntity entity, @Nullable Class fallbackIdType) { - this(entity, null, fallbackIdType); - } - - /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity} and - * custom table name. - * - * @param entity must not be {@literal null}. - * @param customTableName can be {@literal null}. - */ - public MappingRelationalEntityInformation(RelationalPersistentEntity entity, String customTableName) { - this(entity, customTableName, null); - } - - /** - * Creates a new {@link MappingRelationalEntityInformation} for the given {@link RelationalPersistentEntity}, - * collection name and identifier type. - * - * @param entity must not be {@literal null}. - * @param customTableName can be {@literal null}. - * @param idType can be {@literal null}. - */ - @SuppressWarnings("unchecked") - private MappingRelationalEntityInformation(RelationalPersistentEntity entity, @Nullable String customTableName, - @Nullable Class idType) { - - super(entity); - - this.entityMetadata = entity; - this.customTableName = customTableName; - this.fallbackIdType = idType != null ? idType : (Class) Long.class; - } - - /* (non-Javadoc) - * @see org.springframework.data.relational.repository.query.RelationalEntityInformation#getTableName() - */ - public String getTableName() { - return customTableName == null ? entityMetadata.getTableName() : customTableName; - } - - public String getIdAttribute() { - return entityMetadata.getRequiredIdProperty().getName(); - } - - /* (non-Javadoc) - * @see org.springframework.data.repository.core.support.PersistentEntityInformation#getIdType() - */ - @Override - public Class getIdType() { - - if (this.entityMetadata.hasIdProperty()) { - return super.getIdType(); - } - - return fallbackIdType; - } -} diff --git a/src/main/java/org/springframework/data/relational/repository/support/package-info.java b/src/main/java/org/springframework/data/relational/repository/support/package-info.java deleted file mode 100644 index 28aeb251..00000000 --- a/src/main/java/org/springframework/data/relational/repository/support/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Support infrastructure for query derivation of relational database repositories. - */ -@NonNullApi -package org.springframework.data.relational.repository.support; - -import org.springframework.lang.NonNullApi; diff --git a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java similarity index 95% rename from src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java rename to src/test/java/org/springframework/data/r2dbc/DependencyTests.java index fe474423..30da212f 100644 --- a/src/test/java/org/springframework/data/jdbc/degraph/DependencyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2018 the original author or authors. + * Copyright 2018 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.degraph; +package org.springframework.data.r2dbc; import static de.schauderhaft.degraph.check.JCheck.*; import static org.junit.Assert.*; diff --git a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java index 8ab0fda1..614d7972 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DatabaseClientIntegrationTests.java @@ -28,7 +28,7 @@ import org.junit.Test; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java index 6a3d19cd..31c641e4 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClientIntegrationTests.java @@ -30,7 +30,7 @@ import java.util.concurrent.ArrayBlockingQueue; import org.junit.Before; import org.junit.Test; -import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.NoTransactionException; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java index 4dde9d5d..fc2fcb1f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/R2dbcRepositoryIntegrationTests.java @@ -33,12 +33,12 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.repository.query.Query; -import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; +import org.springframework.data.r2dbc.repository.query.Query; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index c697b43f..bf71533d 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -27,7 +27,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index b5e2dfd9..f5a22bcc 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -33,7 +33,7 @@ import java.util.Map; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.jdbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; diff --git a/src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java similarity index 98% rename from src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java rename to src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index 2c6ebe39..7a021e21 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.testing; +package org.springframework.data.r2dbc.testing; import lombok.Builder; diff --git a/src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java similarity index 94% rename from src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java rename to src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java index ea99ac4c..54ecbf71 100644 --- a/src/test/java/org/springframework/data/jdbc/testing/R2dbcIntegrationTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jdbc.testing; +package org.springframework.data.r2dbc.testing; import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; @@ -23,7 +23,7 @@ import javax.sql.DataSource; import org.junit.ClassRule; import org.postgresql.ds.PGSimpleDataSource; -import org.springframework.data.jdbc.testing.ExternalDatabase.ProvidedDatabase; +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; import org.springframework.jdbc.core.JdbcTemplate; /** diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 00000000..c9be4b42 --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + + From 34ddfda0c0e0024ee349a808fbaf6c7e804c9a1a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 17 Oct 2018 11:21:33 +0200 Subject: [PATCH 032/665] #2 - Polishing. Fix GitHub issues url. --- README.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.adoc b/README.adoc index 1c32284e..c03cddbc 100644 --- a/README.adoc +++ b/README.adoc @@ -189,7 +189,7 @@ _Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull r Here are some ways for you to get involved in the community: * Get involved with the Spring community by helping out on http://stackoverflow.com/questions/tagged/spring-data-r2dbc[Stackoverflow] by responding to questions and joining the debate. -* Create https://github.com/spring-data/spring-data-r2dbc[GitHub] tickets for bugs and new features and comment and vote on the ones that you are interested in. +* Create https://github.com/spring-projects/spring-data-r2dbc[GitHub] tickets for bugs and new features and comment and vote on the ones that you are interested in. * Github is for social coding: if you want to write code, we encourage contributions through pull requests from http://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing. * Watch for upcoming articles on Spring by http://spring.io/blog[subscribing] to spring.io. From 07811a40f50dd88cee8bf729acb2271493563db1 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 30 Oct 2018 07:46:08 +0100 Subject: [PATCH 033/665] #5 - Use OpenJdk build. JDK 11 build is no longer optional. --- .travis.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 736933ca..6f3f06af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,11 @@ matrix: - env: - JDK='Oracle JDK 10' - NO_JACOCO='true' - before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 10 -L BCL - - env: - - JDK='Oracle JDK 11' - - NO_JACOCO='true' - before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 11 -L BCL - allow_failures: + before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 10 - env: - JDK='Oracle JDK 11' - NO_JACOCO='true' + before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 11 addons: apt: From c2ebfb8cd3522bc46b6cc7f1f6ab9b90763f46a0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 Nov 2018 14:56:41 +0100 Subject: [PATCH 034/665] #10 - Adapt to removed Statement.executeReturningGeneratedKeys(). --- pom.xml | 4 ++-- .../data/r2dbc/function/DefaultDatabaseClient.java | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 73ed1365..bc625c8d 100644 --- a/pom.xml +++ b/pom.xml @@ -30,8 +30,8 @@ 0.1.4 2.2.8 42.0.0 - 1.0.0.M5 - 1.0.0.M5 + 1.0.0.BUILD-SNAPSHOT + 1.0.0.BUILD-SNAPSHOT 1.7.3 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 18503add..3aa0881e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -867,7 +867,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { .collect(Collectors.joining(",")); builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") - .append(placeholders).append(")"); + .append(placeholders).append(") RETURNING *"); String sql = builder.toString(); Function insertFunction = it -> { @@ -881,7 +881,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { }; Function> resultFunction = it -> Flux - .from(insertFunction.apply(it).executeReturningGeneratedKeys()); + .from(insertFunction.apply(it).execute()); return new DefaultSqlResult<>(DefaultDatabaseClient.this, // sql, // @@ -972,7 +972,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { .collect(Collectors.joining(",")); builder.append("INSERT INTO ").append(table).append(" (").append(fieldNames).append(") ").append(" VALUES(") - .append(placeholders).append(")"); + .append(placeholders).append(") RETURNING *"); String sql = builder.toString(); @@ -999,7 +999,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { }; Function> resultFunction = it -> { - return Flux.from(insertFunction.apply(it).executeReturningGeneratedKeys()); + return Flux.from(insertFunction.apply(it).execute()); }; return new DefaultSqlResult<>(DefaultDatabaseClient.this, // From 93ddaff3c2a232322f1be9a9d769a682586b2acc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 Nov 2018 15:04:29 +0100 Subject: [PATCH 035/665] =?UTF-8?q?#11=20-=20Adapt=20Statement.bind(?= =?UTF-8?q?=E2=80=A6)=20calls=20to=20newly=20introduced=20positional=20(in?= =?UTF-8?q?teger-arg)=20binding.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now use bind(int, …) methods to bind parameters by index. --- .../data/r2dbc/function/DefaultDatabaseClient.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3aa0881e..b363f7b5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -245,9 +245,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { byIndex.forEach((i, o) -> { if (o.getValue() != null) { - statement.bind(i, o.getValue()); + statement.bind(i.intValue(), o.getValue()); } else { - statement.bindNull(i, o.getType()); + statement.bindNull(i.intValue(), o.getType()); } }); From 226c80fb02a0e229c3d642634370327e70f3b0bb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 Nov 2018 15:13:19 +0100 Subject: [PATCH 036/665] =?UTF-8?q?#12=20-=20Retain=20input=20item=20order?= =?UTF-8?q?=20in=20save(=E2=80=A6)=20methods.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now use concatMap(…) instead of flatMap(…) when saving items to retain output item order. --- .../support/SimpleR2dbcRepository.java | 17 +++++----- ...SimpleR2dbcRepositoryIntegrationTests.java | 31 +++++++++++-------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index b72b0169..451d2e97 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -15,16 +15,13 @@ */ package org.springframework.data.r2dbc.repository.support; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import java.util.List; import java.util.Map; import java.util.stream.Collectors; import java.util.stream.IntStream; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import org.reactivestreams.Publisher; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; @@ -35,6 +32,8 @@ import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; /** * Simple {@link ReactiveCrudRepository} implementation using R2DBC through {@link DatabaseClient}. @@ -118,7 +117,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository implements ReactiveCrudRepository !ids.isEmpty()).flatMap(ids -> { + return Flux.from(idPublisher).buffer().filter(ids -> !ids.isEmpty()).concatMap(ids -> { String bindings = getInBinding(ids); @@ -259,7 +258,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository !ids.isEmpty()).flatMap(ids -> { + return Flux.from(idPublisher).buffer().filter(ids -> !ids.isEmpty()).concatMap(ids -> { String bindings = getInBinding(ids); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java index f5a22bcc..fe60a1e7 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepositoryIntegrationTests.java @@ -17,26 +17,21 @@ package org.springframework.data.r2dbc.repository.support; import static org.assertj.core.api.Assertions.*; -import io.r2dbc.spi.ConnectionFactory; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - import java.util.Arrays; import java.util.Collections; import java.util.Map; +import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; -import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -44,6 +39,10 @@ import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; import org.springframework.jdbc.core.JdbcTemplate; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; /** * Integration tests for {@link SimpleR2dbcRepository}. @@ -122,14 +121,20 @@ public class SimpleR2dbcRepositoryIntegrationTests extends R2dbcIntegrationTestS LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); LegoSet legoSet2 = new LegoSet(null, "FORSCHUNGSSCHIFF", 13); + LegoSet legoSet3 = new LegoSet(null, "RALLYEAUTO", 14); + LegoSet legoSet4 = new LegoSet(null, "VOLTRON", 15); - repository.saveAll(Arrays.asList(legoSet1, legoSet2)) // + repository.saveAll(Arrays.asList(legoSet1, legoSet2, legoSet3, legoSet4)) // + .map(LegoSet::getManual) // .as(StepVerifier::create) // - .expectNextCount(2) // + .expectNext(12) // + .expectNext(13) // + .expectNext(14) // + .expectNext(15) // .verifyComplete(); Map map = jdbc.queryForMap("SELECT COUNT(*) FROM repo_legoset"); - assertThat(map).containsEntry("count", 2L); + assertThat(map).containsEntry("count", 4L); } @Test From f85a9a084fc1df355905a223768f4a1e26d22be8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 19 Nov 2018 13:48:06 +0100 Subject: [PATCH 037/665] #18 - Cleanup pom.xml and upgrade dependencies. Remove dependency management for Surefire and Dependency plugins. Upgrade to latest Postgres and H2 drivers. Upgrade to testcontainers 10.0.1. --- pom.xml | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/pom.xml b/pom.xml index bc625c8d..f3b3f60c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 - org.springframework.data + org.springframework.data spring-data-r2dbc 1.0.0.BUILD-SNAPSHOT @@ -28,11 +28,11 @@ reuseReports 0.1.4 - 2.2.8 - 42.0.0 + 2.4.1 + 42.2.5 1.0.0.BUILD-SNAPSHOT 1.0.0.BUILD-SNAPSHOT - 1.7.3 + 1.10.1 @@ -272,20 +272,6 @@ - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.12 - - - org.apache.maven.plugins - maven-dependency-plugin - 3.1.0 - - - + + + org.springframework.data + spring-data-r2dbc + {version} + + + + + io.r2dbc + … + {r2dbcVersion} + + + +---- +. Change the version of Spring in the pom.xml to be ++ +[source,xml,subs="+attributes"] +---- +{springVersion} +---- +. Add the following location of the Spring Milestone repository for Maven to your `pom.xml` such that it is at the same level of your `` element: ++ +[source,xml] +---- + + + spring-milestone + Spring Maven MILESTONE Repository + http://repo.spring.io/libs-milestone + + +---- + +The repository is also http://repo.spring.io/milestone/org/springframework/data/[browseable here]. + +You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content: + +[source] +---- +logging.level.org.springframework.data.r2dbc=DEBUG +---- + +Then you can create a `Person` class to persist: + +[source,java] +---- +package org.spring.r2dbc.example; + +public class Person { + + private String id; + private String name; + private int age; + + public Person(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public String getId() { + return id; + } + public String getName() { + return name; + } + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person [id=" + id + ", name=" + name + ", age=" + age + "]"; + } +} +---- + +Next, you need to create a table structure in your database: + +[source,sql] +---- +CREATE TABLE person + (id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + age INT); +---- + +You also need a main application to run: + +[source,java] +---- +package org.spring.r2dbc.example; + +public class R2dbcApp { + + private static final Log log = LogFactory.getLog(R2dbcApp.class); + + public static void main(String[] args) throws Exception { + + ConnectionFactory connectionFactory = new H2ConnectionFactory(H2ConnectionConfiguration.builder() + .url("mem:test;DB_CLOSE_DELAY=10") + .build()); + + DatabaseClient client = DatabaseClient.create(connectionFactory); + + client.execute() + .sql("CREATE TABLE person" + + " (id VARCHAR(255) PRIMARY KEY," + + " name VARCHAR(255)," + + " age INT)") + .fetch() + .rowsUpdated() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + client.insert() + .into(Person.class) + .using(new Person("joe", "Joe", 34)) + .then() + .as(StepVerifier::create) + .verifyComplete(); + + client.select() + .from(Person.class) + .fetch() + .first() + .doOnNext(it -> log.info(it)) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } +} +---- + +When you run the main program, the preceding examples produce the following output: + +[source] +---- +2018-11-28 10:47:03,893 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person + (id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + age INT)] +2018-11-28 10:47:04,074 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)] +2018-11-28 10:47:04,092 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person] +2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34] +---- + +Even in this simple example, there are few things to notice: + +* You can create an instance of the central helper class in Spring Data R2DBC, <>, by using a standard `io.r2dbc.spi.ConnectionFactory` object. +* The mapper works against standard POJO objects without the need for any additional metadata (though you can optionally provide that information. See <>.). +* Mapping conventions can use field access. Notice that the `Person` class has only getters. +* If the constructor argument names match the column names of the stored row, they are used to instantiate the object. + +[[r2dbc.examples-repo]] +== Examples Repository + +There is a https://github.com/spring-projects/spring-data-examples[GitHub repository with several examples] that you can download and play around with to get a feel for how the library works. + +[[r2dbc.drivers]] +== Connecting to a Relational Database with Spring + +One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object using the IoC container. The following example explains Java-based configuration. + +[[r2dbc.connectionfactory]] +=== Registering a `ConnectionFactory` Instance using Java-based Metadata + +The following example shows an example of using Java-based bean metadata to register an instance of a `io.r2dbc.spi.ConnectionFactory`: + +.Registering a `io.r2dbc.spi.ConnectionFactory` object using Java-based bean metadata +==== +[source,java] +---- +@Configuration +public class ApplicationConfiguration extends AbstractR2dbcConfiguration { + + @Override + @Bean + public ConnectionFactory connectionFactory() { + return …; + } +} +---- +==== + +This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features]. + +`AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation. + +[[r2dbc.drivers]] +=== R2DBC Drivers + +Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC. +R2DBC is a relatively young initiative that gains significance by maturing through adoption. +As of writing the following 3 drivers are available: + +* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) +* https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) +* https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`) + +Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect. +You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC. + +[[r2dbc.datbaseclient]] +== Introduction to `DatabaseClient` + +Spring Data R2DBC includes a reactive, non-blocking `DatabaseClient` for database interaction. The client has a functional, fluent API with reactive types for declarative composition. +`DatabaseClient` encapsulates resource handling such as opening and closing connections so your application code can make use of executing SQL queries or calling higher-level functionality such as inserting or selecting data. + +NOTE: `DatabaseClient` is a young application component providing a minimal set of convenience methods that is likely to be extended through time. + +NOTE: Once configured, `DatabaseClient` is thread-safe and can be reused across multiple instances. + +Another central feature of `DatabaseClient` is translation of exceptions thrown by R2DBC drivers into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. + +The next section contains an example of how to work with the `DatabaseClient` in the context of the Spring container. + +[[r2dbc.datbaseclient.create]] +=== Creating `DatabaseClient` + +The simplest way to create a `DatabaseClient` is through a static factory method: + +[source,java] +---- +DatabaseClient.create(ConnectionFactory connectionFactory) +---- + +The above method creates a `DatabaseClient` with default settings. + +You can also use `DatabaseClient.builder()` with further options to customize the client: + +* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. +* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped. + +Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows: + +[source,java] +---- +DatabaseClient client1 = DatabaseClient.builder() + .exceptionTranslator(exceptionTranslatorA).build(); + +DatabaseClient client2 = client1.mutate() + .exceptionTranslator(exceptionTranslatorB).build(); +---- + +=== Controlling Database Connections + +Spring Data R2DBC obtains a connection to the database through a `ConnectionFactory`. +A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory. +It lets a container or a framework hide connection pooling and transaction management issues from the application code. + +When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your R2DBC driver. +`ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling. +`DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations. + +[[r2dbc.exception]] +== Exception Translation + +The Spring framework provides exception translation for a wide variety of database and mapping technologies. +This has traditionally been for JDBC and JPA. The Spring support for R2DBC extends this feature by providing implementations of the `R2dbcExceptionTranslator` interface. + +`R2dbcExceptionTranslator` is an interface to be implemented by classes that can translate between `R2dbcException` and Spring’s own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy. +Implementations can be generic (for example, using SQLState codes) or proprietary (for example, using Postgres error codes) for greater precision. + +`SqlErrorCodeR2dbcExceptionTranslator` is the implementation of `R2dbcExceptionTranslator` that is used by default. +This implementation uses specific vendor codes. +It is more precise than the SQLState implementation. +The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`. +This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module. +This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`. +The codes for the actual database you are using are used. + +The `SqlErrorCodeR2dbcExceptionTranslator` applies matching rules in the following sequence: + +1. Any custom translation implemented by a subclass. Normally, the provided concrete `SqlErrorCodeR2dbcExceptionTranslator` is used, so this rule does not apply. It applies only if you have actually provided a subclass implementation. +2. Any custom implementation of the `SQLExceptionTranslator` interface that is provided as the `customSqlExceptionTranslator` property of the `SQLErrorCodes` class. +3. Error code matching is applied. +4. Use a fallback translator. + + +NOTE: The `SQLErrorCodesFactory` is used by default to define Error codes and custom exception translations. They are looked up in a file named `sql-error-codes.xml` from the classpath, and the matching `SQLErrorCodes` instance is located based on the database name from the database metadata of the database in use. `SQLErrorCodesFactory` is as of now part of Spring JDBC. Spring Data R2DBC reuses existing translation configurations. + +You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example shows: + +[source,java] +---- +public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator { + + protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) { + if (sqlex.getErrorCode() == -12345) { + return new DeadlockLoserDataAccessException(task, r2dbcex); + } + return null; + } +} +---- + +In the preceding example, the specific error code (`-12345`) is translated, while other errors are left to be translated by the default translator implementation. +To use this custom translator, you must configure `DatabaseClient` through the builder method `exceptionTranslator`, and you must use this `DatabaseClient` for all of the data access processing where this translator is needed. +The following example shows how you can use this custom translator: + +[source,java] +---- +ConnectionFactory connectionFactory = …; + +CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator(); + +DatabaseClient client = DatabaseClient.builder() + .connectionFactory(connectionFactory) + .exceptionTranslator(exceptionTranslator) + .build(); +---- + +[[r2dbc.datbaseclient.statements]] +=== Running 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: + +[source,java] +---- +Mono completion = client.execute() + .sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") + .then(); +---- + +`DatabaseClient` is designed for a convenient fluent usage. +It exposes intermediate, continuation, and terminal methods at each stage of the execution specification. +The example above uses `then()` to return a completion `Publisher` that completes as soon as the query (or queries, if the SQL query contains multiple statements) completes. + +NOTE: `execute().sql(…)` accepts either the SQL query string or a query `Supplier` to defer the actual query creation until execution. + +[[r2dbc.datbaseclient.queries]] +=== Running Queries + +SQL queries can return values or the number of affected rows. +`DatabaseClient` can return the number of updated rows or the rows themself, depending on the issued query. + +The following example shows an `UPDATE` statement that returns the number of updated rows: + +[source,java] +---- +Mono affectedRows = client.execute() + .sql("UPDATE person SET name = 'Joe'") + .fetch().rowsUpdated(); +---- + +Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`. +You might have noticed the use of `fetch()` in the previous example. +`fetch()` is a continuation operator that allows you to specify how much data you want to consume. + +[source,java] +---- +Mono> first = client.execute() + .sql("SELECT id, name FROM person") + .fetch().first(); +---- + +Calling `first()` returns the first row from the result and discards remaining rows. +You can consume data with the following operators: + +* `first()` return the first row of the entire result +* `one()` returns exactly one result and fails if the result contains more rows. +* `all()` returns all rows of the result +* `rowsUpdated()` returns the number of affected rows (`INSERT` count, `UPDATE` count) + +`DatabaseClient` queries return their results by default as `Map` of column name to value. You can customize type mapping by applying an `as(Class)` operator. + +[source,java] +---- +Flux all = client.execute() + .sql("SELECT id, name FROM mytable") + .as(Person.class) + .fetch().all(); +---- + +`as(…)` applies <> and maps the resulting columns to your POJO. + +[[r2dbc.datbaseclient.mapping]] +=== Mapping Results + +You can customize result extraction beyong `Map` and POJO result extraction by providing an extractor `BiFunction`. +The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects). + +The following example extracts the `id` column and emits its value: + +[source,java] +---- +Flux names= client.execute() + .sql("SELECT name FROM person") + .fetch() + .extract((row, rowMetadata) -> row.get("id", String.class)) + .all(); +---- + +[[r2dbc.datbaseclient.mapping.null]] +.What about `null`? +**** +Relational database results may contain `null` values. +Reactive Streams forbids emission of `null` values which requires a proper `null` handling in the extractor function. +While you can obtain `null` values from a `Row`, you must not emit a `null` value. +You must wrap any `null` values in an object (e.g. `Optional` for singular values) to make sure a `null` value is never returned directly by your extractor function. +**** + +[[r2dbc.datbaseclient.binding]] +=== Binding Values to Queries + +A typical application requires parametrized SQL statements to select or update rows according to some input. +These are typically `SELECT` statements constrained by a `WHERE` clause or `INSERT`/`UPDATE` statements accepting input parameters. +Parametrized statements bear the risk of SQL injection if parameters are not escaped properly. +`DatabaseClient` leverages R2DBC's Bind API to eliminate the risk of SQL injection for query parameters. +You can provide a parametrized SQL statement with the `sql(…)` operator and bind parameters to the actual `Statement`. +Your R2DBC driver then executes the statement using prepared statements and parameter substitution. + +Parameter binding supports various binding strategies: + +* By Index using zero-based parameter indexes. +* By Name using the placeholder name. + +The following example shows parameter binding for a PostgreSQL query: + +[source,java] +---- +db.execute() + .sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") + .bind(0, "joe") + .bind(1, "Joe") + .bind(2, 34); +---- + +NOTE: If you are familiar with JDBC, then you're also familiar with `?` (questionmark) bind markers. +JDBC drivers translate questionmark bindmarkers to database-native markers as part of statement execution. +Make sure to use the appropriate bind markers that are supported by your database as R2DBC requires database-native parameter bind markers. + +[[r2dbc.datbaseclient.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. +Using different connections hence results in utilizing different transactions. +Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction. +`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods. + +You can run multiple statements within a transaction using the `inTransaction(Function)` closure: + +[source,java] +---- +TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + +Flux completion = databaseClient.inTransaction(db -> { + + return db.execute().sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") // + .bind(0, "joe") // + .bind(1, "Joe") // + .bind(2, 34) // + .fetch().rowsUpdated() + .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES($1, $2)") + .bind(0, "joe") + .bind(1, "Joe") + .fetch().rowsUpdated()) + .then(); +}); +---- diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt new file mode 100644 index 00000000..ddb413a4 --- /dev/null +++ b/src/main/resources/changelog.txt @@ -0,0 +1,3 @@ +Spring Data R2DBC Changelog +=========================== + diff --git a/src/main/resources/license.txt b/src/main/resources/license.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/src/main/resources/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt new file mode 100644 index 00000000..ba8a2e69 --- /dev/null +++ b/src/main/resources/notice.txt @@ -0,0 +1,10 @@ +Spring Data R2DBC 1.0.0.BUILD-SNAPSHOT +Copyright (c) [2018] Pivotal Software, Inc. + +This product is licensed to you under the Apache License, Version 2.0 (the "License"). +You may not use this product except in compliance with the License. + +This product may include a number of subcomponents with +separate copyright notices and license terms. Your use of the source +code for the these subcomponents is subject to the terms and +conditions of the subcomponent's license, as noted in the LICENSE file. From 8461545670488fa4f1c56c683bdc740d2f59ba48 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 4 Dec 2018 08:45:50 +0100 Subject: [PATCH 048/665] #25 - Polishing. Fix typos. Improve wording. Replace document with row and collection with table. Original pull request: #28. --- pom.xml | 20 +++++++++++ src/main/asciidoc/index.adoc | 2 -- src/main/asciidoc/preface.adoc | 29 ++++++++-------- src/main/asciidoc/reference/mapping.adoc | 4 +-- .../reference/r2dbc-repositories.adoc | 4 ++- src/main/asciidoc/reference/r2dbc.adoc | 33 +++++++++---------- 6 files changed, 57 insertions(+), 35 deletions(-) diff --git a/pom.xml b/pom.xml index 62715933..9ff59187 100644 --- a/pom.xml +++ b/pom.xml @@ -35,6 +35,7 @@ 1.0.0.M6 1.0.0.M6 1.0.0.M6 + 1.0.1 1.10.1 @@ -343,6 +344,25 @@ org.asciidoctor asciidoctor-maven-plugin + + ${project.root}/src/main/asciidoc + index.adoc + book + + ${project.version} + ${project.name} + ${project.version} + ${aspectj} + ${querydsl} + ${spring} + ${r2dbc-spi.version} + ${reactive-streams.version} + ${releasetrain} + true + 3 + true + + diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index aa60b705..f9ad3d6c 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -12,8 +12,6 @@ :imagesdir: images ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]] :spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc -:rr2dbcVersion: 1.0.0.M6 -:reactiveStreamsVersion: 1.0.1 :reactiveStreamsJavadoc: http://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc (C) 2018 The original authors. diff --git a/src/main/asciidoc/preface.adoc b/src/main/asciidoc/preface.adoc index 95a956d5..78a28fbd 100644 --- a/src/main/asciidoc/preface.adoc +++ b/src/main/asciidoc/preface.adoc @@ -50,13 +50,13 @@ While the open source ecosystem hosts various non-blocking relational database d The term, reactive refers to programming models that are built around reacting to change, availability, and processability — network components reacting to I/O events, UI controllers reacting to mouse events, resources being made available and others. In that sense, non-blocking is reactive, because, instead of being blocked, we are now in the mode of reacting to notifications as operations complete or data becomes available. -There is also another important mechanism that we on the Spring team associate with reactive and that is non-blocking back pressure. +There is also another important mechanism that we on the Spring team associated with reactive and that is non-blocking back pressure. In synchronous, imperative code, blocking calls serve as a natural form of back pressure that forces the caller to wait. -In non-blocking code, it becomes important to control the rate of events so that a fast producer does not overwhelm its destination. +In non-blocking code, it becomes essential to control the rate of events so that a fast producer does not overwhelm its destination. -Reactive Streams is a https://github.com/reactive-streams/reactive-streams-jvm/blob/v{reactiveStreamsVersion}/README.md#specification[small spec] (also https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html[adopted] in Java 9) that defines the interaction between asynchronous components with back pressure. -For example a data repository (acting as {reactiveStreamsJavadoc}/org/reactivestreams/Publisher.html[`Publisher`]) can produce data that an HTTP server (acting as {reactiveStreamsJavadoc}/org/reactivestreams/Subscriber.html`[`Subscriber`]) can then write to the response. -The main purpose of Reactive Streams is to let the subscriber to control how quickly or how slowly the publisher produces data. +https://github.com/reactive-streams/reactive-streams-jvm/blob/v{reactiveStreamsVersion}/README.md#specification[Reactive Streams is a small spec] (also https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Flow.html[adopted in Java 9]) that defines the interaction between asynchronous components with back pressure. +For example, a data repository (acting as {reactiveStreamsJavadoc}/org/reactivestreams/Publisher.html[`Publisher`]) can produce data that an HTTP server (acting as {reactiveStreamsJavadoc}/org/reactivestreams/Subscriber.html`[`Subscriber`]) can then write to the response. +The main purpose of Reactive Streams is to let the subscriber control how quickly or how slowly the publisher produces data. [[get-started:first-steps:reactive-api]] == Reactive API @@ -81,8 +81,8 @@ Whenever feasible, Spring Data adapts transparently to the use of RxJava or anot The Spring Data R2DBC 1.x binaries require: * JDK level 8.0 and above -* http://spring.io/docs[Spring Framework] {springVersion} and above -* R2DBC {r2dbcVersion} and above +* https://spring.io/docs[Spring Framework] {springVersion} and above +* https://r2dbc.io[R2DBC] {r2dbcVersion} and above [[get-started:help]] == Additional Help Resources @@ -101,12 +101,15 @@ Professional Support :: Professional, from-the-source support, with guaranteed r [[get-started:up-to-date]] == Following Development -For information on the Spring Data R2DBC source code repository, nightly builds, and snapshot artifacts, see the Spring Data R2DBC http://projects.spring.io/spring-data-r2dbc/[homepage]. -You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on http://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. -To follow developer activity, look for the mailing list information on the Spring Data R2DBC https://projects.spring.io/spring-data-r2dbc/[homepage]. -If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data R2DBC https://github.com/spring-projects/spring-data-r2dbc/issues[issue tracker]. -To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community http://spring.io[Portal]. -You can also follow the Spring http://spring.io/blog[blog] or the Spring Data project team on Twitter (http://twitter.com/SpringData[SpringData]). +* For information on the Spring Data R2DBC source code repository, nightly builds, and snapshot artifacts, see the Spring Data R2DBC http://projects.spring.io/spring-data-r2dbc/[homepage]. + +* You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on http://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. + +* If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data R2DBC https://github.com/spring-projects/spring-data-r2dbc/issues[issue tracker]. + +* To stay up to date with the latest news and announcements in the Spring ecosystem, subscribe to the Spring Community http://spring.io[Portal]. + +* You can also follow the Spring http://spring.io/blog[blog] or the Spring Data project team on Twitter (http://twitter.com/SpringData[SpringData]). == Project Metadata diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 6b10dcaf..138ac9a4 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -72,7 +72,7 @@ The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to * `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved row. * `@Column`: Applied at the field level and described the name of the column as it will be represented in the row thus allowing the name to be different than the fieldname of the class. -The mapping metadata infrastructure is defined in a separate spring-data-commons project that is technology agnostic. Specific subclasses are using in the R2DBC support to support annotation based metadata. Other strategies are also possible to put in place if there is demand. +The mapping metadata infrastructure is defined in the separate spring-data-commons project that is technology agnostic. Specific subclasses are using in the R2DBC support to support annotation based metadata. Other strategies are also possible to put in place if there is demand. [[mapping-custom-object-construction]] @@ -81,7 +81,7 @@ The mapping metadata infrastructure is defined in a separate spring-data-commons The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. The values to be used for the constructor parameters are resolved in the following way: * If a parameter is annotated with the `@Value` annotation, the given expression is evaluated and the result is used as the parameter value. -* If the Java type has a property whose name matches the given field of the input row, then it's property information is used to select the appropriate constructor parameter to pass the input field value to. This works only if the parameter name information is present in the java `.class` files which can be achieved by compiling the source with debug information or using the new `-parameters` command-line switch for javac in Java 8. +* If the Java type has a property whose name matches the given field of the input row, then it's property information is used to select the appropriate constructor parameter to pass the input field value to. This works only if the parameter name information is present in the java `.class` files which can be achieved by compiling the source with debug information or using the `-parameters` command-line switch for javac in Java 8. * Otherwise a `MappingException` will be thrown indicating that the given constructor parameter could not be bound. [source,java] diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index 42aaa712..7a46cf07 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -77,7 +77,7 @@ public class PersonRepositoryTests { @Autowired PersonRepository repository; @Test - public void readsallEntitiesCorrectly() { + public void readsAllEntitiesCorrectly() { repository.findAll() .as(StepVerifier::create) @@ -119,3 +119,5 @@ The annotated query uses native bind markers, which are Postgres bind markers in ==== NOTE: R2DBC repositories do not support query derivation. + +NOTE: R2DBC repositories require native parameter bind markers that are bound by index. diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc index 09aa6bfd..b5d47cea 100644 --- a/src/main/asciidoc/reference/r2dbc.adoc +++ b/src/main/asciidoc/reference/r2dbc.adoc @@ -18,7 +18,7 @@ For most tasks, you should use `DatabaseClient` or the Repository support, which An easy way to bootstrap setting up a working environment is to create a Spring-based project through https://start.spring.io[start.spring.io]. -.Add the following to the pom.xml files `dependencies` element: +. Add the following to the pom.xml files `dependencies` element: + [source,xml,subs="+attributes"] ---- @@ -35,7 +35,7 @@ An easy way to bootstrap setting up a working environment is to create a Spring- io.r2dbc - … + r2dbc-h2 {r2dbcVersion} @@ -127,16 +127,16 @@ public class R2dbcApp { public static void main(String[] args) throws Exception { ConnectionFactory connectionFactory = new H2ConnectionFactory(H2ConnectionConfiguration.builder() - .url("mem:test;DB_CLOSE_DELAY=10") - .build()); + .url("mem:test;DB_CLOSE_DELAY=10") + .build()); DatabaseClient client = DatabaseClient.create(connectionFactory); client.execute() .sql("CREATE TABLE person" + - " (id VARCHAR(255) PRIMARY KEY," + - " name VARCHAR(255)," + - " age INT)") + "(id VARCHAR(255) PRIMARY KEY," + + "name VARCHAR(255)," + + "age INT)") .fetch() .rowsUpdated() .as(StepVerifier::create) @@ -162,7 +162,7 @@ public class R2dbcApp { } ---- -When you run the main program, the preceding examples produce the following output: +When you run the main program, the preceding examples produce output similar to the following: [source] ---- @@ -364,7 +364,7 @@ NOTE: `execute().sql(…)` accepts either the SQL query string or a query `Suppl === Running Queries SQL queries can return values or the number of affected rows. -`DatabaseClient` can return the number of updated rows or the rows themself, depending on the issued query. +`DatabaseClient` can return the number of updated rows or the rows themselves, depending on the issued query. The following example shows an `UPDATE` statement that returns the number of updated rows: @@ -409,7 +409,7 @@ Flux all = client.execute() [[r2dbc.datbaseclient.mapping]] === Mapping Results -You can customize result extraction beyong `Map` and POJO result extraction by providing an extractor `BiFunction`. +You can customize result extraction beyond `Map` and POJO result extraction by providing an extractor `BiFunction`. The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects). The following example extracts the `id` column and emits its value: @@ -418,8 +418,7 @@ The following example extracts the `id` column and emits its value: ---- Flux names= client.execute() .sql("SELECT name FROM person") - .fetch() - .extract((row, rowMetadata) -> row.get("id", String.class)) + .map((row, rowMetadata) -> row.get("id", String.class)) .all(); ---- @@ -435,11 +434,11 @@ You must wrap any `null` values in an object (e.g. `Optional` for singular value [[r2dbc.datbaseclient.binding]] === Binding Values to Queries -A typical application requires parametrized SQL statements to select or update rows according to some input. +A typical application requires parameterized SQL statements to select or update rows according to some input. These are typically `SELECT` statements constrained by a `WHERE` clause or `INSERT`/`UPDATE` statements accepting input parameters. -Parametrized statements bear the risk of SQL injection if parameters are not escaped properly. +Parameterized statements bear the risk of SQL injection if parameters are not escaped properly. `DatabaseClient` leverages R2DBC's Bind API to eliminate the risk of SQL injection for query parameters. -You can provide a parametrized SQL statement with the `sql(…)` operator and bind parameters to the actual `Statement`. +You can provide a parameterized SQL statement with the `sql(…)` operator and bind parameters to the actual `Statement`. Your R2DBC driver then executes the statement using prepared statements and parameter substitution. Parameter binding supports various binding strategies: @@ -458,8 +457,8 @@ db.execute() .bind(2, 34); ---- -NOTE: If you are familiar with JDBC, then you're also familiar with `?` (questionmark) bind markers. -JDBC drivers translate questionmark bindmarkers to database-native markers as part of statement execution. +NOTE: If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers. +JDBC drivers translate question mark bind markers to database-native markers as part of statement execution. Make sure to use the appropriate bind markers that are supported by your database as R2DBC requires database-native parameter bind markers. [[r2dbc.datbaseclient.transactions]] From 536d484012fce730743a288eadb90ab565c1687e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 4 Dec 2018 09:55:43 +0100 Subject: [PATCH 049/665] #32 - Drop oracle-java8-installer from TravisCI build. --- .travis.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6f3f06af..2c41867b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,11 +15,6 @@ matrix: - NO_JACOCO='true' before_install: wget https://github.com/sormuras/bach/raw/master/install-jdk.sh && . ./install-jdk.sh -F 11 -addons: - apt: - packages: - - oracle-java8-installer - cache: directories: - $HOME/.m2 From 5a5310af4d416bb558dd23bc267409eef2522a77 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 28 Nov 2018 14:40:26 +0100 Subject: [PATCH 050/665] =?UTF-8?q?#8=20-=20Replace=20exchange()=20method?= =?UTF-8?q?=20with=20map(=E2=80=A6)=20and=20then()=20methods.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DatabaseClient now no longer exposes the exchange() method but rather provides map(…) and then(…) methods. Calling map(…) extracts values from tabular results by propagating the mapping function to Result.map(…). then() allows statement execution without returning the result propagating only completion and error signals. Original pull request: #33. --- README.adoc | 20 +- .../data/r2dbc/function/DatabaseClient.java | 106 ++++++---- .../r2dbc/function/DefaultDatabaseClient.java | 184 +++++++++++++----- .../data/r2dbc/function/DefaultSqlResult.java | 42 +++- .../data/r2dbc/function/SqlResult.java | 2 +- .../support/SimpleR2dbcRepository.java | 18 +- ...bstractDatabaseClientIntegrationTests.java | 38 +++- ...ctionalDatabaseClientIntegrationTests.java | 6 +- 8 files changed, 297 insertions(+), 119 deletions(-) diff --git a/README.adoc b/README.adoc index c03cddbc..1033deb7 100644 --- a/README.adoc +++ b/README.adoc @@ -8,7 +8,7 @@ The state of R2DBC is incubating to evaluate how an reactive integration could l == This is NOT an ORM -Spring Data R2DBC does not try to be an ORM. +Spring Data R2DBC does not try to be an ORM. Instead it is more of a construction kit for your personal reactive relational data access component that you can define the way you like or need it. == Maven Coordinates @@ -64,12 +64,13 @@ Mono count = databaseClient.execute() Flux> rows = databaseClient.execute() .sql("SELECT id, name, manual FROM legoset") - .fetch().all(); + .fetch() + .all(); Flux result = db.execute() .sql("SELECT txid_current();") - .exchange() - .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + .map((r, md) -> r.get(0, Long.class)) + .all(); ---- === Examples selecting data @@ -100,14 +101,13 @@ Flux ids = databaseClient.insert() .value("id", 42055) .value("name", "Description") .nullValue("manual", Integer.class) - .exchange() // - .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) + .map((r, m) -> r.get("id", Integer.class) + .all(); -Flux ids = databaseClient.insert() +Mono completion = databaseClient.insert() .into(LegoSet.class) .using(legoSet) - .exchange() - .flatMapMany(it -> it.extract((r, m) -> r.get("id", Integer.class)).all()) + .then(); ---- === Examples using reactive repositories @@ -162,7 +162,7 @@ Flux rowsUpdated = databaseClient.inTransaction(db -> { [source,java] ---- Flux txId = databaseClient.execute().sql("SELECT txid_current();").exchange() - .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + .flatMapMany(it -> it.map((r, md) -> r.get(0, Long.class)).all()); Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() // .thenMany(txId)) // 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 4e49dcbe..2ad6c5a8 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -156,17 +156,26 @@ public interface DatabaseClient { */ TypedExecuteSpec as(Class resultType); + /** + * Configure a result mapping {@link java.util.function.BiFunction function}. + * + * @param mappingFunction must not be {@literal null}. + * @param result type. + * @return + */ + FetchSpec map(BiFunction mappingFunction); + /** * Perform the SQL call and retrieve the result. */ FetchSpec> fetch(); /** - * Perform the SQL request and return a {@link SqlResult}. + * Perform the SQL call and return a {@link Mono} that completes without result on statement completion. * - * @return a {@code Mono} for the result + * @return a {@link Mono} ignoring its payload (actively dropping). */ - Mono>> exchange(); + Mono then(); } /** @@ -183,17 +192,26 @@ public interface DatabaseClient { */ TypedExecuteSpec as(Class resultType); + /** + * Configure a result mapping {@link java.util.function.BiFunction function}. + * + * @param mappingFunction must not be {@literal null}. + * @param result type. + * @return + */ + FetchSpec map(BiFunction mappingFunction); + /** * Perform the SQL call and retrieve the result. */ FetchSpec fetch(); /** - * Perform the SQL request and return a {@link SqlResult}. + * Perform the SQL call and return a {@link Mono} that completes without result on statement completion. * - * @return a {@code Mono} for the result + * @return a {@link Mono} ignoring its payload (actively dropping). */ - Mono> exchange(); + Mono then(); } /** @@ -229,7 +247,7 @@ public interface DatabaseClient { * @param table must not be {@literal null} or empty. * @return */ - GenericInsertSpec into(String table); + GenericInsertSpec> into(String table); /** * Specify the target table to insert to using the {@link Class entity class}. @@ -254,17 +272,19 @@ public interface DatabaseClient { */ TypedSelectSpec as(Class resultType); + /** + * Configure a result mapping {@link java.util.function.BiFunction function}. + * + * @param mappingFunction must not be {@literal null}. + * @param result type. + * @return + */ + FetchSpec map(BiFunction mappingFunction); + /** * Perform the SQL call and retrieve the result. */ FetchSpec> fetch(); - - /** - * Perform the SQL request and return a {@link SqlResult}. - * - * @return a {@code Mono} for the result - */ - Mono>> exchange(); } /** @@ -279,28 +299,21 @@ public interface DatabaseClient { * @param resultType must not be {@literal null}. * @param result type. */ - TypedSelectSpec as(Class resultType); + FetchSpec as(Class resultType); /** - * Configure a result mapping {@link java.util.function.Function}. + * Configure a result mapping {@link java.util.function.BiFunction function}. * * @param mappingFunction must not be {@literal null}. * @param result type. * @return */ - TypedSelectSpec extract(BiFunction mappingFunction); + FetchSpec map(BiFunction mappingFunction); /** * Perform the SQL call and retrieve the result. */ FetchSpec fetch(); - - /** - * Perform the SQL request and return a {@link SqlResult}. - * - * @return a {@code Mono} for the result - */ - Mono> exchange(); } /** @@ -332,8 +345,10 @@ public interface DatabaseClient { /** * Contract for specifying {@code INSERT} options leading to the exchange. + * + * @param Result type of tabular insert results. */ - interface GenericInsertSpec extends InsertSpec { + interface GenericInsertSpec extends InsertSpec { /** * Specify a field and non-{@literal null} value to insert. @@ -341,7 +356,7 @@ public interface DatabaseClient { * @param field must not be {@literal null} or empty. * @param value must not be {@literal null} */ - GenericInsertSpec value(String field, Object value); + GenericInsertSpec value(String field, Object value); /** * Specify a {@literal null} value to insert. @@ -349,7 +364,7 @@ public interface DatabaseClient { * @param field must not be {@literal null} or empty. * @param type must not be {@literal null}. */ - GenericInsertSpec nullValue(String field, Class type); + GenericInsertSpec nullValue(String field, Class type); } /** @@ -363,7 +378,7 @@ public interface DatabaseClient { * @param objectToInsert * @return */ - InsertSpec using(T objectToInsert); + InsertSpec> using(T objectToInsert); /** * Use the given {@code tableName} as insert target. @@ -374,30 +389,43 @@ public interface DatabaseClient { TypedInsertSpec table(String tableName); /** - * Insert the given {@link Publisher} to insert one or more objects. + * Insert the given {@link Publisher} to insert one or more objects. Inserts only a single object when calling + * {@link FetchSpec#one()} or {@link FetchSpec#first()}. * * @param objectToInsert * @return + * @see InsertSpec#fetch() */ - InsertSpec using(Publisher objectToInsert); + InsertSpec> using(Publisher objectToInsert); } /** * Contract for specifying {@code INSERT} options leading to the exchange. + * + * @param Result type of tabular insert results. */ - interface InsertSpec { + interface InsertSpec { /** - * Perform the SQL call. + * Configure a result mapping {@link java.util.function.BiFunction function}. + * + * @param mappingFunction must not be {@literal null}. + * @param result type. + * @return + */ + FetchSpec map(BiFunction mappingFunction); + + /** + * Perform the SQL call and retrieve the result. + */ + FetchSpec 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(); - - /** - * Perform the SQL request and return a {@link SqlResult}. - * - * @return a {@code Mono} for the result - */ - Mono>> exchange(); } /** 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 a6545a93..400e7eba 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -41,6 +41,7 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -220,6 +221,15 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); } + /** + * Customization hook. + */ + protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, + Map byName, Supplier sqlSupplier, + BiFunction mappingFunction) { + return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, mappingFunction); + } + /** * Customization hook. */ @@ -295,6 +305,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.sqlSupplier = sqlSupplier; } + ExecuteSpecSupport(ExecuteSpecSupport other) { + + this.byIndex = other.byIndex; + this.byName = other.byName; + this.sqlSupplier = other.sqlSupplier; + } + protected String getSql() { String sql = sqlSupplier.get(); @@ -396,14 +413,22 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createTypedExecuteSpec(this.byIndex, this.byName, this.sqlSupplier, resultType); } + @Override + public FetchSpec map(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return exchange(getSql(), mappingFunction); + } + @Override public FetchSpec> fetch() { return exchange(getSql(), ColumnMapRowMapper.INSTANCE); } @Override - public Mono>> exchange() { - return Mono.just(exchange(getSql(), ColumnMapRowMapper.INSTANCE)); + public Mono then() { + return fetch().rowsUpdated().then(); } @Override @@ -456,6 +481,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); } + DefaultTypedExecuteSpec(Map byIndex, Map byName, + Supplier sqlSupplier, BiFunction mappingFunction) { + + super(byIndex, byName, sqlSupplier); + this.typeToRead = null; + this.mappingFunction = mappingFunction; + } + @Override public TypedExecuteSpec as(Class resultType) { @@ -464,14 +497,22 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createTypedExecuteSpec(this.byIndex, this.byName, this.sqlSupplier, resultType); } + @Override + public FetchSpec map(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return exchange(getSql(), mappingFunction); + } + @Override public FetchSpec fetch() { return exchange(getSql(), mappingFunction); } @Override - public Mono> exchange() { - return Mono.just(exchange(getSql(), mappingFunction)); + public Mono then() { + return fetch().rowsUpdated().then(); } @Override @@ -603,10 +644,21 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public TypedSelectSpec as(Class resultType) { + + Assert.notNull(resultType, "Result type must not be null!"); + return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, resultType, dataAccessStrategy.getRowMapper(resultType)); } + @Override + public FetchSpec map(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return exchange(mappingFunction); + } + @Override public DefaultGenericSelectSpec project(String... selectedFields) { return (DefaultGenericSelectSpec) super.project(selectedFields); @@ -627,11 +679,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return exchange(ColumnMapRowMapper.INSTANCE); } - @Override - public Mono>> exchange() { - return Mono.just(exchange(ColumnMapRowMapper.INSTANCE)); - } - private SqlResult exchange(BiFunction mappingFunction) { Set columns; @@ -660,7 +707,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @SuppressWarnings("unchecked") private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { - private final Class typeToRead; + private final @Nullable Class typeToRead; private final BiFunction mappingFunction; DefaultTypedSelectSpec(Class typeToRead) { @@ -671,7 +718,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); } - DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, + DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, + BiFunction mappingFunction) { + this(table, projectedFields, sort, page, null, mappingFunction); + } + + DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, BiFunction mappingFunction) { super(table, projectedFields, sort, page); this.typeToRead = typeToRead; @@ -679,20 +731,19 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public TypedSelectSpec as(Class resultType) { + public FetchSpec as(Class resultType) { Assert.notNull(resultType, "Result type must not be null!"); - return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, - dataAccessStrategy.getRowMapper(resultType)); + return exchange(dataAccessStrategy.getRowMapper(resultType)); } @Override - public TypedSelectSpec extract(BiFunction mappingFunction) { + public FetchSpec map(BiFunction mappingFunction) { Assert.notNull(mappingFunction, "Mapping function must not be null!"); - return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); + return exchange(mappingFunction); } @Override @@ -711,15 +762,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public FetchSpec fetch() { + public SqlResult fetch() { return exchange(mappingFunction); } - @Override - public Mono> exchange() { - return Mono.just(exchange(mappingFunction)); - } - private SqlResult exchange(BiFunction mappingFunction) { List columns; @@ -749,13 +795,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { class DefaultInsertIntoSpec implements InsertIntoSpec { @Override - public GenericInsertSpec into(String table) { - return new DefaultGenericInsertSpec(table, Collections.emptyMap()); + public GenericInsertSpec> into(String table) { + return new DefaultGenericInsertSpec<>(table, Collections.emptyMap(), ColumnMapRowMapper.INSTANCE); } @Override public TypedInsertSpec into(Class table) { - return new DefaultTypedInsertSpec<>(table); + return new DefaultTypedInsertSpec<>(table, ColumnMapRowMapper.INSTANCE); } } @@ -763,10 +809,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Default implementation of {@link DatabaseClient.GenericInsertSpec}. */ @RequiredArgsConstructor - class DefaultGenericInsertSpec implements GenericInsertSpec { + class DefaultGenericInsertSpec implements GenericInsertSpec { private final String table; private final Map byName; + private final BiFunction mappingFunction; @Override public GenericInsertSpec value(String field, Object value) { @@ -776,7 +823,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Map byName = new LinkedHashMap<>(this.byName); byName.put(field, new SettableValue(field, value, null)); - return new DefaultGenericInsertSpec(this.table, byName); + return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); } @Override @@ -787,20 +834,28 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Map byName = new LinkedHashMap<>(this.byName); byName.put(field, new SettableValue(field, null, type)); - return new DefaultGenericInsertSpec(this.table, byName); + return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); + } + + @Override + public FetchSpec map(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return exchange(mappingFunction); + } + + @Override + public FetchSpec fetch() { + return exchange(this.mappingFunction); } @Override public Mono then() { - return exchange((row, md) -> row).all().then(); + return fetch().rowsUpdated().then(); } - @Override - public Mono>> exchange() { - return Mono.just(exchange(ColumnMapRowMapper.INSTANCE)); - } - - private SqlResult exchange(BiFunction mappingFunction) { + private SqlResult exchange(BiFunction mappingFunction) { if (byName.isEmpty()) { throw new IllegalStateException("Insert fields is empty!"); @@ -809,7 +864,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, byName.keySet()); String sql = bindableInsert.toQuery(); - Function insertFunction = it -> { + Function> insertFunction = it -> { if (logger.isDebugEnabled()) { logger.debug("Executing SQL statement [" + sql + "]"); @@ -837,17 +892,19 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Default implementation of {@link DatabaseClient.TypedInsertSpec}. */ @RequiredArgsConstructor - class DefaultTypedInsertSpec implements TypedInsertSpec, InsertSpec { + class DefaultTypedInsertSpec implements TypedInsertSpec, InsertSpec { private final Class typeToInsert; private final String table; private final Publisher objectToInsert; + private final BiFunction mappingFunction; - DefaultTypedInsertSpec(Class typeToInsert) { + DefaultTypedInsertSpec(Class typeToInsert, BiFunction mappingFunction) { this.typeToInsert = typeToInsert; this.table = dataAccessStrategy.getTableName(typeToInsert); this.objectToInsert = Mono.empty(); + this.mappingFunction = mappingFunction; } @Override @@ -855,7 +912,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.hasText(tableName, "Table name must not be null or empty!"); - return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert); + return new DefaultTypedInsertSpec<>(typeToInsert, tableName, objectToInsert, this.mappingFunction); } @Override @@ -863,7 +920,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(objectToInsert, "Object to insert must not be null!"); - return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert)); + return new DefaultTypedInsertSpec<>(typeToInsert, table, Mono.just(objectToInsert), this.mappingFunction); } @Override @@ -871,7 +928,20 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(objectToInsert, "Publisher to insert must not be null!"); - return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert); + return new DefaultTypedInsertSpec<>(typeToInsert, table, objectToInsert, this.mappingFunction); + } + + @Override + public FetchSpec map(BiFunction mappingFunction) { + + Assert.notNull(mappingFunction, "Mapping function must not be null!"); + + return exchange(mappingFunction); + } + + @Override + public FetchSpec fetch() { + return exchange(this.mappingFunction); } @Override @@ -879,12 +949,33 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return Mono.from(objectToInsert).flatMapMany(toInsert -> exchange(toInsert, (row, md) -> row).all()).then(); } - @Override - public Mono>> exchange() { - return Mono.from(objectToInsert).map(toInsert -> exchange(toInsert, ColumnMapRowMapper.INSTANCE)); + private FetchSpec exchange(BiFunction mappingFunction) { + + return new FetchSpec() { + @Override + public Mono one() { + return Mono.from(objectToInsert).flatMap(toInsert -> exchange(toInsert, mappingFunction).one()); + } + + @Override + public Mono first() { + return Mono.from(objectToInsert).flatMap(toInsert -> exchange(toInsert, mappingFunction).first()); + } + + @Override + public Flux all() { + return Flux.from(objectToInsert).flatMap(toInsert -> exchange(toInsert, mappingFunction).all()); + } + + @Override + public Mono rowsUpdated() { + return Mono.from(objectToInsert).flatMapMany(toInsert -> exchange(toInsert, mappingFunction).rowsUpdated()) + .collect(Collectors.summingInt(Integer::intValue)); + } + }; } - private SqlResult exchange(Object toInsert, BiFunction mappingFunction) { + private SqlResult exchange(Object toInsert, BiFunction mappingFunction) { List insertValues = dataAccessStrategy.getValuesToInsert(toInsert); Set columns = new LinkedHashSet<>(); @@ -919,7 +1010,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return new DefaultSqlResult<>(DefaultDatabaseClient.this, // sql, // resultFunction, // - it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated).next(), // + it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated) + .collect(Collectors.summingInt(Integer::intValue)), // mappingFunction); } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index 378b4731..44f11653 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -34,6 +34,33 @@ import org.springframework.jdbc.core.SqlProvider; */ class DefaultSqlResult implements SqlResult { + private final static SqlResult EMPTY = new SqlResult() { + @Override + public SqlResult map(BiFunction mappingFunction) { + return DefaultSqlResult.empty(); + } + + @Override + public Mono one() { + return Mono.empty(); + } + + @Override + public Mono first() { + return Mono.empty(); + } + + @Override + public Flux all() { + return Flux.empty(); + } + + @Override + public Mono rowsUpdated() { + return Mono.empty(); + } + }; + private final ConnectionAccessor connectionAccessor; private final String sql; private final Function> resultFunction; @@ -71,11 +98,22 @@ class DefaultSqlResult implements SqlResult { }); } + /** + * Returns an empty {@link SqlResult}. + * + * @param + * @return + */ + @SuppressWarnings("unchecked") + public static SqlResult empty() { + return (SqlResult) EMPTY; + } + /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.SqlResult#extract(java.util.function.BiFunction) + * @see org.springframework.data.jdbc.core.function.SqlResult#map(java.util.function.BiFunction) */ @Override - public SqlResult extract(BiFunction mappingFunction) { + public SqlResult map(BiFunction mappingFunction) { return new DefaultSqlResult<>(connectionAccessor, sql, resultFunction, updatedRowsFunction, mappingFunction); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index eabf54a6..a0b3a0ce 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -34,5 +34,5 @@ public interface SqlResult extends FetchSpec { * @param * @return a new {@link SqlResult} with {@link BiFunction mapping function} applied. */ - SqlResult extract(BiFunction mappingFunction); + SqlResult map(BiFunction mappingFunction); } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 23830347..fb1458ba 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -32,7 +32,6 @@ import org.springframework.data.r2dbc.function.BindIdOperation; import org.springframework.data.r2dbc.function.BindableOperation; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.r2dbc.function.FetchSpec; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.function.convert.SettableValue; @@ -66,8 +65,8 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract(converter.populateIdIfNecessary(objectToSave)).one()); + .map(converter.populateIdIfNecessary(objectToSave)) // + .one(); } Object id = entity.getRequiredId(objectToSave); @@ -83,8 +82,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository it.extract((r, md) -> r).first()).hasElement(); + .map((r, md) -> r) // + .first() // + .hasElement(); } /* (non-Javadoc) @@ -220,8 +219,8 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository it.extract((r, md) -> r.get(0, Long.class)).first()) // + .map((r, md) -> r.get(0, Long.class)) // + .first() // .defaultIfEmpty(0L); } @@ -312,7 +311,6 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository deleteAll() { return databaseClient.execute().sql(String.format("DELETE FROM %s", entity.getTableName())) // - .exchange() // .then(); } diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 3ebf5bc3..bfe27ad1 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -20,6 +20,7 @@ import static org.springframework.data.domain.Sort.Order.*; import io.r2dbc.spi.ConnectionFactory; import lombok.Data; +import reactor.core.publisher.Flux; import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; @@ -108,6 +109,9 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .expectNext(1) // .verifyComplete(); + Flux rows = databaseClient.select().from("legoset").orderBy(Sort.by(desc("id"))).as(LegoSet.class).fetch() + .all(); + assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } @@ -160,8 +164,8 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .value("id", 42055) // .value("name", "SCHAUFELRADBAGGER") // .nullValue("manual", Integer.class) // - .exchange() // - .flatMapMany(FetchSpec::rowsUpdated) // + .fetch() // + .rowsUpdated() // .as(StepVerifier::create) // .expectNext(1).verifyComplete(); @@ -195,16 +199,18 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); databaseClient.insert().into(LegoSet.class)// - .using(legoSet).exchange() // - .flatMapMany(FetchSpec::rowsUpdated) // + .using(legoSet) // + .fetch() // + .rowsUpdated() // .as(StepVerifier::create) // - .expectNext(1).verifyComplete(); + .expectNext(1) // + .verifyComplete(); assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } @Test - public void select() { + public void selectAsMap() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -213,7 +219,8 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr databaseClient.select().from(LegoSet.class) // .project("id", "name", "manual") // .orderBy(Sort.by("id")) // - .fetch().all() // + .fetch() // + .all() // .as(StepVerifier::create) // .assertNext(actual -> { assertThat(actual.getId()).isEqualTo(42055); @@ -222,6 +229,23 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr }).verifyComplete(); } + @Test + public void selectExtracting() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from("legoset") // + .project("id", "name", "manual") // + .orderBy(Sort.by("id")) // + .map((r, md) -> r.get("id", Integer.class)) // + .all() // + .as(StepVerifier::create) // + .expectNext(42055) // + .verifyComplete(); + } + @Test public void selectOrderByIdDesc() { diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index c50b3050..a57c5f87 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -148,8 +148,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend Queue transactionIds = new ArrayBlockingQueue<>(5); TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); - Flux txId = databaseClient.execute().sql(getCurrentTransactionIdStatement()).exchange() - .flatMapMany(it -> it.extract((r, md) -> r.get(0, Long.class)).all()); + Flux txId = databaseClient.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0, Long.class)).all(); Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() // .thenMany(txId.concatWith(txId).doOnNext(transactionIds::add)) // @@ -207,8 +206,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend Flux transactionIds = databaseClient.inTransaction(db -> { - Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).exchange() - .flatMapMany(it -> it.extract((r, md) -> r.get(0)).all()); + Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0)).all(); return txId.concatWith(txId); }); From 7db8e64393babc22632f1ba7a681432371592785 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 4 Dec 2018 16:37:12 +0100 Subject: [PATCH 051/665] #8 - Polishing. Formatting, JavaDoc, issue comments on `@Test` annotations. Removed some dead code. Original pull request: #33. --- .../data/r2dbc/function/DatabaseClient.java | 38 +++--- .../r2dbc/function/DefaultDatabaseClient.java | 115 +++++++++--------- .../data/r2dbc/function/DefaultSqlResult.java | 4 +- .../data/r2dbc/function/SqlResult.java | 2 +- .../support/SimpleR2dbcRepository.java | 10 +- ...bstractDatabaseClientIntegrationTests.java | 53 ++++---- ...ctionalDatabaseClientIntegrationTests.java | 67 +++++----- 7 files changed, 143 insertions(+), 146 deletions(-) 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 2ad6c5a8..af76033a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -64,7 +64,9 @@ public interface DatabaseClient { // Static, factory methods /** - * A variant of {@link #create()} that accepts a {@link io.r2dbc.spi.ConnectionFactory} + * Creates a {@code DatabaseClient} that will use the provided {@link io.r2dbc.spi.ConnectionFactory}. + * @param factory The {@code ConnectionFactory} to use for obtaining connections. + * @return a new {@code DatabaseClient}. Guaranteed to be not {@code null}. */ static DatabaseClient create(ConnectionFactory factory) { return new DefaultDatabaseClientBuilder().connectionFactory(factory).build(); @@ -161,7 +163,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. */ FetchSpec map(BiFunction mappingFunction); @@ -197,7 +199,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. */ FetchSpec map(BiFunction mappingFunction); @@ -223,7 +225,7 @@ public interface DatabaseClient { * Specify the source {@literal table} to select from. * * @param table must not be {@literal null} or empty. - * @return + * @return a {@link GenericSelectSpec} for further configuration of the select. Guaranteed to be not {@code null}. */ GenericSelectSpec from(String table); @@ -231,7 +233,7 @@ public interface DatabaseClient { * Specify the source table to select from to using the {@link Class entity class}. * * @param table must not be {@literal null}. - * @return + * @return a {@link TypedSelectSpec} for further configuration of the select. Guaranteed to be not {@code null}. */ TypedSelectSpec from(Class table); } @@ -245,7 +247,7 @@ public interface DatabaseClient { * Specify the target {@literal table} to insert into. * * @param table must not be {@literal null} or empty. - * @return + * @return a {@link GenericInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. */ GenericInsertSpec> into(String table); @@ -253,7 +255,7 @@ public interface DatabaseClient { * Specify the target table to insert to using the {@link Class entity class}. * * @param table must not be {@literal null}. - * @return + * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. */ TypedInsertSpec into(Class table); } @@ -277,7 +279,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. */ FetchSpec map(BiFunction mappingFunction); @@ -306,7 +308,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. */ FetchSpec map(BiFunction mappingFunction); @@ -375,8 +377,8 @@ public interface DatabaseClient { /** * Insert the given {@code objectToInsert}. * - * @param objectToInsert - * @return + * @param objectToInsert the object of which the attributes will provide the values for the insert. Must not be {@code null}. + * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. */ InsertSpec> using(T objectToInsert); @@ -384,7 +386,7 @@ public interface DatabaseClient { * Use the given {@code tableName} as insert target. * * @param tableName must not be {@literal null} or empty. - * @return + * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. */ TypedInsertSpec table(String tableName); @@ -392,8 +394,8 @@ public interface DatabaseClient { * Insert the given {@link Publisher} to insert one or more objects. Inserts only a single object when calling * {@link FetchSpec#one()} or {@link FetchSpec#first()}. * - * @param objectToInsert - * @return + * @param objectToInsert a publisher providing the objects of which the attributes will provide the values for the insert. Must not be {@code null}. + * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. * @see InsertSpec#fetch() */ InsertSpec> using(Publisher objectToInsert); @@ -411,7 +413,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. */ FetchSpec map(BiFunction mappingFunction); @@ -436,15 +438,15 @@ public interface DatabaseClient { /** * Bind a non-{@literal null} value to a parameter identified by its {@code index}. * - * @param index - * @param value must not be {@literal null}. + * @param index zero based index to bind the parameter to. + * @param value to bind. Must not be {@literal null}. */ S bind(int index, Object value); /** * Bind a {@literal null} value to a parameter identified by its {@code index}. * - * @param index + * @param index zero based index to bind the parameter to. * @param type must not be {@literal null}. */ S bindNull(int index, Class type); 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 400e7eba..c8b8dacf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -23,6 +23,20 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Statement; import lombok.RequiredArgsConstructor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; +import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -43,21 +57,6 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.springframework.dao.DataAccessException; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.r2dbc.UncategorizedR2dbcException; -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; -import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; -import org.springframework.data.r2dbc.function.convert.SettableValue; -import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.jdbc.core.SqlProvider; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - /** * Default implementation of {@link DatabaseClient}. * @@ -65,7 +64,9 @@ import org.springframework.util.Assert; */ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { - /** Logger available to subclasses */ + /** + * Logger available to subclasses + */ private final Log logger = LogFactory.getLog(getClass()); private final ConnectionFactory connector; @@ -77,7 +78,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final DefaultDatabaseClientBuilder builder; DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { + ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { this.connector = connector; this.exceptionTranslator = exceptionTranslator; @@ -113,7 +114,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * * @param action must not be {@literal null}. * @return the resulting {@link Mono}. - * @throws DataAccessException + * @throws DataAccessException when during construction of the {@link Mono} a problem occurs. */ @Override public Mono inConnection(Function> action) throws DataAccessException { @@ -140,7 +141,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * * @param action must not be {@literal null}. * @return the resulting {@link Flux}. - * @throws DataAccessException + * @throws DataAccessException when during construction of the {@link Mono} a problem occurs. */ @Override public Flux inConnectionMany(Function> action) throws DataAccessException { @@ -162,7 +163,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** * Obtain a {@link Connection}. * - * @return + * @return a {@link Mono} able to emit a {@link Connection}. */ protected Mono getConnection() { return Mono.from(obtainConnectionFactory().create()); @@ -171,8 +172,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { /** * Release the {@link Connection}. * - * @param connection - * @return + * @param connection to close. + * @return a {@link Publisher} that completes successfully when the connection is closed. */ protected Publisher closeConnection(Connection connection) { return connection.close(); @@ -196,15 +197,15 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { */ protected Connection createConnectionProxy(Connection con) { return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), - new Class[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(con)); + new Class[]{ConnectionProxy.class}, new CloseSuppressingInvocationHandler(con)); } /** * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}. * * @param task readable text describing the task being attempted. - * @param sql SQL query or update that caused the problem (may be {@literal null}). - * @param ex the offending {@link R2dbcException}. + * @param sql SQL query or update that caused the problem (may be {@literal null}). + * @param ex the offending {@link R2dbcException}. * @return a DataAccessException wrapping the {@link R2dbcException} (never {@literal null}). */ protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) { @@ -217,7 +218,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier, Class typeToRead) { + Map byName, Supplier sqlSupplier, Class typeToRead) { return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); } @@ -225,8 +226,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier, - BiFunction mappingFunction) { + Map byName, Supplier sqlSupplier, + BiFunction mappingFunction) { return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, mappingFunction); } @@ -234,7 +235,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected ExecuteSpecSupport createGenericExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier) { + Map byName, Supplier sqlSupplier) { return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); } @@ -246,7 +247,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } private static void doBind(Statement statement, Map byName, - Map byIndex) { + Map byIndex) { byIndex.forEach((i, o) -> { @@ -305,13 +306,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.sqlSupplier = sqlSupplier; } - ExecuteSpecSupport(ExecuteSpecSupport other) { - - this.byIndex = other.byIndex; - this.byName = other.byName; - this.sqlSupplier = other.sqlSupplier; - } - protected String getSql() { String sql = sqlSupplier.get(); @@ -379,7 +373,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { return new ExecuteSpecSupport(byIndex, byName, sqlSupplier); } @@ -397,7 +391,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { protected class DefaultGenericExecuteSpec extends ExecuteSpecSupport implements GenericExecuteSpec { DefaultGenericExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { super(byIndex, byName, sqlSupplier); } @@ -458,7 +452,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { return createGenericExecuteSpec(byIndex, byName, sqlSupplier); } } @@ -473,7 +467,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final BiFunction mappingFunction; DefaultTypedExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier, Class typeToRead) { + Supplier sqlSupplier, Class typeToRead) { super(byIndex, byName, sqlSupplier); @@ -482,9 +476,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } DefaultTypedExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier, BiFunction mappingFunction) { + Supplier sqlSupplier, BiFunction mappingFunction) { super(byIndex, byName, sqlSupplier); + this.typeToRead = null; this.mappingFunction = mappingFunction; } @@ -542,7 +537,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedExecuteSpec createInstance(Map byIndex, - Map byName, Supplier sqlSupplier) { + Map byName, Supplier sqlSupplier) { return createTypedExecuteSpec(byIndex, byName, sqlSupplier, typeToRead); } } @@ -629,12 +624,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, Sort sort, - Pageable page); + Pageable page); } private class DefaultGenericSelectSpec extends DefaultSelectSpecSupport implements GenericSelectSpec { - public DefaultGenericSelectSpec(String table, List projectedFields, Sort sort, Pageable page) { + DefaultGenericSelectSpec(String table, List projectedFields, Sort sort, Pageable page) { super(table, projectedFields, sort, page); } @@ -696,7 +691,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { + Pageable page) { return new DefaultGenericSelectSpec(table, projectedFields, sort, page); } } @@ -707,7 +702,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @SuppressWarnings("unchecked") private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { - private final @Nullable Class typeToRead; + private final @Nullable + Class typeToRead; private final BiFunction mappingFunction; DefaultTypedSelectSpec(Class typeToRead) { @@ -719,12 +715,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, - BiFunction mappingFunction) { + BiFunction mappingFunction) { this(table, projectedFields, sort, page, null, mappingFunction); } DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, - BiFunction mappingFunction) { + BiFunction mappingFunction) { super(table, projectedFields, sort, page); this.typeToRead = typeToRead; this.mappingFunction = mappingFunction; @@ -784,7 +780,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { + Pageable page) { return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); } } @@ -872,9 +868,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Statement statement = it.createStatement(sql); - byName.forEach((k, v) -> { - bindableInsert.bind(statement, v); - }); + byName.forEach((k, v) -> bindableInsert.bind(statement, v)); + return statement; }; @@ -1003,14 +998,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return statement; }; - Function> resultFunction = it -> { - return Flux.from(insertFunction.apply(it).execute()); - }; + Function> resultFunction = it -> Flux.from(insertFunction.apply(it).execute()); return new DefaultSqlResult<>(DefaultDatabaseClient.this, // sql, // resultFunction, // - it -> resultFunction.apply(it).flatMap(Result::getRowsUpdated) + it -> resultFunction // + .apply(it) // + .flatMap(Result::getRowsUpdated) // .collect(Collectors.summingInt(Integer::intValue)), // mappingFunction); } @@ -1023,7 +1018,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (R2dbcException e) { String sql = getSql(action); - return Flux.error(new UncategorizedR2dbcException("doInConnectionMany", sql, e) {}); + return Flux.error(new UncategorizedR2dbcException("doInConnectionMany", sql, e) { + }); } } @@ -1034,7 +1030,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (R2dbcException e) { String sql = getSql(action); - return Mono.error(new UncategorizedR2dbcException("doInConnection", sql, e) {}); + return Mono.error(new UncategorizedR2dbcException("doInConnection", sql, e) { + }); } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index 44f11653..01d0c660 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -101,8 +101,8 @@ class DefaultSqlResult implements SqlResult { /** * Returns an empty {@link SqlResult}. * - * @param - * @return + * @param value type of the {@code SqlResult}. + * @return a {@code SqlResult}. */ @SuppressWarnings("unchecked") public static SqlResult empty() { diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index a0b3a0ce..8356fa8c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -31,7 +31,7 @@ public interface SqlResult extends FetchSpec { * Apply a {@link BiFunction mapping function} to the result that emits {@link Row}s. * * @param mappingFunction must not be {@literal null}. - * @param + * @param the value type of the {@code SqlResult}. * @return a new {@link SqlResult} with {@link BiFunction mapping function} applied. */ SqlResult map(BiFunction mappingFunction); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index fb1458ba..4de641da 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -315,14 +315,16 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository bind(BindableOperation operation, Statement statement) { - return (k, v) -> { - operation.bind(statement, v); - }; + return (k, v) -> operation.bind(statement, v); } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index bfe27ad1..6ed34c85 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -15,17 +15,8 @@ */ package org.springframework.data.r2dbc.function; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.domain.Sort.Order.*; - import io.r2dbc.spi.ConnectionFactory; import lombok.Data; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.test.StepVerifier; - -import javax.sql.DataSource; - import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataAccessException; @@ -35,6 +26,14 @@ import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.test.StepVerifier; + +import javax.sql.DataSource; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Order.*; /** * Integration tests for {@link DatabaseClient}. @@ -58,7 +57,8 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr try { jdbc.execute("DROP TABLE legoset"); - } catch (DataAccessException e) {} + } catch (DataAccessException e) { + } jdbc.execute(getCreateTableStatement()); } @@ -90,12 +90,10 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr /** * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values. - * - * @return */ protected abstract String getInsertIntoLegosetStatement(); - @Test + @Test // gh-2 public void executeInsert() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); @@ -115,7 +113,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void shouldTranslateDuplicateKeyException() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); @@ -128,15 +126,13 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .bindNull(2, Integer.class) // .fetch().rowsUpdated() // .as(StepVerifier::create) // - .expectErrorSatisfies(exception -> { - - assertThat(exception).isInstanceOf(DuplicateKeyException.class) - .hasMessageContaining("execute; SQL [INSERT INTO legoset"); - }) // + .expectErrorSatisfies(exception -> assertThat(exception) // + .isInstanceOf(DuplicateKeyException.class) // + .hasMessageContaining("execute; SQL [INSERT INTO legoset")) // .verify(); } - @Test + @Test // gh-2 public void executeSelect() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -155,7 +151,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr }).verifyComplete(); } - @Test + @Test // gh-2 public void insert() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); @@ -172,7 +168,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void insertWithoutResult() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); @@ -188,7 +184,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void insertTypedObject() { LegoSet legoSet = new LegoSet(); @@ -209,7 +205,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void selectAsMap() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -229,7 +225,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr }).verifyComplete(); } - @Test + @Test // gh-8 public void selectExtracting() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -246,7 +242,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .verifyComplete(); } - @Test + @Test // gh-2 public void selectOrderByIdDesc() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -264,7 +260,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .verifyComplete(); } - @Test + @Test // gh-2 public void selectOrderPaged() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -282,7 +278,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .verifyComplete(); } - @Test + @Test // gh-2 public void selectTypedLater() { jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); @@ -304,6 +300,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr @Data @Table("legoset") static class LegoSet { + int id; String name; Integer manual; diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index a57c5f87..a5193fa4 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -15,27 +15,25 @@ */ package org.springframework.data.r2dbc.function; -import static org.assertj.core.api.Assertions.*; - import io.r2dbc.spi.ConnectionFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.ArrayBlockingQueue; - -import javax.sql.DataSource; - import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.NoTransactionException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import javax.sql.DataSource; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ArrayBlockingQueue; + +import static org.assertj.core.api.Assertions.*; /** * Abstract base class for integration tests for {@link TransactionalDatabaseClient}. @@ -58,7 +56,8 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend jdbc = createJdbcTemplate(createDataSource()); try { jdbc.execute("DROP TABLE legoset"); - } catch (DataAccessException e) {} + } catch (DataAccessException e) { + } jdbc.execute(getCreateTableStatement()); jdbc.execute("DELETE FROM legoset"); } @@ -91,31 +90,27 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend /** * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values. - * - * @return */ protected abstract String getInsertIntoLegosetStatement(); /** * Get a statement that returns the current transactionId. - * - * @return */ protected abstract String getCurrentTransactionIdStatement(); - @Test + @Test // gh-2 public void executeInsertInManagedTransaction() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); - Flux integerFlux = databaseClient.inTransaction(db -> { - - return db.execute().sql(getInsertIntoLegosetStatement()) // - .bind(0, 42055) // - .bind(1, "SCHAUFELRADBAGGER") // - .bindNull(2, Integer.class) // - .fetch().rowsUpdated(); - }); + Flux integerFlux = databaseClient.inTransaction(db -> db // + .execute() // + .sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated() // + ); integerFlux.as(StepVerifier::create) // .expectNext(1) // @@ -124,7 +119,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void executeInsertInAutoCommitTransaction() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); @@ -142,13 +137,17 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } - @Test + @Test // gh-2 public void shouldManageUserTransaction() { Queue transactionIds = new ArrayBlockingQueue<>(5); TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); - Flux txId = databaseClient.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0, Long.class)).all(); + Flux txId = databaseClient // + .execute() // + .sql(getCurrentTransactionIdStatement()) // + .map((r, md) -> r.get(0, Long.class)) // + .all(); Mono then = databaseClient.enableTransactionSynchronization(databaseClient.beginTransaction() // .thenMany(txId.concatWith(txId).doOnNext(transactionIds::add)) // @@ -162,7 +161,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(listOfTxIds).containsExactly(listOfTxIds.get(1), listOfTxIds.get(0)); } - @Test + @Test // gh-2 public void userTransactionManagementShouldFailWithoutSynchronizer() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); @@ -177,7 +176,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend }).verify(); } - @Test + @Test // gh-2 public void shouldRollbackTransaction() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); @@ -199,7 +198,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(count).isEqualTo(0); } - @Test + @Test // gh-2 public void emitTransactionIds() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); From 6ad31abd79a574628570ef6a1f46e766c8a933eb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 5 Dec 2018 14:03:43 +0100 Subject: [PATCH 052/665] #8 - Address review feedback. Remove unused code from AbstractDatabaseClientIntegrationTests. Use literal null instead of code null for consistency. Original pull request: #33. --- .../data/r2dbc/function/DatabaseClient.java | 35 +++++++++++-------- ...bstractDatabaseClientIntegrationTests.java | 22 +++++------- 2 files changed, 29 insertions(+), 28 deletions(-) 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 af76033a..2c108e72 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -65,8 +65,9 @@ public interface DatabaseClient { /** * Creates a {@code DatabaseClient} that will use the provided {@link io.r2dbc.spi.ConnectionFactory}. + * * @param factory The {@code ConnectionFactory} to use for obtaining connections. - * @return a new {@code DatabaseClient}. Guaranteed to be not {@code null}. + * @return a new {@code DatabaseClient}. Guaranteed to be not {@literal null}. */ static DatabaseClient create(ConnectionFactory factory) { return new DefaultDatabaseClientBuilder().connectionFactory(factory).build(); @@ -163,7 +164,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ FetchSpec map(BiFunction mappingFunction); @@ -199,7 +200,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ FetchSpec map(BiFunction mappingFunction); @@ -225,7 +226,8 @@ public interface DatabaseClient { * Specify the source {@literal table} to select from. * * @param table must not be {@literal null} or empty. - * @return a {@link GenericSelectSpec} for further configuration of the select. Guaranteed to be not {@code null}. + * @return a {@link GenericSelectSpec} for further configuration of the select. Guaranteed to be not + * {@literal null}. */ GenericSelectSpec from(String table); @@ -233,7 +235,7 @@ public interface DatabaseClient { * Specify the source table to select from to using the {@link Class entity class}. * * @param table must not be {@literal null}. - * @return a {@link TypedSelectSpec} for further configuration of the select. Guaranteed to be not {@code null}. + * @return a {@link TypedSelectSpec} for further configuration of the select. Guaranteed to be not {@literal null}. */ TypedSelectSpec from(Class table); } @@ -247,7 +249,8 @@ public interface DatabaseClient { * Specify the target {@literal table} to insert into. * * @param table must not be {@literal null} or empty. - * @return a {@link GenericInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. + * @return a {@link GenericInsertSpec} for further configuration of the insert. Guaranteed to be not + * {@literal null}. */ GenericInsertSpec> into(String table); @@ -255,7 +258,7 @@ public interface DatabaseClient { * Specify the target table to insert to using the {@link Class entity class}. * * @param table must not be {@literal null}. - * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. + * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@literal null}. */ TypedInsertSpec into(Class table); } @@ -279,7 +282,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ FetchSpec map(BiFunction mappingFunction); @@ -308,7 +311,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ FetchSpec map(BiFunction mappingFunction); @@ -377,8 +380,9 @@ public interface DatabaseClient { /** * Insert the given {@code objectToInsert}. * - * @param objectToInsert the object of which the attributes will provide the values for the insert. Must not be {@code null}. - * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. + * @param objectToInsert the object of which the attributes will provide the values for the insert. Must not be + * {@literal null}. + * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@literal null}. */ InsertSpec> using(T objectToInsert); @@ -386,7 +390,7 @@ public interface DatabaseClient { * Use the given {@code tableName} as insert target. * * @param tableName must not be {@literal null} or empty. - * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. + * @return a {@link TypedInsertSpec} for further configuration of the insert. Guaranteed to be not {@literal null}. */ TypedInsertSpec table(String tableName); @@ -394,8 +398,9 @@ public interface DatabaseClient { * Insert the given {@link Publisher} to insert one or more objects. Inserts only a single object when calling * {@link FetchSpec#one()} or {@link FetchSpec#first()}. * - * @param objectToInsert a publisher providing the objects of which the attributes will provide the values for the insert. Must not be {@code null}. - * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@code null}. + * @param objectToInsert a publisher providing the objects of which the attributes will provide the values for the + * insert. Must not be {@literal null}. + * @return a {@link InsertSpec} for further configuration of the insert. Guaranteed to be not {@literal null}. * @see InsertSpec#fetch() */ InsertSpec> using(Publisher objectToInsert); @@ -413,7 +418,7 @@ public interface DatabaseClient { * * @param mappingFunction must not be {@literal null}. * @param result type. - * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@code null}. + * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ FetchSpec map(BiFunction mappingFunction); diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 6ed34c85..6f78cc72 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -15,8 +15,16 @@ */ package org.springframework.data.r2dbc.function; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.domain.Sort.Order.*; + import io.r2dbc.spi.ConnectionFactory; import lombok.Data; +import reactor.core.publisher.Hooks; +import reactor.test.StepVerifier; + +import javax.sql.DataSource; + import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataAccessException; @@ -26,14 +34,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.test.StepVerifier; - -import javax.sql.DataSource; - -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.domain.Sort.Order.*; /** * Integration tests for {@link DatabaseClient}. @@ -57,8 +57,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr try { jdbc.execute("DROP TABLE legoset"); - } catch (DataAccessException e) { - } + } catch (DataAccessException e) {} jdbc.execute(getCreateTableStatement()); } @@ -107,9 +106,6 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .expectNext(1) // .verifyComplete(); - Flux rows = databaseClient.select().from("legoset").orderBy(Sort.by(desc("id"))).as(LegoSet.class).fetch() - .all(); - assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } From 9e3601a06d798614385193f6e49647a5140a4f22 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 7 Dec 2018 10:49:39 +0100 Subject: [PATCH 053/665] #8 - Reduce API surface for mapped tabular results. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move retrieval methods from FetchSpec into RowsFetchSpec and UpdatedRowsFetchSpec. map(…) now returns RowsFetchSpec to not expose updated rows count as mapped results are consumed as objects. Original pull request: #33. --- .../r2dbc/InvalidResultAccessException.java | 3 +- .../data/r2dbc/function/DatabaseClient.java | 14 +++--- .../r2dbc/function/DefaultDatabaseClient.java | 43 ++++++++-------- .../data/r2dbc/function/FetchSpec.java | 38 ++------------ .../data/r2dbc/function/RowsFetchSpec.java | 50 +++++++++++++++++++ .../data/r2dbc/function/SqlResult.java | 2 +- .../r2dbc/function/UpdatedRowsFetchSpec.java | 33 ++++++++++++ 7 files changed, 119 insertions(+), 64 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java index 82d5fe1d..6effd608 100644 --- a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -25,7 +25,8 @@ import org.springframework.lang.Nullable; * Exception thrown when a {@link io.r2dbc.spi.Result} has been accessed in an invalid fashion. Such exceptions always * have a {@link io.r2dbc.spi.R2dbcException} root cause. *

- * This typically happens when an invalid {@link org.springframework.data.r2dbc.function.SqlResult} column index or name has been specified. + * This typically happens when an invalid {@link org.springframework.data.r2dbc.function.FetchSpec} column index or name + * has been specified. * * @author Mark Paluch * @see BadSqlGrammarException 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 2c108e72..094e6502 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -166,7 +166,7 @@ public interface DatabaseClient { * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ - FetchSpec map(BiFunction mappingFunction); + RowsFetchSpec map(BiFunction mappingFunction); /** * Perform the SQL call and retrieve the result. @@ -202,7 +202,7 @@ public interface DatabaseClient { * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ - FetchSpec map(BiFunction mappingFunction); + RowsFetchSpec map(BiFunction mappingFunction); /** * Perform the SQL call and retrieve the result. @@ -284,7 +284,7 @@ public interface DatabaseClient { * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ - FetchSpec map(BiFunction mappingFunction); + RowsFetchSpec map(BiFunction mappingFunction); /** * Perform the SQL call and retrieve the result. @@ -304,7 +304,7 @@ public interface DatabaseClient { * @param resultType must not be {@literal null}. * @param result type. */ - FetchSpec as(Class resultType); + RowsFetchSpec as(Class resultType); /** * Configure a result mapping {@link java.util.function.BiFunction function}. @@ -313,7 +313,7 @@ public interface DatabaseClient { * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ - FetchSpec map(BiFunction mappingFunction); + RowsFetchSpec map(BiFunction mappingFunction); /** * Perform the SQL call and retrieve the result. @@ -416,11 +416,11 @@ public interface DatabaseClient { /** * Configure a result mapping {@link java.util.function.BiFunction function}. * - * @param mappingFunction must not be {@literal null}. + * @param mappwingFunction must not be {@literal null}. * @param result type. * @return a {@link FetchSpec} for configuration what to fetch. Guaranteed to be not {@literal null}. */ - FetchSpec map(BiFunction mappingFunction); + RowsFetchSpec map(BiFunction mappingFunction); /** * 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 c8b8dacf..d96f57f6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -23,20 +23,6 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Statement; import lombok.RequiredArgsConstructor; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.springframework.dao.DataAccessException; -import org.springframework.data.domain.Pageable; -import org.springframework.data.domain.Sort; -import org.springframework.data.r2dbc.UncategorizedR2dbcException; -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; -import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; -import org.springframework.data.r2dbc.function.convert.SettableValue; -import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.jdbc.core.SqlProvider; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -57,6 +43,21 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; +import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.jdbc.core.SqlProvider; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + /** * Default implementation of {@link DatabaseClient}. * @@ -313,7 +314,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return sql; } - SqlResult exchange(String sql, BiFunction mappingFunction) { + FetchSpec exchange(String sql, BiFunction mappingFunction) { Function> executeFunction = it -> { @@ -603,7 +604,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(table, projectedFields, sort, page); } - SqlResult execute(String sql, BiFunction mappingFunction) { + FetchSpec execute(String sql, BiFunction mappingFunction) { Function> selectFunction = it -> { @@ -674,7 +675,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return exchange(ColumnMapRowMapper.INSTANCE); } - private SqlResult exchange(BiFunction mappingFunction) { + private FetchSpec exchange(BiFunction mappingFunction) { Set columns; @@ -758,11 +759,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public SqlResult fetch() { + public FetchSpec fetch() { return exchange(mappingFunction); } - private SqlResult exchange(BiFunction mappingFunction) { + private FetchSpec exchange(BiFunction mappingFunction) { List columns; @@ -851,7 +852,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return fetch().rowsUpdated().then(); } - private SqlResult exchange(BiFunction mappingFunction) { + private FetchSpec exchange(BiFunction mappingFunction) { if (byName.isEmpty()) { throw new IllegalStateException("Insert fields is empty!"); @@ -970,7 +971,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { }; } - private SqlResult exchange(Object toInsert, BiFunction mappingFunction) { + private FetchSpec exchange(Object toInsert, BiFunction mappingFunction) { List insertValues = dataAccessStrategy.getValuesToInsert(toInsert); Set columns = new LinkedHashSet<>(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java index f7e8f2da..da748b5b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java @@ -15,42 +15,12 @@ */ package org.springframework.data.r2dbc.function; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - /** * Contract for fetching results. * + * @param row result type. * @author Mark Paluch + * @see RowsFetchSpec + * @see UpdatedRowsFetchSpec */ -public interface FetchSpec { - - /** - * Get exactly zero or one result. - * - * @return {@link Mono#empty()} if no match found. Never {@literal null}. - * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. - */ - Mono one(); - - /** - * Get the first or no result. - * - * @return {@link Mono#empty()} if no match found. Never {@literal null}. - */ - Mono first(); - - /** - * Get all matching elements. - * - * @return never {@literal null}. - */ - Flux all(); - - /** - * Get the number of updated rows. - * - * @return {@link Mono} emitting the number of updated rows. Never {@literal null}. - */ - Mono rowsUpdated(); -} +public interface FetchSpec extends RowsFetchSpec, UpdatedRowsFetchSpec {} diff --git a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java new file mode 100644 index 00000000..5399bffc --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java @@ -0,0 +1,50 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * Contract for fetching tabular results. + * + * @param row result type. + * @author Mark Paluch + */ +public interface RowsFetchSpec { + + /** + * Get exactly zero or one result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one match found. + */ + Mono one(); + + /** + * Get the first or no result. + * + * @return {@link Mono#empty()} if no match found. Never {@literal null}. + */ + Mono first(); + + /** + * Get all matching elements. + * + * @return never {@literal null}. + */ + Flux all(); +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index 8356fa8c..ea418205 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -25,7 +25,7 @@ import java.util.function.BiFunction; * * @author Mark Paluch */ -public interface SqlResult extends FetchSpec { +interface SqlResult extends FetchSpec { /** * Apply a {@link BiFunction mapping function} to the result that emits {@link Row}s. diff --git a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java new file mode 100644 index 00000000..e2077355 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java @@ -0,0 +1,33 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import reactor.core.publisher.Mono; + +/** + * Contract for fetching the number of affected rows. + * + * @author Mark Paluch + */ +public interface UpdatedRowsFetchSpec { + + /** + * Get the number of updated rows. + * + * @return {@link Mono} emitting the number of updated rows. Never {@literal null}. + */ + Mono rowsUpdated(); +} From f325f45fb60ad39a586d14f3e2abfd6135bb81b4 Mon Sep 17 00:00:00 2001 From: Ryland Degnan Date: Wed, 21 Nov 2018 13:24:49 -0500 Subject: [PATCH 054/665] #22 - Remove restriction on CollectionLike types. EntityRowMapper now passes-thru values for Collection-like types such as array. Arrays are supported by Postgres. Original pull request: #22. --- .../data/r2dbc/function/convert/EntityRowMapper.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index c1c67b26..33cffd8f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -66,10 +66,7 @@ public class EntityRowMapper implements BiFunction { continue; } - if (property.isCollectionLike()) { - throw new UnsupportedOperationException(); - } else if (property.isMap()) { - + if (property.isMap()) { throw new UnsupportedOperationException(); } else { propertyAccessor.setProperty(property, readFrom(row, property, "")); From 06c2a3a246dc96e003fd1bab8a429e45a958e254 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Dec 2018 17:30:48 +0100 Subject: [PATCH 055/665] #22 - Polishing. Add author tag. Add unit test for EntityRowMapper. Original pull request: #31. --- .../function/convert/EntityRowMapper.java | 30 ++++--- .../convert/EntityRowMapperUnitTests.java | 79 +++++++++++++++++++ 2 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index 33cffd8f..2f9e89c3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -23,7 +23,6 @@ import lombok.RequiredArgsConstructor; import java.sql.ResultSet; import java.util.function.BiFunction; -import org.springframework.core.convert.ConversionService; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentPropertyAccessor; @@ -39,7 +38,7 @@ import org.springframework.lang.Nullable; * Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced. * * @author Mark Paluch - * @since 1.0 + * @author Ryland Degnan */ public class EntityRowMapper implements BiFunction { @@ -52,13 +51,17 @@ public class EntityRowMapper implements BiFunction { this.converter = converter; } + /* + * (non-Javadoc) + * @see java.util.function.BiFunction#apply(java.lang.Object, java.lang.Object) + */ @Override public T apply(Row row, RowMetadata metadata) { T result = createInstance(row, "", entity); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(result), - converter.getConversionService()); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>( + entity.getPropertyAccessor(result), converter.getConversionService()); for (RelationalPersistentProperty property : entity) { @@ -93,7 +96,7 @@ public class EntityRowMapper implements BiFunction { return readEntityFrom(row, property); } - return row.get(prefix + property.getColumnName()); + return converter.readValue(row.get(prefix + property.getColumnName()), property.getTypeInformation()); } catch (Exception o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); @@ -104,7 +107,6 @@ public class EntityRowMapper implements BiFunction { String prefix = property.getName() + "_"; - @SuppressWarnings("unchecked") RelationalPersistentEntity entity = (RelationalPersistentEntity) converter.getMappingContext() .getRequiredPersistentEntity(property.getActualType()); @@ -114,8 +116,8 @@ public class EntityRowMapper implements BiFunction { S instance = createInstance(row, prefix, entity); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor(accessor, + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor, converter.getConversionService()); for (RelationalPersistentProperty p : entity) { @@ -129,8 +131,7 @@ public class EntityRowMapper implements BiFunction { private S createInstance(Row row, String prefix, RelationalPersistentEntity entity) { - RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, entity, - converter.getConversionService(), prefix); + RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, entity, converter, prefix); return converter.createInstance(entity, rowParameterValueProvider::getParameterValue); } @@ -140,7 +141,7 @@ public class EntityRowMapper implements BiFunction { private final @NonNull Row resultSet; private final @NonNull RelationalPersistentEntity entity; - private final @NonNull ConversionService conversionService; + private final @NonNull RelationalConverter converter; private final @NonNull String prefix; /* @@ -151,10 +152,13 @@ public class EntityRowMapper implements BiFunction { @Nullable public T getParameterValue(Parameter parameter) { - String column = prefix + entity.getRequiredPersistentProperty(parameter.getName()).getColumnName(); + RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameter.getName()); + String column = prefix + property.getColumnName(); try { - return conversionService.convert(resultSet.get(column), parameter.getType().getType()); + + Object value = converter.readValue(resultSet.get(column), property.getTypeInformation()); + return converter.getConversionService().convert(value, parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); } diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java new file mode 100644 index 00000000..a8ba3da3 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java @@ -0,0 +1,79 @@ +package org.springframework.data.r2dbc.function.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import lombok.RequiredArgsConstructor; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; + +/** + * Unit tests for {@link EntityRowMapper}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class EntityRowMapperUnitTests { + + DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + + Row rowMock = mock(Row.class); + RowMetadata metadata = mock(RowMetadata.class); + + @Test // gh-22 + public void shouldMapSimpleEntity() { + + EntityRowMapper mapper = getRowMapper(SimpleEntity.class); + when(rowMock.get("id")).thenReturn("foo"); + + SimpleEntity result = mapper.apply(rowMock, metadata); + assertThat(result.id).isEqualTo("foo"); + } + + @Test // gh-22 + public void shouldMapSimpleEntityWithConstructorCreation() { + + EntityRowMapper mapper = getRowMapper(SimpleEntityConstructorCreation.class); + when(rowMock.get("id")).thenReturn("foo"); + + SimpleEntityConstructorCreation result = mapper.apply(rowMock, metadata); + assertThat(result.id).isEqualTo("foo"); + } + + @Test // gh-22 + public void shouldApplyConversionWithConstructorCreation() { + + EntityRowMapper mapper = getRowMapper(ConversionWithConstructorCreation.class); + when(rowMock.get("id")).thenReturn((byte) 0x24); + + ConversionWithConstructorCreation result = mapper.apply(rowMock, metadata); + assertThat(result.id).isEqualTo(36L); + } + + private EntityRowMapper getRowMapper(Class type) { + RelationalPersistentEntity entity = (RelationalPersistentEntity) strategy.getMappingContext() + .getRequiredPersistentEntity(type); + return new EntityRowMapper<>(entity, strategy.getRelationalConverter()); + } + + static class SimpleEntity { + String id; + } + + @RequiredArgsConstructor + static class SimpleEntityConstructorCreation { + final String id; + } + + @RequiredArgsConstructor + static class ConversionWithConstructorCreation { + final long id; + } +} From c357e5b543e50df1704cdadf239c0bcb70d47763 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 3 Dec 2018 17:39:07 +0100 Subject: [PATCH 056/665] #30 - Add custom conversion support. We now support custom conversions via R2dbcCustomConversions. Custom conversions introduces simple types that depend on the used dialect. Custom conversions and simple types are held in RelationalConverter and MappingContext. Simple types and conversions are used by DatabaseClient and repository support to properly apply registered converters and support native types such as array-columns. Related tickets: #22, #26. Original pull request: #31. --- .../config/AbstractR2dbcConfiguration.java | 43 +++++++-- .../data/r2dbc/dialect/Dialect.java | 37 ++++++++ .../data/r2dbc/dialect/PostgresDialect.java | 31 ++++++ .../data/r2dbc/dialect/SqlServerDialect.java | 17 ++++ .../DefaultReactiveDataAccessStrategy.java | 95 ++++++++++++++++++- .../function/ReactiveDataAccessStrategy.java | 9 ++ .../convert/MappingR2dbcConverter.java | 27 ------ .../convert/R2dbcCustomConversions.java | 26 +++++ .../config/R2dbcRepositoriesRegistrar.java | 1 - .../support/SimpleR2dbcRepository.java | 2 +- .../dialect/PostgresDialectUnitTests.java | 21 ++++ .../dialect/SqlServerDialectUnitTests.java | 11 +++ ...ltReactiveDataAccessStrategyUnitTests.java | 37 ++++++++ .../convert/EntityRowMapperUnitTests.java | 17 ++++ 14 files changed, 334 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index e6b11e88..4ebfcb5a 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -17,15 +17,20 @@ package org.springframework.data.r2dbc.config; import io.r2dbc.spi.ConnectionFactory; +import java.util.Collections; import java.util.Optional; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; @@ -95,15 +100,21 @@ public abstract class AbstractR2dbcConfiguration { * Register a {@link RelationalMappingContext} and apply an optional {@link NamingStrategy}. * * @param namingStrategy optional {@link NamingStrategy}. Use {@link NamingStrategy#INSTANCE} as fallback. + * @param r2dbcCustomConversions customized R2DBC conversions. * @return must not be {@literal null}. * @throws IllegalArgumentException if any of the required args is {@literal null}. */ @Bean - public RelationalMappingContext r2dbcMappingContext(Optional namingStrategy) { + public RelationalMappingContext r2dbcMappingContext(Optional namingStrategy, + R2dbcCustomConversions r2dbcCustomConversions) { Assert.notNull(namingStrategy, "NamingStrategy must not be null!"); - return new RelationalMappingContext(namingStrategy.orElse(NamingStrategy.INSTANCE)); + RelationalMappingContext relationalMappingContext = new RelationalMappingContext( + namingStrategy.orElse(NamingStrategy.INSTANCE)); + relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder()); + + return relationalMappingContext; } /** @@ -111,17 +122,37 @@ public abstract class AbstractR2dbcConfiguration { * RelationalMappingContext}. * * @param mappingContext the configured {@link RelationalMappingContext}. + * @param r2dbcCustomConversions customized R2DBC conversions. * @return must not be {@literal null}. - * @see #r2dbcMappingContext(Optional) + * @see #r2dbcMappingContext(Optional, R2dbcCustomConversions) * @see #getDialect(ConnectionFactory) * @throws IllegalArgumentException if any of the {@literal mappingContext} is {@literal null}. */ @Bean - public ReactiveDataAccessStrategy reactiveDataAccessStrategy(RelationalMappingContext mappingContext) { + public ReactiveDataAccessStrategy reactiveDataAccessStrategy(RelationalMappingContext mappingContext, + R2dbcCustomConversions r2dbcCustomConversions) { Assert.notNull(mappingContext, "MappingContext must not be null!"); - return new DefaultReactiveDataAccessStrategy(getDialect(connectionFactory()), - new BasicRelationalConverter(mappingContext)); + + BasicRelationalConverter converter = new BasicRelationalConverter(mappingContext, r2dbcCustomConversions); + + return new DefaultReactiveDataAccessStrategy(getDialect(connectionFactory()), converter); + } + + /** + * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These + * {@link CustomConversions} will be registered with the {@link BasicRelationalConverter} and + * {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions)}. Returns an empty {@link R2dbcCustomConversions} + * instance by default. + * + * @return must not be {@literal null}. + */ + @Bean + public R2dbcCustomConversions r2dbcCustomConversions() { + + Dialect dialect = getDialect(connectionFactory()); + StoreConversions storeConversions = StoreConversions.of(dialect.getSimpleTypeHolder()); + return new R2dbcCustomConversions(storeConversions, Collections.emptyList()); } /** diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java index 3371d623..411f3839 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java @@ -1,5 +1,11 @@ package org.springframework.data.r2dbc.dialect; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + /** * Represents a dialect that is implemented by a particular database. * @@ -26,10 +32,41 @@ public interface Dialect { @Deprecated String generatedKeysClause(); + /** + * Return a collection of types that are natively supported by this database/driver. Defaults to + * {@link Collections#emptySet()}. + * + * @return a collection of types that are natively supported by this database/driver. Defaults to + * {@link Collections#emptySet()}. + */ + default Collection> getSimpleTypes() { + return Collections.emptySet(); + } + + /** + * Return the {@link SimpleTypeHolder} for this dialect. + * + * @return the {@link SimpleTypeHolder} for this dialect. + * @see #getSimpleTypes() + */ + default SimpleTypeHolder getSimpleTypeHolder() { + return new SimpleTypeHolder(new HashSet<>(getSimpleTypes()), true); + } + /** * Return the {@link LimitClause} used by this dialect. * * @return the {@link LimitClause} used by this dialect. */ LimitClause limit(); + + /** + * Returns {@literal true} whether this dialect supports array-typed column. Collection-typed columns can map their + * content to native array types. + * + * @return {@literal true} whether this dialect supports array-typed columns. + */ + default boolean supportsArrayColumns() { + return false; + } } diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index fd5da050..8d0c0b3b 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -1,5 +1,15 @@ package org.springframework.data.r2dbc.dialect; +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + /** * An SQL dialect for Postgres. * @@ -7,6 +17,9 @@ package org.springframework.data.r2dbc.dialect; */ public class PostgresDialect implements Dialect { + private static final Set> SIMPLE_TYPES = new HashSet<>( + Arrays.asList(List.class, Collection.class, String[].class, UUID.class, URL.class, URI.class, InetAddress.class)); + /** * Singleton instance. */ @@ -62,6 +75,15 @@ public class PostgresDialect implements Dialect { return "RETURNING *"; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys() + */ + @Override + public Collection> getSimpleTypes() { + return SIMPLE_TYPES; + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.dialect.Dialect#limit() @@ -70,4 +92,13 @@ public class PostgresDialect implements Dialect { public LimitClause limit() { return LIMIT_CLAUSE; } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#supportsArrayColumns() + */ + @Override + public boolean supportsArrayColumns() { + return true; + } } diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java index ccbc993a..bf7199e0 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java @@ -1,5 +1,11 @@ package org.springframework.data.r2dbc.dialect; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + /** * An SQL dialect for Microsoft SQL Server. * @@ -7,6 +13,8 @@ package org.springframework.data.r2dbc.dialect; */ public class SqlServerDialect implements Dialect { + private static final Set> SIMPLE_TYPES = new HashSet<>(Collections.singletonList(UUID.class)); + /** * Singleton instance. */ @@ -63,6 +71,15 @@ public class SqlServerDialect implements Dialect { return "select SCOPE_IDENTITY() AS GENERATED_KEYS"; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys() + */ + @Override + public Collection> getSimpleTypes() { + return SIMPLE_TYPES; + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.dialect.Dialect#limit() 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 dcf77534..5d865e77 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -19,6 +19,7 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Statement; +import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -29,22 +30,28 @@ import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.dialect.LimitClause; import org.springframework.data.r2dbc.dialect.LimitClause.Position; import org.springframework.data.r2dbc.function.convert.EntityRowMapper; +import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; 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.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -57,8 +64,9 @@ import org.springframework.util.StringUtils; */ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy { - private final RelationalConverter relationalConverter; private final Dialect dialect; + private final RelationalConverter relationalConverter; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; /** * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}. @@ -66,7 +74,28 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra * @param dialect the {@link Dialect} to use. */ public DefaultReactiveDataAccessStrategy(Dialect dialect) { - this(dialect, new BasicRelationalConverter(new RelationalMappingContext())); + this(dialect, createConverter(dialect)); + } + + private static BasicRelationalConverter createConverter(Dialect dialect) { + + Assert.notNull(dialect, "Dialect must not be null"); + + R2dbcCustomConversions customConversions = new R2dbcCustomConversions( + StoreConversions.of(dialect.getSimpleTypeHolder()), Collections.emptyList()); + + RelationalMappingContext context = new RelationalMappingContext(); + context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + + return new BasicRelationalConverter(context, customConversions); + } + + public RelationalConverter getRelationalConverter() { + return relationalConverter; + } + + public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() { + return mappingContext; } /** @@ -75,12 +104,15 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra * @param dialect the {@link Dialect} to use. * @param converter must not be {@literal null}. */ + @SuppressWarnings("unchecked") public DefaultReactiveDataAccessStrategy(Dialect dialect, RelationalConverter converter) { Assert.notNull(dialect, "Dialect must not be null"); Assert.notNull(converter, "RelationalConverter must not be null"); this.relationalConverter = converter; + this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) relationalConverter + .getMappingContext(); this.dialect = dialect; } @@ -121,7 +153,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra for (RelationalPersistentProperty property : entity) { - Object value = propertyAccessor.getProperty(property); + Object value = getWriteValue(propertyAccessor, property); if (value == null) { continue; @@ -133,6 +165,31 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return values; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getColumnsToUpdate(java.lang.Object) + */ + public Map getColumnsToUpdate(Object object) { + + Assert.notNull(object, "Entity object must not be null!"); + + Class userClass = ClassUtils.getUserClass(object); + RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass); + + Map update = new LinkedHashMap<>(); + + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + + for (RelationalPersistentProperty property : entity) { + + Object writeValue = getWriteValue(propertyAccessor, property); + + update.put(property.getColumnName(), new SettableValue(property.getColumnName(), writeValue, property.getType())); + } + + return update; + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getMappedSort(java.lang.Class, org.springframework.data.domain.Sort) @@ -181,12 +238,40 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra } private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { - return relationalConverter.getMappingContext().getRequiredPersistentEntity(typeToRead); + return mappingContext.getRequiredPersistentEntity(typeToRead); } @Nullable private RelationalPersistentEntity getPersistentEntity(Class typeToRead) { - return relationalConverter.getMappingContext().getPersistentEntity(typeToRead); + return mappingContext.getPersistentEntity(typeToRead); + } + + private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) { + + TypeInformation type = property.getTypeInformation(); + Object value = relationalConverter.writeValue(propertyAccessor.getProperty(property), type); + + if (type.isCollectionLike()) { + + RelationalPersistentEntity nestedEntity = mappingContext + .getPersistentEntity(type.getRequiredActualType().getType()); + + if (nestedEntity != null) { + throw new InvalidDataAccessApiUsageException("Nested entities are not supported"); + } + + if (!dialect.supportsArrayColumns()) { + throw new InvalidDataAccessResourceUsageException( + "Dialect " + dialect.getClass().getName() + " does not support array columns"); + } + + if (!property.isArray()) { + Object zeroLengthArray = Array.newInstance(property.getActualType(), 0); + return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass()); + } + } + + return value; } /* diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index f3c8a9f2..8b038e98 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -20,6 +20,7 @@ import io.r2dbc.spi.RowMetadata; import io.r2dbc.spi.Statement; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.BiFunction; @@ -49,6 +50,14 @@ public interface ReactiveDataAccessStrategy { */ List getValuesToInsert(Object object); + /** + * Returns a {@link Map} that maps column names to a {@link SettableValue} value. + * + * @param object must not be {@literal null}. + * @return + */ + Map getColumnsToUpdate(Object object); + /** * Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}. * diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 6b291e86..54b26ec1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -21,7 +21,6 @@ import io.r2dbc.spi.RowMetadata; import java.util.LinkedHashMap; import java.util.Map; -import java.util.Optional; import java.util.function.BiFunction; import org.springframework.core.convert.ConversionService; @@ -65,32 +64,6 @@ public class MappingR2dbcConverter { this.relationalConverter = converter; } - /** - * Returns a {@link Map} that maps column names to an {@link Optional} value. Used {@link Optional#empty()} if the - * underlying property is {@literal null}. - * - * @param object must not be {@literal null}. - * @return - */ - public Map getColumnsToUpdate(Object object) { - - Assert.notNull(object, "Entity object must not be null!"); - - Class userClass = ClassUtils.getUserClass(object); - RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(userClass); - - Map update = new LinkedHashMap<>(); - - PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); - - for (RelationalPersistentProperty property : entity) { - update.put(property.getColumnName(), - new SettableValue(property.getColumnName(), propertyAccessor.getProperty(property), property.getType())); - } - - return update; - } - /** * Returns a {@link java.util.function.Function} that populates the id property of the {@code object} from a * {@link Row}. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java new file mode 100644 index 00000000..8c3a692e --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java @@ -0,0 +1,26 @@ +package org.springframework.data.r2dbc.function.convert; + +import java.util.Collection; + +import org.springframework.data.convert.CustomConversions; + +/** + * Value object to capture custom conversion. {@link R2dbcCustomConversions} also act as factory for + * {@link org.springframework.data.mapping.model.SimpleTypeHolder} + * + * @author Mark Paluch + * @see CustomConversions + * @see org.springframework.data.mapping.model.SimpleTypeHolder + */ +public class R2dbcCustomConversions extends CustomConversions { + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param storeConversions must not be {@literal null}. + * @param converters must not be {@literal null}. + */ + public R2dbcCustomConversions(StoreConversions storeConversions, Collection converters) { + super(storeConversions, converters); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java index 5279e09f..91949b31 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java @@ -24,7 +24,6 @@ import org.springframework.data.repository.config.RepositoryConfigurationExtensi * R2DBC-specific {@link org.springframework.context.annotation.ImportBeanDefinitionRegistrar}. * * @author Mark Paluch - * @since 2.0 */ class R2dbcRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport { diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 4de641da..e7e37cce 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -70,7 +70,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = converter.getColumnsToUpdate(objectToSave); + Map columns = accessStrategy.getColumnsToUpdate(objectToSave); columns.remove(getIdColumnName()); // do not update the Id column. String idColumnName = getIdColumnName(); BindIdOperation update = accessStrategy.updateById(entity.getTableName(), columns.keySet(), idColumnName); diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java index 3b0168d5..29f5d2ca 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java @@ -2,7 +2,11 @@ package org.springframework.data.r2dbc.dialect; import static org.assertj.core.api.Assertions.*; +import java.util.Collection; +import java.util.List; + import org.junit.Test; +import org.springframework.data.mapping.model.SimpleTypeHolder; /** * Unit tests for {@link PostgresDialect}. @@ -22,4 +26,21 @@ public class PostgresDialectUnitTests { assertThat(first.getPlaceholder()).isEqualTo("$1"); assertThat(second.getPlaceholder()).isEqualTo("$2"); } + + @Test // gh-30 + public void shouldConsiderCollectionTypesAsSimple() { + + SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + + assertThat(holder.isSimpleType(List.class)).isTrue(); + assertThat(holder.isSimpleType(Collection.class)).isTrue(); + } + + @Test // gh-30 + public void shouldConsiderStringArrayTypeAsSimple() { + + SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + + assertThat(holder.isSimpleType(String[].class)).isTrue(); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java index 0e848015..1e96e389 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java @@ -2,7 +2,10 @@ package org.springframework.data.r2dbc.dialect; import static org.assertj.core.api.Assertions.*; +import java.util.UUID; + import org.junit.Test; +import org.springframework.data.mapping.model.SimpleTypeHolder; /** * Unit tests for {@link SqlServerDialect}. @@ -22,4 +25,12 @@ public class SqlServerDialectUnitTests { assertThat(first.getPlaceholder()).isEqualTo("@P0"); assertThat(second.getPlaceholder()).isEqualTo("@P1_foobar"); } + + @Test // gh-30 + public void shouldConsiderUuidAsSimple() { + + SimpleTypeHolder holder = SqlServerDialect.INSTANCE.getSimpleTypeHolder(); + + assertThat(holder.isSimpleType(UUID.class)).isTrue(); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java index df279946..a8e7a5d3 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java @@ -8,9 +8,12 @@ import io.r2dbc.spi.Statement; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.List; +import java.util.Map; import org.junit.Test; import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.function.convert.SettableValue; /** * Unit tests for {@link DefaultReactiveDataAccessStrategy}. @@ -101,4 +104,38 @@ public class DefaultReactiveDataAccessStrategyUnitTests { operation.bindId(statement, "bar"); assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1, $2)"); } + + @Test // gh-22 + public void shouldUpdateArray() { + + Map columnsToUpdate = strategy + .getColumnsToUpdate(new WithCollectionTypes(new String[] { "one", "two" }, null)); + + Object stringArray = columnsToUpdate.get("string_array").getValue(); + assertThat(stringArray).isInstanceOf(String[].class); + assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); + } + + @Test // gh-22 + public void shouldConvertListToArray() { + + Map columnsToUpdate = strategy + .getColumnsToUpdate(new WithCollectionTypes(null, Arrays.asList("one", "two"))); + + Object stringArray = columnsToUpdate.get("string_collection").getValue(); + assertThat(stringArray).isInstanceOf(String[].class); + assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); + } + + static class WithCollectionTypes { + + String[] stringArray; + + List stringCollection; + + WithCollectionTypes(String[] stringArray, List stringCollection) { + this.stringArray = stringArray; + this.stringCollection = stringCollection; + } + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java index a8ba3da3..b17c080d 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java @@ -7,6 +7,8 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import lombok.RequiredArgsConstructor; +import java.util.List; + import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -57,6 +59,17 @@ public class EntityRowMapperUnitTests { assertThat(result.id).isEqualTo(36L); } + @Test // gh-30 + public void shouldConvertArrayToCollection() { + + EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); + when(rowMock.get("ids")).thenReturn((new String[] { "foo", "bar" })); + + EntityWithCollection result = mapper.apply(rowMock, metadata); + assertThat(result.ids).contains("foo", "bar"); + } + + @SuppressWarnings("unchecked") private EntityRowMapper getRowMapper(Class type) { RelationalPersistentEntity entity = (RelationalPersistentEntity) strategy.getMappingContext() .getRequiredPersistentEntity(type); @@ -76,4 +89,8 @@ public class EntityRowMapperUnitTests { static class ConversionWithConstructorCreation { final long id; } + + static class EntityWithCollection { + List ids; + } } From be5383abed6490b7dd9afed71c321e68d76a5efa Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 4 Dec 2018 14:25:03 +0100 Subject: [PATCH 057/665] #30 - Polishing. Minor formatting. Add suggestions. Update src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java Co-Authored-By: mp911de Original pull request: #31. --- .../config/AbstractR2dbcConfiguration.java | 2 +- .../DefaultReactiveDataAccessStrategy.java | 2 ++ .../dialect/PostgresDialectUnitTests.java | 16 ++++++++++ ...ltReactiveDataAccessStrategyUnitTests.java | 3 ++ .../convert/EntityRowMapperUnitTests.java | 32 +++++++++++++++++++ 5 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 4ebfcb5a..57c1e3c9 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -118,7 +118,7 @@ public abstract class AbstractR2dbcConfiguration { } /** - * Creates a {@link ReactiveDataAccessStrategy} using the configured {@link #r2dbcMappingContext(Optional) + * Creates a {@link ReactiveDataAccessStrategy} using the configured {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions)} * RelationalMappingContext}. * * @param mappingContext the configured {@link RelationalMappingContext}. 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 5d865e77..abe26fc3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -261,11 +261,13 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra } if (!dialect.supportsArrayColumns()) { + throw new InvalidDataAccessResourceUsageException( "Dialect " + dialect.getClass().getName() + " does not support array columns"); } if (!property.isArray()) { + Object zeroLengthArray = Array.newInstance(property.getActualType(), 0); return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass()); } diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java index 29f5d2ca..7df2e22f 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java @@ -42,5 +42,21 @@ public class PostgresDialectUnitTests { SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); assertThat(holder.isSimpleType(String[].class)).isTrue(); + + @Test // gh-30 + public void shouldConsiderIntArrayTypeAsSimple() { + + SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + + assertThat(holder.isSimpleType(int[].class)).isTrue(); + } + + @Test // gh-30 + public void shouldConsiderIntegerArrayTypeAsSimple() { + + SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + + assertThat(holder.isSimpleType(Integer[].class)).isTrue(); + } } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java index a8e7a5d3..120d6d4e 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java @@ -112,6 +112,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests { .getColumnsToUpdate(new WithCollectionTypes(new String[] { "one", "two" }, null)); Object stringArray = columnsToUpdate.get("string_array").getValue(); + assertThat(stringArray).isInstanceOf(String[].class); assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); } @@ -123,6 +124,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests { .getColumnsToUpdate(new WithCollectionTypes(null, Arrays.asList("one", "two"))); Object stringArray = columnsToUpdate.get("string_collection").getValue(); + assertThat(stringArray).isInstanceOf(String[].class); assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); } @@ -134,6 +136,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests { List stringCollection; WithCollectionTypes(String[] stringArray, List stringCollection) { + this.stringArray = stringArray; this.stringCollection = stringCollection; } diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java index b17c080d..d7023ace 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java @@ -69,6 +69,35 @@ public class EntityRowMapperUnitTests { assertThat(result.ids).contains("foo", "bar"); } + @Test // gh-30 + public void shouldConvertArrayToSet() { + + EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); + when(rowMock.get("integerSet")).thenReturn((new int[] { 3, 14 })); + + EntityWithCollection result = mapper.apply(rowMock, metadata); + assertThat(result.integerSet).contains(3, 14); + } + + @Test // gh-30 + public void shouldConvertArrayMembers() { + + EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); + when(rowMock.get("primitiveIntegers")).thenReturn((new long[] { 3L, 14L })); + + EntityWithCollection result = mapper.apply(rowMock, metadata); + assertThat(result.primitiveIntegers).contains(3, 14); + } + + @Test // gh-30 + public void shouldConvertArrayToBoxedArray() { + + EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); + when(rowMock.get("boxedIntegers")).thenReturn((new int[] { 3, 11 })); + + EntityWithCollection result = mapper.apply(rowMock, metadata); + assertThat(result.boxedIntegers).contains(3, 11); + } @SuppressWarnings("unchecked") private EntityRowMapper getRowMapper(Class type) { RelationalPersistentEntity entity = (RelationalPersistentEntity) strategy.getMappingContext() @@ -92,5 +121,8 @@ public class EntityRowMapperUnitTests { static class EntityWithCollection { List ids; + Set integerSet; + Integer[] boxedIntegers; + int[] primitiveIntegers; } } From 9f68352ac7771c27aec83d2c40663a6a6b0d6210 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 5 Dec 2018 10:07:25 +0100 Subject: [PATCH 058/665] #30 - Address review feedback. Introduce ArrayColumns type to encapsulate Dialect-specific array support. Apply array conversion for properties that do not match the native array type. Add integration tests for Postgres array columns. Original pull request: #31. --- .../data/r2dbc/dialect/ArrayColumns.java | 53 +++++++ .../data/r2dbc/dialect/Dialect.java | 10 +- .../data/r2dbc/dialect/PostgresDialect.java | 48 +++++- .../DefaultReactiveDataAccessStrategy.java | 24 ++- .../function/convert/EntityRowMapper.java | 7 +- .../dialect/PostgresDialectUnitTests.java | 49 ++++-- .../dialect/SqlServerDialectUnitTests.java | 9 ++ .../function/PostgresIntegrationTests.java | 147 ++++++++++++++++++ .../convert/EntityRowMapperUnitTests.java | 9 +- 9 files changed, 316 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/ArrayColumns.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/ArrayColumns.java b/src/main/java/org/springframework/data/r2dbc/dialect/ArrayColumns.java new file mode 100644 index 00000000..5259af22 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/ArrayColumns.java @@ -0,0 +1,53 @@ +package org.springframework.data.r2dbc.dialect; + +/** + * Interface declaring methods that express how a dialect supports array-typed columns. + * + * @author Mark Paluch + */ +public interface ArrayColumns { + + /** + * Returns {@literal true} if the dialect supports array-typed columns. + * + * @return {@literal true} if the dialect supports array-typed columns. + */ + boolean isSupported(); + + /** + * Translate the {@link Class user type} of an array into the dialect-specific type. This method considers only the + * component type. + * + * @param userType component type of the array. + * @return the dialect-supported array type. + * @throws UnsupportedOperationException if array typed columns are not supported. + * @throws IllegalArgumentException if the {@code userType} is not a supported array type. + */ + Class getArrayType(Class userType); + + /** + * Default {@link ArrayColumns} implementation for dialects that do not support array-typed columns. + */ + enum Unsupported implements ArrayColumns { + + INSTANCE; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.ArrayColumns#isSupported() + */ + @Override + public boolean isSupported() { + return false; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.ArrayColumns#getArrayType(java.lang.Class) + */ + @Override + public Class getArrayType(Class userType) { + throw new UnsupportedOperationException("Array types not supported"); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java index 411f3839..e909f722 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java @@ -5,6 +5,7 @@ import java.util.Collections; import java.util.HashSet; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.r2dbc.dialect.ArrayColumns.Unsupported; /** * Represents a dialect that is implemented by a particular database. @@ -61,12 +62,11 @@ public interface Dialect { LimitClause limit(); /** - * Returns {@literal true} whether this dialect supports array-typed column. Collection-typed columns can map their - * content to native array types. + * Returns the array support object that describes how array-typed columns are supported by this dialect. * - * @return {@literal true} whether this dialect supports array-typed columns. + * @return the array support object that describes how array-typed columns are supported by this dialect. */ - default boolean supportsArrayColumns() { - return false; + default ArrayColumns getArraySupport() { + return Unsupported.INSTANCE; } } diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index 8d0c0b3b..d0b1bf0b 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -1,15 +1,20 @@ package org.springframework.data.r2dbc.dialect; +import lombok.RequiredArgsConstructor; + import java.net.InetAddress; import java.net.URI; import java.net.URL; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.UUID; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + /** * An SQL dialect for Postgres. * @@ -18,7 +23,7 @@ import java.util.UUID; public class PostgresDialect implements Dialect { private static final Set> SIMPLE_TYPES = new HashSet<>( - Arrays.asList(List.class, Collection.class, String[].class, UUID.class, URL.class, URI.class, InetAddress.class)); + Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); /** * Singleton instance. @@ -57,6 +62,8 @@ public class PostgresDialect implements Dialect { } }; + private final PostgresArrayColumns ARRAY_COLUMNS = new PostgresArrayColumns(getSimpleTypeHolder()); + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory() @@ -95,10 +102,41 @@ public class PostgresDialect implements Dialect { /* * (non-Javadoc) - * @see org.springframework.data.r2dbc.dialect.Dialect#supportsArrayColumns() + * @see org.springframework.data.r2dbc.dialect.Dialect#getArraySupport() */ @Override - public boolean supportsArrayColumns() { - return true; + public ArrayColumns getArraySupport() { + return ARRAY_COLUMNS; + } + + @RequiredArgsConstructor + static class PostgresArrayColumns implements ArrayColumns { + + private final SimpleTypeHolder simpleTypes; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.ArrayColumns#isSupported() + */ + @Override + public boolean isSupported() { + return true; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.ArrayColumns#getArrayType(java.lang.Class) + */ + @Override + public Class getArrayType(Class userType) { + + Assert.notNull(userType, "Array component type must not be null"); + + if (!simpleTypes.isSimpleType(userType)) { + throw new IllegalArgumentException("Unsupported array type: " + ClassUtils.getQualifiedName(userType)); + } + + return ClassUtils.resolvePrimitiveIfNecessary(userType); + } } } 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 abe26fc3..537dcdd0 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -38,6 +38,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.dialect.ArrayColumns; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Dialect; @@ -249,7 +250,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) { TypeInformation type = property.getTypeInformation(); - Object value = relationalConverter.writeValue(propertyAccessor.getProperty(property), type); + Object value = propertyAccessor.getProperty(property); if (type.isCollectionLike()) { @@ -260,17 +261,28 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra throw new InvalidDataAccessApiUsageException("Nested entities are not supported"); } - if (!dialect.supportsArrayColumns()) { + ArrayColumns arrayColumns = dialect.getArraySupport(); + + if (!arrayColumns.isSupported()) { throw new InvalidDataAccessResourceUsageException( "Dialect " + dialect.getClass().getName() + " does not support array columns"); } - if (!property.isArray()) { + return getArrayValue(arrayColumns, property, value); + } - Object zeroLengthArray = Array.newInstance(property.getActualType(), 0); - return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass()); - } + return value; + } + + private Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) { + + Class targetType = arrayColumns.getArrayType(property.getActualType()); + + if (!property.isArray() || !property.getActualType().equals(targetType)) { + + Object zeroLengthArray = Array.newInstance(targetType, 0); + return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass()); } return value; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index 2f9e89c3..515718cc 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -96,7 +96,8 @@ public class EntityRowMapper implements BiFunction { return readEntityFrom(row, property); } - return converter.readValue(row.get(prefix + property.getColumnName()), property.getTypeInformation()); + Object value = row.get(prefix + property.getColumnName()); + return converter.readValue(value, property.getTypeInformation()); } catch (Exception o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); @@ -156,9 +157,7 @@ public class EntityRowMapper implements BiFunction { String column = prefix + property.getColumnName(); try { - - Object value = converter.readValue(resultSet.get(column), property.getTypeInformation()); - return converter.getConversionService().convert(value, parameter.getType().getType()); + return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); } diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java index 7df2e22f..e1fe8607 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/PostgresDialectUnitTests.java @@ -1,8 +1,8 @@ package org.springframework.data.r2dbc.dialect; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; -import java.util.Collection; import java.util.List; import org.junit.Test; @@ -28,35 +28,50 @@ public class PostgresDialectUnitTests { } @Test // gh-30 - public void shouldConsiderCollectionTypesAsSimple() { + public void shouldConsiderSimpleTypes() { SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); - assertThat(holder.isSimpleType(List.class)).isTrue(); - assertThat(holder.isSimpleType(Collection.class)).isTrue(); + assertSoftly(it -> { + it.assertThat(holder.isSimpleType(String.class)).isTrue(); + it.assertThat(holder.isSimpleType(int.class)).isTrue(); + it.assertThat(holder.isSimpleType(Integer.class)).isTrue(); + }); } @Test // gh-30 - public void shouldConsiderStringArrayTypeAsSimple() { + public void shouldSupportArrays() { - SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); - assertThat(holder.isSimpleType(String[].class)).isTrue(); + assertThat(arrayColumns.isSupported()).isTrue(); + } - @Test // gh-30 - public void shouldConsiderIntArrayTypeAsSimple() { + @Test // gh-30 + public void shouldUseBoxedArrayTypesForPrimitiveTypes() { - SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); - assertThat(holder.isSimpleType(int[].class)).isTrue(); - } + assertSoftly(it -> { + it.assertThat(arrayColumns.getArrayType(int.class)).isEqualTo(Integer.class); + it.assertThat(arrayColumns.getArrayType(double.class)).isEqualTo(Double.class); + it.assertThat(arrayColumns.getArrayType(String.class)).isEqualTo(String.class); + }); + } - @Test // gh-30 - public void shouldConsiderIntegerArrayTypeAsSimple() { + @Test // gh-30 + public void shouldRejectNonSimpleArrayTypes() { - SimpleTypeHolder holder = PostgresDialect.INSTANCE.getSimpleTypeHolder(); + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); - assertThat(holder.isSimpleType(Integer[].class)).isTrue(); - } + assertThatThrownBy(() -> arrayColumns.getArrayType(getClass())).isInstanceOf(IllegalArgumentException.class); + } + + @Test // gh-30 + public void shouldRejectNestedCollections() { + + ArrayColumns arrayColumns = PostgresDialect.INSTANCE.getArraySupport(); + + assertThatThrownBy(() -> arrayColumns.getArrayType(List.class)).isInstanceOf(IllegalArgumentException.class); } } diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java index 1e96e389..cb39be65 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/SqlServerDialectUnitTests.java @@ -33,4 +33,13 @@ public class SqlServerDialectUnitTests { assertThat(holder.isSimpleType(UUID.class)).isTrue(); } + + @Test // gh-30 + public void shouldNotSupportArrays() { + + ArrayColumns arrayColumns = SqlServerDialect.INSTANCE.getArraySupport(); + + assertThat(arrayColumns.isSupported()).isFalse(); + assertThatThrownBy(() -> arrayColumns.getArrayType(String.class)).isInstanceOf(UnsupportedOperationException.class); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java new file mode 100644 index 00000000..8321eddb --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import javax.sql.DataSource; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.PostgresTestSupport; +import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Integration tests for PostgreSQL-specific features such as array support. + * + * @author Mark Paluch + */ +public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport { + + @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + + DataSource dataSource = PostgresTestSupport.createDataSource(database); + ConnectionFactory connectionFactory = PostgresTestSupport.createConnectionFactory(database); + JdbcTemplate template = createJdbcTemplate(dataSource); + DatabaseClient client = DatabaseClient.create(connectionFactory); + + @Before + public void before() { + + template.execute("DROP TABLE IF EXISTS with_arrays"); + template.execute("CREATE TABLE with_arrays (" // + + "boxed_array INT[]," // + + "primitive_array INT[]," // + + "multidimensional_array INT[]," // + + "collection_array INT[][])"); + } + + @Test // gh-30 + @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/40, r2dbc-postgresql returns Object[] instead of Integer[]") + public void shouldReadAndWritePrimitiveSingleDimensionArrays() { + + EntityWithArrays withArrays = new EntityWithArrays(null, new int[] { 1, 2, 3 }, null, null); + + insert(withArrays); + selectAndAssert(actual -> { + assertThat(actual.primitiveArray).containsExactly(1, 2, 3); + }); + } + + @Test // gh-30 + public void shouldReadAndWriteBoxedSingleDimensionArrays() { + + EntityWithArrays withArrays = new EntityWithArrays(new Integer[] { 1, 2, 3 }, null, null, null); + + insert(withArrays); + + selectAndAssert(actual -> { + + assertThat(actual.boxedArray).containsExactly(1, 2, 3); + + }); + } + + @Test // gh-30 + public void shouldReadAndWriteConvertedDimensionArrays() { + + EntityWithArrays withArrays = new EntityWithArrays(null, null, null, Arrays.asList(5, 6, 7)); + + insert(withArrays); + + selectAndAssert(actual -> { + assertThat(actual.collectionArray).containsExactly(5, 6, 7); + }); + } + + @Test // gh-30 + @Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/42, Multi-dimensional arrays not supported yet") + public void shouldReadAndWriteMultiDimensionArrays() { + + EntityWithArrays withArrays = new EntityWithArrays(null, null, new int[][] { { 1, 2, 3 }, { 4, 5 } }, null); + + insert(withArrays); + + selectAndAssert(actual -> { + + assertThat(actual.multidimensionalArray).hasSize(2); + assertThat(actual.multidimensionalArray[0]).containsExactly(1, 2, 3); + assertThat(actual.multidimensionalArray[1]).containsExactly(4, 5, 6); + }); + } + + private void insert(EntityWithArrays object) { + + client.insert() // + .into(EntityWithArrays.class) // + .using(object) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + } + + private void selectAndAssert(Consumer assertion) { + + client.select() // + .from(EntityWithArrays.class).fetch() // + .first() // + .as(StepVerifier::create) // + .consumeNextWith(assertion).verifyComplete(); + } + + @Table("with_arrays") + @AllArgsConstructor + static class EntityWithArrays { + + Integer[] boxedArray; + int[] primitiveArray; + int[][] multidimensionalArray; + List collectionArray; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java index d7023ace..91dceff2 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java @@ -8,6 +8,7 @@ import io.r2dbc.spi.RowMetadata; import lombok.RequiredArgsConstructor; import java.util.List; +import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; @@ -20,6 +21,7 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti * Unit tests for {@link EntityRowMapper}. * * @author Mark Paluch + * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.class) public class EntityRowMapperUnitTests { @@ -73,7 +75,7 @@ public class EntityRowMapperUnitTests { public void shouldConvertArrayToSet() { EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); - when(rowMock.get("integerSet")).thenReturn((new int[] { 3, 14 })); + when(rowMock.get("integer_set")).thenReturn((new int[] { 3, 14 })); EntityWithCollection result = mapper.apply(rowMock, metadata); assertThat(result.integerSet).contains(3, 14); @@ -83,7 +85,7 @@ public class EntityRowMapperUnitTests { public void shouldConvertArrayMembers() { EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); - when(rowMock.get("primitiveIntegers")).thenReturn((new long[] { 3L, 14L })); + when(rowMock.get("primitive_integers")).thenReturn((new Long[] { 3L, 14L })); EntityWithCollection result = mapper.apply(rowMock, metadata); assertThat(result.primitiveIntegers).contains(3, 14); @@ -93,11 +95,12 @@ public class EntityRowMapperUnitTests { public void shouldConvertArrayToBoxedArray() { EntityRowMapper mapper = getRowMapper(EntityWithCollection.class); - when(rowMock.get("boxedIntegers")).thenReturn((new int[] { 3, 11 })); + when(rowMock.get("boxed_integers")).thenReturn((new int[] { 3, 11 })); EntityWithCollection result = mapper.apply(rowMock, metadata); assertThat(result.boxedIntegers).contains(3, 11); } + @SuppressWarnings("unchecked") private EntityRowMapper getRowMapper(Class type) { RelationalPersistentEntity entity = (RelationalPersistentEntity) strategy.getMappingContext() From db24229bf4dd3c64c79e68b6018aa4accaacf904 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 10 Dec 2018 10:24:41 +0100 Subject: [PATCH 059/665] #35 - Simplify reference documentation setup. --- src/main/asciidoc/index.adoc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index f9ad3d6c..fdfaf524 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -2,14 +2,6 @@ Mark Paluch :revnumber: {version} :revdate: {localdate} -:toc: -:toc-placement!: -:linkcss: -:doctype: book -:docinfo: shared -:source-highlighter: prettify -:icons: font -:imagesdir: images ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]] :spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc :reactiveStreamsJavadoc: http://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc From 8f8462d8fdd99614f7a9645f1b64a07205f4609e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Dec 2018 14:07:11 +0100 Subject: [PATCH 060/665] #36 - Updated changelog. --- src/main/resources/changelog.txt | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index ddb413a4..13f04817 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,3 +1,30 @@ Spring Data R2DBC Changelog =========================== +Changes in version 1.0.0.M1 (2018-12-12) +---------------------------------------- +* #36 - Release 1.0 M1. +* #35 - Simplify reference documentation setup. +* #32 - Drop oracle-java8-installer from TravisCI build. +* #30 - Add support for Custom Conversions for array-types. +* #27 - Add project site redirect. +* #26 - Add support to write simple type collections as arrays. +* #25 - Provide reference documentation. +* #21 - Upgrade to R2DBC 1.0M6. +* #20 - Add Dialect support to apply driver-specific bind markers. +* #18 - Cleanup pom.xml and upgrade dependencies. +* #16 - Add abstract configuration class for R2DBC. +* #15 - Add support for parameter bind markers. +* #14 - RETURNING * does not work on H2. +* #13 - Add configuration components for @EnableR2dbcRepositories. +* #12 - SimpleR2dbcRepository does not retain item order on save(…). +* #11 - Adapt Statement.bind(…) calls to newly introduced positional (integer-arg) binding. +* #10 - Adapt to removed Statement.executeReturningGeneratedKeys(). +* #9 - H2Statement does not define or inherit an implementation of bind(Ljava/lang/Integer;Ljava/lang/Object;). +* #8 - exchange() should allow to deal with DROP or CREATE requests. +* #6 - Preserving order on multiple inserts. +* #5 - Build failures due to failing Oracle JDK downloads. +* #2 - Add initial support for DatabaseClient and Reactive Repositories. +* #1 - Setup repository. + + From fcd9cc75dda324bf5506bf02ba83f866bd7fe39a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Dec 2018 14:07:12 +0100 Subject: [PATCH 061/665] #36 - Prepare 1.0 M1. --- pom.xml | 13 ++++++------- src/main/resources/notice.txt | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 9ff59187..8ea4064e 100644 --- a/pom.xml +++ b/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 @@ -15,15 +14,15 @@ org.springframework.data.build spring-data-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.M1 DATAR2DBC - 2.2.0.BUILD-SNAPSHOT - 1.1.0.BUILD-SNAPSHOT + 2.2.0.M1 + 1.1.0.M1 spring.data.r2dbc reuseReports @@ -369,8 +368,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-milestone + https://repo.spring.io/libs-milestone diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index ba8a2e69..dff0ef63 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data R2DBC 1.0.0.BUILD-SNAPSHOT +Spring Data R2DBC 1.0 M1 Copyright (c) [2018] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). From ace23974678aa22c6da6ccd939a3718feaee9d4e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Dec 2018 14:07:14 +0100 Subject: [PATCH 062/665] #36 - Release version 1.0 M1. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 8ea4064e..f35c10b2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.M1 Spring Data R2DBC Spring Data module for R2DBC. From 6e0e9adaef851d5d0845cd6c33cdc5607c1b5752 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Dec 2018 14:12:46 +0100 Subject: [PATCH 063/665] #36 - Prepare next development iteration. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f35c10b2..8ea4064e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.M1 + 1.0.0.BUILD-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC. From 3a47509369d43abbf49315bd533c804dcb9355cb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Dec 2018 14:12:46 +0100 Subject: [PATCH 064/665] #36 - After release cleanups. --- pom.xml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index 8ea4064e..665d56cc 100644 --- a/pom.xml +++ b/pom.xml @@ -14,15 +14,15 @@ org.springframework.data.build spring-data-parent - 2.2.0.M1 + 2.2.0.BUILD-SNAPSHOT DATAR2DBC - 2.2.0.M1 - 1.1.0.M1 + 2.2.0.BUILD-SNAPSHOT + 1.1.0.BUILD-SNAPSHOT spring.data.r2dbc reuseReports @@ -368,8 +368,8 @@ - spring-libs-milestone - https://repo.spring.io/libs-milestone + spring-libs-snapshot + https://repo.spring.io/libs-snapshot From b321278df76dab517ba584228f1db1a1aa6a6e93 Mon Sep 17 00:00:00 2001 From: Anbu Sampath Date: Wed, 12 Dec 2018 23:28:54 +0530 Subject: [PATCH 065/665] #38 - Add project metadata anchor to reference docs. --- src/main/asciidoc/preface.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/asciidoc/preface.adoc b/src/main/asciidoc/preface.adoc index 78a28fbd..8582eea1 100644 --- a/src/main/asciidoc/preface.adoc +++ b/src/main/asciidoc/preface.adoc @@ -111,6 +111,7 @@ Professional Support :: Professional, from-the-source support, with guaranteed r * You can also follow the Spring http://spring.io/blog[blog] or the Spring Data project team on Twitter (http://twitter.com/SpringData[SpringData]). +[[project-metadata]] == Project Metadata * Version control: http://github.com/spring-projects/spring-data-r2dbc From 2dfb1ff20e29cb124af623370fe1f5e45f812668 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 2 Jan 2019 12:04:47 +0100 Subject: [PATCH 066/665] #45 - Update copyright years to 2019. --- src/main/asciidoc/index.adoc | 2 +- .../springframework/data/r2dbc/BadSqlGrammarException.java | 2 +- .../data/r2dbc/InvalidResultAccessException.java | 2 +- .../data/r2dbc/UncategorizedR2dbcException.java | 2 +- .../data/r2dbc/config/AbstractR2dbcConfiguration.java | 2 +- .../data/r2dbc/function/ConnectionAccessor.java | 2 +- .../springframework/data/r2dbc/function/DatabaseClient.java | 2 +- .../data/r2dbc/function/DefaultDatabaseClient.java | 2 +- .../data/r2dbc/function/DefaultDatabaseClientBuilder.java | 2 +- .../springframework/data/r2dbc/function/DefaultFetchSpec.java | 2 +- .../r2dbc/function/DefaultReactiveDataAccessStrategy.java | 2 +- .../springframework/data/r2dbc/function/DefaultSqlResult.java | 2 +- .../r2dbc/function/DefaultTransactionalDatabaseClient.java | 2 +- .../function/DefaultTransactionalDatabaseClientBuilder.java | 2 +- .../org/springframework/data/r2dbc/function/FetchSpec.java | 2 +- .../data/r2dbc/function/ReactiveDataAccessStrategy.java | 2 +- .../springframework/data/r2dbc/function/RowsFetchSpec.java | 2 +- .../org/springframework/data/r2dbc/function/SqlResult.java | 2 +- .../data/r2dbc/function/TransactionalDatabaseClient.java | 2 +- .../data/r2dbc/function/UpdatedRowsFetchSpec.java | 2 +- .../function/connectionfactory/ConnectionFactoryUtils.java | 2 +- .../r2dbc/function/connectionfactory/ConnectionProxy.java | 2 +- .../connectionfactory/DefaultTransactionResources.java | 2 +- .../connectionfactory/ReactiveTransactionSynchronization.java | 2 +- .../connectionfactory/SingletonConnectionFactory.java | 2 +- .../function/connectionfactory/SmartConnectionFactory.java | 2 +- .../function/connectionfactory/TransactionResources.java | 2 +- .../data/r2dbc/function/convert/ColumnMapRowMapper.java | 2 +- .../data/r2dbc/function/convert/EntityRowMapper.java | 2 +- .../data/r2dbc/function/convert/IterableUtils.java | 2 +- .../data/r2dbc/function/convert/MappingR2dbcConverter.java | 2 +- .../data/r2dbc/function/convert/SettableValue.java | 2 +- .../data/r2dbc/repository/R2dbcRepository.java | 2 +- .../data/r2dbc/repository/config/EnableR2dbcRepositories.java | 2 +- .../r2dbc/repository/config/R2dbcRepositoriesRegistrar.java | 2 +- .../config/R2dbcRepositoryConfigurationExtension.java | 2 +- .../data/r2dbc/repository/query/AbstractR2dbcQuery.java | 2 +- .../data/r2dbc/repository/query/BindableQuery.java | 2 +- .../springframework/data/r2dbc/repository/query/Query.java | 4 ++-- .../data/r2dbc/repository/query/R2dbcParameterAccessor.java | 2 +- .../data/r2dbc/repository/query/R2dbcQueryExecution.java | 2 +- .../data/r2dbc/repository/query/R2dbcQueryMethod.java | 2 +- .../data/r2dbc/repository/query/StringBasedR2dbcQuery.java | 2 +- .../data/r2dbc/repository/support/R2dbcRepositoryFactory.java | 2 +- .../r2dbc/repository/support/R2dbcRepositoryFactoryBean.java | 2 +- .../data/r2dbc/repository/support/SimpleR2dbcRepository.java | 2 +- .../support/AbstractFallbackR2dbcExceptionTranslator.java | 2 +- .../data/r2dbc/support/R2dbcExceptionTranslator.java | 2 +- .../r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java | 2 +- .../data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java | 2 +- src/main/resources/notice.txt | 2 +- .../java/org/springframework/data/r2dbc/DependencyTests.java | 2 +- .../function/AbstractDatabaseClientIntegrationTests.java | 2 +- .../AbstractTransactionalDatabaseClientIntegrationTests.java | 2 +- .../function/PostgresDatabaseClientIntegrationTests.java | 2 +- .../data/r2dbc/function/PostgresIntegrationTests.java | 2 +- .../function/SqlServerDatabaseClientIntegrationTests.java | 2 +- .../connectionfactory/ConnectionFactoryUtilsUnitTests.java | 2 +- .../repository/AbstractR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/PostgresR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/SqlServerR2dbcRepositoryIntegrationTests.java | 2 +- .../springframework/data/r2dbc/repository/config/Person.java | 2 +- .../data/r2dbc/repository/config/PersonRepository.java | 2 +- .../repository/config/R2dbcRepositoriesRegistrarTests.java | 2 +- .../R2dbcRepositoryConfigurationExtensionUnitTests.java | 2 +- .../r2dbc/repository/query/R2dbcQueryMethodUnitTests.java | 2 +- .../repository/query/StringBasedR2dbcQueryUnitTests.java | 2 +- .../AbstractSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../PostgresSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/support/R2dbcRepositoryFactoryUnitTests.java | 2 +- .../SqlServerSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java | 2 +- .../support/SqlStateR2dbcExceptionTranslatorUnitTests.java | 2 +- .../springframework/data/r2dbc/testing/ExternalDatabase.java | 2 +- .../data/r2dbc/testing/R2dbcIntegrationTestSupport.java | 2 +- 75 files changed, 76 insertions(+), 76 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index fdfaf524..89358348 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -6,7 +6,7 @@ ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1 :spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc :reactiveStreamsJavadoc: http://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc -(C) 2018 The original authors. +(C) 2018-2019 The original authors. NOTE: Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. diff --git a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java index f1dd079f..bb507a34 100644 --- a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java +++ b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java index 6effd608..37a6ca9e 100644 --- a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java index da20c40c..fd161c3f 100644 --- a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java +++ b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 57c1e3c9..34e1b515 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java index 6f9b4b48..fac0d213 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. 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 094e6502..5893c3c2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. 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 d96f57f6..56bb5fe8 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index de61a819..5d1beab9 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java index c51bac8a..4ddabc8f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. 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 537dcdd0..72059aae 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index 01d0c660..d1acae5d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java index 9206c244..1edd16d1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java index 5dbf6fa7..c25e2c82 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java index da748b5b..96a4a8b5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 8b038e98..8670961b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java index 5399bffc..e7ab42de 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index ea418205..4132bdff 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java index a747032c..b596d8d0 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java index e2077355..6e45f04a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java index d2e60f96..3b320398 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index 2bb0ff29..e02011e9 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java index 853448dc..ba761646 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java index af2ab934..7c819247 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java index 5610bf79..b73597d2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java index 221cdc54..07514b35 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java index 119c6ca8..b4c88832 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java index d7acd3c4..a5a2dcf2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index 515718cc..f84f0bf2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java index c65010e0..f2544007 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 54b26ec1..58f337e6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java index 8338b169..fe4e8118 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java index eba2d5bf..264fd1bc 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java index 75e4ab47..bb7b0157 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java index 91949b31..36615549 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java index efad3af3..4ca96a9c 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java index 3fbeeb6b..5cb02f92 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java index dc2957ee..7ba29c27 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java index 43b451da..66b5bee8 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. @@ -25,7 +25,7 @@ import org.springframework.data.annotation.QueryAnnotation; /** * Annotation to provide SQL statements that will get used for executing the method. - * + * * @author Mark Paluch */ @Retention(RetentionPolicy.RUNTIME) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index c859d7a5..bf46b453 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index 41c53396..67d0090f 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index 63adcae1..ee73b6cf 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 6bbb32ae..11b21092 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index b5e749a0..78e80d12 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 8a2ea87f..766787ca 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index e7e37cce..bee011ce 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java index dded5da8..cc4bf6c9 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java index 5bbcd4a2..f2a39448 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java index 7f29fbc3..cb2ecb31 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java index 97eb4e62..f0d1c489 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index dff0ef63..04e43adc 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,5 +1,5 @@ Spring Data R2DBC 1.0 M1 -Copyright (c) [2018] Pivotal Software, Inc. +Copyright (c) [2018-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). You may not use this product except in compliance with the License. diff --git a/src/test/java/org/springframework/data/r2dbc/DependencyTests.java b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java index 30da212f..3b6e75b0 100644 --- a/src/test/java/org/springframework/data/r2dbc/DependencyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 6f78cc72..9a5f3136 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index a5193fa4..52d267d8 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java index df86faf7..d5ec20b9 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java index 8321eddb..3260eb47 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java index b9ee4bc1..9e77bee0 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java index 75634058..9580af4c 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 51a573da..ded79a63 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index 1899ae5b..290160e8 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java index 5d27027d..8bea7d50 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java index f9201001..8b022fd0 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java index b8141ced..6d59968b 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index 6db3bf59..d40e212b 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java index eb40cb78..81e17d11 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java index 415fcc2d..d2a730df 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index 3902b6fe..9af26b2c 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index 5201362c..b1c916d4 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java index 11157ab0..8fc212e4 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 79dbfbdb..3efc75cf 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java index 6a16d4e8..fa93e7fd 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java index 3f3fb136..6dab0d84 100644 --- a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java index 39b6f8f5..86eb135e 100644 --- a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index 2582983f..6a1832ae 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. diff --git a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java index d20fc476..f1467efe 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. From 836c5b4ec8833bde1c511ad063c016fa3c987439 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jan 2019 10:25:55 +0100 Subject: [PATCH 067/665] #48 - Atomically close connections. We now make sure to close connections only once by tracking the cleanup state. Flux.usingWhen/Mono.usingWhen do not ensure atomic cleanup in situations where the subscription completes and then the subscription is terminated. This behavior has lead to closing a connection multiple times. Related ticket: reactor/reactor-core#1486 --- .../r2dbc/function/DefaultDatabaseClient.java | 41 +++++++-- .../DefaultDatabaseClientUnitTests.java | 87 +++++++++++++++++++ 2 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java 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 56bb5fe8..d158fb6d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -38,6 +38,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; @@ -122,15 +123,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(action, "Callback object must not be null"); - Mono connectionMono = getConnection(); - // Create close-suppressing Connection proxy, also preparing returned Statements. + Mono connectionMono = getConnection() + .map(it -> new ConnectionCloseHolder(it, this::closeConnection)); return Mono.usingWhen(connectionMono, it -> { - Connection connectionToUse = createConnectionProxy(it); + // Create close-suppressing Connection proxy + Connection connectionToUse = createConnectionProxy(it.connection); return doInConnection(connectionToUse, action); - }, this::closeConnection, this::closeConnection, this::closeConnection) // + }, ConnectionCloseHolder::close, ConnectionCloseHolder::close, ConnectionCloseHolder::close) // .onErrorMap(R2dbcException.class, ex -> translateException("execute", getSql(action), ex)); } @@ -149,15 +151,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(action, "Callback object must not be null"); - Mono connectionMono = getConnection(); - // Create close-suppressing Connection proxy, also preparing returned Statements. + Mono connectionMono = getConnection() + .map(it -> new ConnectionCloseHolder(it, this::closeConnection)); return Flux.usingWhen(connectionMono, it -> { - Connection connectionToUse = createConnectionProxy(it); + // Create close-suppressing Connection proxy, also preparing returned Statements. + Connection connectionToUse = createConnectionProxy(it.connection); return doInConnectionMany(connectionToUse, action); - }, this::closeConnection, this::closeConnection, this::closeConnection) // + }, ConnectionCloseHolder::close, ConnectionCloseHolder::close, ConnectionCloseHolder::close) // .onErrorMap(R2dbcException.class, ex -> translateException("executeMany", getSql(action), ex)); } @@ -1104,4 +1107,26 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } } } + + /** + * Holder for a connection that makes sure the close action is invoked atomically only once. + */ + @RequiredArgsConstructor + static class ConnectionCloseHolder extends AtomicBoolean { + + final Connection connection; + final Function> closeFunction; + + Mono close() { + + return Mono.defer(() -> { + + if (compareAndSet(false, true)) { + return Mono.from(closeFunction.apply(connection)); + } + + return Mono.empty(); + }); + } + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java new file mode 100644 index 00000000..ac1d7eaa --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; + +/** + * Unit tests for {@link DefaultDatabaseClient}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultDatabaseClientUnitTests { + + @Mock ConnectionFactory connectionFactory; + @Mock Connection connection; + @Mock ReactiveDataAccessStrategy strategy; + @Mock R2dbcExceptionTranslator translator; + + @Before + public void before() { + when(connectionFactory.create()).thenReturn((Publisher) Mono.just(connection)); + when(connection.close()).thenReturn(Mono.empty()); + } + + @Test // gh-48 + public void shouldCloseConnectionOnlyOnce() { + + DefaultDatabaseClient databaseClient = (DefaultDatabaseClient) DatabaseClient.builder() + .connectionFactory(connectionFactory).dataAccessStrategy(strategy).exceptionTranslator(translator).build(); + + Flux flux = databaseClient.inConnectionMany(it -> { + return Flux.empty(); + }); + + flux.subscribe(new CoreSubscriber() { + Subscription subscription; + + @Override + public void onSubscribe(Subscription s) { + s.request(1); + subscription = s; + } + + @Override + public void onNext(Object o) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onComplete() { + subscription.cancel(); + } + }); + + verify(connection, times(1)).close(); + } +} From 3a1085e244ff26adedccdd495d14ad589e62b06b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Jan 2019 10:27:30 +0100 Subject: [PATCH 068/665] #48 - Polishing. Formatting. --- .../r2dbc/function/DefaultDatabaseClient.java | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) 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 d158fb6d..34c8e825 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -80,7 +80,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final DefaultDatabaseClientBuilder builder; DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { + ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { this.connector = connector; this.exceptionTranslator = exceptionTranslator; @@ -201,15 +201,15 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { */ protected Connection createConnectionProxy(Connection con) { return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), - new Class[]{ConnectionProxy.class}, new CloseSuppressingInvocationHandler(con)); + new Class[] { ConnectionProxy.class }, new CloseSuppressingInvocationHandler(con)); } /** * Translate the given {@link R2dbcException} into a generic {@link DataAccessException}. * * @param task readable text describing the task being attempted. - * @param sql SQL query or update that caused the problem (may be {@literal null}). - * @param ex the offending {@link R2dbcException}. + * @param sql SQL query or update that caused the problem (may be {@literal null}). + * @param ex the offending {@link R2dbcException}. * @return a DataAccessException wrapping the {@link R2dbcException} (never {@literal null}). */ protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) { @@ -222,7 +222,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier, Class typeToRead) { + Map byName, Supplier sqlSupplier, Class typeToRead) { return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, typeToRead); } @@ -230,8 +230,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected DefaultTypedExecuteSpec createTypedExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier, - BiFunction mappingFunction) { + Map byName, Supplier sqlSupplier, + BiFunction mappingFunction) { return new DefaultTypedExecuteSpec<>(byIndex, byName, sqlSupplier, mappingFunction); } @@ -239,7 +239,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * Customization hook. */ protected ExecuteSpecSupport createGenericExecuteSpec(Map byIndex, - Map byName, Supplier sqlSupplier) { + Map byName, Supplier sqlSupplier) { return new DefaultGenericExecuteSpec(byIndex, byName, sqlSupplier); } @@ -251,7 +251,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } private static void doBind(Statement statement, Map byName, - Map byIndex) { + Map byIndex) { byIndex.forEach((i, o) -> { @@ -377,7 +377,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { return new ExecuteSpecSupport(byIndex, byName, sqlSupplier); } @@ -395,7 +395,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { protected class DefaultGenericExecuteSpec extends ExecuteSpecSupport implements GenericExecuteSpec { DefaultGenericExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { super(byIndex, byName, sqlSupplier); } @@ -456,7 +456,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, - Supplier sqlSupplier) { + Supplier sqlSupplier) { return createGenericExecuteSpec(byIndex, byName, sqlSupplier); } } @@ -471,7 +471,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final BiFunction mappingFunction; DefaultTypedExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier, Class typeToRead) { + Supplier sqlSupplier, Class typeToRead) { super(byIndex, byName, sqlSupplier); @@ -480,7 +480,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } DefaultTypedExecuteSpec(Map byIndex, Map byName, - Supplier sqlSupplier, BiFunction mappingFunction) { + Supplier sqlSupplier, BiFunction mappingFunction) { super(byIndex, byName, sqlSupplier); @@ -541,7 +541,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedExecuteSpec createInstance(Map byIndex, - Map byName, Supplier sqlSupplier) { + Map byName, Supplier sqlSupplier) { return createTypedExecuteSpec(byIndex, byName, sqlSupplier, typeToRead); } } @@ -628,7 +628,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } protected abstract DefaultSelectSpecSupport createInstance(String table, List projectedFields, Sort sort, - Pageable page); + Pageable page); } private class DefaultGenericSelectSpec extends DefaultSelectSpecSupport implements GenericSelectSpec { @@ -695,7 +695,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { + Pageable page) { return new DefaultGenericSelectSpec(table, projectedFields, sort, page); } } @@ -706,8 +706,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @SuppressWarnings("unchecked") private class DefaultTypedSelectSpec extends DefaultSelectSpecSupport implements TypedSelectSpec { - private final @Nullable - Class typeToRead; + private final @Nullable Class typeToRead; private final BiFunction mappingFunction; DefaultTypedSelectSpec(Class typeToRead) { @@ -719,12 +718,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, - BiFunction mappingFunction) { + BiFunction mappingFunction) { this(table, projectedFields, sort, page, null, mappingFunction); } DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, - BiFunction mappingFunction) { + BiFunction mappingFunction) { super(table, projectedFields, sort, page); this.typeToRead = typeToRead; this.mappingFunction = mappingFunction; @@ -784,7 +783,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override protected DefaultTypedSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { + Pageable page) { return new DefaultTypedSelectSpec<>(table, projectedFields, sort, page, typeToRead, mappingFunction); } } @@ -1022,8 +1021,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (R2dbcException e) { String sql = getSql(action); - return Flux.error(new UncategorizedR2dbcException("doInConnectionMany", sql, e) { - }); + return Flux.error(new UncategorizedR2dbcException("doInConnectionMany", sql, e)); } } @@ -1034,8 +1032,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } catch (R2dbcException e) { String sql = getSql(action); - return Mono.error(new UncategorizedR2dbcException("doInConnection", sql, e) { - }); + return Mono.error(new UncategorizedR2dbcException("doInConnection", sql, e)); } } From 6f8bca613527f68f1c572840e9f983cad133849d Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 15 Jan 2019 12:49:52 +0100 Subject: [PATCH 069/665] #29 - Use Testcontainers to run Postgres integration tests. Postgres integration tests now run either with a locally installed and started database or if no such database can be found an instance gets started in a Docker container via Testcontainers. We prefer a database based on TestContainers. Only if this can't be obtained do we try to access a local database for Postgres. This minimizes the risk of accessing a database during tests that is not intended for that purpose. If the system property `spring.data.r2dbc.test.preferLocalDatabase` is set to "true" the local database is preferred. Original pull request: #51. --- .../data/r2dbc/testing/ExternalDatabase.java | 69 ++++++++++++++- .../r2dbc/testing/PostgresTestSupport.java | 84 +++++++++++++++++-- .../r2dbc/testing/SqlServerTestSupport.java | 18 ++-- 3 files changed, 152 insertions(+), 19 deletions(-) diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index 6a1832ae..a538f630 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -24,6 +24,8 @@ import java.util.concurrent.TimeUnit; import org.junit.AssumptionViolatedException; import org.junit.rules.ExternalResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * {@link ExternalResource} wrapper to encapsulate {@link ProvidedDatabase} and @@ -33,6 +35,8 @@ import org.junit.rules.ExternalResource; */ public abstract class ExternalDatabase extends ExternalResource { + private static Logger LOG = LoggerFactory.getLogger(ExternalDatabase.class); + /** * @return the post of the database service. */ @@ -53,18 +57,37 @@ public abstract class ExternalDatabase extends ExternalResource { */ public abstract String getUsername(); + /** + * Throws an {@link AssumptionViolatedException} if the database cannot be reached. + */ @Override protected void before() { - try (Socket socket = new Socket()) { - socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5))); - - } catch (IOException e) { + if (!checkValidity()) { throw new AssumptionViolatedException( String.format("Cannot connect to %s:%d. Skipping tests.", getHostname(), getPort())); } } + /** + * performs a test if the database can actually be reached. + * + * @return true, if the database could be reached. + */ + boolean checkValidity() { + + try (Socket socket = new Socket()) { + + socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5))); + return true; + + } catch (IOException e) { + LOG.debug("external database not available.", e); + } + + return false; + } + /** * @return password for the database user. */ @@ -122,4 +145,42 @@ public abstract class ExternalDatabase extends ExternalResource { return password; } } + + /** + * An {@link ExternalDatabase} that couldn't get constructed. + * + * @author Jens Schauder + */ + static class NoSuchDatabase extends ExternalDatabase { + + @Override + boolean checkValidity() { + return false; + } + + @Override + public int getPort() { + throw new UnsupportedOperationException(); + } + + @Override + public String getHostname() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDatabase() { + throw new UnsupportedOperationException(); + } + + @Override + public String getUsername() { + throw new UnsupportedOperationException(); + } + + @Override + public String getPassword() { + throw new UnsupportedOperationException(); + } + } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java index d7608f09..74159cb8 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -4,18 +4,24 @@ import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; import io.r2dbc.postgresql.PostgresqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; +import java.util.function.Supplier; + import javax.sql.DataSource; import org.postgresql.ds.PGSimpleDataSource; import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; +import org.testcontainers.containers.PostgreSQLContainer; /** * Utility class for testing against Postgres. * * @author Mark Paluch + * @author Jens Schauder */ public class PostgresTestSupport { + private static ExternalDatabase testContainerDatabase; + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + " id integer CONSTRAINT id PRIMARY KEY,\n" // + " name varchar(255) NOT NULL,\n" // @@ -31,30 +37,91 @@ public class PostgresTestSupport { public static String INSERT_INTO_LEGOSET = "INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)"; /** - * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + * Returns a database either hosted locally at {@code postgres:@localhost:5432/postgres} or running inside Docker. * - * @return + * @return information about the database. Guaranteed to be not {@literal null}. */ public static ExternalDatabase database() { - return local(); + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + PostgresTestSupport::local, // + PostgresTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + PostgresTestSupport::testContainer, // + PostgresTestSupport::local // + ); + } + } + + private static ExternalDatabase getFirstWorkingDatabase(Supplier first, + Supplier second) { + + ExternalDatabase database = first.get(); + if (database.checkValidity()) { + return database; + } else { + return second.get(); + } } /** * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. - * - * @return */ private static ExternalDatabase local() { - return ProvidedDatabase.builder().hostname("localhost").port(5432).database("postgres").username("postgres") + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(5432) // + .database("postgres") // + .username("postgres") // .password("").build(); } + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(); + postgreSQLContainer.start(); + + testContainerDatabase = ProvidedDatabase.builder() // + .hostname("localhost") // + .port(postgreSQLContainer.getFirstMappedPort()) // + .database(postgreSQLContainer.getDatabaseName()) // + .username(postgreSQLContainer.getUsername()) // + .password(postgreSQLContainer.getPassword()).build(); + + } catch (IllegalStateException ise) { + // docker is not available. + testContainerDatabase = new ExternalDatabase.NoSuchDatabase(); + } + + } + + return testContainerDatabase; + } + /** * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. */ public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { - return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder().host(database.getHostname()) - .database(database.getDatabase()).username(database.getUsername()).password(database.getPassword()).build()); + + return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() // + .host(database.getHostname()) // + .database(database.getDatabase()) // + .port(database.getPort()) // + .username(database.getUsername()) // + .password(database.getPassword()) // + .build()); } /** @@ -72,4 +139,5 @@ public class PostgresTestSupport { return dataSource; } + } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java index 3fb08ad7..d52c8476 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java @@ -33,31 +33,35 @@ public class SqlServerTestSupport { /** * Returns a locally provided database at {@code sqlserver:@localhost:1433/master}. - * - * @return */ public static ExternalDatabase database() { return local(); } /** - * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. - * - * @return + * Returns a locally provided database at {@code sqlserver:@localhost:1433/master}. */ private static ExternalDatabase local() { - return ProvidedDatabase.builder().hostname("localhost").port(1433).database("master").username("sa") - .password("A_Str0ng_Required_Password").build(); + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(1433) // + .database("master") // + .username("sa") // + .password("A_Str0ng_Required_Password") // + .build(); } /** * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. */ public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + return new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host(database.getHostname()) // .database(database.getDatabase()) // .username(database.getUsername()) // .password(database.getPassword()) // + .port(database.getPort()) // .build()); } From f90667b8ffd26067737b8fee5db7bddce6a3bb63 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 18 Jan 2019 13:50:17 +0100 Subject: [PATCH 070/665] #29 - Polishing. Add author tags. Add static factory method to create an unavailable database object instance. Simplify database selection. Javadoc. Original pull request: #51. --- .../data/r2dbc/testing/ExternalDatabase.java | 43 +++++++++++++++---- .../r2dbc/testing/PostgresTestSupport.java | 19 ++++---- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index a538f630..dca944b9 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -32,11 +32,21 @@ import org.slf4j.LoggerFactory; * {@link org.testcontainers.containers.PostgreSQLContainer}. * * @author Mark Paluch + * @author Jens Schauder */ public abstract class ExternalDatabase extends ExternalResource { private static Logger LOG = LoggerFactory.getLogger(ExternalDatabase.class); + /** + * Construct an absent database that is used as {@literal null} object if no database is available. + * + * @return an absent database. + */ + public static ExternalDatabase unavailable() { + return NoAvailableDatabase.INSTANCE; + } + /** * @return the post of the database service. */ @@ -70,7 +80,7 @@ public abstract class ExternalDatabase extends ExternalResource { } /** - * performs a test if the database can actually be reached. + * Performs a test if the database can actually be reached. * * @return true, if the database could be reached. */ @@ -79,7 +89,7 @@ public abstract class ExternalDatabase extends ExternalResource { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5))); - return true; + return true; } catch (IOException e) { LOG.debug("external database not available.", e); @@ -151,8 +161,13 @@ public abstract class ExternalDatabase extends ExternalResource { * * @author Jens Schauder */ - static class NoSuchDatabase extends ExternalDatabase { + private static class NoAvailableDatabase extends ExternalDatabase { + private static final NoAvailableDatabase INSTANCE = new NoAvailableDatabase(); + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPort() + */ @Override boolean checkValidity() { return false; @@ -160,27 +175,39 @@ public abstract class ExternalDatabase extends ExternalResource { @Override public int getPort() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getSimpleName()); } + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname() + */ @Override public String getHostname() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getSimpleName()); } + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() + */ @Override public String getDatabase() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getSimpleName()); } + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getUsername() + */ @Override public String getUsername() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getSimpleName()); } + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPassword() + */ @Override public String getPassword() { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException(getClass().getSimpleName()); } } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java index 74159cb8..180e9940 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -5,6 +5,7 @@ import io.r2dbc.postgresql.PostgresqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; import java.util.function.Supplier; +import java.util.stream.Stream; import javax.sql.DataSource; @@ -58,15 +59,13 @@ public class PostgresTestSupport { } } - private static ExternalDatabase getFirstWorkingDatabase(Supplier first, - Supplier second) { + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... suppliers) { - ExternalDatabase database = first.get(); - if (database.checkValidity()) { - return database; - } else { - return second.get(); - } + return Stream.of(suppliers).map(Supplier::get) // + .filter(ExternalDatabase::checkValidity) // + .findFirst() // + .orElse(ExternalDatabase.unavailable()); } /** @@ -101,8 +100,8 @@ public class PostgresTestSupport { .password(postgreSQLContainer.getPassword()).build(); } catch (IllegalStateException ise) { - // docker is not available. - testContainerDatabase = new ExternalDatabase.NoSuchDatabase(); + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); } } From b43b11936f14789e9252d6980b60a68fd00ec7ea Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 4 Jan 2019 14:37:37 +0100 Subject: [PATCH 071/665] #23 - Add support for named parameters. DatabaseClient now supports named parameters prefixed with a colon such as :name in addition to database-native bind markers. Named parameters thus are supported in annotated repository query methods which also increases portability of queries across database vendors. Named parameter support unrolls collection arguments to reduce the need for argument-specific SQL statements: db.execute() .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") .bind("ages", Arrays.asList(35, 50)); Results in a query: SELECT id, name, state FROM table WHERE age IN (35, 50) Collection arguments containing nested object arrays can be used to use select lists: List tuples = new ArrayList<>(); tuples.add(new Object[] {"John", 35}); tuples.add(new Object[] {"Ann", 50}); db.execute() .sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") .bind("tuples", tuples); translates to: SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) Original pull request: #47. --- src/main/asciidoc/new-features.adoc | 13 +- .../reference/r2dbc-repositories.adoc | 6 +- src/main/asciidoc/reference/r2dbc.adoc | 69 ++- .../r2dbc/function/BindParameterSource.java | 61 +++ .../data/r2dbc/function/DatabaseClient.java | 17 +- .../r2dbc/function/DefaultDatabaseClient.java | 26 +- .../DefaultDatabaseClientBuilder.java | 53 +- .../DefaultReactiveDataAccessStrategy.java | 10 + .../DefaultTransactionalDatabaseClient.java | 5 +- ...ultTransactionalDatabaseClientBuilder.java | 18 +- .../function/MapBindParameterSource.java | 114 ++++ .../function/NamedParameterExpander.java | 160 ++++++ .../r2dbc/function/NamedParameterUtils.java | 485 ++++++++++++++++++ .../data/r2dbc/function/ParsedSql.java | 143 ++++++ .../function/ReactiveDataAccessStrategy.java | 8 + .../function/TransactionalDatabaseClient.java | 35 +- ...bstractDatabaseClientIntegrationTests.java | 10 +- ...ctionalDatabaseClientIntegrationTests.java | 33 +- .../NamedParameterUtilsUnitTests.java | 295 +++++++++++ ...ostgresDatabaseClientIntegrationTests.java | 5 - ...ctionalDatabaseClientIntegrationTests.java | 5 - ...lServerDatabaseClientIntegrationTests.java | 5 - ...stgresR2dbcRepositoryIntegrationTests.java | 2 +- ...ServerR2dbcRepositoryIntegrationTests.java | 2 +- 24 files changed, 1494 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index a475570d..b47c2ee7 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -1,10 +1,15 @@ [[new-features]] = New & Noteworthy +[[new-features.1-0-0-M2]] +== What's New in Spring Data R2DBC 1.0.0 M2 + +* Support for named parameters. + [[new-features.1-0-0-M1]] == What's New in Spring Data R2DBC 1.0.0 M1 -* Initial R2DBC support through `DatabaseClient` -* Initial Transaction support through `TransactionalDatabaseClient` -* Initial R2DBC Repository Support through `R2dbcRepository` -* Initial Dialect support for Postgres and Microsoft SQL Server +* Initial R2DBC support through `DatabaseClient`. +* Initial Transaction support through `TransactionalDatabaseClient`. +* Initial R2DBC Repository Support through `R2dbcRepository`. +* Initial Dialect support for Postgres and Microsoft SQL Server. diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index 7a46cf07..1c62b291 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -104,7 +104,7 @@ Defining such a query is a matter of declaring a method on the repository interf ---- public interface PersonRepository extends ReactiveCrudRepository { - @Query("SELECT * FROM person WHERE lastname = $1") + @Query("SELECT * FROM person WHERE lastname = :lastname") Flux findByLastname(String lastname); <1> @Query("SELECT firstname, lastname FROM person WHERE lastname = $1") @@ -114,10 +114,10 @@ public interface PersonRepository extends ReactiveCrudRepository { ---- <1> The `findByLastname` method shows a query for all people with the given last name. The query is provided as R2DBC repositories do not support query derivation. +<2> A query for a single `Person` entity projecting only `firstname` and `lastname` columns. The annotated query uses native bind markers, which are Postgres bind markers in this example. -<4> A query for a single `Person` entity projecting only `firstname` and `lastname` columns. ==== NOTE: R2DBC repositories do not support query derivation. -NOTE: R2DBC repositories require native parameter bind markers that are bound by index. +NOTE: R2DBC repositories bind parameters to placeholders by index. diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc index b5d47cea..8fb038df 100644 --- a/src/main/asciidoc/reference/r2dbc.adoc +++ b/src/main/asciidoc/reference/r2dbc.adoc @@ -446,20 +446,61 @@ Parameter binding supports various binding strategies: * By Index using zero-based parameter indexes. * By Name using the placeholder name. -The following example shows parameter binding for a PostgreSQL query: +The following example shows parameter binding for a query: [source,java] ---- db.execute() - .sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") - .bind(0, "joe") - .bind(1, "Joe") - .bind(2, 34); + .sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34); ---- -NOTE: If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers. +.R2DBC Native Bind Markers +**** +R2DBC uses database-native bind markers that depend on the actual database. +If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers. JDBC drivers translate question mark bind markers to database-native markers as part of statement execution. -Make sure to use the appropriate bind markers that are supported by your database as R2DBC requires database-native parameter bind markers. + +Postgres uses indexed markers (`$1`, `$2`), SQL Server uses named bind markers prefixed with `@` as its native bind marker syntax. +Spring Data R2DBC leverages `Dialect` implementations to expand named parameters to native bind markers at the time of query execution which gives you a certain degree of query portability across various database vendors. +You can still use native bind markers if you prefer to do so. +**** + +The query-preprocessor unrolls named `Collection` parameters into a series of bind markers to remove the need of dynamic query creation based on the number of arguments. +Nested object arrays are expanded to allow usage of e.g. select lists. + +Consider the following query: + +[source,sql] +---- +SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) +---- + +This query can be parametrized and executed as: + +[source,java] +---- +List tuples = new ArrayList<>(); +tuples.add(new Object[] {"John", 35}); +tuples.add(new Object[] {"Ann", 50}); + +db.execute() + .sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") + .bind("tuples", tuples); +---- + +NOTE: Usage of select lists is vendor-dependent. + +A simpler variant using `IN` predicates: + +[source,java] +---- +db.execute() + .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") + .bind("ages", Arrays.asList(35, 50)); +---- [[r2dbc.datbaseclient.transactions]] === Transactions @@ -478,14 +519,14 @@ TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create( Flux completion = databaseClient.inTransaction(db -> { - return db.execute().sql("INSERT INTO person (id, name, age) VALUES($1, $2, $3)") // - .bind(0, "joe") // - .bind(1, "Joe") // - .bind(2, 34) // + return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34) .fetch().rowsUpdated() - .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES($1, $2)") - .bind(0, "joe") - .bind(1, "Joe") + .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .bind("id", "joe") + .bind("name", "Joe") .fetch().rowsUpdated()) .then(); }); diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java new file mode 100644 index 00000000..c89eae8b --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java @@ -0,0 +1,61 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import org.springframework.lang.Nullable; + +/** + * Interface that defines common functionality for objects that can offer parameter values for named bind parameters, + * serving as argument for {@link NamedParameterExpander} operations. + *

+ * This interface allows for the specification of the type in addition to parameter values. All parameter values and + * types are identified by specifying the name of the parameter. + *

+ * Intended to wrap various implementations like a {@link java.util.Map} with a consistent interface. + * + * @author Mark Paluch + * @see MapBindParameterSource + */ +public interface BindParameterSource { + + /** + * Determine whether there is a value for the specified named parameter. + * + * @param paramName the name of the parameter. + * @return {@literal true} if there is a value defined; {@literal false} otherwise. + */ + boolean hasValue(String paramName); + + /** + * Return the parameter value for the requested named parameter. + * + * @param paramName the name of the parameter. + * @return the value of the specified parameter, can be {@literal null}. + * @throws IllegalArgumentException if there is no value for the requested parameter. + */ + @Nullable + Object getValue(String paramName) throws IllegalArgumentException; + + /** + * Determine the type for the specified named parameter. + * + * @param paramName the name of the parameter. + * @return the type of the specified parameter, or {@link Object#getClass()} if not known. + */ + default Class getType(String paramName) { + return Object.class; + } +} 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 5893c3c2..242dedab 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -109,6 +109,16 @@ public interface DatabaseClient { */ Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy); + /** + * Configures {@link NamedParameterExpander}. + * + * @param namedParameters must not be {@literal null}. + * @return {@code this} {@link Builder}. + * @see NamedParameterExpander#enabled() + * @see NamedParameterExpander#disabled() + */ + Builder namedParameters(NamedParameterExpander namedParameters); + /** * Configures a {@link Consumer} to configure this builder. * @@ -124,7 +134,12 @@ public interface DatabaseClient { } /** - * Contract for specifying a SQL call along with options leading to the exchange. + * Contract for specifying a SQL call along with options leading to the exchange. The SQL string can contain either + * native parameter bind markers (e.g. {@literal $1, $2} for Postgres, {@literal @P0, @P1} for SQL Server) or named + * parameters (e.g. {@literal :foo, :bar}) when {@link NamedParameterExpander} is enabled. + * + * @see NamedParameterExpander + * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) */ interface SqlSpec { 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 34c8e825..2b56479d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -66,9 +66,6 @@ import org.springframework.util.Assert; */ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { - /** - * Logger available to subclasses - */ private final Log logger = LogFactory.getLog(getClass()); private final ConnectionFactory connector; @@ -77,14 +74,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final ReactiveDataAccessStrategy dataAccessStrategy; + private final NamedParameterExpander namedParameters; + private final DefaultDatabaseClientBuilder builder; DefaultDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { + ReactiveDataAccessStrategy dataAccessStrategy, NamedParameterExpander namedParameters, + DefaultDatabaseClientBuilder builder) { this.connector = connector; this.exceptionTranslator = exceptionTranslator; this.dataAccessStrategy = dataAccessStrategy; + this.namedParameters = namedParameters; this.builder = builder; } @@ -325,8 +326,21 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { logger.debug("Executing SQL statement [" + sql + "]"); } - Statement statement = it.createStatement(sql); - doBind(statement, byName, byIndex); + BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(), + new MapBindParameterSource(byName)); + + Statement statement = it.createStatement(operation.toQuery()); + + byName.forEach((name, o) -> { + + if (o.getValue() != null) { + operation.bind(statement, name, o.getValue()); + } else { + operation.bindNull(statement, name, o.getType()); + } + }); + + doBind(statement, Collections.emptyMap(), byIndex); return statement; }; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 5d1beab9..b45df52c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -38,6 +38,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { private @Nullable ConnectionFactory connectionFactory; private @Nullable R2dbcExceptionTranslator exceptionTranslator; private ReactiveDataAccessStrategy accessStrategy; + private NamedParameterExpander namedParameters; DefaultDatabaseClientBuilder() {} @@ -48,8 +49,13 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { this.connectionFactory = other.connectionFactory; this.exceptionTranslator = other.exceptionTranslator; this.accessStrategy = other.accessStrategy; + this.namedParameters = other.namedParameters; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#connectionFactory(io.r2dbc.spi.ConnectionFactory) + */ @Override public Builder connectionFactory(ConnectionFactory factory) { @@ -59,6 +65,10 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#exceptionTranslator(org.springframework.data.r2dbc.support.R2dbcExceptionTranslator) + */ @Override public Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator) { @@ -68,6 +78,10 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#dataAccessStrategy(org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy) + */ @Override public Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy) { @@ -77,6 +91,23 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#namedParameters(org.springframework.data.r2dbc.function.NamedParameterExpander) + */ + @Override + public Builder namedParameters(NamedParameterExpander namedParameters) { + + Assert.notNull(namedParameters, "NamedParameterExpander must not be null!"); + + this.namedParameters = namedParameters; + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#build() + */ @Override public DatabaseClient build() { @@ -97,19 +128,35 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { accessStrategy = new DefaultReactiveDataAccessStrategy(dialect); } - return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, new DefaultDatabaseClientBuilder(this)); + NamedParameterExpander namedParameters = this.namedParameters; + + if (namedParameters == null) { + namedParameters = NamedParameterExpander.enabled(); + } + + return doBuild(this.connectionFactory, exceptionTranslator, accessStrategy, namedParameters, + new DefaultDatabaseClientBuilder(this)); } protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) { - return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, builder); + ReactiveDataAccessStrategy accessStrategy, NamedParameterExpander namedParameters, + DefaultDatabaseClientBuilder builder) { + return new DefaultDatabaseClient(connector, exceptionTranslator, accessStrategy, namedParameters, builder); } + /* + * (non-Javadoc) + * @see java.lang.Object#clone() + */ @Override public DatabaseClient.Builder clone() { return new DefaultDatabaseClientBuilder(this); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#apply(java.util.function.Consumer) + */ @Override public DatabaseClient.Builder apply(Consumer builderConsumer) { Assert.notNull(builderConsumer, "BuilderConsumer must not be null"); 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 72059aae..6a56cea2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -41,6 +41,7 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.ArrayColumns; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.dialect.LimitClause; import org.springframework.data.r2dbc.dialect.LimitClause.Position; @@ -238,6 +239,15 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return getRequiredPersistentEntity(type).getTableName(); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getBindMarkersFactory() + */ + @Override + public BindMarkersFactory getBindMarkersFactory() { + return dialect.getBindMarkersFactory(); + } + private RelationalPersistentEntity getRequiredPersistentEntity(Class typeToRead) { return mappingContext.getRequiredPersistentEntity(typeToRead); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java index 1edd16d1..99f93129 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java @@ -39,8 +39,9 @@ import org.springframework.transaction.NoTransactionException; class DefaultTransactionalDatabaseClient extends DefaultDatabaseClient implements TransactionalDatabaseClient { DefaultTransactionalDatabaseClient(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy dataAccessStrategy, DefaultDatabaseClientBuilder builder) { - super(connector, exceptionTranslator, dataAccessStrategy, builder); + ReactiveDataAccessStrategy dataAccessStrategy, NamedParameterExpander namedParameters, + DefaultDatabaseClientBuilder builder) { + super(connector, exceptionTranslator, dataAccessStrategy, namedParameters, builder); } @Override diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java index c25e2c82..b9ab26a5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java @@ -69,6 +69,15 @@ class DefaultTransactionalDatabaseClientBuilder extends DefaultDatabaseClientBui return this; } + /* (non-Javadoc) + * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#dataAccessStrategy(org.springframework.data.r2dbc.function.NamedParameterSupport) + */ + @Override + public TransactionalDatabaseClient.Builder namedParameters(NamedParameterExpander namedParameters) { + super.namedParameters(namedParameters); + return this; + } + /* (non-Javadoc) * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#apply(java.util.function.Consumer) */ @@ -86,12 +95,11 @@ class DefaultTransactionalDatabaseClientBuilder extends DefaultDatabaseClientBui return (TransactionalDatabaseClient) super.build(); } - /* (non-Javadoc) - * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#doBuild(io.r2dbc.spi.ConnectionFactory, org.springframework.data.r2dbc.support.R2dbcExceptionTranslator, org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy, org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder) - */ @Override protected DatabaseClient doBuild(ConnectionFactory connector, R2dbcExceptionTranslator exceptionTranslator, - ReactiveDataAccessStrategy accessStrategy, DefaultDatabaseClientBuilder builder) { - return new DefaultTransactionalDatabaseClient(connector, exceptionTranslator, accessStrategy, builder); + ReactiveDataAccessStrategy accessStrategy, NamedParameterExpander namedParameters, + DefaultDatabaseClientBuilder builder) { + return new DefaultTransactionalDatabaseClient(connector, exceptionTranslator, accessStrategy, namedParameters, + builder); } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java new file mode 100644 index 00000000..09de5d88 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java @@ -0,0 +1,114 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.util.Assert; + +/** + * {@link BindParameterSource} implementation that holds a given {@link Map} of parameters encapsulated as + * {@link SettableValue}. + *

+ * This class is intended for passing in a simple Map of parameter values to the methods of the + * {@link NamedParameterExpander} class. + * + * @author Mark Paluch + */ +class MapBindParameterSource implements BindParameterSource { + + private final Map values; + + /** + * Creates a new empty {@link MapBindParameterSource}. + */ + MapBindParameterSource() { + this(new LinkedHashMap<>()); + } + + /** + * Creates a new {@link MapBindParameterSource} given {@link Map} of {@link SettableValue}. + * + * @param values the parameter mapping. + */ + MapBindParameterSource(Map values) { + + Assert.notNull(values, "Values must not be null"); + + this.values = values; + } + + /** + * Add a key-value pair to the {@link MapBindParameterSource}. The value must not be {@literal null}. + * + * @param paramName must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@code this} {@link MapBindParameterSource} + */ + MapBindParameterSource addValue(String paramName, Object value) { + + Assert.notNull(paramName, "Parameter name must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + this.values.put(paramName, new SettableValue(paramName, value, value.getClass())); + return this; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.SqlParameterSource#hasValue(java.lang.String) + */ + @Override + public boolean hasValue(String paramName) { + + Assert.notNull(paramName, "Parameter name must not be null!"); + + return values.containsKey(paramName); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.SqlParameterSource#getType(java.lang.String) + */ + @Override + public Class getType(String paramName) { + + Assert.notNull(paramName, "Parameter name must not be null!"); + + SettableValue settableValue = this.values.get(paramName); + if (settableValue != null) { + return settableValue.getType(); + } + + return Object.class; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.SqlParameterSource#getValue(java.lang.String) + */ + @Override + public Object getValue(String paramName) throws IllegalArgumentException { + + if (!hasValue(paramName)) { + throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); + } + + return this.values.get(paramName).getValue(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java new file mode 100644 index 00000000..372e9364 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java @@ -0,0 +1,160 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.Statement; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; + +/** + * SQL translation support allowing the use of named parameters rather than native placeholders. + *

+ * This class expands SQL from named parameters to native style placeholders at execution time. It also allows for + * expanding a {@link java.util.List} of values to the appropriate number of placeholders. + *

+ * NOTE: An instance of this class is thread-safe once configured. + * + * @author Mark Paluch + */ +public class NamedParameterExpander { + + /** Default maximum number of entries for the SQL cache: 256. */ + public static final int DEFAULT_CACHE_LIMIT = 256; + + private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; + + private final Log logger = LogFactory.getLog(getClass()); + + /** Cache of original SQL String to ParsedSql representation. */ + @SuppressWarnings("serial") private final Map parsedSqlCache = new LinkedHashMap( + DEFAULT_CACHE_LIMIT, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > getCacheLimit(); + } + }; + + private NamedParameterExpander() {} + + /** + * Creates a disabled instance of {@link NamedParameterExpander}. + * + * @return a disabled instance of {@link NamedParameterExpander}. + */ + public static NamedParameterExpander disabled() { + return Disabled.INSTANCE; + } + + /** + * Creates a new enabled instance of {@link NamedParameterExpander}. + * + * @return a new enabled instance of {@link NamedParameterExpander}. + */ + public static NamedParameterExpander enabled() { + return new NamedParameterExpander(); + } + + /** + * Specify the maximum number of entries for the SQL cache. Default is 256. + */ + public void setCacheLimit(int cacheLimit) { + this.cacheLimit = cacheLimit; + } + + /** + * Return the maximum number of entries for the SQL cache. + */ + public int getCacheLimit() { + return this.cacheLimit; + } + + /** + * Obtain a parsed representation of the given SQL statement. + *

+ * The default implementation uses an LRU cache with an upper limit of 256 entries. + * + * @param sql the original SQL statement + * @return a representation of the parsed SQL statement + */ + protected ParsedSql getParsedSql(String sql) { + + if (getCacheLimit() <= 0) { + return NamedParameterUtils.parseSqlStatement(sql); + } + + synchronized (this.parsedSqlCache) { + ParsedSql parsedSql = this.parsedSqlCache.get(sql); + if (parsedSql == null) { + parsedSql = NamedParameterUtils.parseSqlStatement(sql); + this.parsedSqlCache.put(sql, parsedSql); + } + return parsedSql; + } + } + + BindableOperation expand(String sql, BindMarkersFactory bindMarkersFactory, BindParameterSource paramSource) { + + ParsedSql parsedSql = getParsedSql(sql); + + BindableOperation expanded = NamedParameterUtils.substituteNamedParameters(parsedSql, bindMarkersFactory, + paramSource); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("Expanding SQL statement [%s] to [%s]", sql, expanded.toQuery())); + } + + return expanded; + } + + /** + * Disabled named parameter support. + */ + static class Disabled extends NamedParameterExpander { + + private static final Disabled INSTANCE = new Disabled(); + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.NamedParameterSupport#expand(java.lang.String, org.springframework.data.r2dbc.dialect.BindMarkersFactory, org.springframework.data.r2dbc.function.SqlParameterSource) + */ + @Override + BindableOperation expand(String sql, BindMarkersFactory bindMarkersFactory, BindParameterSource paramSource) { + + return new BindableOperation() { + + @Override + public void bind(Statement statement, String identifier, Object value) { + statement.bind(identifier, value); + } + + @Override + public void bindNull(Statement statement, String identifier, Class valueType) { + statement.bindNull(identifier, valueType); + } + + @Override + public String toQuery() { + return sql; + } + }; + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java new file mode 100644 index 00000000..3b15a900 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java @@ -0,0 +1,485 @@ +/* + * Copyright 2002-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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.Statement; +import lombok.Value; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.r2dbc.dialect.BindMarker; +import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +import org.springframework.util.Assert; + +/** + * Helper methods for named parameter parsing. + *

+ * Only intended for internal use within Spring's Data's R2DBC framework. Partially extracted from Spring's JDBC named + * parameter support. + *

+ * This is a subset of Spring Frameworks's {@code org.springframework.jdbc.core.namedparam.NamedParameterUtils}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + * @author Mark Paluch + */ +abstract class NamedParameterUtils { + + /** + * Set of characters that qualify as comment or quotes starting characters. + */ + private static final String[] START_SKIP = new String[] { "'", "\"", "--", "/*" }; + + /** + * Set of characters that at are the corresponding comment or quotes ending characters. + */ + private static final String[] STOP_SKIP = new String[] { "'", "\"", "\n", "*/" }; + + /** + * Set of characters that qualify as parameter separators, indicating that a parameter name in a SQL String has ended. + */ + private static final String PARAMETER_SEPARATORS = "\"':&,;()|=+-*%/\\<>^"; + + /** + * An index with separator flags per character code. Technically only needed between 34 and 124 at this point. + */ + private static final boolean[] separatorIndex = new boolean[128]; + + static { + for (char c : PARAMETER_SEPARATORS.toCharArray()) { + separatorIndex[c] = true; + } + } + + // ------------------------------------------------------------------------- + // Core methods used by NamedParameterSupport. + // ------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. Named parameters are substituted for a + * placeholder. + * + * @param sql the SQL statement + * @return the parsed statement, represented as {@link ParsedSql} instance. + */ + public static ParsedSql parseSqlStatement(String sql) { + + Assert.notNull(sql, "SQL must not be null"); + + Set namedParameters = new HashSet<>(); + String sqlToUse = sql; + List parameterList = new ArrayList<>(); + + char[] statement = sql.toCharArray(); + int namedParameterCount = 0; + int unnamedParameterCount = 0; + int totalParameterCount = 0; + + int escapes = 0; + int i = 0; + while (i < statement.length) { + int skipToPosition = i; + while (i < statement.length) { + skipToPosition = skipCommentsAndQuotes(statement, i); + if (i == skipToPosition) { + break; + } else { + i = skipToPosition; + } + } + if (i >= statement.length) { + break; + } + char c = statement[i]; + if (c == ':' || c == '&') { + int j = i + 1; + if (c == ':' && j < statement.length && statement[j] == ':') { + // Postgres-style "::" casting operator should be skipped + i = i + 2; + continue; + } + String parameter = null; + if (c == ':' && j < statement.length && statement[j] == '{') { + // :{x} style parameter + while (statement[j] != '}') { + j++; + if (j >= statement.length) { + throw new InvalidDataAccessApiUsageException( + "Non-terminated named parameter declaration " + "at position " + i + " in statement: " + sql); + } + if (statement[j] == ':' || statement[j] == '{') { + throw new InvalidDataAccessApiUsageException("Parameter name contains invalid character '" + statement[j] + + "' at position " + i + " in statement: " + sql); + } + } + if (j - i > 2) { + parameter = sql.substring(i + 2, j); + namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); + totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j + 1, parameter); + } + j++; + } else { + while (j < statement.length && !isParameterSeparator(statement[j])) { + j++; + } + if (j - i > 1) { + parameter = sql.substring(i + 1, j); + namedParameterCount = addNewNamedParameter(namedParameters, namedParameterCount, parameter); + totalParameterCount = addNamedParameter(parameterList, totalParameterCount, escapes, i, j, parameter); + } + } + i = j - 1; + } else { + if (c == '\\') { + int j = i + 1; + if (j < statement.length && statement[j] == ':') { + // escaped ":" should be skipped + sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1); + escapes++; + i = i + 2; + continue; + } + } + if (c == '?') { + int j = i + 1; + if (j < statement.length && (statement[j] == '?' || statement[j] == '|' || statement[j] == '&')) { + // Postgres-style "??", "?|", "?&" operator should be skipped + i = i + 2; + continue; + } + unnamedParameterCount++; + totalParameterCount++; + } + } + i++; + } + ParsedSql parsedSql = new ParsedSql(sqlToUse); + for (ParameterHolder ph : parameterList) { + parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex()); + } + parsedSql.setNamedParameterCount(namedParameterCount); + parsedSql.setUnnamedParameterCount(unnamedParameterCount); + parsedSql.setTotalParameterCount(totalParameterCount); + return parsedSql; + } + + private static int addNamedParameter(List parameterList, int totalParameterCount, int escapes, int i, + int j, String parameter) { + + parameterList.add(new ParameterHolder(parameter, i - escapes, j - escapes)); + totalParameterCount++; + return totalParameterCount; + } + + private static int addNewNamedParameter(Set namedParameters, int namedParameterCount, String parameter) { + if (!namedParameters.contains(parameter)) { + namedParameters.add(parameter); + namedParameterCount++; + } + return namedParameterCount; + } + + /** + * Skip over comments and quoted names present in an SQL statement. + * + * @param statement character array containing SQL statement. + * @param position current position of statement. + * @return next position to process after any comments or quotes are skipped. + */ + private static int skipCommentsAndQuotes(char[] statement, int position) { + + for (int i = 0; i < START_SKIP.length; i++) { + if (statement[position] == START_SKIP[i].charAt(0)) { + boolean match = true; + for (int j = 1; j < START_SKIP[i].length(); j++) { + if (statement[position + j] != START_SKIP[i].charAt(j)) { + match = false; + break; + } + } + if (match) { + int offset = START_SKIP[i].length(); + for (int m = position + offset; m < statement.length; m++) { + if (statement[m] == STOP_SKIP[i].charAt(0)) { + boolean endMatch = true; + int endPos = m; + for (int n = 1; n < STOP_SKIP[i].length(); n++) { + if (m + n >= statement.length) { + // last comment not closed properly + return statement.length; + } + if (statement[m + n] != STOP_SKIP[i].charAt(n)) { + endMatch = false; + break; + } + endPos = m + n; + } + if (endMatch) { + // found character sequence ending comment or quote + return endPos + 1; + } + } + } + // character sequence ending comment or quote not found + return statement.length; + } + } + } + return position; + } + + /** + * Parse the SQL statement and locate any placeholders or named parameters. Named parameters are substituted for a + * native placeholder, and any select list is expanded to the required number of placeholders. Select lists may + * contain an array of objects, and in that case the placeholders will be grouped and enclosed with parentheses. This + * allows for the use of "expression lists" in the SQL statement like:
+ *
+ * {@code select id, name, state from table where (name, age) in (('John', 35), ('Ann', 50))} + *

+ * The parameter values passed in are used to determine the number of placeholders to be used for a select list. + * Select lists should be limited to 100 or fewer elements. A larger number of elements is not guaranteed to be + * supported by the database and is strictly vendor-dependent. + * + * @param parsedSql the parsed representation of the SQL statement. + * @param bindMarkersFactory the bind marker factory. + * @param paramSource the source for named parameters. + * @return the expanded query that accepts bind parameters and allows for execution without further translation. + * @see #parseSqlStatement + */ + public static BindableOperation substituteNamedParameters(ParsedSql parsedSql, BindMarkersFactory bindMarkersFactory, + BindParameterSource paramSource) { + + BindMarkerHolder markerHolder = new BindMarkerHolder(bindMarkersFactory.create()); + + String originalSql = parsedSql.getOriginalSql(); + List paramNames = parsedSql.getParameterNames(); + if (paramNames.isEmpty()) { + return new ExpandedQuery(originalSql, markerHolder); + } + + StringBuilder actualSql = new StringBuilder(originalSql.length()); + int lastIndex = 0; + for (int i = 0; i < paramNames.size(); i++) { + String paramName = paramNames.get(i); + int[] indexes = parsedSql.getParameterIndexes(i); + int startIndex = indexes[0]; + int endIndex = indexes[1]; + actualSql.append(originalSql, lastIndex, startIndex); + if (paramSource.hasValue(paramName)) { + Object value = paramSource.getValue(paramName); + if (value instanceof Collection) { + Iterator entryIter = ((Collection) value).iterator(); + int k = 0; + while (entryIter.hasNext()) { + if (k > 0) { + actualSql.append(", "); + } + k++; + Object entryItem = entryIter.next(); + if (entryItem instanceof Object[]) { + Object[] expressionList = (Object[]) entryItem; + actualSql.append('('); + for (int m = 0; m < expressionList.length; m++) { + if (m > 0) { + actualSql.append(", "); + } + actualSql.append(markerHolder.addMarker(paramName)); + } + actualSql.append(')'); + } else { + actualSql.append(markerHolder.addMarker(paramName)); + } + + } + } else { + actualSql.append(markerHolder.addMarker(paramName)); + } + } else { + actualSql.append(markerHolder.addMarker(paramName)); + } + lastIndex = endIndex; + } + actualSql.append(originalSql, lastIndex, originalSql.length()); + + return new ExpandedQuery(actualSql.toString(), markerHolder); + } + + /** + * Determine whether a parameter name ends at the current position, that is, whether the given character qualifies as + * a separator. + */ + private static boolean isParameterSeparator(char c) { + return (c < 128 && separatorIndex[c]) || Character.isWhitespace(c); + } + + // ------------------------------------------------------------------------- + // Convenience methods operating on a plain SQL String + // ------------------------------------------------------------------------- + + /** + * Parse the SQL statement and locate any placeholders or named parameters. Named parameters are substituted for a + * native placeholder and any select list is expanded to the required number of placeholders. + *

+ * + * @param sql the SQL statement. + * @param bindMarkersFactory the bind marker factory. + * @param paramSource the source for named parameters. + * @return the expanded query that accepts bind parameters and allows for execution without further translation. + */ + public static BindableOperation substituteNamedParameters(String sql, BindMarkersFactory bindMarkersFactory, + BindParameterSource paramSource) { + ParsedSql parsedSql = parseSqlStatement(sql); + return substituteNamedParameters(parsedSql, bindMarkersFactory, paramSource); + } + + @Value + private static class ParameterHolder { + + String parameterName; + + int startIndex; + + int endIndex; + } + + /** + * Holder for bind marker progress. + */ + private static class BindMarkerHolder { + + private final BindMarkers bindMarkers; + private final Map> markers = new TreeMap<>(); + + BindMarkerHolder(BindMarkers bindMarkers) { + this.bindMarkers = bindMarkers; + } + + String addMarker(String name) { + + BindMarker bindMarker = bindMarkers.next(name); + markers.computeIfAbsent(name, ignore -> new ArrayList<>()).add(bindMarker); + return bindMarker.getPlaceholder(); + } + } + + /** + * Expanded query that allows binding of parameters using parameter names that were used to expand the query. Binding + * unrolls {@link Collection}s and nested arrays. + */ + private static class ExpandedQuery implements BindableOperation { + + private final String expandedSql; + + private final Map> markers; + + ExpandedQuery(String expandedSql, BindMarkerHolder bindMarkerHolder) { + this.expandedSql = expandedSql; + this.markers = bindMarkerHolder.markers; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public void bind(Statement statement, String identifier, Object value) { + + List bindMarkers = getBindMarkers(identifier); + + if (bindMarkers.size() == 1) { + bindMarkers.get(0).bind(statement, value); + } else { + + Assert.isInstanceOf(Collection.class, value, + () -> String.format("Value [%s] must be an Collection with a size of [%d]", value, bindMarkers.size())); + + Collection collection = (Collection) value; + + Iterator iterator = collection.iterator(); + Iterator markers = bindMarkers.iterator(); + + while (iterator.hasNext()) { + + Object valueToBind = iterator.next(); + + if (valueToBind instanceof Object[]) { + Object[] objects = (Object[]) valueToBind; + for (Object object : objects) { + bind(statement, markers, object); + } + } else { + bind(statement, markers, valueToBind); + } + } + } + } + + private void bind(Statement statement, Iterator markers, Object valueToBind) { + + Assert.isTrue(markers.hasNext(), + () -> String.format( + "No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.", + valueToBind, toQuery())); + + markers.next().bind(statement, valueToBind); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class) + */ + @Override + public void bindNull(Statement statement, String identifier, Class valueType) { + + List bindMarkers = getBindMarkers(identifier); + + if (bindMarkers.size() == 1) { + bindMarkers.get(0).bindNull(statement, valueType); + return; + } + + throw new UnsupportedOperationException("bindNull(…) can bind only singular values"); + } + + private List getBindMarkers(String identifier) { + + List bindMarkers = markers.get(identifier); + + Assert.notNull(bindMarkers, () -> String.format("Parameter name [%s] is unknown. Known parameters names are: %s", + identifier, markers.keySet())); + return bindMarkers; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery() + */ + @Override + public String toQuery() { + return expandedSql; + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java new file mode 100644 index 00000000..706931ea --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import java.util.ArrayList; +import java.util.List; + +/** + * Holds information about a parsed SQL statement. + *

+ * This is a copy of Spring Frameworks's {@code org.springframework.jdbc.core.namedparam.ParsedSql}. + * + * @author Thomas Risberg + * @author Juergen Hoeller + */ +class ParsedSql { + + private String originalSql; + + private List parameterNames = new ArrayList<>(); + + private List parameterIndexes = new ArrayList<>(); + + private int namedParameterCount; + + private int unnamedParameterCount; + + private int totalParameterCount; + + /** + * Create a new instance of the {@link ParsedSql} class. + * + * @param originalSql the SQL statement that is being (or is to be) parsed + */ + ParsedSql(String originalSql) { + this.originalSql = originalSql; + } + + /** + * Return the SQL statement that is being parsed. + */ + String getOriginalSql() { + return this.originalSql; + } + + /** + * Add a named parameter parsed from this SQL statement. + * + * @param parameterName the name of the parameter + * @param startIndex the start index in the original SQL String + * @param endIndex the end index in the original SQL String + */ + void addNamedParameter(String parameterName, int startIndex, int endIndex) { + this.parameterNames.add(parameterName); + this.parameterIndexes.add(new int[] { startIndex, endIndex }); + } + + /** + * Return all of the parameters (bind variables) in the parsed SQL statement. Repeated occurrences of the same + * parameter name are included here. + */ + List getParameterNames() { + return this.parameterNames; + } + + /** + * Return the parameter indexes for the specified parameter. + * + * @param parameterPosition the position of the parameter (as index in the parameter names List) + * @return the start index and end index, combined into a int array of length 2 + */ + int[] getParameterIndexes(int parameterPosition) { + return this.parameterIndexes.get(parameterPosition); + } + + /** + * Set the count of named parameters in the SQL statement. Each parameter name counts once; repeated occurrences do + * not count here. + */ + void setNamedParameterCount(int namedParameterCount) { + this.namedParameterCount = namedParameterCount; + } + + /** + * Return the count of named parameters in the SQL statement. Each parameter name counts once; repeated occurrences do + * not count here. + */ + int getNamedParameterCount() { + return this.namedParameterCount; + } + + /** + * Set the count of all of the unnamed parameters in the SQL statement. + */ + void setUnnamedParameterCount(int unnamedParameterCount) { + this.unnamedParameterCount = unnamedParameterCount; + } + + /** + * Return the count of all of the unnamed parameters in the SQL statement. + */ + int getUnnamedParameterCount() { + return this.unnamedParameterCount; + } + + /** + * Set the total count of all of the parameters in the SQL statement. Repeated occurrences of the same parameter name + * do count here. + */ + void setTotalParameterCount(int totalParameterCount) { + this.totalParameterCount = totalParameterCount; + } + + /** + * Return the total count of all of the parameters in the SQL statement. Repeated occurrences of the same parameter + * name do count here. + */ + int getTotalParameterCount() { + return this.totalParameterCount; + } + + /** + * Exposes the original SQL String. + */ + @Override + public String toString() { + return this.originalSql; + } + +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 8670961b..f715e6e4 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -26,6 +26,7 @@ import java.util.function.BiFunction; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.function.convert.SettableValue; /** @@ -76,6 +77,13 @@ public interface ReactiveDataAccessStrategy { */ String getTableName(Class type); + /** + * Returns the configured {@link BindMarkersFactory} to create native parameter placeholder markers. + * + * @return the configured {@link BindMarkersFactory}. + */ + BindMarkersFactory getBindMarkersFactory(); + // ------------------------------------------------------------------------- // Methods creating SQL operations. // Subject to be moved into a SQL creation DSL. diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java index b596d8d0..266fbb95 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java @@ -40,10 +40,10 @@ import org.springframework.util.Assert; *

  * Flux transactionalFlux = databaseClient.inTransaction(db -> {
  *
- * 	return db.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES($1, $2, $3)") //
- * 			.bind(0, 1) //
- * 			.bind(1, "Walter") //
- * 			.bind(2, "White") //
+ * 	return db.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
+ * 			.bind("id", 1) //
+ * 			.bind("firstname", "Walter") //
+ * 			.bind("lastname", "White") //
  * 			.fetch().rowsUpdated();
  * });
  * 
@@ -54,10 +54,11 @@ import org.springframework.util.Assert; * *
  * Mono mono = databaseClient.beginTransaction()
- * 		.then(databaseClient.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES($1, $2, $3)") //
- * 				.bind(0, 1) //
- * 				.bind(1, "Walter") //
- * 				.bind(2, "White") //
+ * 		.then(databaseClient.execute()
+ * 				.sql("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
+ * 				.bind("id", 1) //
+ * 				.bind("firstname", "Walter") //
+ * 				.bind("lastname", "White") //
  * 				.fetch().rowsUpdated())
  * 		.then(databaseClient.commitTransaction());
  *
@@ -168,7 +169,7 @@ public interface TransactionalDatabaseClient extends DatabaseClient {
 		 * Configures the {@link ConnectionFactory R2DBC connector}.
 		 *
 		 * @param factory must not be {@literal null}.
-		 * @return {@code this} {@link DatabaseClient.Builder}.
+		 * @return {@code this} {@link Builder}.
 		 */
 		Builder connectionFactory(ConnectionFactory factory);
 
@@ -176,7 +177,7 @@ public interface TransactionalDatabaseClient extends DatabaseClient {
 		 * Configures a {@link R2dbcExceptionTranslator}.
 		 *
 		 * @param exceptionTranslator must not be {@literal null}.
-		 * @return {@code this} {@link DatabaseClient.Builder}.
+		 * @return {@code this} {@link Builder}.
 		 */
 		Builder exceptionTranslator(R2dbcExceptionTranslator exceptionTranslator);
 
@@ -184,15 +185,25 @@ public interface TransactionalDatabaseClient extends DatabaseClient {
 		 * Configures a {@link ReactiveDataAccessStrategy}.
 		 *
 		 * @param accessStrategy must not be {@literal null}.
-		 * @return {@code this} {@link DatabaseClient.Builder}.
+		 * @return {@code this} {@link Builder}.
 		 */
 		Builder dataAccessStrategy(ReactiveDataAccessStrategy accessStrategy);
 
+		/**
+		 * Configures {@link NamedParameterExpander}.
+		 *
+		 * @param namedParameters must not be {@literal null}.
+		 * @return {@code this} {@link Builder}.
+		 * @see NamedParameterExpander#enabled()
+		 * @see NamedParameterExpander#disabled()
+		 */
+		Builder namedParameters(NamedParameterExpander namedParameters);
+
 		/**
 		 * Configures a {@link Consumer} to configure this builder.
 		 *
 		 * @param builderConsumer must not be {@literal null}.
-		 * @return {@code this} {@link DatabaseClient.Builder}.
+		 * @return {@code this} {@link Builder}.
 		 */
 		Builder apply(Consumer builderConsumer);
 
diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
index 9a5f3136..8700e4a9 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java
@@ -90,7 +90,9 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
 	/**
 	 * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values.
 	 */
-	protected abstract String getInsertIntoLegosetStatement();
+	protected String getInsertIntoLegosetStatement() {
+		return "INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)";
+	}
 
 	@Test // gh-2
 	public void executeInsert() {
@@ -98,9 +100,9 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
 		DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
 
 		databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
-				.bind(0, 42055) //
-				.bind(1, "SCHAUFELRADBAGGER") //
-				.bindNull(2, Integer.class) //
+				.bind("id", 42055) //
+				.bind("name", "SCHAUFELRADBAGGER") //
+				.bindNull("manual", Integer.class) //
 				.fetch().rowsUpdated() //
 				.as(StepVerifier::create) //
 				.expectNext(1) //
diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
index 52d267d8..fc406a21 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java
@@ -15,25 +15,27 @@
  */
 package org.springframework.data.r2dbc.function;
 
+import static org.assertj.core.api.Assertions.*;
+
 import io.r2dbc.spi.ConnectionFactory;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Hooks;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.ArrayBlockingQueue;
+
+import javax.sql.DataSource;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
 import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.transaction.NoTransactionException;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Hooks;
-import reactor.core.publisher.Mono;
-import reactor.test.StepVerifier;
-
-import javax.sql.DataSource;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.ArrayBlockingQueue;
-
-import static org.assertj.core.api.Assertions.*;
 
 /**
  * Abstract base class for integration tests for {@link TransactionalDatabaseClient}.
@@ -56,8 +58,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 		jdbc = createJdbcTemplate(createDataSource());
 		try {
 			jdbc.execute("DROP TABLE legoset");
-		} catch (DataAccessException e) {
-		}
+		} catch (DataAccessException e) {}
 		jdbc.execute(getCreateTableStatement());
 		jdbc.execute("DELETE FROM legoset");
 	}
@@ -91,7 +92,9 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 	/**
 	 * Get a parameterized {@code INSERT INTO legoset} statement setting id, name, and manual values.
 	 */
-	protected abstract String getInsertIntoLegosetStatement();
+	protected String getInsertIntoLegosetStatement() {
+		return "INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)";
+	}
 
 	/**
 	 * Get a statement that returns the current transactionId.
diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
new file mode 100644
index 00000000..4ab1acca
--- /dev/null
+++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
@@ -0,0 +1,295 @@
+/*
+ * 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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+import io.r2dbc.spi.Statement;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+import org.junit.Test;
+import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
+import org.springframework.data.r2dbc.dialect.PostgresDialect;
+import org.springframework.data.r2dbc.dialect.SqlServerDialect;
+
+/**
+ * Unit tests for {@link NamedParameterUtils}.
+ *
+ * @author Mark Paluch
+ */
+public class NamedParameterUtilsUnitTests {
+
+	private final BindMarkersFactory BIND_MARKERS = PostgresDialect.INSTANCE.getBindMarkersFactory();
+
+	@Test // gh-23
+	public void shouldParseSql() {
+
+		String sql = "xxx :a yyyy :b :c :a zzzzz";
+		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
+		assertThat(psql.getParameterNames()).containsSequence("a", "b", "c", "a");
+		assertThat(psql.getTotalParameterCount()).isEqualTo(4);
+		assertThat(psql.getNamedParameterCount()).isEqualTo(3);
+
+		String sql2 = "xxx &a yyyy ? zzzzz";
+		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
+		assertThat(psql2.getParameterNames().get(0)).isEqualTo("a");
+		assertThat(psql2.getTotalParameterCount()).isEqualTo(2);
+		assertThat(psql2.getNamedParameterCount()).isEqualTo(1);
+
+		String sql3 = "xxx &ä+:ö" + '\t' + ":ü%10 yyyy ? zzzzz";
+		ParsedSql psql3 = NamedParameterUtils.parseSqlStatement(sql3);
+		assertThat(psql3.getParameterNames()).containsSequence("ä", "ö", "ü");
+	}
+
+	@Test // gh-23
+	public void substituteNamedParameters() {
+
+		MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
+		namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c");
+
+		BindableOperation operation = NamedParameterUtils.substituteNamedParameters("xxx :a :b :c",
+				PostgresDialect.INSTANCE.getBindMarkersFactory(), namedParams);
+
+		assertThat(operation.toQuery()).isEqualTo("xxx $1 $2 $3");
+
+		BindableOperation operation2 = NamedParameterUtils.substituteNamedParameters("xxx :a :b :c",
+				SqlServerDialect.INSTANCE.getBindMarkersFactory(), namedParams);
+
+		assertThat(operation2.toQuery()).isEqualTo("xxx @P0_a @P1_b @P2_c");
+	}
+
+	@Test // gh-23
+	public void substituteObjectArray() {
+
+		MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
+		namedParams.addValue("a",
+				Arrays.asList(new Object[] { "Walter", "Heisenberg" }, new Object[] { "Walt Jr.", "Flynn" }));
+
+		BindableOperation operation = NamedParameterUtils.substituteNamedParameters("xxx :a", BIND_MARKERS, namedParams);
+
+		assertThat(operation.toQuery()).isEqualTo("xxx ($1, $2), ($3, $4)");
+	}
+
+	@Test // gh-23
+	public void shouldBindObjectArray() {
+
+		MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
+		namedParams.addValue("a",
+				Arrays.asList(new Object[] { "Walter", "Heisenberg" }, new Object[] { "Walt Jr.", "Flynn" }));
+
+		Statement mockStatement = mock(Statement.class);
+
+		BindableOperation operation = NamedParameterUtils.substituteNamedParameters("xxx :a", BIND_MARKERS, namedParams);
+		operation.bind(mockStatement, "a", namedParams.getValue("a"));
+
+		verify(mockStatement).bind(0, "Walter");
+		verify(mockStatement).bind(1, "Heisenberg");
+		verify(mockStatement).bind(2, "Walt Jr.");
+		verify(mockStatement).bind(3, "Flynn");
+	}
+
+	@Test // gh-23
+	public void parseSqlContainingComments() {
+
+		String sql1 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX\n";
+
+		ParsedSql psql1 = NamedParameterUtils.parseSqlStatement(sql1);
+		assertThat(expand(psql1)).isEqualTo("/*+ HINT */ xxx /* comment ? */ $1 yyyy $2 $3 $4 zzzzz -- :xx XX\n");
+
+		MapBindParameterSource paramMap = new MapBindParameterSource(new HashMap<>());
+		paramMap.addValue("a", "a");
+		paramMap.addValue("b", "b");
+		paramMap.addValue("c", "c");
+
+		String sql2 = "/*+ HINT */ xxx /* comment ? */ :a yyyy :b :c :a zzzzz -- :xx XX";
+		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
+		assertThat(expand(psql2)).isEqualTo("/*+ HINT */ xxx /* comment ? */ $1 yyyy $2 $3 $4 zzzzz -- :xx XX");
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithPostgresCasting() {
+
+		String expectedSql = "select 'first name' from artists where id = $1 and birth_date=$2::timestamp";
+		String sql = "select 'first name' from artists where id = :id and birth_date=:birthDate::timestamp";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+		BindableOperation operation = NamedParameterUtils.substituteNamedParameters(parsedSql, BIND_MARKERS,
+				new MapBindParameterSource());
+
+		assertThat(operation.toQuery()).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithPostgresContainedOperator() {
+
+		String expectedSql = "select 'first name' from artists where info->'stat'->'albums' = ?? $1 and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'";
+		String sql = "select 'first name' from artists where info->'stat'->'albums' = ?? :album and '[\"1\",\"2\",\"3\"]'::jsonb ?? '4'";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+
+		assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1);
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithPostgresAnyArrayStringsExistsOperator() {
+
+		String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]";
+		String sql = "select '[\"3\", \"11\"]'::jsonb ?| '{1,3,11,12,17}'::text[]";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+
+		assertThat(parsedSql.getTotalParameterCount()).isEqualTo(0);
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithPostgresAllArrayStringsExistsOperator() {
+
+		String expectedSql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND $1 = 'Back in Black'";
+		String sql = "select '[\"3\", \"11\"]'::jsonb ?& '{1,3,11,12,17}'::text[] AND :album = 'Back in Black'";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+		assertThat(parsedSql.getTotalParameterCount()).isEqualTo(1);
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithEscapedColon() {
+
+		String expectedSql = "select '0\\:0' as a, foo from bar where baz < DATE($1 23:59:59) and baz = $2";
+		String sql = "select '0\\:0' as a, foo from bar where baz < DATE(:p1 23\\:59\\:59) and baz = :p2";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+		assertThat(parsedSql.getParameterNames()).hasSize(2);
+		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1");
+		assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2");
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithBracketDelimitedParameterNames() {
+
+		String expectedSql = "select foo from bar where baz = b$1$2z";
+		String sql = "select foo from bar where baz = b:{p1}:{p2}z";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+		assertThat(parsedSql.getParameterNames()).hasSize(2);
+		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1");
+		assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2");
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithEmptyBracketsOrBracketsInQuotes() {
+
+		String expectedSql = "select foo from bar where baz = b:{}z";
+		String sql = "select foo from bar where baz = b:{}z";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+
+		assertThat(parsedSql.getParameterNames()).isEmpty();
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+
+		String expectedSql2 = "select foo from bar where baz = 'b:{p1}z'";
+		String sql2 = "select foo from bar where baz = 'b:{p1}z'";
+
+		ParsedSql parsedSql2 = NamedParameterUtils.parseSqlStatement(sql2);
+		assertThat(parsedSql2.getParameterNames()).isEmpty();
+		assertThat(expand(parsedSql2)).isEqualTo(expectedSql2);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithSingleLetterInBrackets() {
+		String expectedSql = "select foo from bar where baz = b$1z";
+		String sql = "select foo from bar where baz = b:{p}z";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
+		assertThat(parsedSql.getParameterNames()).hasSize(1);
+		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p");
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithLogicalAnd() {
+
+		String expectedSql = "xxx & yyyy";
+
+		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(expectedSql);
+
+		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void substituteNamedParametersWithLogicalAnd() {
+
+		String expectedSql = "xxx & yyyy";
+
+		assertThat(expand(expectedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void variableAssignmentOperator() {
+
+		String expectedSql = "x := 1";
+
+		assertThat(expand(expectedSql)).isEqualTo(expectedSql);
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithQuotedSingleQuote() {
+
+		String sql = "SELECT ':foo'':doo', :xxx FROM DUAL";
+
+		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
+
+		assertThat(psql.getTotalParameterCount()).isEqualTo(1);
+		assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx");
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithQuotesAndCommentBefore() {
+
+		String sql = "SELECT /*:doo*/':foo', :xxx FROM DUAL";
+
+		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
+
+		assertThat(psql.getTotalParameterCount()).isEqualTo(1);
+		assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx");
+	}
+
+	@Test // gh-23
+	public void parseSqlStatementWithQuotesAndCommentAfter() {
+
+		String sql2 = "SELECT ':foo'/*:doo*/, :xxx FROM DUAL";
+
+		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
+
+		assertThat(psql2.getTotalParameterCount()).isEqualTo(1);
+		assertThat(psql2.getParameterNames().get(0)).isEqualTo("xxx");
+	}
+
+	private String expand(ParsedSql sql) {
+		return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS, new MapBindParameterSource()).toQuery();
+	}
+
+	private String expand(String sql) {
+		return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS, new MapBindParameterSource()).toQuery();
+	}
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
index d5ec20b9..a2598004 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
@@ -48,11 +48,6 @@ public class PostgresDatabaseClientIntegrationTests extends AbstractDatabaseClie
 		return PostgresTestSupport.CREATE_TABLE_LEGOSET;
 	}
 
-	@Override
-	protected String getInsertIntoLegosetStatement() {
-		return PostgresTestSupport.INSERT_INTO_LEGOSET;
-	}
-
 	@Ignore("Adding RETURNING * lets Postgres report 0 affected rows.")
 	@Override
 	public void insert() {}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java
index cfd88545..039df349 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java
@@ -33,11 +33,6 @@ public class PostgresTransactionalDatabaseClientIntegrationTests
 		return PostgresTestSupport.CREATE_TABLE_LEGOSET;
 	}
 
-	@Override
-	protected String getInsertIntoLegosetStatement() {
-		return PostgresTestSupport.INSERT_INTO_LEGOSET;
-	}
-
 	@Override
 	protected String getCurrentTransactionIdStatement() {
 		return "SELECT txid_current();";
diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java
index 9e77bee0..296fff03 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java
@@ -46,9 +46,4 @@ public class SqlServerDatabaseClientIntegrationTests extends AbstractDatabaseCli
 	protected String getCreateTableStatement() {
 		return SqlServerTestSupport.CREATE_TABLE_LEGOSET;
 	}
-
-	@Override
-	protected String getInsertIntoLegosetStatement() {
-		return SqlServerTestSupport.INSERT_INTO_LEGOSET;
-	}
 }
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
index 290160e8..37ec7fce 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
@@ -88,7 +88,7 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
 		Flux findAsProjection();
 
 		@Override
-		@Query("SELECT * FROM legoset WHERE manual = $1")
+		@Query("SELECT * FROM legoset WHERE manual = :manual")
 		Mono findByManual(int manual);
 	}
 }
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
index 8bea7d50..7a537540 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
@@ -93,7 +93,7 @@ public class SqlServerR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepos
 		Flux findAsProjection();
 
 		@Override
-		@Query("SELECT * FROM legoset WHERE manual = @P0")
+		@Query("SELECT * FROM legoset WHERE manual = :manual")
 		Mono findByManual(int manual);
 	}
 }

From 29f1edce0e42dfe669bf1f71ce5aa06a5cdf4a06 Mon Sep 17 00:00:00 2001
From: Jens Schauder 
Date: Thu, 10 Jan 2019 13:59:36 +0100
Subject: [PATCH 072/665] #23 - Polishing.

Formatting.
Made Tests simpler and stricter by using `containsExactly`.
Added @Test annotation to ignored database specific tests so they actually show up as ignored.

Original pull request: #47.
---
 .../function/NamedParameterExpander.java      | 10 +++++--
 .../NamedParameterUtilsUnitTests.java         | 26 +++++++++----------
 ...ostgresDatabaseClientIntegrationTests.java |  3 +++
 .../r2dbc/testing/PostgresTestSupport.java    |  2 --
 4 files changed, 23 insertions(+), 18 deletions(-)

diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
index 372e9364..69699a85 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
@@ -36,14 +36,18 @@ import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
  */
 public class NamedParameterExpander {
 
-	/** Default maximum number of entries for the SQL cache: 256. */
+	/**
+	 * Default maximum number of entries for the SQL cache: 256.
+	 */
 	public static final int DEFAULT_CACHE_LIMIT = 256;
 
 	private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
 
 	private final Log logger = LogFactory.getLog(getClass());
 
-	/** Cache of original SQL String to ParsedSql representation. */
+	/**
+	 * Cache of original SQL String to ParsedSql representation.
+	 */
 	@SuppressWarnings("serial") private final Map parsedSqlCache = new LinkedHashMap(
 			DEFAULT_CACHE_LIMIT, 0.75f, true) {
 		@Override
@@ -101,8 +105,10 @@ public class NamedParameterExpander {
 		}
 
 		synchronized (this.parsedSqlCache) {
+
 			ParsedSql parsedSql = this.parsedSqlCache.get(sql);
 			if (parsedSql == null) {
+
 				parsedSql = NamedParameterUtils.parseSqlStatement(sql);
 				this.parsedSqlCache.put(sql, parsedSql);
 			}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
index 4ab1acca..543163f3 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
@@ -32,6 +32,7 @@ import org.springframework.data.r2dbc.dialect.SqlServerDialect;
  * Unit tests for {@link NamedParameterUtils}.
  *
  * @author Mark Paluch
+ * @author Jens Schauder
  */
 public class NamedParameterUtilsUnitTests {
 
@@ -42,19 +43,19 @@ public class NamedParameterUtilsUnitTests {
 
 		String sql = "xxx :a yyyy :b :c :a zzzzz";
 		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
-		assertThat(psql.getParameterNames()).containsSequence("a", "b", "c", "a");
+		assertThat(psql.getParameterNames()).containsExactly("a", "b", "c", "a");
 		assertThat(psql.getTotalParameterCount()).isEqualTo(4);
 		assertThat(psql.getNamedParameterCount()).isEqualTo(3);
 
 		String sql2 = "xxx &a yyyy ? zzzzz";
 		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
-		assertThat(psql2.getParameterNames().get(0)).isEqualTo("a");
+		assertThat(psql2.getParameterNames()).containsExactly("a");
 		assertThat(psql2.getTotalParameterCount()).isEqualTo(2);
 		assertThat(psql2.getNamedParameterCount()).isEqualTo(1);
 
 		String sql3 = "xxx &ä+:ö" + '\t' + ":ü%10 yyyy ? zzzzz";
 		ParsedSql psql3 = NamedParameterUtils.parseSqlStatement(sql3);
-		assertThat(psql3.getParameterNames()).containsSequence("ä", "ö", "ü");
+		assertThat(psql3.getParameterNames()).containsExactly("ä", "ö", "ü");
 	}
 
 	@Test // gh-23
@@ -177,9 +178,8 @@ public class NamedParameterUtilsUnitTests {
 		String sql = "select '0\\:0' as a, foo from bar where baz < DATE(:p1 23\\:59\\:59) and baz = :p2";
 
 		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
-		assertThat(parsedSql.getParameterNames()).hasSize(2);
-		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1");
-		assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2");
+
+		assertThat(parsedSql.getParameterNames()).containsExactly("p1", "p2");
 		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
 	}
 
@@ -190,9 +190,7 @@ public class NamedParameterUtilsUnitTests {
 		String sql = "select foo from bar where baz = b:{p1}:{p2}z";
 
 		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
-		assertThat(parsedSql.getParameterNames()).hasSize(2);
-		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p1");
-		assertThat(parsedSql.getParameterNames().get(1)).isEqualTo("p2");
+		assertThat(parsedSql.getParameterNames()).containsExactly("p1", "p2");
 		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
 	}
 
@@ -217,12 +215,12 @@ public class NamedParameterUtilsUnitTests {
 
 	@Test // gh-23
 	public void parseSqlStatementWithSingleLetterInBrackets() {
+
 		String expectedSql = "select foo from bar where baz = b$1z";
 		String sql = "select foo from bar where baz = b:{p}z";
 
 		ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
-		assertThat(parsedSql.getParameterNames()).hasSize(1);
-		assertThat(parsedSql.getParameterNames().get(0)).isEqualTo("p");
+		assertThat(parsedSql.getParameterNames()).containsExactly("p");
 		assertThat(expand(parsedSql)).isEqualTo(expectedSql);
 	}
 
@@ -260,7 +258,7 @@ public class NamedParameterUtilsUnitTests {
 		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
 
 		assertThat(psql.getTotalParameterCount()).isEqualTo(1);
-		assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx");
+		assertThat(psql.getParameterNames()).containsExactly("xxx");
 	}
 
 	@Test // gh-23
@@ -271,7 +269,7 @@ public class NamedParameterUtilsUnitTests {
 		ParsedSql psql = NamedParameterUtils.parseSqlStatement(sql);
 
 		assertThat(psql.getTotalParameterCount()).isEqualTo(1);
-		assertThat(psql.getParameterNames().get(0)).isEqualTo("xxx");
+		assertThat(psql.getParameterNames()).containsExactly("xxx");
 	}
 
 	@Test // gh-23
@@ -282,7 +280,7 @@ public class NamedParameterUtilsUnitTests {
 		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
 
 		assertThat(psql2.getTotalParameterCount()).isEqualTo(1);
-		assertThat(psql2.getParameterNames().get(0)).isEqualTo("xxx");
+		assertThat(psql2.getParameterNames()).containsExactly("xxx");
 	}
 
 	private String expand(ParsedSql sql) {
diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
index a2598004..264e8dba 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java
@@ -21,6 +21,7 @@ import javax.sql.DataSource;
 
 import org.junit.ClassRule;
 import org.junit.Ignore;
+import org.junit.Test;
 import org.springframework.data.r2dbc.testing.ExternalDatabase;
 import org.springframework.data.r2dbc.testing.PostgresTestSupport;
 
@@ -49,10 +50,12 @@ public class PostgresDatabaseClientIntegrationTests extends AbstractDatabaseClie
 	}
 
 	@Ignore("Adding RETURNING * lets Postgres report 0 affected rows.")
+	@Test
 	@Override
 	public void insert() {}
 
 	@Ignore("Adding RETURNING * lets Postgres report 0 affected rows.")
+	@Test
 	@Override
 	public void insertTypedObject() {}
 }
diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java
index 180e9940..834af652 100644
--- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java
+++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java
@@ -35,8 +35,6 @@ public class PostgresTestSupport {
 			+ "    manual      integer NULL\n" //
 			+ ");";
 
-	public static String INSERT_INTO_LEGOSET = "INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)";
-
 	/**
 	 * Returns a database either hosted locally at {@code postgres:@localhost:5432/postgres} or running inside Docker.
 	 *

From 3d1041c0c8be84ad3c1805c7a34ea1c8e891d1a5 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Fri, 11 Jan 2019 10:50:59 +0100
Subject: [PATCH 073/665] #23 - Address review comments.

Original pull request: #47.
---
 .../reference/r2dbc-repositories.adoc         |  2 +-
 src/main/asciidoc/reference/r2dbc.adoc        | 15 ++++++-----
 .../data/r2dbc/function/DatabaseClient.java   |  4 +--
 .../r2dbc/function/DefaultDatabaseClient.java | 25 +++++++++++++------
 .../DefaultDatabaseClientBuilder.java         |  6 ++---
 ...ultTransactionalDatabaseClientBuilder.java |  4 +--
 .../r2dbc/function/NamedParameterUtils.java   | 10 --------
 .../function/TransactionalDatabaseClient.java |  4 +--
 .../NamedParameterUtilsUnitTests.java         |  2 +-
 9 files changed, 37 insertions(+), 35 deletions(-)

diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc
index 1c62b291..3a243aee 100644
--- a/src/main/asciidoc/reference/r2dbc-repositories.adoc
+++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc
@@ -120,4 +120,4 @@ The annotated query uses native bind markers, which are Postgres bind markers in
 
 NOTE: R2DBC repositories do not support query derivation.
 
-NOTE: R2DBC repositories bind parameters to placeholders by index.
+NOTE: R2DBC repositories bind internally parameters to placeholders via `Statement.bind(…)` by index.
diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc
index 8fb038df..55c737ca 100644
--- a/src/main/asciidoc/reference/r2dbc.adoc
+++ b/src/main/asciidoc/reference/r2dbc.adoc
@@ -459,13 +459,16 @@ db.execute()
 
 .R2DBC Native Bind Markers
 ****
-R2DBC uses database-native bind markers that depend on the actual database.
-If you are familiar with JDBC, then you're also familiar with `?` (question mark) bind markers.
-JDBC drivers translate question mark bind markers to database-native markers as part of statement execution.
+R2DBC uses database-native bind markers that depend on the actual database vendor.
+As an example, Postgres uses indexed markers such as `$1`, `$2`, `$n`.
+Another example is SQL Server that uses named bind markers prefixed with `@` (at).
 
-Postgres uses indexed markers (`$1`, `$2`), SQL Server uses named bind markers prefixed with `@` as its native bind marker syntax.
-Spring Data R2DBC leverages `Dialect` implementations to expand named parameters to native bind markers at the time of query execution which gives you a certain degree of query portability across various database vendors.
-You can still use native bind markers if you prefer to do so.
+This is different from JDBC which requires `?` (question mark) as bind markers.
+In JDBC, the actual drivers translate question mark bind markers to database-native markers as part of their statement execution.
+
+Spring Data R2DBC allows you to use native bind markers or named bind markers with the `:name` syntax.
+
+Named parameter support leverages ``Dialect``s  to expand named parameters to native bind markers at the time of query execution which gives you a certain degree of query portability across various database vendors.
 ****
 
 The query-preprocessor unrolls named `Collection` parameters into a series of bind markers to remove the need of dynamic query creation based on the number of arguments.
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 242dedab..fb4f48bc 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java
@@ -112,12 +112,12 @@ public interface DatabaseClient {
 		/**
 		 * Configures {@link NamedParameterExpander}.
 		 *
-		 * @param namedParameters must not be {@literal null}.
+		 * @param expander must not be {@literal null}.
 		 * @return {@code this} {@link Builder}.
 		 * @see NamedParameterExpander#enabled()
 		 * @see NamedParameterExpander#disabled()
 		 */
-		Builder namedParameters(NamedParameterExpander namedParameters);
+		Builder namedParameters(NamedParameterExpander expander);
 
 		/**
 		 * Configures a {@link Consumer} to configure this builder.
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 2b56479d..27f3c250 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -254,14 +254,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 	private static void doBind(Statement statement, Map byName,
 			Map byIndex) {
 
-		byIndex.forEach((i, o) -> {
+		bindByIndex(statement, byIndex);
+		bindByName(statement, byName);
+	}
 
-			if (o.getValue() != null) {
-				statement.bind(i.intValue(), o.getValue());
-			} else {
-				statement.bindNull(i.intValue(), o.getType());
-			}
-		});
+	private static void bindByName(Statement statement, Map byName) {
 
 		byName.forEach((name, o) -> {
 
@@ -273,6 +270,18 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		});
 	}
 
+	private static void bindByIndex(Statement statement, Map byIndex) {
+
+		byIndex.forEach((i, o) -> {
+
+			if (o.getValue() != null) {
+				statement.bind(i.intValue(), o.getValue());
+			} else {
+				statement.bindNull(i.intValue(), o.getType());
+			}
+		});
+	}
+
 	/**
 	 * Default {@link DatabaseClient.SqlSpec} implementation.
 	 */
@@ -340,7 +349,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 					}
 				});
 
-				doBind(statement, Collections.emptyMap(), byIndex);
+				bindByIndex(statement, byIndex);
 
 				return statement;
 			};
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
index b45df52c..224771d4 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java
@@ -96,11 +96,11 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
 	 * @see org.springframework.data.r2dbc.function.DatabaseClient.Builder#namedParameters(org.springframework.data.r2dbc.function.NamedParameterExpander)
 	 */
 	@Override
-	public Builder namedParameters(NamedParameterExpander namedParameters) {
+	public Builder namedParameters(NamedParameterExpander expander) {
 
-		Assert.notNull(namedParameters, "NamedParameterExpander must not be null!");
+		Assert.notNull(expander, "NamedParameterExpander must not be null!");
 
-		this.namedParameters = namedParameters;
+		this.namedParameters = expander;
 		return this;
 	}
 
diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java
index b9ab26a5..3c827e4a 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java
@@ -73,8 +73,8 @@ class DefaultTransactionalDatabaseClientBuilder extends DefaultDatabaseClientBui
 	 * @see org.springframework.data.r2dbc.function.DefaultDatabaseClientBuilder#dataAccessStrategy(org.springframework.data.r2dbc.function.NamedParameterSupport)
 	 */
 	@Override
-	public TransactionalDatabaseClient.Builder namedParameters(NamedParameterExpander namedParameters) {
-		super.namedParameters(namedParameters);
+	public TransactionalDatabaseClient.Builder namedParameters(NamedParameterExpander expander) {
+		super.namedParameters(expander);
 		return this;
 	}
 
diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
index 3b15a900..07513339 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
@@ -162,16 +162,6 @@ abstract class NamedParameterUtils {
 						continue;
 					}
 				}
-				if (c == '?') {
-					int j = i + 1;
-					if (j < statement.length && (statement[j] == '?' || statement[j] == '|' || statement[j] == '&')) {
-						// Postgres-style "??", "?|", "?&" operator should be skipped
-						i = i + 2;
-						continue;
-					}
-					unnamedParameterCount++;
-					totalParameterCount++;
-				}
 			}
 			i++;
 		}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java
index 266fbb95..3acc780c 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java
@@ -192,12 +192,12 @@ public interface TransactionalDatabaseClient extends DatabaseClient {
 		/**
 		 * Configures {@link NamedParameterExpander}.
 		 *
-		 * @param namedParameters must not be {@literal null}.
+		 * @param expander must not be {@literal null}.
 		 * @return {@code this} {@link Builder}.
 		 * @see NamedParameterExpander#enabled()
 		 * @see NamedParameterExpander#disabled()
 		 */
-		Builder namedParameters(NamedParameterExpander namedParameters);
+		Builder namedParameters(NamedParameterExpander expander);
 
 		/**
 		 * Configures a {@link Consumer} to configure this builder.
diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
index 543163f3..89176550 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
@@ -50,7 +50,7 @@ public class NamedParameterUtilsUnitTests {
 		String sql2 = "xxx &a yyyy ? zzzzz";
 		ParsedSql psql2 = NamedParameterUtils.parseSqlStatement(sql2);
 		assertThat(psql2.getParameterNames()).containsExactly("a");
-		assertThat(psql2.getTotalParameterCount()).isEqualTo(2);
+		assertThat(psql2.getTotalParameterCount()).isEqualTo(1);
 		assertThat(psql2.getNamedParameterCount()).isEqualTo(1);
 
 		String sql3 = "xxx &ä+:ö" + '\t' + ":ü%10 yyyy ? zzzzz";

From 3a5278ce87ac5cfd42651285a05dc853196c16e4 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Mon, 21 Jan 2019 08:55:10 +0100
Subject: [PATCH 074/665] #52 - Switch mssql-jdbc driver dependency to test
 scope.

We use JDBC drivers only during testing so converting mssql-jdbc to a test-only dependency.
---
 pom.xml | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 665d56cc..2c747325 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,6 @@
 
-
+
 
 	4.0.0
 
@@ -242,6 +243,7 @@
 			com.microsoft.sqlserver
 			mssql-jdbc
 			${mssql-jdbc.version}
+			test
 		
 
 		

From 432b885853dd397372bb15194a5d65431a85baba Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Fri, 1 Feb 2019 09:43:51 +0100
Subject: [PATCH 075/665] #54 - Build against R2DBC 1.0 snapshots.

---
 pom.xml                                       |  9 +++--
 .../data/r2dbc/dialect/BindMarker.java        |  4 +-
 .../data/r2dbc/dialect/Dialect.java           | 11 ------
 .../data/r2dbc/dialect/H2Dialect.java         |  9 -----
 .../r2dbc/dialect/IndexedBindMarkers.java     |  4 +-
 .../data/r2dbc/dialect/NamedBindMarkers.java  |  4 +-
 .../data/r2dbc/dialect/PostgresDialect.java   |  9 -----
 .../data/r2dbc/dialect/SqlServerDialect.java  |  9 -----
 .../data/r2dbc/function/BindIdOperation.java  |  4 +-
 .../r2dbc/function/BindableOperation.java     |  6 +--
 .../r2dbc/function/DefaultDatabaseClient.java | 18 ++++-----
 .../DefaultReactiveDataAccessStrategy.java    | 38 ++++++++-----------
 .../function/NamedParameterExpander.java      |  4 +-
 .../r2dbc/function/NamedParameterUtils.java   | 19 +++++-----
 .../repository/support/BindSpecAdapter.java   |  2 +-
 .../support/SimpleR2dbcRepository.java        |  2 +-
 .../dialect/IndexedBindMarkersUnitTests.java  |  6 +--
 .../dialect/NamedBindMarkersUnitTests.java    |  4 +-
 ...ltReactiveDataAccessStrategyUnitTests.java |  6 +--
 .../NamedParameterUtilsUnitTests.java         |  2 +-
 .../function/PostgresIntegrationTests.java    |  2 +
 21 files changed, 66 insertions(+), 106 deletions(-)

diff --git a/pom.xml b/pom.xml
index 2c747325..c46dfc45 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,10 +31,11 @@
 		2.4.1
 		42.2.5
 		7.1.2.jre8-preview
-		1.0.0.M6
-		1.0.0.M6
-		1.0.0.M6
-		1.0.0.M6
+		1.0.0.BUILD-SNAPSHOT
+		${r2dbc.version}
+		${r2dbc.version}
+		${r2dbc.version}
+		${r2dbc.version}
 		1.0.1
 		1.10.1
 
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
index 415a6281..fa08a290 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java
@@ -28,7 +28,7 @@ public interface BindMarker {
 	 *          {@literal null} values.
 	 * @see Statement#bind
 	 */
-	void bind(Statement statement, Object value);
+	void bind(Statement statement, Object value);
 
 	/**
 	 * Bind a {@literal null} value to the {@link Statement} using the underlying binding strategy.
@@ -37,5 +37,5 @@ public interface BindMarker {
 	 * @param valueType value type, must not be {@literal null}.
 	 * @see Statement#bindNull
 	 */
-	void bindNull(Statement statement, Class valueType);
+	void bindNull(Statement statement, Class valueType);
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java
index e909f722..be1d493f 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java
@@ -22,17 +22,6 @@ public interface Dialect {
 	 */
 	BindMarkersFactory getBindMarkersFactory();
 
-	/**
-	 * Returns the clause to include for returning generated keys. The returned query is directly appended to
-	 * {@code INSERT} statements.
-	 *
-	 * @return the clause to include for returning generated keys.
-	 * @deprecated to be removed after upgrading to R2DBC 1.0M7 in favor of using the driver's direct support for
-	 *             retrieving generated keys.
-	 */
-	@Deprecated
-	String generatedKeysClause();
-
 	/**
 	 * Return a collection of types that are natively supported by this database/driver. Defaults to
 	 * {@link Collections#emptySet()}.
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
index 382dc3cc..0ce3f083 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/H2Dialect.java
@@ -11,13 +11,4 @@ public class H2Dialect extends PostgresDialect {
 	 * Singleton instance.
 	 */
 	public static final H2Dialect INSTANCE = new H2Dialect();
-
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
-	 */
-	@Override
-	public String generatedKeysClause() {
-		return "";
-	}
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
index 5389675b..54b5e750 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java
@@ -74,7 +74,7 @@ class IndexedBindMarkers implements BindMarkers {
 		 * @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, Object value) {
+		public void bind(Statement statement, Object value) {
 			statement.bind(this.index, value);
 		}
 
@@ -83,7 +83,7 @@ class IndexedBindMarkers implements BindMarkers {
 		 * @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, Class valueType) {
+		public void bindNull(Statement statement, Class valueType) {
 			statement.bindNull(this.index, valueType);
 		}
 	}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
index 7b94767a..80bf779b 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java
@@ -101,7 +101,7 @@ class NamedBindMarkers implements BindMarkers {
 		 * @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, Object value) {
+		public void bind(Statement statement, Object value) {
 			statement.bind(this.identifier, value);
 		}
 
@@ -110,7 +110,7 @@ class NamedBindMarkers implements BindMarkers {
 		 * @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, Class valueType) {
+		public void bindNull(Statement statement, Class valueType) {
 			statement.bindNull(this.identifier, valueType);
 		}
 	}
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java
index d0b1bf0b..9ccd41b0 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java
@@ -73,15 +73,6 @@ public class PostgresDialect implements Dialect {
 		return INDEXED;
 	}
 
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
-	 */
-	@Override
-	public String generatedKeysClause() {
-		return "RETURNING *";
-	}
-
 	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys()
diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java
index bf7199e0..d1b0a9ae 100644
--- a/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java
+++ b/src/main/java/org/springframework/data/r2dbc/dialect/SqlServerDialect.java
@@ -62,15 +62,6 @@ public class SqlServerDialect implements Dialect {
 		return NAMED;
 	}
 
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.dialect.Dialect#returnGeneratedKeys()
-	 */
-	@Override
-	public String generatedKeysClause() {
-		return "select SCOPE_IDENTITY() AS GENERATED_KEYS";
-	}
-
 	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys()
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java
index 08c9361c..71f437ca 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java
@@ -19,7 +19,7 @@ public interface BindIdOperation extends BindableOperation {
 	 * @param value the actual value. Must not be {@literal null}.
 	 * @see Statement#bind
 	 */
-	void bindId(Statement statement, Object value);
+	void bindId(Statement statement, Object value);
 
 	/**
 	 * Bind the given {@code values} to the {@link Statement} using the underlying binding strategy.
@@ -28,5 +28,5 @@ public interface BindIdOperation extends BindableOperation {
 	 * @param values the actual values.
 	 * @see Statement#bind
 	 */
-	void bindIds(Statement statement, Iterable values);
+	void bindIds(Statement statement, Iterable values);
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
index edccfcd6..29c3c0b1 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
@@ -24,7 +24,7 @@ public interface BindableOperation extends QueryOperation {
 	 *          {@literal null} values.
 	 * @see Statement#bind
 	 */
-	void bind(Statement statement, String identifier, Object value);
+	void bind(Statement statement, String identifier, Object value);
 
 	/**
 	 * Bind a {@literal null} value to the {@link Statement} using the underlying binding strategy.
@@ -34,7 +34,7 @@ public interface BindableOperation extends QueryOperation {
 	 * @param valueType value type, must not be {@literal null}.
 	 * @see Statement#bindNull
 	 */
-	void bindNull(Statement statement, String identifier, Class valueType);
+	void bindNull(Statement statement, String identifier, Class valueType);
 
 	/**
 	 * Bind a {@link SettableValue} to the {@link Statement} using the underlying binding strategy. Binds either the
@@ -45,7 +45,7 @@ public interface BindableOperation extends QueryOperation {
 	 * @see Statement#bind
 	 * @see Statement#bindNull
 	 */
-	default void bind(Statement statement, SettableValue value) {
+	default void bind(Statement statement, SettableValue value) {
 
 		if (value.getValue() == null) {
 			bindNull(statement, value.getIdentifier().toString(), value.getType());
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 27f3c250..b8587e06 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -251,14 +251,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		return new DefaultGenericExecuteSpec(sqlSupplier);
 	}
 
-	private static void doBind(Statement statement, Map byName,
+	private static void doBind(Statement statement, Map byName,
 			Map byIndex) {
 
 		bindByIndex(statement, byIndex);
 		bindByName(statement, byName);
 	}
 
-	private static void bindByName(Statement statement, Map byName) {
+	private static void bindByName(Statement statement, Map byName) {
 
 		byName.forEach((name, o) -> {
 
@@ -270,7 +270,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		});
 	}
 
-	private static void bindByIndex(Statement statement, Map byIndex) {
+	private static void bindByIndex(Statement statement, Map byIndex) {
 
 		byIndex.forEach((i, o) -> {
 
@@ -329,7 +329,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 		 FetchSpec exchange(String sql, BiFunction mappingFunction) {
 
-			Function> executeFunction = it -> {
+			Function executeFunction = it -> {
 
 				if (logger.isDebugEnabled()) {
 					logger.debug("Executing SQL statement [" + sql + "]");
@@ -338,7 +338,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 				BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(),
 						new MapBindParameterSource(byName));
 
-				Statement statement = it.createStatement(operation.toQuery());
+				Statement statement = it.createStatement(operation.toQuery());
 
 				byName.forEach((name, o) -> {
 
@@ -632,7 +632,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 		 FetchSpec execute(String sql, BiFunction mappingFunction) {
 
-			Function> selectFunction = it -> {
+			Function selectFunction = it -> {
 
 				if (logger.isDebugEnabled()) {
 					logger.debug("Executing SQL statement [" + sql + "]");
@@ -886,13 +886,13 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 			BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, byName.keySet());
 
 			String sql = bindableInsert.toQuery();
-			Function> insertFunction = it -> {
+			Function insertFunction = it -> {
 
 				if (logger.isDebugEnabled()) {
 					logger.debug("Executing SQL statement [" + sql + "]");
 				}
 
-				Statement statement = it.createStatement(sql);
+				Statement statement = it.createStatement(sql).returnGeneratedValues();
 
 				byName.forEach((k, v) -> bindableInsert.bind(statement, v));
 
@@ -1015,7 +1015,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 					logger.debug("Executing SQL statement [" + sql + "]");
 				}
 
-				Statement statement = it.createStatement(sql);
+				Statement statement = it.createStatement(sql).returnGeneratedValues();
 
 				for (SettableValue settable : insertValues) {
 					bindableInsert.bind(statement, settable);
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 6a56cea2..54740652 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -304,8 +304,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	 */
 	@Override
 	public BindableOperation insertAndReturnGeneratedKeys(String table, Set columns) {
-		return new DefaultBindableInsert(dialect.getBindMarkersFactory().create(), table, columns,
-				dialect.generatedKeysClause());
+		return new DefaultBindableInsert(dialect.getBindMarkersFactory().create(), table, columns);
 	}
 
 	/*
@@ -442,8 +441,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		private final Map markers = new LinkedHashMap<>();
 		private final String query;
 
-		DefaultBindableInsert(BindMarkers bindMarkers, String table, Collection columns,
-				String returningStatement) {
+		DefaultBindableInsert(BindMarkers bindMarkers, String table, Collection columns) {
 
 			StringBuilder builder = new StringBuilder();
 			List placeholders = new ArrayList<>(columns.size());
@@ -459,10 +457,6 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 			builder.append("INSERT INTO ").append(table).append(" (").append(columnsString).append(")").append(" VALUES(")
 					.append(placeholdersString).append(")");
 
-			if (StringUtils.hasText(returningStatement)) {
-				builder.append(' ').append(returningStatement);
-			}
-
 			this.query = builder.toString();
 		}
 
@@ -471,7 +465,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, String identifier, Object value) {
+		public void bind(Statement statement, String identifier, Object value) {
 			markers.get(identifier).bind(statement, value);
 		}
 
@@ -480,7 +474,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, String identifier, Class valueType) {
+		public void bindNull(Statement statement, String identifier, Class valueType) {
 			markers.get(identifier).bindNull(statement, valueType);
 		}
 
@@ -529,7 +523,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, String identifier, Object value) {
+		public void bind(Statement statement, String identifier, Object value) {
 			markers.get(identifier).bind(statement, value);
 		}
 
@@ -538,7 +532,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, String identifier, Class valueType) {
+		public void bindNull(Statement statement, String identifier, Class valueType) {
 			markers.get(identifier).bindNull(statement, valueType);
 		}
 
@@ -547,7 +541,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
 		 */
 		@Override
-		public void bindId(Statement statement, Object value) {
+		public void bindId(Statement statement, Object value) {
 			idMarker.bind(statement, value);
 		}
 
@@ -556,7 +550,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -590,7 +584,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, String identifier, Object value) {
+		public void bind(Statement statement, String identifier, Object value) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -599,7 +593,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, String identifier, Class valueType) {
+		public void bindNull(Statement statement, String identifier, Class valueType) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -608,7 +602,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
 		 */
 		@Override
-		public void bindId(Statement statement, Object value) {
+		public void bindId(Statement statement, Object value) {
 			idMarker.bind(statement, value);
 		}
 
@@ -617,7 +611,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -654,7 +648,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object)
 		 */
 		@Override
-		public void bind(Statement statement, String identifier, Object value) {
+		public void bind(Statement statement, String identifier, Object value) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -663,7 +657,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, String identifier, Class valueType) {
+		public void bindNull(Statement statement, String identifier, Class valueType) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -672,7 +666,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object)
 		 */
 		@Override
-		public void bindId(Statement statement, Object value) {
+		public void bindId(Statement statement, Object value) {
 
 			BindMarker bindMarker = bindMarkers.next();
 			markers.add(bindMarker.getPlaceholder());
@@ -684,7 +678,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 
 			for (Object value : values) {
 				bindId(statement, value);
diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
index 69699a85..c904b123 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java
@@ -147,12 +147,12 @@ public class NamedParameterExpander {
 			return new BindableOperation() {
 
 				@Override
-				public void bind(Statement statement, String identifier, Object value) {
+				public void bind(Statement statement, String identifier, Object value) {
 					statement.bind(identifier, value);
 				}
 
 				@Override
-				public void bindNull(Statement statement, String identifier, Class valueType) {
+				public void bindNull(Statement statement, String identifier, Class valueType) {
 					statement.bindNull(identifier, valueType);
 				}
 
diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
index 07513339..c012ed93 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java
@@ -395,10 +395,16 @@ abstract class NamedParameterUtils {
 		 */
 		@Override
 		@SuppressWarnings("unchecked")
-		public void bind(Statement statement, String identifier, Object value) {
+		public void bind(Statement statement, String identifier, Object value) {
 
 			List bindMarkers = getBindMarkers(identifier);
 
+			if (bindMarkers == null) {
+
+				statement.bind(identifier, value);
+				return;
+			}
+
 			if (bindMarkers.size() == 1) {
 				bindMarkers.get(0).bind(statement, value);
 			} else {
@@ -427,7 +433,7 @@ abstract class NamedParameterUtils {
 			}
 		}
 
-		private void bind(Statement statement, Iterator markers, Object valueToBind) {
+		private void bind(Statement statement, Iterator markers, Object valueToBind) {
 
 			Assert.isTrue(markers.hasNext(),
 					() -> String.format(
@@ -442,7 +448,7 @@ abstract class NamedParameterUtils {
 		 * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class)
 		 */
 		@Override
-		public void bindNull(Statement statement, String identifier, Class valueType) {
+		public void bindNull(Statement statement, String identifier, Class valueType) {
 
 			List bindMarkers = getBindMarkers(identifier);
 
@@ -455,12 +461,7 @@ abstract class NamedParameterUtils {
 		}
 
 		private List getBindMarkers(String identifier) {
-
-			List bindMarkers = markers.get(identifier);
-
-			Assert.notNull(bindMarkers, () -> String.format("Parameter name [%s] is unknown. Known parameters names are: %s",
-					identifier, markers.keySet()));
-			return bindMarkers;
+			return markers.get(identifier);
 		}
 
 		/*
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java
index 14406434..6b2cd6b0 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java
@@ -13,7 +13,7 @@ import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec;
  * @param  type of the bind specification.
  * @author Mark Paluch
  */
-class BindSpecAdapter> implements Statement> {
+class BindSpecAdapter> implements Statement {
 
 	private S bindSpec;
 
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index bee011ce..24558f89 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -323,7 +323,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository bind(BindableOperation operation, Statement statement) {
+	private BiConsumer bind(BindableOperation operation, Statement statement) {
 
 		return (k, v) -> operation.bind(statement, v);
 	}
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
index fc2a31a4..94e7f5d3 100644
--- a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java
@@ -29,7 +29,7 @@ public class IndexedBindMarkersUnitTests {
 	@Test // gh-15
 	public void shouldCreateNewBindMarkersWithOffset() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 
 		BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 1).create();
 
@@ -65,7 +65,7 @@ public class IndexedBindMarkersUnitTests {
 	@Test // gh-15
 	public void bindValueShouldBindByIndex() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 
 		BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create();
 
@@ -79,7 +79,7 @@ public class IndexedBindMarkersUnitTests {
 	@Test // gh-15
 	public void bindNullShouldBindByIndex() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 
 		BindMarkers bindMarkers = BindMarkersFactory.indexed("$", 0).create();
 
diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
index a267ba5f..5febb2e6 100644
--- a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java
@@ -85,7 +85,7 @@ public class NamedBindMarkersUnitTests {
 	@Test // gh-15
 	public void bindValueShouldBindByName() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 
 		BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32).create();
 
@@ -99,7 +99,7 @@ public class NamedBindMarkersUnitTests {
 	@Test // gh-15
 	public void bindNullShouldBindByName() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 
 		BindMarkers bindMarkers = BindMarkersFactory.named("@", "p", 32).create();
 
diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
index 120d6d4e..01e1b162 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
@@ -30,7 +30,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests {
 		BindableOperation operation = strategy.insertAndReturnGeneratedKeys("table",
 				new HashSet<>(Arrays.asList("firstname", "lastname")));
 
-		assertThat(operation.toQuery()).isEqualTo("INSERT INTO table (firstname, lastname) VALUES($1, $2) RETURNING *");
+		assertThat(operation.toQuery()).isEqualTo("INSERT INTO table (firstname, lastname) VALUES($1, $2)");
 	}
 
 	@Test // gh-20
@@ -73,7 +73,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests {
 	@Test // gh-20
 	public void shouldRenderSelectByIdInQuery() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 		BindIdOperation operation = strategy.selectByIdIn("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
 				"id");
 
@@ -95,7 +95,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests {
 	@Test // gh-20
 	public void shouldRenderDeleteByIdInQuery() {
 
-		Statement statement = mock(Statement.class);
+		Statement statement = mock(Statement.class);
 		BindIdOperation operation = strategy.deleteByIdIn("table", "id");
 
 		operation.bindId(statement, Collections.singleton("foo"));
diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
index 89176550..b4f6ca25 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java
@@ -94,7 +94,7 @@ public class NamedParameterUtilsUnitTests {
 		namedParams.addValue("a",
 				Arrays.asList(new Object[] { "Walter", "Heisenberg" }, new Object[] { "Walt Jr.", "Flynn" }));
 
-		Statement mockStatement = mock(Statement.class);
+		Statement mockStatement = mock(Statement.class);
 
 		BindableOperation operation = NamedParameterUtils.substituteNamedParameters("xxx :a", BIND_MARKERS, namedParams);
 		operation.bind(mockStatement, "a", namedParams.getValue("a"));
diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java
index 3260eb47..10d3b725 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java
@@ -75,6 +75,7 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport {
 	}
 
 	@Test // gh-30
+	@Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/67")
 	public void shouldReadAndWriteBoxedSingleDimensionArrays() {
 
 		EntityWithArrays withArrays = new EntityWithArrays(new Integer[] { 1, 2, 3 }, null, null, null);
@@ -89,6 +90,7 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport {
 	}
 
 	@Test // gh-30
+	@Ignore("https://github.com/r2dbc/r2dbc-postgresql/issues/67")
 	public void shouldReadAndWriteConvertedDimensionArrays() {
 
 		EntityWithArrays withArrays = new EntityWithArrays(null, null, null, Arrays.asList(5, 6, 7));

From 4423e038c072e82b4b96f59924b9c1550f337c7d Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Mon, 11 Feb 2019 10:06:55 +0100
Subject: [PATCH 076/665] #54 - Upgrade to R2DBC 1.0 M7.

---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index c46dfc45..5c7cfd0e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,7 @@
 		2.4.1
 		42.2.5
 		7.1.2.jre8-preview
-		1.0.0.BUILD-SNAPSHOT
+		1.0.0.M7
 		${r2dbc.version}
 		${r2dbc.version}
 		${r2dbc.version}

From b53437b59d9c02b1607e5e3a7251ba14bb5821a2 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Thu, 14 Feb 2019 13:37:33 +0100
Subject: [PATCH 077/665] #60 - Use R2DBC's BOM for dependency management.

---
 pom.xml | 230 +++++++++++++++++++++++++++++---------------------------
 1 file changed, 119 insertions(+), 111 deletions(-)

diff --git a/pom.xml b/pom.xml
index 5c7cfd0e..3891e1d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,5 +1,6 @@
 
-
 
 	4.0.0
@@ -31,11 +32,7 @@
 		2.4.1
 		42.2.5
 		7.1.2.jre8-preview
-		1.0.0.M7
-		${r2dbc.version}
-		${r2dbc.version}
-		${r2dbc.version}
-		${r2dbc.version}
+		Arabba-M7
 		1.0.1
 		1.10.1
 
@@ -68,107 +65,17 @@
 		
 	
 
-	
-		
-			release
-			
-				
-					
-						org.jfrog.buildinfo
-						artifactory-maven-plugin
-						false
-					
-				
-			
-		
-
-		
-			no-jacoco
-			
-				
-					
-						org.jacoco
-						jacoco-maven-plugin
-						
-							
-								jacoco-initialize
-								none
-							
-						
-					
-				
-			
-
-		
-
-		
-			all-dbs
-			
-				
-					
-						org.apache.maven.plugins
-						maven-surefire-plugin
-						
-							
-								mysql-test
-								test
-								
-									test
-								
-								
-									
-										**/*IntegrationTests.java
-									
-									
-										**/*HsqlIntegrationTests.java
-									
-									
-										mysql
-									
-								
-							
-							
-								postgres-test
-								test
-								
-									test
-								
-								
-									
-										**/*IntegrationTests.java
-									
-									
-										**/*HsqlIntegrationTests.java
-									
-									
-										postgres
-									
-								
-							
-							
-								mariadb-test
-								test
-								
-									test
-								
-								
-									
-										**/*IntegrationTests.java
-									
-									
-										**/*HsqlIntegrationTests.java
-									
-									
-										mariadb
-									
-								
-							
-						
-					
-				
-			
-		
-	
+	
+		
+			
+				io.r2dbc
+				r2dbc-bom
+				${r2dbc-releasetrain.version}
+				pom
+				import
+			
+		
+	
 
 	
 
@@ -212,7 +119,6 @@
 		
 			io.r2dbc
 			r2dbc-spi
-			${r2dbc-spi.version}
 		
 
 		
@@ -250,21 +156,18 @@
 		
 			io.r2dbc
 			r2dbc-postgresql
-			${r2dbc-postgresql.version}
 			test
 		
 
 		
 			io.r2dbc
 			r2dbc-h2
-			${r2dbc-h2.version}
 			test
 		
 
 		
 			io.r2dbc
 			r2dbc-mssql
-			${r2dbc-mssql.version}
 			test
 		
 
@@ -369,6 +272,111 @@
 		
 	
 
+	
+		
+			release
+			
+				
+					
+						org.jfrog.buildinfo
+						artifactory-maven-plugin
+						false
+					
+				
+			
+		
+
+		
+			no-jacoco
+			
+				
+					
+						org.jacoco
+						jacoco-maven-plugin
+						
+							
+								jacoco-initialize
+								none
+							
+						
+					
+				
+			
+
+		
+
+		
+			all-dbs
+			
+				
+					
+						org.apache.maven.plugins
+						maven-surefire-plugin
+						
+							
+								mysql-test
+								test
+								
+									test
+								
+								
+									
+										**/*IntegrationTests.java
+									
+									
+										**/*HsqlIntegrationTests.java
+									
+									
+										mysql
+										
+									
+								
+							
+							
+								postgres-test
+								test
+								
+									test
+								
+								
+									
+										**/*IntegrationTests.java
+									
+									
+										**/*HsqlIntegrationTests.java
+									
+									
+										postgres
+										
+									
+								
+							
+							
+								mariadb-test
+								test
+								
+									test
+								
+								
+									
+										**/*IntegrationTests.java
+									
+									
+										**/*HsqlIntegrationTests.java
+									
+									
+										mariadb
+										
+									
+								
+							
+						
+					
+				
+			
+		
+	
+
 	
 		
 			spring-libs-snapshot

From 66b84d23f1a36a2565c7709117da464b49096b8e Mon Sep 17 00:00:00 2001
From: Sebastien Deleuze 
Date: Tue, 19 Feb 2019 17:31:40 +0100
Subject: [PATCH 078/665] #63 - Add DatabaseClient Coroutines extensions.

This commit introduces Coroutines support for `DatabaseClient`
functional API via Kotlin extensions that provide suspendable
functions prefixed by `await` for `Mono` based APIs.

Extensions for `Flux` will be added when Kotlin/kotlinx.coroutines#254
will be fixed.

It also provides `asType()` extensions useful for Reactive API
as well.

Original pull request: #63.
---
 pom.xml                                       |  34 ++++++
 .../function/DatabaseClientExtensions.kt      |  82 +++++++++++++
 .../r2dbc/function/RowsFetchSpecExtensions.kt |  37 ++++++
 .../function/DatabaseClientExtensionsTests.kt | 114 ++++++++++++++++++
 .../function/RowsFetchSpecExtensionsTests.kt  |  51 ++++++++
 5 files changed, 318 insertions(+)
 create mode 100644 src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt
 create mode 100644 src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt
 create mode 100644 src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
 create mode 100644 src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt

diff --git a/pom.xml b/pom.xml
index 3891e1d5..0aa6dcd0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,6 +35,7 @@
 		Arabba-M7
 		1.0.1
 		1.10.1
+		1.1.1
 
 	
 
@@ -126,6 +127,32 @@
 			reactor-core
 		
 
+		
+		
+			org.jetbrains.kotlin
+			kotlin-stdlib
+			${kotlin}
+			true
+		
+		
+			org.jetbrains.kotlin
+			kotlin-reflect
+			${kotlin}
+			true
+		
+		
+			org.jetbrains.kotlinx
+			kotlinx-coroutines-core
+			${coroutines.version}
+			true
+		
+		
+			org.jetbrains.kotlinx
+			kotlinx-coroutines-reactor
+			${coroutines.version}
+			true
+		
+
 		
 			org.assertj
 			assertj-core
@@ -198,6 +225,13 @@
 			test
 		
 
+		
+			io.mockk
+			mockk
+			1.9.1
+			test
+		
+
 	
 
 	
diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt
new file mode 100644
index 00000000..7799e7ca
--- /dev/null
+++ b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2018-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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function
+
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+
+/**
+ * Coroutines variant of [DatabaseClient.GenericExecuteSpec.then].
+ *
+ * @author Sebastien Deleuze
+ */
+suspend fun DatabaseClient.GenericExecuteSpec.await() {
+    then().awaitFirstOrNull()
+}
+
+/**
+ * Extension for [DatabaseClient.GenericExecuteSpec.as] providing a
+ * `asType()` variant.
+ *
+ * @author Sebastien Deleuze
+ */
+inline fun  DatabaseClient.GenericExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec
+        = `as`(T::class.java)
+
+/**
+ * Extension for [DatabaseClient.GenericSelectSpec.as] providing a
+ * `asType()` variant.
+ *
+ * @author Sebastien Deleuze
+ */
+inline fun  DatabaseClient.GenericSelectSpec.asType(): DatabaseClient.TypedSelectSpec
+        = `as`(T::class.java)
+
+/**
+ * Coroutines variant of [DatabaseClient.TypedExecuteSpec.then].
+ *
+ * @author Sebastien Deleuze
+ */
+suspend fun  DatabaseClient.TypedExecuteSpec.await() {
+    then().awaitFirstOrNull()
+}
+
+/**
+ * Extension for [DatabaseClient.TypedExecuteSpec.as] providing a
+ * `asType()` variant.
+ *
+ * @author Sebastien Deleuze
+ */
+inline fun  DatabaseClient.TypedExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec
+        = `as`(T::class.java)
+
+/**
+ * Coroutines variant of [DatabaseClient.InsertSpec.then].
+ *
+ * @author Sebastien Deleuze
+ */
+suspend fun  DatabaseClient.InsertSpec.await() {
+    then().awaitFirstOrNull()
+}
+
+/**
+ * Extension for [DatabaseClient.InsertIntoSpec.into] providing a
+ * `into()` variant.
+ *
+ * @author Sebastien Deleuze
+ */
+inline fun  DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec
+        = into(T::class.java)
+
diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt
new file mode 100644
index 00000000..8b11402e
--- /dev/null
+++ b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2018-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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function
+
+import kotlinx.coroutines.reactive.awaitFirstOrNull
+
+/**
+ * Coroutines variant of [RowsFetchSpec.one].
+ *
+ * @author Sebastien Deleuze
+ */
+suspend fun  RowsFetchSpec.awaitOne(): T?
+        = one().awaitFirstOrNull()
+
+/**
+ * Coroutines variant of [RowsFetchSpec.first].
+ *
+ * @author Sebastien Deleuze
+ */
+suspend fun  RowsFetchSpec.awaitFirst(): T?
+        = first().awaitFirstOrNull()
+
+// TODO Coroutines variant of [RowsFetchSpec.all], depends on [kotlinx.coroutines#254](https://github.com/Kotlin/kotlinx.coroutines/issues/254).
+// suspend fun  RowsFetchSpec.awaitAll() = all()...
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
new file mode 100644
index 00000000..62dc461d
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2018-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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import reactor.core.publisher.Mono
+
+class DatabaseClientExtensionsTests {
+
+    @Test
+    fun genericExecuteSpecAwait() {
+        val spec = mockk()
+        every { spec.then() } returns Mono.empty()
+        runBlocking {
+            spec.await()
+        }
+        verify {
+            spec.then()
+        }
+    }
+
+    @Test
+    fun genericExecuteSpecAsType() {
+        val genericSpec = mockk()
+        val typedSpec: DatabaseClient.TypedExecuteSpec = mockk()
+        every { genericSpec.`as`(String::class.java) } returns typedSpec
+        runBlocking {
+            assertEquals(typedSpec, genericSpec.asType())
+        }
+        verify {
+            genericSpec.`as`(String::class.java)
+        }
+    }
+
+    @Test
+    fun genericSelectSpecAsType() {
+        val genericSpec = mockk()
+        val typedSpec: DatabaseClient.TypedSelectSpec = mockk()
+        every { genericSpec.`as`(String::class.java) } returns typedSpec
+        runBlocking {
+            assertEquals(typedSpec, genericSpec.asType())
+        }
+        verify {
+            genericSpec.`as`(String::class.java)
+        }
+    }
+
+    @Test
+    fun typedExecuteSpecAwait() {
+        val spec = mockk>()
+        every { spec.then() } returns Mono.empty()
+        runBlocking {
+            spec.await()
+        }
+        verify {
+            spec.then()
+        }
+    }
+
+    @Test
+    fun typedExecuteSpecAsType() {
+        val spec: DatabaseClient.TypedExecuteSpec = mockk()
+        every { spec.`as`(String::class.java) } returns spec
+        runBlocking {
+            assertEquals(spec, spec.asType())
+        }
+        verify {
+            spec.`as`(String::class.java)
+        }
+    }
+
+    @Test
+    fun insertSpecAwait() {
+        val spec = mockk>()
+        every { spec.then() } returns Mono.empty()
+        runBlocking {
+            spec.await()
+        }
+        verify {
+            spec.then()
+        }
+    }
+
+    @Test
+    fun insertIntoSpecInto() {
+        val spec = mockk()
+        val typedSpec: DatabaseClient.TypedInsertSpec = mockk()
+        every { spec.into(String::class.java) } returns typedSpec
+        runBlocking {
+            assertEquals(typedSpec, spec.into())
+        }
+        verify {
+            spec.into(String::class.java)
+        }
+    }
+}
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt
new file mode 100644
index 00000000..760e9ef3
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018-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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import reactor.core.publisher.Mono
+
+class RowsFetchSpecExtensionsTests {
+
+    @Test
+    fun awaitOne() {
+        val spec = mockk>()
+        every { spec.one() } returns Mono.just("foo")
+        runBlocking {
+            assertEquals("foo", spec.awaitOne())
+        }
+        verify {
+            spec.one()
+        }
+    }
+
+    @Test
+    fun awaitFirst() {
+        val spec = mockk>()
+        every { spec.first() } returns Mono.just("foo")
+        runBlocking {
+            assertEquals("foo", spec.awaitFirst())
+        }
+        verify {
+            spec.first()
+        }
+    }
+}

From 2cefdc8bdd9f5c6409f5e53b4d482f685f4362f4 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Thu, 21 Feb 2019 13:37:54 +0100
Subject: [PATCH 079/665] #63 - Polishing.

Convert spaces to tabs. Replace version strings with managed dependencies. Javadoc.

Original pull request: #63.
---
 pom.xml                                       |  12 +-
 .../function/DatabaseClientExtensionsTests.kt | 188 ++++++++++--------
 .../function/RowsFetchSpecExtensionsTests.kt  |  57 +++---
 3 files changed, 147 insertions(+), 110 deletions(-)

diff --git a/pom.xml b/pom.xml
index 0aa6dcd0..2f3b6ed4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,7 +35,6 @@
 		Arabba-M7
 		1.0.1
 		1.10.1
-		1.1.1
 
 	
 
@@ -131,25 +130,26 @@
 		
 			org.jetbrains.kotlin
 			kotlin-stdlib
-			${kotlin}
 			true
 		
+
 		
 			org.jetbrains.kotlin
 			kotlin-reflect
-			${kotlin}
 			true
 		
+
 		
 			org.jetbrains.kotlinx
 			kotlinx-coroutines-core
-			${coroutines.version}
+			${kotlin-coroutines}
 			true
 		
+
 		
 			org.jetbrains.kotlinx
 			kotlinx-coroutines-reactor
-			${coroutines.version}
+			${kotlin-coroutines}
 			true
 		
 
@@ -228,7 +228,7 @@
 		
 			io.mockk
 			mockk
-			1.9.1
+			${mockk}
 			test
 		
 
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
index 62dc461d..3ace37d4 100644
--- a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
+++ b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt
@@ -19,96 +19,122 @@ import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
 import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
+import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
 import reactor.core.publisher.Mono
 
+/**
+ * Unit tests for [DatabaseClient] extensions.
+ *
+ * @author Sebastien Deleuze
+ */
 class DatabaseClientExtensionsTests {
 
-    @Test
-    fun genericExecuteSpecAwait() {
-        val spec = mockk()
-        every { spec.then() } returns Mono.empty()
-        runBlocking {
-            spec.await()
-        }
-        verify {
-            spec.then()
-        }
-    }
+	@Test // gh-63
+	fun genericExecuteSpecAwait() {
 
-    @Test
-    fun genericExecuteSpecAsType() {
-        val genericSpec = mockk()
-        val typedSpec: DatabaseClient.TypedExecuteSpec = mockk()
-        every { genericSpec.`as`(String::class.java) } returns typedSpec
-        runBlocking {
-            assertEquals(typedSpec, genericSpec.asType())
-        }
-        verify {
-            genericSpec.`as`(String::class.java)
-        }
-    }
+		val spec = mockk()
+		every { spec.then() } returns Mono.empty()
 
-    @Test
-    fun genericSelectSpecAsType() {
-        val genericSpec = mockk()
-        val typedSpec: DatabaseClient.TypedSelectSpec = mockk()
-        every { genericSpec.`as`(String::class.java) } returns typedSpec
-        runBlocking {
-            assertEquals(typedSpec, genericSpec.asType())
-        }
-        verify {
-            genericSpec.`as`(String::class.java)
-        }
-    }
+		runBlocking {
+			spec.await()
+		}
 
-    @Test
-    fun typedExecuteSpecAwait() {
-        val spec = mockk>()
-        every { spec.then() } returns Mono.empty()
-        runBlocking {
-            spec.await()
-        }
-        verify {
-            spec.then()
-        }
-    }
+		verify {
+			spec.then()
+		}
+	}
 
-    @Test
-    fun typedExecuteSpecAsType() {
-        val spec: DatabaseClient.TypedExecuteSpec = mockk()
-        every { spec.`as`(String::class.java) } returns spec
-        runBlocking {
-            assertEquals(spec, spec.asType())
-        }
-        verify {
-            spec.`as`(String::class.java)
-        }
-    }
+	@Test // gh-63
+	fun genericExecuteSpecAsType() {
 
-    @Test
-    fun insertSpecAwait() {
-        val spec = mockk>()
-        every { spec.then() } returns Mono.empty()
-        runBlocking {
-            spec.await()
-        }
-        verify {
-            spec.then()
-        }
-    }
+		val genericSpec = mockk()
+		val typedSpec: DatabaseClient.TypedExecuteSpec = mockk()
+		every { genericSpec.`as`(String::class.java) } returns typedSpec
 
-    @Test
-    fun insertIntoSpecInto() {
-        val spec = mockk()
-        val typedSpec: DatabaseClient.TypedInsertSpec = mockk()
-        every { spec.into(String::class.java) } returns typedSpec
-        runBlocking {
-            assertEquals(typedSpec, spec.into())
-        }
-        verify {
-            spec.into(String::class.java)
-        }
-    }
+		runBlocking {
+			assertThat(genericSpec.asType()).isEqualTo(typedSpec)
+		}
+
+		verify {
+			genericSpec.`as`(String::class.java)
+		}
+	}
+
+	@Test // gh-63
+	fun genericSelectSpecAsType() {
+
+		val genericSpec = mockk()
+		val typedSpec: DatabaseClient.TypedSelectSpec = mockk()
+		every { genericSpec.`as`(String::class.java) } returns typedSpec
+
+		runBlocking {
+			assertThat(genericSpec.asType()).isEqualTo(typedSpec)
+		}
+
+		verify {
+			genericSpec.`as`(String::class.java)
+		}
+	}
+
+	@Test // gh-63
+	fun typedExecuteSpecAwait() {
+
+		val spec = mockk>()
+		every { spec.then() } returns Mono.empty()
+
+		runBlocking {
+			spec.await()
+		}
+
+		verify {
+			spec.then()
+		}
+	}
+
+	@Test // gh-63
+	fun typedExecuteSpecAsType() {
+
+		val spec: DatabaseClient.TypedExecuteSpec = mockk()
+		every { spec.`as`(String::class.java) } returns spec
+
+		runBlocking {
+			assertThat(spec.asType()).isEqualTo(spec)
+		}
+
+		verify {
+			spec.`as`(String::class.java)
+		}
+	}
+
+	@Test // gh-63
+	fun insertSpecAwait() {
+
+		val spec = mockk>()
+		every { spec.then() } returns Mono.empty()
+
+		runBlocking {
+			spec.await()
+		}
+
+		verify {
+			spec.then()
+		}
+	}
+
+	@Test // gh-63
+	fun insertIntoSpecInto() {
+
+		val spec = mockk()
+		val typedSpec: DatabaseClient.TypedInsertSpec = mockk()
+		every { spec.into(String::class.java) } returns typedSpec
+
+		runBlocking {
+			assertThat(spec.into()).isEqualTo(typedSpec)
+		}
+
+		verify {
+			spec.into(String::class.java)
+		}
+	}
 }
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt
index 760e9ef3..5f1d92e0 100644
--- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt
+++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt
@@ -19,33 +19,44 @@ import io.mockk.every
 import io.mockk.mockk
 import io.mockk.verify
 import kotlinx.coroutines.runBlocking
-import org.junit.Assert.assertEquals
+import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
 import reactor.core.publisher.Mono
 
+/**
+ * Unit tests for [RowsFetchSpec] extensions.
+ *
+ * @author Sebastien Deleuze
+ */
 class RowsFetchSpecExtensionsTests {
 
-    @Test
-    fun awaitOne() {
-        val spec = mockk>()
-        every { spec.one() } returns Mono.just("foo")
-        runBlocking {
-            assertEquals("foo", spec.awaitOne())
-        }
-        verify {
-            spec.one()
-        }
-    }
+	@Test // gh-63
+	fun awaitOne() {
 
-    @Test
-    fun awaitFirst() {
-        val spec = mockk>()
-        every { spec.first() } returns Mono.just("foo")
-        runBlocking {
-            assertEquals("foo", spec.awaitFirst())
-        }
-        verify {
-            spec.first()
-        }
-    }
+		val spec = mockk>()
+		every { spec.one() } returns Mono.just("foo")
+
+		runBlocking {
+			assertThat(spec.awaitOne()).isEqualTo("foo")
+		}
+
+		verify {
+			spec.one()
+		}
+	}
+
+	@Test // gh-63
+	fun awaitFirst() {
+
+		val spec = mockk>()
+		every { spec.first() } returns Mono.just("foo")
+
+		runBlocking {
+			assertThat(spec.awaitFirst()).isEqualTo("foo")
+		}
+
+		verify {
+			spec.first()
+		}
+	}
 }

From c241cd982ee73fff32bc13bac6fda9931d83faaa Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Tue, 26 Feb 2019 13:50:20 +0100
Subject: [PATCH 080/665] #56 - Use Statement Builder API for SELECT
 statements.

Original pull request: #66.
---
 .../r2dbc/function/DefaultDatabaseClient.java |  17 +--
 .../DefaultReactiveDataAccessStrategy.java    | 114 ++++++------------
 .../function/ReactiveDataAccessStrategy.java  |  37 +-----
 .../support/SimpleR2dbcRepository.java        |  70 ++++++++---
 .../r2dbc/support/StatementRenderUtil.java    |  65 ++++++++++
 ...ltReactiveDataAccessStrategyUnitTests.java |  42 -------
 6 files changed, 158 insertions(+), 187 deletions(-)
 create mode 100644 src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java

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 b8587e06..6d0ed6f9 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -703,17 +703,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 		private  FetchSpec exchange(BiFunction mappingFunction) {
 
-			Set columns;
+			String select = dataAccessStrategy.select(table, new LinkedHashSet<>(this.projectedFields), sort, page);
 
-			if (this.projectedFields.isEmpty()) {
-				columns = Collections.singleton("*");
-			} else {
-				columns = new LinkedHashSet<>(this.projectedFields);
-			}
-
-			QueryOperation select = dataAccessStrategy.select(table, columns, sort, page);
-
-			return execute(select.toQuery(), mappingFunction);
+			return execute(select, mappingFunction);
 		}
 
 		@Override
@@ -797,11 +789,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 			} else {
 				columns = this.projectedFields;
 			}
-			Sort sortToUse = sort.isSorted() ? dataAccessStrategy.getMappedSort(typeToRead, sort) : Sort.unsorted();
 
-			QueryOperation select = dataAccessStrategy.select(table, new LinkedHashSet<>(columns), sortToUse, page);
+			String select = dataAccessStrategy.select(table, new LinkedHashSet<>(columns), sort, page);
 
-			return execute(select.get(), mappingFunction);
+			return execute(select, mappingFunction);
 		}
 
 		@Override
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 54740652..2b5f94da 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -26,6 +26,7 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.OptionalLong;
 import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -43,16 +44,20 @@ import org.springframework.data.r2dbc.dialect.BindMarker;
 import org.springframework.data.r2dbc.dialect.BindMarkers;
 import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
 import org.springframework.data.r2dbc.dialect.Dialect;
-import org.springframework.data.r2dbc.dialect.LimitClause;
-import org.springframework.data.r2dbc.dialect.LimitClause.Position;
 import org.springframework.data.r2dbc.function.convert.EntityRowMapper;
 import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
 import org.springframework.data.r2dbc.function.convert.SettableValue;
+import org.springframework.data.r2dbc.support.StatementRenderUtil;
 import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
 import org.springframework.data.relational.core.conversion.RelationalConverter;
 import org.springframework.data.relational.core.mapping.RelationalMappingContext;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.OrderByField;
+import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy;
+import org.springframework.data.relational.core.sql.StatementBuilder;
+import org.springframework.data.relational.core.sql.Table;
 import org.springframework.data.util.TypeInformation;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
@@ -312,94 +317,47 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#select(java.lang.String, java.util.Set, org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable)
 	 */
 	@Override
-	public QueryOperation select(String table, Set columns, Sort sort, Pageable page) {
+	public String select(String table, Set columns, Sort sort, Pageable page) {
 
-		StringBuilder selectBuilder = new StringBuilder();
+		Table tableToUse = Table.create(table);
 
-		selectBuilder.append("SELECT").append(' ') //
-				.append(StringUtils.collectionToDelimitedString(columns, ", ")).append(' ') //
-				.append("FROM").append(' ').append(table);
+		Collection selectList;
 
-		if (sort.isSorted()) {
-			selectBuilder.append(' ').append("ORDER BY").append(' ').append(getSortClause(sort));
+		if (columns.isEmpty()) {
+			selectList = Collections.singletonList(tableToUse.asterisk());
+		} else {
+			selectList = tableToUse.columns(columns);
 		}
 
+		SelectFromAndOrderBy selectBuilder = StatementBuilder.select(selectList).from(table)
+				.orderBy(createOrderByFields(tableToUse, sort));
+		OptionalLong limit = OptionalLong.empty();
+		OptionalLong offset = OptionalLong.empty();
+
 		if (page.isPaged()) {
+			limit = OptionalLong.of(page.getPageSize());
+			offset = OptionalLong.of(page.getOffset());
+		}
 
-			LimitClause limitClause = dialect.limit();
+		return StatementRenderUtil.render(selectBuilder.build(), limit, offset, this.dialect);
+	}
 
-			if (limitClause.getClausePosition() == Position.END) {
+	private Collection createOrderByFields(Table table, Sort sortToUse) {
 
-				selectBuilder.append(' ').append(limitClause.getClause(page.getPageSize(), page.getOffset()));
+		List fields = new ArrayList<>();
+
+		for (Order order : sortToUse) {
+
+			OrderByField orderByField = OrderByField.from(table.column(order.getProperty()));
+
+			if (order.getDirection() != null) {
+				fields.add(order.isAscending() ? orderByField.asc() : orderByField.desc());
+			} else {
+				fields.add(orderByField);
 			}
 		}
 
-		return selectBuilder::toString;
-	}
-
-	private StringBuilder getSortClause(Sort sort) {
-
-		StringBuilder sortClause = new StringBuilder();
-
-		for (Order order : sort) {
-
-			if (sortClause.length() != 0) {
-				sortClause.append(',').append(' ');
-			}
-
-			sortClause.append(order.getProperty()).append(' ').append(order.getDirection().isAscending() ? "ASC" : "DESC");
-		}
-		return sortClause;
-	}
-
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectById(java.lang.String, java.util.Set, java.lang.String)
-	 */
-	@Override
-	public BindIdOperation selectById(String table, Set columns, String idColumn) {
-
-		return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(), marker -> {
-
-			String columnClause = StringUtils.collectionToDelimitedString(columns, ", ");
-
-			return String.format("SELECT %s FROM %s WHERE %s = %s", columnClause, table, idColumn, marker.getPlaceholder());
-		});
-	}
-
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectById(java.lang.String, java.util.Set, java.lang.String, int)
-	 */
-	@Override
-	public BindIdOperation selectById(String table, Set columns, String idColumn, int limit) {
-
-		LimitClause limitClause = dialect.limit();
-
-		return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(), marker -> {
-
-			String columnClause = StringUtils.collectionToDelimitedString(columns, ", ");
-
-			if (limitClause.getClausePosition() == Position.END) {
-
-				return String.format("SELECT %s FROM %s WHERE %s = %s ORDER BY %s %s", columnClause, table, idColumn,
-						marker.getPlaceholder(), idColumn, limitClause.getClause(limit));
-			}
-
-			throw new UnsupportedOperationException(
-					String.format("Limit clause position %s not supported!", limitClause.getClausePosition()));
-		});
-	}
-
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#selectByIdIn(java.lang.String, java.util.Set, java.lang.String)
-	 */
-	@Override
-	public BindIdOperation selectByIdIn(String table, Set columns, String idColumn) {
-
-		String query = String.format("SELECT %s FROM %s", StringUtils.collectionToDelimitedString(columns, ", "), table);
-		return new DefaultBindIdIn(dialect.getBindMarkersFactory().create(), query, idColumn);
+		return fields;
 	}
 
 	/*
diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
index f715e6e4..2d3b1b5b 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
@@ -108,42 +108,7 @@ public interface ReactiveDataAccessStrategy {
 	 * @param page
 	 * @return
 	 */
-	QueryOperation select(String table, Set columns, Sort sort, Pageable page);
-
-	/**
-	 * Create a {@code SELECT … WHERE id = ?} operation for the given {@code table} using {@code columns} to project and
-	 * {@code idColumn}.
-	 *
-	 * @param table the table to insert data to.
-	 * @param columns columns to return.
-	 * @param idColumn name of the primary key.
-	 * @return
-	 */
-	BindIdOperation selectById(String table, Set columns, String idColumn);
-
-	/**
-	 * Create a {@code SELECT … WHERE id = ?} operation for the given {@code table} using {@code columns} to project and
-	 * {@code idColumn} applying a limit (TOP, LIMIT, …).
-	 *
-	 * @param table the table to insert data to.
-	 * @param columns columns to return.
-	 * @param idColumn name of the primary key.
-	 * @param limit number of rows to return.
-	 * @return
-	 */
-	BindIdOperation selectById(String table, Set columns, String idColumn, int limit);
-
-	/**
-	 * Create a {@code SELECT … WHERE id IN (?)} operation for the given {@code table} using {@code columns} to project
-	 * and {@code idColumn}. The actual {@link BindableOperation#toQuery() query} string depends on
-	 * {@link BindIdOperation#bindIds(Statement, Iterable) bound parameters}.
-	 *
-	 * @param table the table to insert data to.
-	 * @param columns columns to return.
-	 * @param idColumn name of the primary key.
-	 * @return
-	 */
-	BindIdOperation selectByIdIn(String table, Set columns, String idColumn);
+	String select(String table, Set columns, Sort sort, Pageable page);
 
 	/**
 	 * Create a {@code UPDATE … SET … WHERE id = ?} operation for the given {@code table} updating {@code columns} and
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index 24558f89..4a6ee07b 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -21,13 +21,17 @@ import lombok.RequiredArgsConstructor;
 import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 
-import java.util.Collections;
+import java.util.ArrayList;
 import java.util.LinkedHashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
 import org.reactivestreams.Publisher;
+
+import org.springframework.data.r2dbc.dialect.BindMarker;
+import org.springframework.data.r2dbc.dialect.BindMarkers;
 import org.springframework.data.r2dbc.function.BindIdOperation;
 import org.springframework.data.r2dbc.function.BindableOperation;
 import org.springframework.data.r2dbc.function.DatabaseClient;
@@ -35,6 +39,14 @@ import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec
 import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
 import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
 import org.springframework.data.r2dbc.function.convert.SettableValue;
+import org.springframework.data.relational.core.sql.Conditions;
+import org.springframework.data.relational.core.sql.Expression;
+import org.springframework.data.relational.core.sql.Functions;
+import org.springframework.data.relational.core.sql.SQL;
+import org.springframework.data.relational.core.sql.Select;
+import org.springframework.data.relational.core.sql.StatementBuilder;
+import org.springframework.data.relational.core.sql.Table;
+import org.springframework.data.relational.core.sql.render.SqlRenderer;
 import org.springframework.data.relational.repository.query.RelationalEntityInformation;
 import org.springframework.data.repository.reactive.ReactiveCrudRepository;
 import org.springframework.util.Assert;
@@ -118,13 +130,17 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
 		String idColumnName = getIdColumnName();
-		BindIdOperation select = accessStrategy.selectById(entity.getTableName(), columns, idColumnName);
 
-		GenericExecuteSpec sql = databaseClient.execute().sql(select);
-		BindSpecAdapter wrapper = BindSpecAdapter.create(sql);
-		select.bindId(wrapper, id);
+		BindMarkers bindMarkers = accessStrategy.getBindMarkersFactory().create();
+		BindMarker bindMarker = bindMarkers.next("id");
 
-		return wrapper.getBoundOperation().as(entity.getJavaType()) //
+		Table table = Table.create(entity.getTableName());
+		Select select = StatementBuilder.select(table.columns(columns)).from(table)
+				.where(Conditions.isEqual(table.column(idColumnName), SQL.bindMarker(bindMarker.getPlaceholder()))).build();
+
+		return databaseClient.execute().sql(SqlRenderer.render(select)) //
+				.bind(0, id) //
+				.as(entity.getJavaType()) //
 				.fetch() //
 				.one();
 	}
@@ -146,14 +162,16 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecAdapter.create(sql);
-		select.bindId(wrapper, id);
+		BindMarkers bindMarkers = accessStrategy.getBindMarkersFactory().create();
+		BindMarker bindMarker = bindMarkers.next("id");
 
-		return wrapper.getBoundOperation().as(entity.getJavaType()) //
+		Table table = Table.create(entity.getTableName());
+		Select select = StatementBuilder.select(table.column(idColumnName)).from(table)
+				.where(Conditions.isEqual(table.column(idColumnName), SQL.bindMarker(bindMarker.getPlaceholder()))).build();
+
+		return databaseClient.execute().sql(SqlRenderer.render(select)) //
+				.bind(0, id) //
 				.map((r, md) -> r) //
 				.first() //
 				.hasElement();
@@ -202,12 +220,26 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType()));
 			String idColumnName = getIdColumnName();
-			BindIdOperation select = accessStrategy.selectByIdIn(entity.getTableName(), columns, idColumnName);
 
-			BindSpecAdapter wrapper = BindSpecAdapter.create(databaseClient.execute().sql(select));
-			select.bindIds(wrapper, ids);
+			BindMarkers bindMarkers = accessStrategy.getBindMarkersFactory().create();
 
-			return wrapper.getBoundOperation().as(entity.getJavaType()).fetch().all();
+			List markers = new ArrayList<>();
+
+			for (int i = 0; i < ids.size(); i++) {
+				markers.add(SQL.bindMarker(bindMarkers.next("id").getPlaceholder()));
+			}
+
+			Table table = Table.create(entity.getTableName());
+			Select select = StatementBuilder.select(table.columns(columns)).from(table)
+					.where(Conditions.in(table.column(idColumnName), markers)).build();
+
+			GenericExecuteSpec executeSpec = databaseClient.execute().sql(SqlRenderer.render(select));
+
+			for (int i = 0; i < ids.size(); i++) {
+				executeSpec = executeSpec.bind(i, ids.get(i));
+			}
+
+			return executeSpec.as(entity.getJavaType()).fetch().all();
 		});
 	}
 
@@ -217,8 +249,10 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository count() {
 
-		return databaseClient.execute()
-				.sql(String.format("SELECT COUNT(%s) FROM %s", getIdColumnName(), entity.getTableName())) //
+		Table table = Table.create(entity.getTableName());
+		Select select = StatementBuilder.select(Functions.count(table.column(getIdColumnName()))).from(table).build();
+
+		return databaseClient.execute().sql(SqlRenderer.render(select)) //
 				.map((r, md) -> r.get(0, Long.class)) //
 				.first() //
 				.defaultIfEmpty(0L);
diff --git a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java
new file mode 100644
index 00000000..184cf678
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java
@@ -0,0 +1,65 @@
+/*
+ * 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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.support;
+
+import java.util.OptionalLong;
+
+import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.LimitClause;
+import org.springframework.data.relational.core.sql.Select;
+import org.springframework.data.relational.core.sql.render.SqlRenderer;
+
+/**
+ * Utility class to assist with SQL rendering. Mainly for internal use within the framework.
+ *
+ * @author Mark Paluch
+ */
+public abstract class StatementRenderUtil {
+
+	/**
+	 * Render {@link Select} to SQL considering {@link Dialect} specifics.
+	 *
+	 * @param select must not be {@literal null}.
+	 * @param limit must not be {@literal null}.
+	 * @param offset must not be {@literal null}.
+	 * @param dialect must not be {@literal null}.
+	 * @return the rendered SQL statement.
+	 */
+	public static String render(Select select, OptionalLong limit, OptionalLong offset, Dialect dialect) {
+
+		String sql = SqlRenderer.render(select);
+
+		// TODO: Replace with proper {@link Dialect} rendering for limit/offset.
+		if (limit.isPresent()) {
+
+			LimitClause limitClause = dialect.limit();
+
+			String clause;
+			if (offset.isPresent()) {
+				clause = limitClause.getClause(limit.getAsLong(), offset.getAsLong());
+			} else {
+				clause = limitClause.getClause(limit.getAsLong());
+			}
+
+			return sql + " " + clause;
+		}
+
+		return sql;
+	}
+
+	private StatementRenderUtil() {}
+
+}
diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
index 01e1b162..2f1e51ec 100644
--- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java
@@ -42,48 +42,6 @@ public class DefaultReactiveDataAccessStrategyUnitTests {
 		assertThat(operation.toQuery()).isEqualTo("UPDATE table SET firstname = $2, lastname = $3 WHERE id = $1");
 	}
 
-	@Test // gh-20
-	public void shouldRenderSelectByIdQuery() {
-
-		BindableOperation operation = strategy.selectById("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
-				"id");
-
-		assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id = $1");
-	}
-
-	@Test // gh-20
-	public void shouldRenderSelectByIdQueryWithLimit() {
-
-		BindableOperation operation = strategy.selectById("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
-				"id", 10);
-
-		assertThat(operation.toQuery())
-				.isEqualTo("SELECT firstname, lastname FROM table WHERE id = $1 ORDER BY id LIMIT 10");
-	}
-
-	@Test // gh-20
-	public void shouldFailRenderingSelectByIdInQueryWithoutBindings() {
-
-		BindableOperation operation = strategy.selectByIdIn("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
-				"id");
-
-		assertThatThrownBy(operation::toQuery).isInstanceOf(UnsupportedOperationException.class);
-	}
-
-	@Test // gh-20
-	public void shouldRenderSelectByIdInQuery() {
-
-		Statement statement = mock(Statement.class);
-		BindIdOperation operation = strategy.selectByIdIn("table", new HashSet<>(Arrays.asList("firstname", "lastname")),
-				"id");
-
-		operation.bindId(statement, Collections.singleton("foo"));
-		assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id IN ($1)");
-
-		operation.bindId(statement, "bar");
-		assertThat(operation.toQuery()).isEqualTo("SELECT firstname, lastname FROM table WHERE id IN ($1, $2)");
-	}
-
 	@Test // gh-20
 	public void shouldRenderDeleteByIdQuery() {
 

From 169de9df0aa6df18ec54094475e3b8295959a7c2 Mon Sep 17 00:00:00 2001
From: Jens Schauder 
Date: Tue, 5 Mar 2019 13:35:58 +0100
Subject: [PATCH 081/665] #56 - Polishing.

Replaced calls to `SqlRenderer.render(.)` with `SqlRenderer.toString(.)`.
And code formatting.

Original pull request: #66.
---
 .../DefaultReactiveDataAccessStrategy.java    | 23 ++++++++-----
 .../support/SimpleR2dbcRepository.java        | 34 +++++++++++++------
 .../r2dbc/support/StatementRenderUtil.java    |  2 +-
 3 files changed, 38 insertions(+), 21 deletions(-)

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 2b5f94da..eefc3d97 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -229,6 +229,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getRowMapper(java.lang.Class)
 	 */
+	@SuppressWarnings("unchecked")
 	@Override
 	public  BiFunction getRowMapper(Class typeToRead) {
 		return new EntityRowMapper((RelationalPersistentEntity) getRequiredPersistentEntity(typeToRead),
@@ -262,6 +263,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		return mappingContext.getPersistentEntity(typeToRead);
 	}
 
+	@SuppressWarnings("unchecked")
 	private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
 
 		TypeInformation type = property.getTypeInformation();
@@ -317,20 +319,23 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#select(java.lang.String, java.util.Set, org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable)
 	 */
 	@Override
-	public String select(String table, Set columns, Sort sort, Pageable page) {
+	public String select(String tableName, Set columns, Sort sort, Pageable page) {
 
-		Table tableToUse = Table.create(table);
+		Table table = Table.create(tableName);
 
 		Collection selectList;
 
 		if (columns.isEmpty()) {
-			selectList = Collections.singletonList(tableToUse.asterisk());
+			selectList = Collections.singletonList(table.asterisk());
 		} else {
-			selectList = tableToUse.columns(columns);
+			selectList = table.columns(columns);
 		}
 
-		SelectFromAndOrderBy selectBuilder = StatementBuilder.select(selectList).from(table)
-				.orderBy(createOrderByFields(tableToUse, sort));
+		SelectFromAndOrderBy selectBuilder = StatementBuilder //
+				.select(selectList) //
+				.from(tableName) //
+				.orderBy(createOrderByFields(table, sort));
+
 		OptionalLong limit = OptionalLong.empty();
 		OptionalLong offset = OptionalLong.empty();
 
@@ -508,7 +513,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -569,7 +574,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 			throw new UnsupportedOperationException();
 		}
 
@@ -636,7 +641,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		 * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable)
 		 */
 		@Override
-		public void bindIds(Statement statement, Iterable values) {
+		public void bindIds(Statement statement, Iterable values) {
 
 			for (Object value : values) {
 				bindId(statement, value);
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index 4a6ee07b..c6b99f5c 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -135,10 +135,13 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository r) //
 				.first() //
@@ -230,10 +236,13 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository count() {
 
 		Table table = Table.create(entity.getTableName());
-		Select select = StatementBuilder.select(Functions.count(table.column(getIdColumnName()))).from(table).build();
+		Select select = StatementBuilder //
+				.select(Functions.count(table.column(getIdColumnName()))) //
+				.from(table) //
+				.build();
 
-		return databaseClient.execute().sql(SqlRenderer.render(select)) //
+		return databaseClient.execute().sql(SqlRenderer.toString(select)) //
 				.map((r, md) -> r.get(0, Long.class)) //
 				.first() //
 				.defaultIfEmpty(0L);
diff --git a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java
index 184cf678..4ea19aaf 100644
--- a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java
+++ b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java
@@ -40,7 +40,7 @@ public abstract class StatementRenderUtil {
 	 */
 	public static String render(Select select, OptionalLong limit, OptionalLong offset, Dialect dialect) {
 
-		String sql = SqlRenderer.render(select);
+		String sql = SqlRenderer.toString(select);
 
 		// TODO: Replace with proper {@link Dialect} rendering for limit/offset.
 		if (limit.isPresent()) {

From feabe477a77a559251dae28bd38f07f9d655f353 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Tue, 19 Feb 2019 12:52:06 +0100
Subject: [PATCH 082/665] #61 - Move Conversion-related functionality to
 MappingR2dbcConverter.

All conversion functionality is now pulled together into MappingR2dbcConverter.

Introduce OutboundRow to provide mapping between column names and settable values. Remove identifier from SettableValue.

Original pull request: #62.
---
 .../config/AbstractR2dbcConfiguration.java    |   7 +-
 .../r2dbc/function/BindableOperation.java     |   6 +-
 .../r2dbc/function/DefaultDatabaseClient.java |  42 ++--
 .../DefaultReactiveDataAccessStrategy.java    | 138 ++++------
 .../function/MapBindParameterSource.java      |   2 +-
 .../function/ReactiveDataAccessStrategy.java  |  20 +-
 .../function/convert/EntityRowMapper.java     | 128 +---------
 .../convert/MappingR2dbcConverter.java        | 206 +++++++++++++--
 .../r2dbc/function/convert/OutboundRow.java   | 235 ++++++++++++++++++
 .../function/convert/R2dbcConverter.java      |  73 ++++++
 .../r2dbc/function/convert/SettableValue.java |  70 ++++--
 .../repository/query/AbstractR2dbcQuery.java  |   9 +-
 .../query/StringBasedR2dbcQuery.java          |   9 +-
 .../support/R2dbcRepositoryFactory.java       |  21 +-
 .../support/R2dbcRepositoryFactoryBean.java   |  17 +-
 .../support/SimpleR2dbcRepository.java        |  26 +-
 ...ltReactiveDataAccessStrategyUnitTests.java |   5 +-
 .../convert/EntityRowMapperUnitTests.java     |   7 +-
 .../MappingR2dbcConverterUnitTests.java       |  53 ++++
 ...stractR2dbcRepositoryIntegrationTests.java |   7 +-
 .../R2dbcRepositoriesRegistrarTests.java      |   5 +-
 .../query/StringBasedR2dbcQueryUnitTests.java |   4 +-
 ...SimpleR2dbcRepositoryIntegrationTests.java |   4 +-
 .../R2dbcRepositoryFactoryUnitTests.java      |  13 +-
 24 files changed, 753 insertions(+), 354 deletions(-)
 create mode 100644 src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java
 create mode 100644 src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java
 create mode 100644 src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java

diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
index 34e1b515..d32a74df 100644
--- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
+++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
@@ -30,6 +30,7 @@ import org.springframework.data.r2dbc.dialect.Dialect;
 import org.springframework.data.r2dbc.function.DatabaseClient;
 import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy;
 import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy;
+import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
 import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
 import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
 import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator;
@@ -118,8 +119,8 @@ public abstract class AbstractR2dbcConfiguration {
 	}
 
 	/**
-	 * Creates a {@link ReactiveDataAccessStrategy} using the configured {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions)}
-	 * RelationalMappingContext}.
+	 * Creates a {@link ReactiveDataAccessStrategy} using the configured
+	 * {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions)} RelationalMappingContext}.
 	 *
 	 * @param mappingContext the configured {@link RelationalMappingContext}.
 	 * @param r2dbcCustomConversions customized R2DBC conversions.
@@ -134,7 +135,7 @@ public abstract class AbstractR2dbcConfiguration {
 
 		Assert.notNull(mappingContext, "MappingContext must not be null!");
 
-		BasicRelationalConverter converter = new BasicRelationalConverter(mappingContext, r2dbcCustomConversions);
+		MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext, r2dbcCustomConversions);
 
 		return new DefaultReactiveDataAccessStrategy(getDialect(connectionFactory()), converter);
 	}
diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
index 29c3c0b1..8038c3b0 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java
@@ -45,12 +45,12 @@ public interface BindableOperation extends QueryOperation {
 	 * @see Statement#bind
 	 * @see Statement#bindNull
 	 */
-	default void bind(Statement statement, SettableValue value) {
+	default void bind(Statement statement, String identifier, SettableValue value) {
 
 		if (value.getValue() == null) {
-			bindNull(statement, value.getIdentifier().toString(), value.getType());
+			bindNull(statement, identifier, value.getType());
 		} else {
-			bind(statement, value.getIdentifier().toString(), value.getValue());
+			bind(statement, identifier, value.getValue());
 		}
 	}
 
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 6d0ed6f9..8da6d444 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java
@@ -47,12 +47,14 @@ import java.util.stream.Collectors;
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
 import org.reactivestreams.Publisher;
+
 import org.springframework.dao.DataAccessException;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.r2dbc.UncategorizedR2dbcException;
 import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy;
 import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper;
+import org.springframework.data.r2dbc.function.convert.OutboundRow;
 import org.springframework.data.r2dbc.function.convert.SettableValue;
 import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
 import org.springframework.jdbc.core.SqlProvider;
@@ -365,8 +367,10 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 		public ExecuteSpecSupport bind(int index, Object value) {
 
+			Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index));
+
 			Map byIndex = new LinkedHashMap<>(this.byIndex);
-			byIndex.put(index, new SettableValue(index, value, null));
+			byIndex.put(index, new SettableValue(value, value.getClass()));
 
 			return createInstance(byIndex, this.byName, this.sqlSupplier);
 		}
@@ -374,7 +378,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		public ExecuteSpecSupport bindNull(int index, Class type) {
 
 			Map byIndex = new LinkedHashMap<>(this.byIndex);
-			byIndex.put(index, new SettableValue(index, null, type));
+			byIndex.put(index, new SettableValue(null, type));
 
 			return createInstance(byIndex, this.byName, this.sqlSupplier);
 		}
@@ -382,9 +386,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		public ExecuteSpecSupport bind(String name, Object value) {
 
 			Assert.hasText(name, "Parameter name must not be null or empty!");
+			Assert.notNull(value,
+					() -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name));
 
 			Map byName = new LinkedHashMap<>(this.byName);
-			byName.put(name, new SettableValue(name, value, null));
+			byName.put(name, new SettableValue(value, value.getClass()));
 
 			return createInstance(this.byIndex, byName, this.sqlSupplier);
 		}
@@ -394,7 +400,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 			Assert.hasText(name, "Parameter name must not be null or empty!");
 
 			Map byName = new LinkedHashMap<>(this.byName);
-			byName.put(name, new SettableValue(name, null, type));
+			byName.put(name, new SettableValue(null, type));
 
 			return createInstance(this.byIndex, byName, this.sqlSupplier);
 		}
@@ -832,9 +838,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 		public GenericInsertSpec value(String field, Object value) {
 
 			Assert.notNull(field, "Field must not be null!");
+			Assert.notNull(value,
+					() -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field));
 
 			Map byName = new LinkedHashMap<>(this.byName);
-			byName.put(field, new SettableValue(field, value, null));
+			byName.put(field, new SettableValue(value, value.getClass()));
 
 			return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
 		}
@@ -845,7 +853,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 			Assert.notNull(field, "Field must not be null!");
 
 			Map byName = new LinkedHashMap<>(this.byName);
-			byName.put(field, new SettableValue(field, null, type));
+			byName.put(field, new SettableValue(null, type));
 
 			return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction);
 		}
@@ -885,7 +893,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 				Statement statement = it.createStatement(sql).returnGeneratedValues();
 
-				byName.forEach((k, v) -> bindableInsert.bind(statement, v));
+				byName.forEach((k, v) -> bindableInsert.bind(statement, k, v));
 
 				return statement;
 			};
@@ -989,12 +997,16 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 		private  FetchSpec exchange(Object toInsert, BiFunction mappingFunction) {
 
-			List insertValues = dataAccessStrategy.getValuesToInsert(toInsert);
+			OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(toInsert);
+
 			Set columns = new LinkedHashSet<>();
 
-			for (SettableValue insertValue : insertValues) {
-				columns.add(insertValue.getIdentifier().toString());
-			}
+			outboundRow.forEach((k, v) -> {
+
+				if (v.hasValue()) {
+					columns.add(k);
+				}
+			});
 
 			BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, columns);
 
@@ -1008,9 +1020,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor {
 
 				Statement statement = it.createStatement(sql).returnGeneratedValues();
 
-				for (SettableValue settable : insertValues) {
-					bindableInsert.bind(statement, settable);
-				}
+				outboundRow.forEach((k, v) -> {
+					if (v.hasValue()) {
+						bindableInsert.bind(statement, k, v);
+					}
+				});
 
 				return statement;
 			};
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 eefc3d97..ca482d1f 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java
@@ -19,7 +19,6 @@ import io.r2dbc.spi.Row;
 import io.r2dbc.spi.RowMetadata;
 import io.r2dbc.spi.Statement;
 
-import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -31,13 +30,11 @@ import java.util.Set;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 
-import org.springframework.dao.InvalidDataAccessApiUsageException;
 import org.springframework.dao.InvalidDataAccessResourceUsageException;
 import org.springframework.data.convert.CustomConversions.StoreConversions;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.domain.Sort.Order;
-import org.springframework.data.mapping.PersistentPropertyAccessor;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.r2dbc.dialect.ArrayColumns;
 import org.springframework.data.r2dbc.dialect.BindMarker;
@@ -45,11 +42,12 @@ 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.function.convert.EntityRowMapper;
+import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter;
+import org.springframework.data.r2dbc.function.convert.OutboundRow;
+import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
 import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions;
 import org.springframework.data.r2dbc.function.convert.SettableValue;
 import org.springframework.data.r2dbc.support.StatementRenderUtil;
-import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
-import org.springframework.data.relational.core.conversion.RelationalConverter;
 import org.springframework.data.relational.core.mapping.RelationalMappingContext;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
@@ -58,7 +56,6 @@ import org.springframework.data.relational.core.sql.OrderByField;
 import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy;
 import org.springframework.data.relational.core.sql.StatementBuilder;
 import org.springframework.data.relational.core.sql.Table;
-import org.springframework.data.util.TypeInformation;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
@@ -72,7 +69,7 @@ import org.springframework.util.StringUtils;
 public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
 
 	private final Dialect dialect;
-	private final RelationalConverter relationalConverter;
+	private final R2dbcConverter converter;
 	private final MappingContext, ? extends RelationalPersistentProperty> mappingContext;
 
 	/**
@@ -84,7 +81,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		this(dialect, createConverter(dialect));
 	}
 
-	private static BasicRelationalConverter createConverter(Dialect dialect) {
+	private static R2dbcConverter createConverter(Dialect dialect) {
 
 		Assert.notNull(dialect, "Dialect must not be null");
 
@@ -94,11 +91,11 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		RelationalMappingContext context = new RelationalMappingContext();
 		context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
 
-		return new BasicRelationalConverter(context, customConversions);
+		return new MappingR2dbcConverter(context, customConversions);
 	}
 
-	public RelationalConverter getRelationalConverter() {
-		return relationalConverter;
+	public R2dbcConverter getConverter() {
+		return converter;
 	}
 
 	public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() {
@@ -106,19 +103,19 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	}
 
 	/**
-	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and {@link RelationalConverter}.
+	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and {@link R2dbcConverter}.
 	 *
 	 * @param dialect the {@link Dialect} to use.
 	 * @param converter must not be {@literal null}.
 	 */
 	@SuppressWarnings("unchecked")
-	public DefaultReactiveDataAccessStrategy(Dialect dialect, RelationalConverter converter) {
+	public DefaultReactiveDataAccessStrategy(Dialect dialect, R2dbcConverter converter) {
 
 		Assert.notNull(dialect, "Dialect must not be null");
 		Assert.notNull(converter, "RelationalConverter must not be null");
 
-		this.relationalConverter = converter;
-		this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) relationalConverter
+		this.converter = converter;
+		this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) this.converter
 				.getMappingContext();
 		this.dialect = dialect;
 	}
@@ -146,55 +143,47 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 
 	/*
 	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getValuesToInsert(java.lang.Object)
+	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getOutboundRow(java.lang.Object)
 	 */
-	@Override
-	public List getValuesToInsert(Object object) {
-
-		Class userClass = ClassUtils.getUserClass(object);
-
-		RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass);
-		PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object);
-
-		List values = new ArrayList<>();
-
-		for (RelationalPersistentProperty property : entity) {
-
-			Object value = getWriteValue(propertyAccessor, property);
-
-			if (value == null) {
-				continue;
-			}
-
-			values.add(new SettableValue(property.getColumnName(), value, property.getType()));
-		}
-
-		return values;
-	}
-
-	/*
-	 * (non-Javadoc)
-	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getColumnsToUpdate(java.lang.Object)
-	 */
-	public Map getColumnsToUpdate(Object object) {
+	public OutboundRow getOutboundRow(Object object) {
 
 		Assert.notNull(object, "Entity object must not be null!");
 
-		Class userClass = ClassUtils.getUserClass(object);
-		RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass);
+		OutboundRow row = new OutboundRow();
 
-		Map update = new LinkedHashMap<>();
+		converter.write(object, row);
 
-		PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object);
+		RelationalPersistentEntity entity = getRequiredPersistentEntity(ClassUtils.getUserClass(object));
 
 		for (RelationalPersistentProperty property : entity) {
 
-			Object writeValue = getWriteValue(propertyAccessor, property);
+			SettableValue value = row.get(property.getColumnName());
+			if (shouldConvertArrayValue(property, value)) {
 
-			update.put(property.getColumnName(), new SettableValue(property.getColumnName(), writeValue, property.getType()));
+				SettableValue writeValue = getArrayValue(value, property);
+				row.put(property.getColumnName(), writeValue);
+			}
 		}
 
-		return update;
+		return row;
+	}
+
+	private boolean shouldConvertArrayValue(RelationalPersistentProperty property, SettableValue value) {
+		return value != null && value.hasValue() && property.isCollectionLike();
+	}
+
+	private SettableValue getArrayValue(SettableValue value, RelationalPersistentProperty property) {
+
+		ArrayColumns arrayColumns = dialect.getArraySupport();
+
+		if (!arrayColumns.isSupported()) {
+
+			throw new InvalidDataAccessResourceUsageException(
+					"Dialect " + dialect.getClass().getName() + " does not support array columns");
+		}
+
+		return new SettableValue(converter.getArrayValue(arrayColumns, property, value.getValue()),
+				property.getActualType());
 	}
 
 	/*
@@ -232,8 +221,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	@SuppressWarnings("unchecked")
 	@Override
 	public  BiFunction getRowMapper(Class typeToRead) {
-		return new EntityRowMapper((RelationalPersistentEntity) getRequiredPersistentEntity(typeToRead),
-				relationalConverter);
+		return new EntityRowMapper<>(typeToRead, converter);
 	}
 
 	/*
@@ -263,48 +251,6 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 		return mappingContext.getPersistentEntity(typeToRead);
 	}
 
-	@SuppressWarnings("unchecked")
-	private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
-
-		TypeInformation type = property.getTypeInformation();
-		Object value = propertyAccessor.getProperty(property);
-
-		if (type.isCollectionLike()) {
-
-			RelationalPersistentEntity nestedEntity = mappingContext
-					.getPersistentEntity(type.getRequiredActualType().getType());
-
-			if (nestedEntity != null) {
-				throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
-			}
-
-			ArrayColumns arrayColumns = dialect.getArraySupport();
-
-			if (!arrayColumns.isSupported()) {
-
-				throw new InvalidDataAccessResourceUsageException(
-						"Dialect " + dialect.getClass().getName() + " does not support array columns");
-			}
-
-			return getArrayValue(arrayColumns, property, value);
-		}
-
-		return value;
-	}
-
-	private Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
-
-		Class targetType = arrayColumns.getArrayType(property.getActualType());
-
-		if (!property.isArray() || !property.getActualType().equals(targetType)) {
-
-			Object zeroLengthArray = Array.newInstance(targetType, 0);
-			return relationalConverter.getConversionService().convert(value, zeroLengthArray.getClass());
-		}
-
-		return value;
-	}
-
 	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#insertAndReturnGeneratedKeys(java.lang.String, java.util.Set)
diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
index 09de5d88..a6d79f4d 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java
@@ -65,7 +65,7 @@ class MapBindParameterSource implements BindParameterSource {
 		Assert.notNull(paramName, "Parameter name must not be null!");
 		Assert.notNull(value, "Value must not be null!");
 
-		this.values.put(paramName, new SettableValue(paramName, value, value.getClass()));
+		this.values.put(paramName, new SettableValue(value, value.getClass()));
 		return this;
 	}
 
diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
index 2d3b1b5b..aff9f794 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java
@@ -20,13 +20,14 @@ import io.r2dbc.spi.RowMetadata;
 import io.r2dbc.spi.Statement;
 
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 import java.util.function.BiFunction;
 
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
 import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
+import org.springframework.data.r2dbc.function.convert.OutboundRow;
+import org.springframework.data.r2dbc.function.convert.R2dbcConverter;
 import org.springframework.data.r2dbc.function.convert.SettableValue;
 
 /**
@@ -46,18 +47,12 @@ public interface ReactiveDataAccessStrategy {
 	List getAllColumns(Class typeToRead);
 
 	/**
-	 * @param object
-	 * @return {@link SettableValue} that represent an {@code INSERT} of {@code object}.
-	 */
-	List getValuesToInsert(Object object);
-
-	/**
-	 * Returns a {@link Map} that maps column names to a {@link SettableValue} value.
+	 * Returns a {@link OutboundRow} that maps column names to a {@link SettableValue} value.
 	 *
 	 * @param object must not be {@literal null}.
 	 * @return
 	 */
-	Map getColumnsToUpdate(Object object);
+	OutboundRow getOutboundRow(Object object);
 
 	/**
 	 * Map the {@link Sort} object to apply field name mapping using {@link Class the type to read}.
@@ -84,6 +79,13 @@ public interface ReactiveDataAccessStrategy {
 	 */
 	BindMarkersFactory getBindMarkersFactory();
 
+	/**
+	 * Returns the {@link R2dbcConverter}.
+	 *
+	 * @return the {@link R2dbcConverter}.
+	 */
+	R2dbcConverter getConverter();
+
 	// -------------------------------------------------------------------------
 	// Methods creating SQL operations.
 	// Subject to be moved into a SQL creation DSL.
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java
index f84f0bf2..dfb6e119 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java
@@ -17,23 +17,9 @@ package org.springframework.data.r2dbc.function.convert;
 
 import io.r2dbc.spi.Row;
 import io.r2dbc.spi.RowMetadata;
-import lombok.NonNull;
-import lombok.RequiredArgsConstructor;
 
-import java.sql.ResultSet;
 import java.util.function.BiFunction;
 
-import org.springframework.data.mapping.MappingException;
-import org.springframework.data.mapping.PersistentProperty;
-import org.springframework.data.mapping.PersistentPropertyAccessor;
-import org.springframework.data.mapping.PreferredConstructor.Parameter;
-import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
-import org.springframework.data.mapping.model.ParameterValueProvider;
-import org.springframework.data.relational.core.conversion.RelationalConverter;
-import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
-import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
-import org.springframework.lang.Nullable;
-
 /**
  * Maps a {@link io.r2dbc.spi.Row} to an entity of type {@code T}, including entities referenced.
  *
@@ -42,12 +28,12 @@ import org.springframework.lang.Nullable;
  */
 public class EntityRowMapper implements BiFunction {
 
-	private final RelationalPersistentEntity entity;
-	private final RelationalConverter converter;
+	private final Class typeRoRead;
+	private final R2dbcConverter converter;
 
-	public EntityRowMapper(RelationalPersistentEntity entity, RelationalConverter converter) {
+	public EntityRowMapper(Class typeRoRead, R2dbcConverter converter) {
 
-		this.entity = entity;
+		this.typeRoRead = typeRoRead;
 		this.converter = converter;
 	}
 
@@ -57,110 +43,6 @@ public class EntityRowMapper implements BiFunction {
 	 */
 	@Override
 	public T apply(Row row, RowMetadata metadata) {
-
-		T result = createInstance(row, "", entity);
-
-		ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(
-				entity.getPropertyAccessor(result), converter.getConversionService());
-
-		for (RelationalPersistentProperty property : entity) {
-
-			if (entity.isConstructorArgument(property)) {
-				continue;
-			}
-
-			if (property.isMap()) {
-				throw new UnsupportedOperationException();
-			} else {
-				propertyAccessor.setProperty(property, readFrom(row, property, ""));
-			}
-		}
-
-		return result;
-	}
-
-	/**
-	 * Read a single value or a complete Entity from the {@link ResultSet} passed as an argument.
-	 *
-	 * @param row the {@link Row} to extract the value from. Must not be {@literal null}.
-	 * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be
-	 *          {@literal null}.
-	 * @param prefix to be used for all column names accessed by this method. Must not be {@literal null}.
-	 * @return the value read from the {@link ResultSet}. May be {@literal null}.
-	 */
-	private Object readFrom(Row row, RelationalPersistentProperty property, String prefix) {
-
-		try {
-
-			if (property.isEntity()) {
-				return readEntityFrom(row, property);
-			}
-
-			Object value = row.get(prefix + property.getColumnName());
-			return converter.readValue(value, property.getTypeInformation());
-
-		} catch (Exception o_O) {
-			throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
-		}
-	}
-
-	private  S readEntityFrom(Row row, PersistentProperty property) {
-
-		String prefix = property.getName() + "_";
-
-		RelationalPersistentEntity entity = (RelationalPersistentEntity) converter.getMappingContext()
-				.getRequiredPersistentEntity(property.getActualType());
-
-		if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) {
-			return null;
-		}
-
-		S instance = createInstance(row, prefix, entity);
-
-		PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
-		ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor,
-				converter.getConversionService());
-
-		for (RelationalPersistentProperty p : entity) {
-			if (!entity.isConstructorArgument(property)) {
-				propertyAccessor.setProperty(p, readFrom(row, p, prefix));
-			}
-		}
-
-		return instance;
-	}
-
-	private  S createInstance(Row row, String prefix, RelationalPersistentEntity entity) {
-
-		RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, entity, converter, prefix);
-
-		return converter.createInstance(entity, rowParameterValueProvider::getParameterValue);
-	}
-
-	@RequiredArgsConstructor
-	private static class RowParameterValueProvider implements ParameterValueProvider {
-
-		private final @NonNull Row resultSet;
-		private final @NonNull RelationalPersistentEntity entity;
-		private final @NonNull RelationalConverter converter;
-		private final @NonNull String prefix;
-
-		/*
-		 * (non-Javadoc)
-		 * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
-		 */
-		@Override
-		@Nullable
-		public  T getParameterValue(Parameter parameter) {
-
-			RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameter.getName());
-			String column = prefix + property.getColumnName();
-
-			try {
-				return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType());
-			} catch (Exception o_O) {
-				throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O);
-			}
-		}
+		return converter.read(typeRoRead, row);
 	}
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
index 58f337e6..e24c2dbb 100644
--- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java
@@ -18,18 +18,32 @@ package org.springframework.data.r2dbc.function.convert;
 import io.r2dbc.spi.ColumnMetadata;
 import io.r2dbc.spi.Row;
 import io.r2dbc.spi.RowMetadata;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
 
+import java.lang.reflect.Array;
+import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 
 import org.springframework.core.convert.ConversionService;
+import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.data.convert.CustomConversions;
+import org.springframework.data.mapping.MappingException;
+import org.springframework.data.mapping.PersistentProperty;
 import org.springframework.data.mapping.PersistentPropertyAccessor;
+import org.springframework.data.mapping.PreferredConstructor.Parameter;
 import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
+import org.springframework.data.mapping.model.ParameterValueProvider;
+import org.springframework.data.r2dbc.dialect.ArrayColumns;
 import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
 import org.springframework.data.relational.core.conversion.RelationalConverter;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
+import org.springframework.data.util.TypeInformation;
+import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
 
@@ -38,9 +52,7 @@ import org.springframework.util.ClassUtils;
  *
  * @author Mark Paluch
  */
-public class MappingR2dbcConverter {
-
-	private final RelationalConverter relationalConverter;
+public class MappingR2dbcConverter extends BasicRelationalConverter implements R2dbcConverter {
 
 	/**
 	 * Creates a new {@link MappingR2dbcConverter} given {@link MappingContext}.
@@ -49,21 +61,159 @@ public class MappingR2dbcConverter {
 	 */
 	public MappingR2dbcConverter(
 			MappingContext, ? extends RelationalPersistentProperty> context) {
-		this(new BasicRelationalConverter(context));
+		super(context, new R2dbcCustomConversions(CustomConversions.StoreConversions.NONE, Collections.emptyList()));
 	}
 
 	/**
-	 * Creates a new {@link MappingR2dbcConverter} given {@link RelationalConverter}.
+	 * Creates a new {@link MappingR2dbcConverter} given {@link MappingContext} and {@link CustomConversions}.
 	 *
-	 * @param converter must not be {@literal null}.
+	 * @param context must not be {@literal null}.
 	 */
-	public MappingR2dbcConverter(RelationalConverter converter) {
-
-		Assert.notNull(converter, "RelationalConverter must not be null!");
-
-		this.relationalConverter = converter;
+	public MappingR2dbcConverter(
+			MappingContext, ? extends RelationalPersistentProperty> context,
+			CustomConversions conversions) {
+		super(context, conversions);
 	}
 
+	// ----------------------------------
+	// Entity reading
+	// ----------------------------------
+
+	@Override
+	public  R read(Class type, Row row) {
+		return read(getRequiredPersistentEntity(type), row);
+	}
+
+	private  R read(RelationalPersistentEntity entity, Row row) {
+
+		R result = createInstance(row, "", entity);
+
+		ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(
+				entity.getPropertyAccessor(result), getConversionService());
+
+		for (RelationalPersistentProperty property : entity) {
+
+			if (entity.isConstructorArgument(property)) {
+				continue;
+			}
+
+			propertyAccessor.setProperty(property, readFrom(row, property, ""));
+		}
+
+		return result;
+	}
+
+	/**
+	 * Read a single value or a complete Entity from the {@link Row} passed as an argument.
+	 *
+	 * @param row the {@link Row} to extract the value from. Must not be {@literal null}.
+	 * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be
+	 *          {@literal null}.
+	 * @param prefix to be used for all column names accessed by this method. Must not be {@literal null}.
+	 * @return the value read from the {@link Row}. May be {@literal null}.
+	 */
+	private Object readFrom(Row row, RelationalPersistentProperty property, String prefix) {
+
+		try {
+
+			if (property.isEntity()) {
+				return readEntityFrom(row, property);
+			}
+
+			Object value = row.get(prefix + property.getColumnName());
+			return readValue(value, property.getTypeInformation());
+
+		} catch (Exception o_O) {
+			throw new MappingException(String.format("Could not read property %s from result set!", property), o_O);
+		}
+	}
+
+	private  S readEntityFrom(Row row, PersistentProperty property) {
+
+		String prefix = property.getName() + "_";
+
+		RelationalPersistentEntity entity = (RelationalPersistentEntity) getMappingContext()
+				.getRequiredPersistentEntity(property.getActualType());
+
+		if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) {
+			return null;
+		}
+
+		S instance = createInstance(row, prefix, entity);
+
+		PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
+		ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor, getConversionService());
+
+		for (RelationalPersistentProperty p : entity) {
+			if (!entity.isConstructorArgument(property)) {
+				propertyAccessor.setProperty(p, readFrom(row, p, prefix));
+			}
+		}
+
+		return instance;
+	}
+
+	private  S createInstance(Row row, String prefix, RelationalPersistentEntity entity) {
+
+		RowParameterValueProvider rowParameterValueProvider = new RowParameterValueProvider(row, entity, this, prefix);
+
+		return createInstance(entity, rowParameterValueProvider::getParameterValue);
+	}
+
+	// ----------------------------------
+	// Entity writing
+	// ----------------------------------
+
+	@Override
+	public void write(Object source, OutboundRow sink) {
+
+		Class userClass = ClassUtils.getUserClass(source);
+		RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass);
+
+		PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source);
+
+		for (RelationalPersistentProperty property : entity) {
+
+			Object writeValue = getWriteValue(propertyAccessor, property);
+
+			sink.put(property.getColumnName(), new SettableValue(writeValue, property.getType()));
+		}
+
+	}
+
+	@SuppressWarnings("unchecked")
+	private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) {
+
+		TypeInformation type = property.getTypeInformation();
+		Object value = propertyAccessor.getProperty(property);
+
+		RelationalPersistentEntity nestedEntity = getMappingContext()
+				.getPersistentEntity(type.getRequiredActualType().getType());
+
+		if (nestedEntity != null) {
+			throw new InvalidDataAccessApiUsageException("Nested entities are not supported");
+		}
+
+		return value;
+	}
+
+	public Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
+
+		Class targetType = arrayColumns.getArrayType(property.getActualType());
+
+		if (!property.isArray() || !property.getActualType().equals(targetType)) {
+
+			Object zeroLengthArray = Array.newInstance(targetType, 0);
+			return getConversionService().convert(value, zeroLengthArray.getClass());
+		}
+
+		return value;
+	}
+
+	// ----------------------------------
+	// Id handling
+	// ----------------------------------
+
 	/**
 	 * Returns a {@link java.util.function.Function} that populates the id property of the {@code object} from a
 	 * {@link Row}.
@@ -113,7 +263,7 @@ public class MappingR2dbcConverter {
 
 		if (generatedIdValue != null) {
 
-			ConversionService conversionService = relationalConverter.getConversionService();
+			ConversionService conversionService = getConversionService();
 			propertyAccessor.setProperty(idProperty, conversionService.convert(generatedIdValue, idProperty.getType()));
 			return true;
 		}
@@ -121,6 +271,11 @@ public class MappingR2dbcConverter {
 		return false;
 	}
 
+	@SuppressWarnings("unchecked")
+	private  RelationalPersistentEntity getRequiredPersistentEntity(Class type) {
+		return (RelationalPersistentEntity) getMappingContext().getRequiredPersistentEntity(type);
+	}
+
 	private static Map createMetadataMap(RowMetadata metadata) {
 
 		Map columns = new LinkedHashMap<>();
@@ -132,7 +287,30 @@ public class MappingR2dbcConverter {
 		return columns;
 	}
 
-	public MappingContext, ? extends RelationalPersistentProperty> getMappingContext() {
-		return relationalConverter.getMappingContext();
+	@RequiredArgsConstructor
+	private static class RowParameterValueProvider implements ParameterValueProvider {
+
+		private final @NonNull Row resultSet;
+		private final @NonNull RelationalPersistentEntity entity;
+		private final @NonNull RelationalConverter converter;
+		private final @NonNull String prefix;
+
+		/*
+		 * (non-Javadoc)
+		 * @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
+		 */
+		@Override
+		@Nullable
+		public  T getParameterValue(Parameter parameter) {
+
+			RelationalPersistentProperty property = entity.getRequiredPersistentProperty(parameter.getName());
+			String column = prefix + property.getColumnName();
+
+			try {
+				return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType());
+			} catch (Exception o_O) {
+				throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O);
+			}
+		}
 	}
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java b/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java
new file mode 100644
index 00000000..1ea2cc5e
--- /dev/null
+++ b/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java
@@ -0,0 +1,235 @@
+/*
+ * 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
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.data.r2dbc.function.convert;
+
+import io.r2dbc.spi.Row;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+import org.springframework.util.Assert;
+
+/**
+ * Representation of a {@link Row} to be written through a {@code INSERT} or {@code UPDATE} statement.
+ *
+ * @author Mark Paluch
+ * @see SettableValue
+ */
+public class OutboundRow implements Map {
+
+	private final Map rowAsMap;
+
+	/**
+	 * Creates an empty {@link OutboundRow} instance.
+	 */
+	public OutboundRow() {
+		rowAsMap = new LinkedHashMap<>();
+	}
+
+	/**
+	 * Creates a new {@link OutboundRow} from a {@link Map}.
+	 *
+	 * @param map the map used to initialize the {@link OutboundRow}.
+	 */
+	public OutboundRow(Map map) {
+
+		Assert.notNull(map, "Map must not be null");
+
+		rowAsMap = new LinkedHashMap<>(map);
+	}
+
+	/**
+	 * Create a {@link OutboundRow} instance initialized with the given key/value pair.
+	 *
+	 * @param key key.
+	 * @param value value.
+	 */
+	public OutboundRow(String key, SettableValue value) {
+		rowAsMap = new LinkedHashMap<>();
+		rowAsMap.put(key, value);
+	}
+
+	/**
+	 * Put the given key/value pair into this {@link OutboundRow} and return this. Useful for chaining puts in a single
+	 * expression:
+	 *
+	 * 
+	 * row.append("a", 1).append("b", 2)}
+	 * 
+ * + * @param key key. + * @param value value. + * @return this + */ + public OutboundRow append(String key, SettableValue value) { + rowAsMap.put(key, value); + return this; + } + + /* + * (non-Javadoc) + * @see java.util.Map#size() + */ + @Override + public int size() { + return rowAsMap.size(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#isEmpty() + */ + @Override + public boolean isEmpty() { + return rowAsMap.isEmpty(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#containsKey(java.lang.Object) + */ + @Override + public boolean containsKey(Object key) { + return rowAsMap.containsKey(key); + } + + /* + * (non-Javadoc) + * @see java.util.Map#containsValue(java.lang.Object) + */ + @Override + public boolean containsValue(Object value) { + return rowAsMap.containsValue(value); + } + + /* + * (non-Javadoc) + * @see java.util.Map#get(java.lang.Object) + */ + @Override + public SettableValue get(Object key) { + return rowAsMap.get(key); + } + + /* + * (non-Javadoc) + * @see java.util.Map#put(java.lang.Object, java.lang.Object) + */ + @Override + public SettableValue put(String key, SettableValue value) { + return rowAsMap.put(key, value); + } + + /* + * (non-Javadoc) + * @see java.util.Map#remove(java.lang.Object) + */ + @Override + public SettableValue remove(Object key) { + return rowAsMap.remove(key); + } + + /* + * (non-Javadoc) + * @see java.util.Map#putAll(java.util.Map) + */ + @Override + public void putAll(Map m) { + rowAsMap.putAll(m); + } + + /* + * (non-Javadoc) + * @see java.util.Map#clear() + */ + @Override + public void clear() { + rowAsMap.clear(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#keySet() + */ + @Override + public Set keySet() { + return rowAsMap.keySet(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#values() + */ + @Override + public Collection values() { + return rowAsMap.values(); + } + + /* + * (non-Javadoc) + * @see java.util.Map#entrySet() + */ + @Override + public Set> entrySet() { + return rowAsMap.entrySet(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(final Object o) { + + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + OutboundRow row = (OutboundRow) o; + + return rowAsMap.equals(row.rowAsMap); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#hashCode() + */ + @Override + public int hashCode() { + return rowAsMap.hashCode(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return "OutboundRow[" + rowAsMap + "]"; + } + + @Override + public void forEach(BiConsumer action) { + rowAsMap.forEach(action); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java new file mode 100644 index 00000000..6cf89316 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java @@ -0,0 +1,73 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.convert; + +import io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; + +import java.util.function.BiFunction; + +import org.springframework.core.convert.ConversionService; +import org.springframework.data.convert.EntityReader; +import org.springframework.data.convert.EntityWriter; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.dialect.ArrayColumns; +import org.springframework.data.relational.core.conversion.RelationalConverter; +import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; +import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; + +/** + * Central R2DBC specific converter interface. + * + * @author Mark Paluch + * @see EntityReader + */ +public interface R2dbcConverter + extends EntityReader, EntityWriter, RelationalConverter { + + /** + * Returns the underlying {@link MappingContext} used by the converter. + * + * @return never {@literal null} + */ + MappingContext, ? extends RelationalPersistentProperty> getMappingContext(); + + /** + * Returns the underlying {@link ConversionService} used by the converter. + * + * @return never {@literal null}. + */ + ConversionService getConversionService(); + + /** + * Convert a {@code value} into an array representation according to {@link ArrayColumns}. + * + * @param arrayColumns dialect-specific array handling configuration. + * @param property + * @param value + * @return + */ + Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value); + + /** + * Returns a {@link java.util.function.Function} that populates the id property of the {@code object} from a + * {@link Row}. + * + * @param object must not be {@literal null}. + * @return + */ + BiFunction populateIdIfNecessary(T object); +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java index fe4e8118..edce1794 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java @@ -15,57 +15,87 @@ */ package org.springframework.data.r2dbc.function.convert; +import java.util.Objects; + import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * A database value that can be set in a statement. * * @author Mark Paluch + * @see OutboundRow */ public class SettableValue { - private final Object identifier; private final @Nullable Object value; private final Class type; /** - * Create a {@link SettableValue} using an integer index. + * Create a {@link SettableValue}. * - * @param index * @param value * @param type */ - public SettableValue(int index, @Nullable Object value, Class type) { + public SettableValue(@Nullable Object value, Class type) { + + Assert.notNull(type, "Type must not be null"); - this.identifier = index; this.value = value; this.type = type; } /** - * Create a {@link SettableValue} using a {@link String} identifier. + * Returns the column value. Can be {@literal null}. * - * @param identifier - * @param value - * @param type + * @return the column value. Can be {@literal null}. + * @see #hasValue() */ - public SettableValue(String identifier, @Nullable Object value, Class type) { - - this.identifier = identifier; - this.value = value; - this.type = type; - } - - public Object getIdentifier() { - return identifier; - } - @Nullable public Object getValue() { return value; } + /** + * Returns the column value type. Must be also present if the {@code value} is {@literal null}. + * + * @return the column value type + */ public Class getType() { return type; } + + /** + * Returns whether this {@link SettableValue} has a value. + * + * @return whether this {@link SettableValue} has a value. {@literal false} if {@link #getValue()} is {@literal null}. + */ + public boolean hasValue() { + return value != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof SettableValue)) + return false; + SettableValue value1 = (SettableValue) o; + return Objects.equals(value, value1.value) && Objects.equals(type, value1.type); + } + + @Override + public int hashCode() { + return Objects.hash(value, type); + } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer(); + sb.append(getClass().getSimpleName()); + sb.append(" [value=").append(value); + sb.append(", type=").append(type); + sb.append(']'); + return sb.toString(); + } } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java index 5cb02f92..dc15e384 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java @@ -19,12 +19,13 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.reactivestreams.Publisher; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.FetchSpec; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter; import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; @@ -44,7 +45,7 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { private final R2dbcQueryMethod method; private final DatabaseClient databaseClient; - private final MappingR2dbcConverter converter; + private final R2dbcConverter converter; private final EntityInstantiators instantiators; /** @@ -54,11 +55,11 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery { * @param databaseClient must not be {@literal null}. * @param converter must not be {@literal null}. */ - public AbstractR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, MappingR2dbcConverter converter) { + public AbstractR2dbcQuery(R2dbcQueryMethod method, DatabaseClient databaseClient, R2dbcConverter converter) { Assert.notNull(method, "R2dbcQueryMethod must not be null!"); Assert.notNull(databaseClient, "DatabaseClient must not be null!"); - Assert.notNull(converter, "MappingR2dbcConverter must not be null!"); + Assert.notNull(converter, "R2dbcConverter must not be null!"); this.method = method; this.databaseClient = databaseClient; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 11b21092..bfa034d3 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -17,7 +17,7 @@ package org.springframework.data.r2dbc.repository.query; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -46,9 +46,8 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { * @param expressionParser must not be {@literal null}. * @param evaluationContextProvider must not be {@literal null}. */ - public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, DatabaseClient databaseClient, - MappingR2dbcConverter converter, SpelExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { + public StringBasedR2dbcQuery(R2dbcQueryMethod queryMethod, DatabaseClient databaseClient, R2dbcConverter converter, + SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { this(queryMethod.getRequiredAnnotatedQuery(), queryMethod, databaseClient, converter, expressionParser, evaluationContextProvider); @@ -65,7 +64,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { * @param evaluationContextProvider must not be {@literal null}. */ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, DatabaseClient databaseClient, - MappingR2dbcConverter converter, SpelExpressionParser expressionParser, + R2dbcConverter converter, SpelExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { super(method, databaseClient, converter); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 78e80d12..0c82c087 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -25,11 +25,10 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod; import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.RelationalEntityInformation; @@ -56,28 +55,25 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private final DatabaseClient databaseClient; - private final MappingContext, RelationalPersistentProperty> mappingContext; - private final MappingR2dbcConverter converter; + private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; + private final R2dbcConverter converter; private final ReactiveDataAccessStrategy dataAccessStrategy; /** * Creates a new {@link R2dbcRepositoryFactory} given {@link DatabaseClient} and {@link MappingContext}. * * @param databaseClient must not be {@literal null}. - * @param mappingContext must not be {@literal null}. + * @param dataAccessStrategy must not be {@literal null}. */ - public R2dbcRepositoryFactory(DatabaseClient databaseClient, - MappingContext, RelationalPersistentProperty> mappingContext, - ReactiveDataAccessStrategy dataAccessStrategy) { + public R2dbcRepositoryFactory(DatabaseClient databaseClient, ReactiveDataAccessStrategy dataAccessStrategy) { Assert.notNull(databaseClient, "DatabaseClient must not be null!"); - Assert.notNull(mappingContext, "MappingContext must not be null!"); Assert.notNull(dataAccessStrategy, "ReactiveDataAccessStrategy must not be null!"); this.databaseClient = databaseClient; - this.mappingContext = mappingContext; + this.converter = dataAccessStrategy.getConverter(); + this.mappingContext = this.converter.getMappingContext(); this.dataAccessStrategy = dataAccessStrategy; - this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext)); } /* @@ -140,7 +136,7 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { private final DatabaseClient databaseClient; private final QueryMethodEvaluationContextProvider evaluationContextProvider; - private final MappingR2dbcConverter converter; + private final R2dbcConverter converter; /* * (non-Javadoc) @@ -163,7 +159,6 @@ public class R2dbcRepositoryFactory extends ReactiveRepositoryFactorySupport { } throw new UnsupportedOperationException("Query derivation not yet supported!"); - } } } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 766787ca..506f7ee5 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -20,9 +20,6 @@ import java.io.Serializable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -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.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; @@ -41,7 +38,6 @@ public class R2dbcRepositoryFactoryBean, S, ID exten extends RepositoryFactoryBeanSupport { private @Nullable DatabaseClient client; - private @Nullable MappingContext, RelationalPersistentProperty> mappingContext; private @Nullable ReactiveDataAccessStrategy dataAccessStrategy; private boolean mappingContextConfigured = false; @@ -69,14 +65,11 @@ public class R2dbcRepositoryFactoryBean, S, ID exten * @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#setMappingContext(org.springframework.data.mapping.context.MappingContext) */ @Override - @SuppressWarnings("unchecked") protected void setMappingContext(@Nullable MappingContext mappingContext) { super.setMappingContext(mappingContext); if (mappingContext != null) { - - this.mappingContext = (MappingContext, RelationalPersistentProperty>) mappingContext; this.mappingContextConfigured = true; } } @@ -91,19 +84,19 @@ public class R2dbcRepositoryFactoryBean, S, ID exten */ @Override protected final RepositoryFactorySupport createRepositoryFactory() { - return getFactoryInstance(client, this.mappingContext); + return getFactoryInstance(client, dataAccessStrategy); } /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * * @param client must not be {@literal null}. - * @param mappingContext must not be {@literal null}. + * @param dataAccessStrategy must not be {@literal null}. * @return new instance of {@link RepositoryFactorySupport}. */ protected RepositoryFactorySupport getFactoryInstance(DatabaseClient client, - MappingContext, RelationalPersistentProperty> mappingContext) { - return new R2dbcRepositoryFactory(client, mappingContext, dataAccessStrategy); + ReactiveDataAccessStrategy dataAccessStrategy) { + return new R2dbcRepositoryFactory(client, dataAccessStrategy); } /* @@ -117,7 +110,7 @@ public class R2dbcRepositoryFactoryBean, S, ID exten Assert.state(dataAccessStrategy != null, "ReactiveDataAccessStrategy must not be null!"); if (!mappingContextConfigured) { - setMappingContext(new RelationalMappingContext()); + setMappingContext(dataAccessStrategy.getConverter().getMappingContext()); } super.afterPropertiesSet(); diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index c6b99f5c..4b28127c 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -15,7 +15,6 @@ */ package org.springframework.data.r2dbc.repository.support; -import io.r2dbc.spi.Statement; import lombok.NonNull; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; @@ -26,18 +25,16 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.BiConsumer; import org.reactivestreams.Publisher; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.function.BindIdOperation; -import org.springframework.data.r2dbc.function.BindableOperation; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.core.sql.Conditions; import org.springframework.data.relational.core.sql.Expression; @@ -61,7 +58,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository entity; private final @NonNull DatabaseClient databaseClient; - private final @NonNull MappingR2dbcConverter converter; + private final @NonNull R2dbcConverter converter; private final @NonNull ReactiveDataAccessStrategy accessStrategy; /* (non-Javadoc) @@ -82,7 +79,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = accessStrategy.getColumnsToUpdate(objectToSave); + Map columns = accessStrategy.getOutboundRow(objectToSave); columns.remove(getIdColumnName()); // do not update the Id column. String idColumnName = getIdColumnName(); BindIdOperation update = accessStrategy.updateById(entity.getTableName(), columns.keySet(), idColumnName); @@ -90,7 +87,10 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecAdapter.create(exec); - columns.forEach(bind(update, wrapper)); + columns.forEach((k, v) -> { + update.bind(wrapper, k, v); + + }); update.bindId(wrapper, id); return wrapper.getBoundOperation().as(entity.getJavaType()) // @@ -236,11 +236,8 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository implements ReactiveCrudRepository bind(BindableOperation operation, Statement statement) { - - return (k, v) -> operation.bind(statement, v); - } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java index 2f1e51ec..59690d5b 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java @@ -12,6 +12,7 @@ import java.util.List; import java.util.Map; import org.junit.Test; + import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.function.convert.SettableValue; @@ -67,7 +68,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests { public void shouldUpdateArray() { Map columnsToUpdate = strategy - .getColumnsToUpdate(new WithCollectionTypes(new String[] { "one", "two" }, null)); + .getOutboundRow(new WithCollectionTypes(new String[] { "one", "two" }, null)); Object stringArray = columnsToUpdate.get("string_array").getValue(); @@ -79,7 +80,7 @@ public class DefaultReactiveDataAccessStrategyUnitTests { public void shouldConvertListToArray() { Map columnsToUpdate = strategy - .getColumnsToUpdate(new WithCollectionTypes(null, Arrays.asList("one", "two"))); + .getOutboundRow(new WithCollectionTypes(null, Arrays.asList("one", "two"))); Object stringArray = columnsToUpdate.get("string_collection").getValue(); diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java index 91dceff2..307d710e 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java @@ -13,9 +13,9 @@ import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; -import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; /** * Unit tests for {@link EntityRowMapper}. @@ -101,11 +101,8 @@ public class EntityRowMapperUnitTests { assertThat(result.boxedIntegers).contains(3, 11); } - @SuppressWarnings("unchecked") private EntityRowMapper getRowMapper(Class type) { - RelationalPersistentEntity entity = (RelationalPersistentEntity) strategy.getMappingContext() - .getRequiredPersistentEntity(type); - return new EntityRowMapper<>(entity, strategy.getRelationalConverter()); + return new EntityRowMapper<>(type, strategy.getConverter()); } static class SimpleEntity { diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java new file mode 100644 index 00000000..dbc9e4e7 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java @@ -0,0 +1,53 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.convert; + +import static org.assertj.core.api.Assertions.*; + +import lombok.AllArgsConstructor; + +import org.junit.Test; + +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; + +/** + * Unit tests for {@link MappingR2dbcConverter}. + * + * @author Mark Paluch + */ +public class MappingR2dbcConverterUnitTests { + + MappingR2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); + + @Test // gh-61 + public void shouldIncludeAllPropertiesInOutboundRow() { + + OutboundRow row = new OutboundRow(); + + converter.write(new Person("id", "Walter", "White"), row); + + assertThat(row).containsEntry("id", new SettableValue("id", String.class)); + assertThat(row).containsEntry("firstname", new SettableValue("Walter", String.class)); + assertThat(row).containsEntry("lastname", new SettableValue("White", String.class)); + } + + @AllArgsConstructor + static class Person { + @Id String id; + String firstname, lastname; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index ded79a63..7f92d93f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -34,15 +34,16 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.repository.NoRepositoryBean; @@ -161,11 +162,11 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg Database database = Database.findDatabase(createConnectionFactory()).get(); DefaultReactiveDataAccessStrategy dataAccessStrategy = new DefaultReactiveDataAccessStrategy( - database.defaultDialect(), new BasicRelationalConverter(mappingContext)); + database.defaultDialect(), new MappingR2dbcConverter(mappingContext)); TransactionalDatabaseClient client = TransactionalDatabaseClient.builder() .connectionFactory(createConnectionFactory()).dataAccessStrategy(dataAccessStrategy).build(); - LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, mappingContext, dataAccessStrategy) + LegoSetRepository transactionalRepository = new R2dbcRepositoryFactory(client, dataAccessStrategy) .getRepository(getRepositoryInterfaceType()); LegoSet legoSet1 = new LegoSet(null, "SCHAUFELRADBAGGER", 12); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index d40e212b..db97506e 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -19,11 +19,14 @@ import static org.mockito.Mockito.*; import org.junit.Test; import org.junit.runner.RunWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -48,7 +51,7 @@ public class R2dbcRepositoriesRegistrarTests { @Bean public ReactiveDataAccessStrategy reactiveDataAccessStrategy() { - return mock(ReactiveDataAccessStrategy.class); + return new DefaultReactiveDataAccessStrategy(new PostgresDialect()); } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index 9af26b2c..bd2aa716 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -26,12 +26,12 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; @@ -62,7 +62,7 @@ public class StringBasedR2dbcQueryUnitTests { public void setUp() { this.mappingContext = new RelationalMappingContext(); - this.converter = new MappingR2dbcConverter(new BasicRelationalConverter(this.mappingContext)); + this.converter = new MappingR2dbcConverter(this.mappingContext); this.metadata = AbstractRepositoryMetadata.getMetadata(SampleRepository.class); this.factory = new SpelAwareProxyProjectionFactory(); diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index b1c916d4..29e731e9 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -34,6 +34,7 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; @@ -41,7 +42,6 @@ import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; -import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.Table; @@ -74,7 +74,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); this.repository = new SimpleR2dbcRepository<>(entityInformation, databaseClient, - new MappingR2dbcConverter(new BasicRelationalConverter(mappingContext)), strategy); + new MappingR2dbcConverter(mappingContext), strategy); this.jdbc = createJdbcTemplate(createDataSource()); try { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 3efc75cf..a1119355 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -23,9 +23,11 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; + import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; @@ -40,31 +42,32 @@ import org.springframework.data.repository.Repository; public class R2dbcRepositoryFactoryUnitTests { @Mock DatabaseClient databaseClient; + @Mock R2dbcConverter r2dbcConverter; + @Mock ReactiveDataAccessStrategy dataAccessStrategy; @Mock @SuppressWarnings("rawtypes") MappingContext mappingContext; @Mock @SuppressWarnings("rawtypes") RelationalPersistentEntity entity; - @Mock ReactiveDataAccessStrategy dataAccessStrategy; @Before @SuppressWarnings("unchecked") public void before() { when(mappingContext.getRequiredPersistentEntity(Person.class)).thenReturn(entity); + when(dataAccessStrategy.getConverter()).thenReturn(r2dbcConverter); + when(r2dbcConverter.getMappingContext()).thenReturn(mappingContext); } @Test - @SuppressWarnings("unchecked") public void usesMappingRelationalEntityInformationIfMappingContextSet() { - R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext, dataAccessStrategy); + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, dataAccessStrategy); RelationalEntityInformation entityInformation = factory.getEntityInformation(Person.class); assertThat(entityInformation).isInstanceOf(MappingRelationalEntityInformation.class); } @Test - @SuppressWarnings("unchecked") public void createsRepositoryWithIdTypeLong() { - R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, mappingContext, dataAccessStrategy); + R2dbcRepositoryFactory factory = new R2dbcRepositoryFactory(databaseClient, dataAccessStrategy); MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); assertThat(repository).isNotNull(); From 455e9a59de4657730070677c37a1d3bfdfd9b398 Mon Sep 17 00:00:00 2001 From: Hebert Coelho Date: Thu, 21 Feb 2019 18:32:34 +0100 Subject: [PATCH 083/665] #41 - Add R2DBC converters. Original pull request: #65. --- .../repository/query/RowDataConverter.java | 220 ++++++++++++++++++ .../query/RowDataConverterTests.java | 125 ++++++++++ 2 files changed, 345 insertions(+) create mode 100644 src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java b/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java new file mode 100644 index 00000000..2b3c8aa9 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java @@ -0,0 +1,220 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.r2dbc.repository.query; + +import io.r2dbc.spi.Row; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Base class for row data converter. + * + * @author Hebert Coelho + */ +abstract class RowDataConverter { + private RowDataConverter() { + } + + /** + * @return A list of the registered converters + */ + static Collection getConvertersToRegister() { + List converters = new ArrayList<>(); + + converters.add(RowToBooleanConverter.INSTANCE); + converters.add(RowToLocalDateConverter.INSTANCE); + converters.add(RowToLocalDateTimeConverter.INSTANCE); + converters.add(RowToLocalTimeConverter.INSTANCE); + converters.add(RowToOffsetDateTimeConverter.INSTANCE); + converters.add(RowToStringConverter.INSTANCE); + converters.add(RowToUuidConverter.INSTANCE); + converters.add(RowToZonedDateTimeConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. + * + * @author Hebert Coelho + */ + public enum RowToBooleanConverter implements Converter { + INSTANCE; + + @Override + public Boolean convert(Row row) { + return row.get(0, Boolean.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDate} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(Row row) { + return row.get(0, LocalDate.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalDateTime convert(Row row) { + return row.get(0, LocalDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalTime convert(Row row) { + return row.get(0, LocalTime.class); + } + } + + /** + * Singleton converter factory to convert the first column of a {@link Row} to a {@link Number}. + *

+ * Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @see Byte + * @see Short + * @see Integer + * @see Long + * @see java.math.BigInteger + * @see Float + * @see Double + * @see java.math.BigDecimal + * + * @author Hebert Coelho + */ + public enum RowToNumberConverterFactory implements ConverterFactory { + INSTANCE; + + @Override + public Converter getConverter(Class targetType) { + Assert.notNull(targetType, "Target type must not be null"); + return new RowToNumber<>(targetType); + } + + private static final class RowToNumber implements Converter { + private final Class targetType; + + RowToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Row source) { + + Object object = source.get(0, targetType); + + return (object != null ? NumberUtils.convertNumberToTargetClass((Number) object, this.targetType) : null); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link OffsetDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToOffsetDateTimeConverter implements Converter { + INSTANCE; + + @Override + public OffsetDateTime convert(Row row) { + return row.get(0, OffsetDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link String} representation. + * + * @author Hebert Coelho + */ + public enum RowToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(Row row) { + return row.get(0, String.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link UUID} representation. + * + * @author Hebert Coelho + */ + public enum RowToUuidConverter implements Converter { + INSTANCE; + + @Override + public UUID convert(Row row) { + return row.get(0, UUID.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link ZonedDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToZonedDateTimeConverter implements Converter { + INSTANCE; + + @Override + public ZonedDateTime convert(Row row) { + return row.get(0, ZonedDateTime.class); + } + } + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java new file mode 100644 index 00000000..413553cf --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java @@ -0,0 +1,125 @@ +package org.springframework.data.r2dbc.repository.query; + +import io.r2dbc.spi.Row; +import org.junit.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToBooleanConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RowDataConverterTests { + private static final int TOTAL_REGISTERED_CONVERTERS = 8; + + @Test + public void isReturningAllCreatedConverts() { + assertThat(RowDataConverter.getConvertersToRegister().size()) + .isEqualTo(TOTAL_REGISTERED_CONVERTERS); + } + + @Test + public void isConvertingBoolean() { + Row row = mock(Row.class); + when(row.get(0, Boolean.class)).thenReturn(true); + + assertTrue(RowToBooleanConverter.INSTANCE.convert(row)); + } + + @Test + public void isConvertingLocalDate() { + LocalDate now = LocalDate.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDate.class)).thenReturn(now); + + assertThat(RowToLocalDateConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingLocalDateTime() { + LocalDateTime now = LocalDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDateTime.class)).thenReturn(now); + + assertThat(RowToLocalDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingLocalTime() { + LocalTime now = LocalTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalTime.class)).thenReturn(now); + + assertThat(RowToLocalTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingOffsetDateTime() { + OffsetDateTime now = OffsetDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, OffsetDateTime.class)).thenReturn(now); + + assertThat(RowToOffsetDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingString() { + String value = "aValue"; + Row row = mock(Row.class); + when(row.get(0, String.class)).thenReturn(value); + + assertThat(RowToStringConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test + public void isConvertingUUID() { + UUID value = UUID.randomUUID(); + Row row = mock(Row.class); + when(row.get(0, UUID.class)).thenReturn(value); + + assertThat(RowToUuidConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test + public void isConvertingZonedDateTime() { + ZonedDateTime now = ZonedDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, ZonedDateTime.class)).thenReturn(now); + + assertThat(RowToZonedDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingNumber() { + Row row = mock(Row.class); + when(row.get(0, Integer.class)).thenReturn(33); + + final Converter converter = RowToNumberConverterFactory.INSTANCE.getConverter(Integer.class); + + assertThat(converter.convert(row)).isEqualTo(33); + } + + @Test + public void isRaisingExceptionForInvalidNumber() { + assertThatIllegalArgumentException().isThrownBy( + () -> RowToNumberConverterFactory.INSTANCE.getConverter(null) + ); + } +} From 369522231ffcbab9e03d823cdea162f22692cd42 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Sun, 10 Mar 2019 11:29:11 +0100 Subject: [PATCH 084/665] #41 - Polishing. Rename RowDataConverter to R2dbcConverters. Introduce R2dbcSimpleTypeHolder. Apply custom conversions check in MappingR2dbcConverter. Extend tests. Original pull request: #65. --- .../config/AbstractR2dbcConfiguration.java | 3 +- .../data/r2dbc/dialect/Dialect.java | 7 +- .../r2dbc/dialect/R2dbcSimpleTypeHolder.java | 46 ++++ .../convert/MappingR2dbcConverter.java | 16 +- .../function/convert/R2dbcConverters.java | 233 ++++++++++++++++++ .../convert/R2dbcCustomConversions.java | 31 ++- .../repository/query/RowDataConverter.java | 220 ----------------- .../MappingR2dbcConverterUnitTests.java | 23 ++ .../convert/R2dbcConvertersUnitTests.java | 150 +++++++++++ ...stractR2dbcRepositoryIntegrationTests.java | 15 ++ ...stgresR2dbcRepositoryIntegrationTests.java | 5 + ...ServerR2dbcRepositoryIntegrationTests.java | 5 + .../query/RowDataConverterTests.java | 125 ---------- 13 files changed, 529 insertions(+), 350 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java delete mode 100644 src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java delete mode 100644 src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index d32a74df..8d7f8fa3 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -17,7 +17,6 @@ package org.springframework.data.r2dbc.config; import io.r2dbc.spi.ConnectionFactory; -import java.util.Collections; import java.util.Optional; import org.springframework.context.annotation.Bean; @@ -153,7 +152,7 @@ public abstract class AbstractR2dbcConfiguration { Dialect dialect = getDialect(connectionFactory()); StoreConversions storeConversions = StoreConversions.of(dialect.getSimpleTypeHolder()); - return new R2dbcCustomConversions(storeConversions, Collections.emptyList()); + return new R2dbcCustomConversions(storeConversions, R2dbcCustomConversions.STORE_CONVERTERS); } /** diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java index be1d493f..9314527a 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java @@ -3,6 +3,7 @@ package org.springframework.data.r2dbc.dialect; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Set; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.r2dbc.dialect.ArrayColumns.Unsupported; @@ -40,7 +41,11 @@ public interface Dialect { * @see #getSimpleTypes() */ default SimpleTypeHolder getSimpleTypeHolder() { - return new SimpleTypeHolder(new HashSet<>(getSimpleTypes()), true); + + Set> simpleTypes = new HashSet<>(getSimpleTypes()); + simpleTypes.addAll(R2dbcSimpleTypeHolder.R2DBC_SIMPLE_TYPES); + + return new SimpleTypeHolder(simpleTypes, true); } /** diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java new file mode 100644 index 00000000..d32d8156 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.dialect; + +import io.r2dbc.spi.Row; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Simple constant holder for a {@link SimpleTypeHolder} enriched with R2DBC specific simple types. + * + * @author Mark Paluch + */ +public class R2dbcSimpleTypeHolder extends SimpleTypeHolder { + + /** + * Set of R2DBC simple types. + */ + public static final Set> R2DBC_SIMPLE_TYPES = Collections.singleton(Row.class); + + public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder(); + + /** + * Create a new {@link R2dbcSimpleTypeHolder} instance. + */ + private R2dbcSimpleTypeHolder() { + super(R2DBC_SIMPLE_TYPES, true); + } + +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index e24c2dbb..f851534f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -42,6 +42,7 @@ import org.springframework.data.relational.core.conversion.BasicRelationalConver import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -61,7 +62,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R */ public MappingR2dbcConverter( MappingContext, ? extends RelationalPersistentProperty> context) { - super(context, new R2dbcCustomConversions(CustomConversions.StoreConversions.NONE, Collections.emptyList())); + super(context, new R2dbcCustomConversions(Collections.emptyList())); } /** @@ -81,6 +82,19 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R @Override public R read(Class type, Row row) { + + TypeInformation typeInfo = ClassTypeInformation.from(type); + Class rawType = typeInfo.getType(); + + if (Row.class.isAssignableFrom(rawType)) { + return type.cast(row); + } + + if (getConversions().hasCustomReadTarget(Row.class, rawType) + || getConversionService().canConvert(Row.class, rawType)) { + return getConversionService().convert(row, rawType); + } + return read(getRequiredPersistentEntity(type), row); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java new file mode 100644 index 00000000..62b3d412 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java @@ -0,0 +1,233 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.r2dbc.function.convert; + +import io.r2dbc.spi.Row; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +/** + * Wrapper class to contain useful converters for the usage with R2DBC. + * + * @author Hebert Coelho + * @author Mark Paluch + */ +abstract class R2dbcConverters { + + private R2dbcConverters() {} + + /** + * @return A list of the registered converters + */ + public static Collection getConvertersToRegister() { + + List converters = new ArrayList<>(); + + converters.add(RowToBooleanConverter.INSTANCE); + converters.add(RowToNumberConverterFactory.INSTANCE); + converters.add(RowToLocalDateConverter.INSTANCE); + converters.add(RowToLocalDateTimeConverter.INSTANCE); + converters.add(RowToLocalTimeConverter.INSTANCE); + converters.add(RowToOffsetDateTimeConverter.INSTANCE); + converters.add(RowToStringConverter.INSTANCE); + converters.add(RowToUuidConverter.INSTANCE); + converters.add(RowToZonedDateTimeConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. + * + * @author Hebert Coelho + */ + public enum RowToBooleanConverter implements Converter { + + INSTANCE; + + @Override + public Boolean convert(Row row) { + return row.get(0, Boolean.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDate} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateConverter implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(Row row) { + return row.get(0, LocalDate.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalDateTime convert(Row row) { + return row.get(0, LocalDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalTimeConverter implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(Row row) { + return row.get(0, LocalTime.class); + } + } + + /** + * Singleton converter factory to convert the first column of a {@link Row} to a {@link Number}. + *

+ * Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @see Byte + * @see Short + * @see Integer + * @see Long + * @see java.math.BigInteger + * @see Float + * @see Double + * @see java.math.BigDecimal + * @author Hebert Coelho + */ + public enum RowToNumberConverterFactory implements ConverterFactory { + + INSTANCE; + + @Override + public Converter getConverter(Class targetType) { + Assert.notNull(targetType, "Target type must not be null"); + return new RowToNumber<>(targetType); + } + + static class RowToNumber implements Converter { + + private final Class targetType; + + RowToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Row source) { + + Object object = source.get(0, targetType); + + return (object != null ? NumberUtils.convertNumberToTargetClass((Number) object, this.targetType) : null); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link OffsetDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToOffsetDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public OffsetDateTime convert(Row row) { + return row.get(0, OffsetDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link String} representation. + * + * @author Hebert Coelho + */ + public enum RowToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Row row) { + return row.get(0, String.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link UUID} representation. + * + * @author Hebert Coelho + */ + public enum RowToUuidConverter implements Converter { + + INSTANCE; + + @Override + public UUID convert(Row row) { + return row.get(0, UUID.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link ZonedDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToZonedDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public ZonedDateTime convert(Row row) { + return row.get(0, ZonedDateTime.class); + } + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java index 8c3a692e..da6be5fa 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java @@ -1,8 +1,13 @@ package org.springframework.data.r2dbc.function.convert; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.List; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.JodaTimeConverters; +import org.springframework.data.r2dbc.dialect.R2dbcSimpleTypeHolder; /** * Value object to capture custom conversion. {@link R2dbcCustomConversions} also act as factory for @@ -14,8 +19,32 @@ import org.springframework.data.convert.CustomConversions; */ public class R2dbcCustomConversions extends CustomConversions { + public static final List STORE_CONVERTERS; + + private static final StoreConversions STORE_CONVERSIONS; + + static { + + List converters = new ArrayList<>(); + + converters.addAll(R2dbcConverters.getConvertersToRegister()); + converters.addAll(JodaTimeConverters.getConvertersToRegister()); + + STORE_CONVERTERS = Collections.unmodifiableList(converters); + STORE_CONVERSIONS = StoreConversions.of(R2dbcSimpleTypeHolder.HOLDER, STORE_CONVERTERS); + } + /** - * Creates a new {@link CustomConversions} instance registering the given converters. + * Creates a new {@link R2dbcCustomConversions} instance registering the given converters. + * + * @param converters must not be {@literal null}. + */ + public R2dbcCustomConversions(Collection converters) { + super(STORE_CONVERSIONS, converters); + } + + /** + * Creates a new {@link R2dbcCustomConversions} instance registering the given converters. * * @param storeConversions must not be {@literal null}. * @param converters must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java b/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java deleted file mode 100644 index 2b3c8aa9..00000000 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright 2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.data.r2dbc.repository.query; - -import io.r2dbc.spi.Row; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterFactory; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; -import org.springframework.util.Assert; -import org.springframework.util.NumberUtils; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.UUID; - -/** - * Base class for row data converter. - * - * @author Hebert Coelho - */ -abstract class RowDataConverter { - private RowDataConverter() { - } - - /** - * @return A list of the registered converters - */ - static Collection getConvertersToRegister() { - List converters = new ArrayList<>(); - - converters.add(RowToBooleanConverter.INSTANCE); - converters.add(RowToLocalDateConverter.INSTANCE); - converters.add(RowToLocalDateTimeConverter.INSTANCE); - converters.add(RowToLocalTimeConverter.INSTANCE); - converters.add(RowToOffsetDateTimeConverter.INSTANCE); - converters.add(RowToStringConverter.INSTANCE); - converters.add(RowToUuidConverter.INSTANCE); - converters.add(RowToZonedDateTimeConverter.INSTANCE); - - return converters; - } - - /** - * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. - * - * @author Hebert Coelho - */ - public enum RowToBooleanConverter implements Converter { - INSTANCE; - - @Override - public Boolean convert(Row row) { - return row.get(0, Boolean.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link LocalDate} representation. - * - * @author Hebert Coelho - */ - public enum RowToLocalDateConverter implements Converter { - INSTANCE; - - @Override - public LocalDate convert(Row row) { - return row.get(0, LocalDate.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link LocalDateTime} representation. - * - * @author Hebert Coelho - */ - public enum RowToLocalDateTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalDateTime convert(Row row) { - return row.get(0, LocalDateTime.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link LocalTime} representation. - * - * @author Hebert Coelho - */ - public enum RowToLocalTimeConverter implements Converter { - INSTANCE; - - @Override - public LocalTime convert(Row row) { - return row.get(0, LocalTime.class); - } - } - - /** - * Singleton converter factory to convert the first column of a {@link Row} to a {@link Number}. - *

- * Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class - * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. - * - * @see Byte - * @see Short - * @see Integer - * @see Long - * @see java.math.BigInteger - * @see Float - * @see Double - * @see java.math.BigDecimal - * - * @author Hebert Coelho - */ - public enum RowToNumberConverterFactory implements ConverterFactory { - INSTANCE; - - @Override - public Converter getConverter(Class targetType) { - Assert.notNull(targetType, "Target type must not be null"); - return new RowToNumber<>(targetType); - } - - private static final class RowToNumber implements Converter { - private final Class targetType; - - RowToNumber(Class targetType) { - this.targetType = targetType; - } - - @Override - public T convert(Row source) { - - Object object = source.get(0, targetType); - - return (object != null ? NumberUtils.convertNumberToTargetClass((Number) object, this.targetType) : null); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link OffsetDateTime} representation. - * - * @author Hebert Coelho - */ - public enum RowToOffsetDateTimeConverter implements Converter { - INSTANCE; - - @Override - public OffsetDateTime convert(Row row) { - return row.get(0, OffsetDateTime.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link String} representation. - * - * @author Hebert Coelho - */ - public enum RowToStringConverter implements Converter { - INSTANCE; - - @Override - public String convert(Row row) { - return row.get(0, String.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link UUID} representation. - * - * @author Hebert Coelho - */ - public enum RowToUuidConverter implements Converter { - INSTANCE; - - @Override - public UUID convert(Row row) { - return row.get(0, UUID.class); - } - } - - /** - * Simple singleton to convert {@link Row}s to their {@link ZonedDateTime} representation. - * - * @author Hebert Coelho - */ - public enum RowToZonedDateTimeConverter implements Converter { - INSTANCE; - - @Override - public ZonedDateTime convert(Row row) { - return row.get(0, ZonedDateTime.class); - } - } - } -} diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java index dbc9e4e7..9fcbe4c0 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java @@ -16,7 +16,9 @@ package org.springframework.data.r2dbc.function.convert; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import io.r2dbc.spi.Row; import lombok.AllArgsConstructor; import org.junit.Test; @@ -45,6 +47,27 @@ public class MappingR2dbcConverterUnitTests { assertThat(row).containsEntry("lastname", new SettableValue("White", String.class)); } + @Test // gh-41 + public void shouldPassThroughRow() { + + Row rowMock = mock(Row.class); + + Row result = converter.read(Row.class, rowMock); + + assertThat(result).isSameAs(rowMock); + } + + @Test // gh-41 + public void shouldConvertRowToNumber() { + + Row rowMock = mock(Row.class); + when(rowMock.get(0, Integer.class)).thenReturn(42); + + Integer result = converter.read(Integer.class, rowMock); + + assertThat(result).isEqualTo(42); + } + @AllArgsConstructor static class Person { @Id String id; diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java new file mode 100644 index 00000000..bbbf931b --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java @@ -0,0 +1,150 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.convert; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Row; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.UUID; + +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToBooleanConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalDateConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalDateTimeConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalTimeConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; + +/** + * Unit tests for {@link R2dbcConverters}. + * + * @author Hebert Coelho + * @author Mark Paluch + */ +public class R2dbcConvertersUnitTests { + + @Test // gh-41 + public void isReturningAllCreatedConverts() { + assertThat(R2dbcConverters.getConvertersToRegister()).hasSize(9); + } + + @Test // gh-41 + public void isConvertingBoolean() { + + Row row = mock(Row.class); + when(row.get(0, Boolean.class)).thenReturn(true); + + assertThat(RowToBooleanConverter.INSTANCE.convert(row)).isTrue(); + } + + @Test // gh-41 + public void isConvertingLocalDate() { + + LocalDate now = LocalDate.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDate.class)).thenReturn(now); + + assertThat(RowToLocalDateConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test // gh-41 + public void isConvertingLocalDateTime() { + + LocalDateTime now = LocalDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDateTime.class)).thenReturn(now); + + assertThat(RowToLocalDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test // gh-41 + public void isConvertingLocalTime() { + + LocalTime now = LocalTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalTime.class)).thenReturn(now); + + assertThat(RowToLocalTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test // gh-41 + public void isConvertingOffsetDateTime() { + + OffsetDateTime now = OffsetDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, OffsetDateTime.class)).thenReturn(now); + + assertThat(RowToOffsetDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test // gh-41 + public void isConvertingString() { + + String value = "aValue"; + Row row = mock(Row.class); + when(row.get(0, String.class)).thenReturn(value); + + assertThat(RowToStringConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test // gh-41 + public void isConvertingUUID() { + + UUID value = UUID.randomUUID(); + Row row = mock(Row.class); + when(row.get(0, UUID.class)).thenReturn(value); + + assertThat(RowToUuidConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test // gh-41 + public void isConvertingZonedDateTime() { + + ZonedDateTime now = ZonedDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, ZonedDateTime.class)).thenReturn(now); + + assertThat(RowToZonedDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test // gh-41 + public void isConvertingNumber() { + + Row row = mock(Row.class); + when(row.get(0, Integer.class)).thenReturn(33); + + final Converter converter = RowToNumberConverterFactory.INSTANCE.getConverter(Integer.class); + + assertThat(converter.convert(row)).isEqualTo(33); + } + + @Test // gh-41 + public void isRaisingExceptionForInvalidNumber() { + assertThatIllegalArgumentException().isThrownBy(() -> RowToNumberConverterFactory.INSTANCE.getConverter(null)); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 7f92d93f..6e880357 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -157,6 +157,19 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg }).verifyComplete(); } + @Test // gh-41 + public void shouldFindApplyingSimpleTypeProjection() { + + shouldInsertNewItems(); + + repository.findAllIds() // + .collectList() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(2).allMatch(Integer.class::isInstance); + }).verifyComplete(); + } + @Test public void shouldInsertItemsTransactional() { @@ -196,6 +209,8 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg Flux findAsProjection(); Mono findByManual(int manual); + + Flux findAllIds(); } @Data diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index 37ec7fce..626cb40a 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -23,6 +23,7 @@ import javax.sql.DataSource; import org.junit.ClassRule; import org.junit.runner.RunWith; + import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @@ -90,5 +91,9 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi @Override @Query("SELECT * FROM legoset WHERE manual = :manual") Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java index 7a537540..8661d9f0 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java @@ -24,6 +24,7 @@ import javax.sql.DataSource; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.runner.RunWith; + import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; @@ -95,5 +96,9 @@ public class SqlServerR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepos @Override @Query("SELECT * FROM legoset WHERE manual = :manual") Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java deleted file mode 100644 index 413553cf..00000000 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.springframework.data.r2dbc.repository.query; - -import io.r2dbc.spi.Row; -import org.junit.Test; -import org.springframework.core.convert.converter.Converter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToBooleanConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateTimeConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalTimeConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; -import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class RowDataConverterTests { - private static final int TOTAL_REGISTERED_CONVERTERS = 8; - - @Test - public void isReturningAllCreatedConverts() { - assertThat(RowDataConverter.getConvertersToRegister().size()) - .isEqualTo(TOTAL_REGISTERED_CONVERTERS); - } - - @Test - public void isConvertingBoolean() { - Row row = mock(Row.class); - when(row.get(0, Boolean.class)).thenReturn(true); - - assertTrue(RowToBooleanConverter.INSTANCE.convert(row)); - } - - @Test - public void isConvertingLocalDate() { - LocalDate now = LocalDate.now(); - Row row = mock(Row.class); - when(row.get(0, LocalDate.class)).thenReturn(now); - - assertThat(RowToLocalDateConverter.INSTANCE.convert(row)).isEqualTo(now); - } - - @Test - public void isConvertingLocalDateTime() { - LocalDateTime now = LocalDateTime.now(); - Row row = mock(Row.class); - when(row.get(0, LocalDateTime.class)).thenReturn(now); - - assertThat(RowToLocalDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); - } - - @Test - public void isConvertingLocalTime() { - LocalTime now = LocalTime.now(); - Row row = mock(Row.class); - when(row.get(0, LocalTime.class)).thenReturn(now); - - assertThat(RowToLocalTimeConverter.INSTANCE.convert(row)).isEqualTo(now); - } - - @Test - public void isConvertingOffsetDateTime() { - OffsetDateTime now = OffsetDateTime.now(); - Row row = mock(Row.class); - when(row.get(0, OffsetDateTime.class)).thenReturn(now); - - assertThat(RowToOffsetDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); - } - - @Test - public void isConvertingString() { - String value = "aValue"; - Row row = mock(Row.class); - when(row.get(0, String.class)).thenReturn(value); - - assertThat(RowToStringConverter.INSTANCE.convert(row)).isEqualTo(value); - } - - @Test - public void isConvertingUUID() { - UUID value = UUID.randomUUID(); - Row row = mock(Row.class); - when(row.get(0, UUID.class)).thenReturn(value); - - assertThat(RowToUuidConverter.INSTANCE.convert(row)).isEqualTo(value); - } - - @Test - public void isConvertingZonedDateTime() { - ZonedDateTime now = ZonedDateTime.now(); - Row row = mock(Row.class); - when(row.get(0, ZonedDateTime.class)).thenReturn(now); - - assertThat(RowToZonedDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); - } - - @Test - public void isConvertingNumber() { - Row row = mock(Row.class); - when(row.get(0, Integer.class)).thenReturn(33); - - final Converter converter = RowToNumberConverterFactory.INSTANCE.getConverter(Integer.class); - - assertThat(converter.convert(row)).isEqualTo(33); - } - - @Test - public void isRaisingExceptionForInvalidNumber() { - assertThatIllegalArgumentException().isThrownBy( - () -> RowToNumberConverterFactory.INSTANCE.getConverter(null) - ); - } -} From 6fab5e65eae28616299ff351de21319e56d8d90b Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Tue, 5 Mar 2019 23:37:53 -0600 Subject: [PATCH 085/665] URL Cleanup This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed Success These URLs were fixed successfully. * http://projects.spring.io/spring-data-r2dbc migrated to: https://projects.spring.io/spring-data-r2dbc ([https](https://projects.spring.io/spring-data-r2dbc) result 301). # Ignored These URLs were intentionally ignored. * http://maven.apache.org/POM/4.0.0 * http://maven.apache.org/xsd/maven-4.0.0.xsd * http://www.w3.org/2001/XMLSchema-instance --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 2f3b6ed4..f50708c4 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ Spring Data R2DBC Spring Data module for R2DBC. - http://projects.spring.io/spring-data-r2dbc + https://projects.spring.io/spring-data-r2dbc org.springframework.data.build From 39936c67fa45afd6a60e811e69ca0c12bc487b00 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Mar 2019 11:25:11 +0100 Subject: [PATCH 086/665] #59 - Simplify CustomConversions bean construction. Original pull request: #70. --- .../r2dbc/config/AbstractR2dbcConfiguration.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 8d7f8fa3..f6570c6b 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -17,6 +17,7 @@ package org.springframework.data.r2dbc.config; import io.r2dbc.spi.ConnectionFactory; +import java.util.Collections; import java.util.Optional; import org.springframework.context.annotation.Bean; @@ -149,10 +150,18 @@ public abstract class AbstractR2dbcConfiguration { */ @Bean public R2dbcCustomConversions r2dbcCustomConversions() { + return new R2dbcCustomConversions(getStoreConversions(), Collections.emptyList()); + } + + /** + * Returns the {@link Dialect}-specific {@link StoreConversions}. + * + * @return the {@link Dialect}-specific {@link StoreConversions}. + */ + protected StoreConversions getStoreConversions() { Dialect dialect = getDialect(connectionFactory()); - StoreConversions storeConversions = StoreConversions.of(dialect.getSimpleTypeHolder()); - return new R2dbcCustomConversions(storeConversions, R2dbcCustomConversions.STORE_CONVERTERS); + return StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS); } /** From 6654db34c49e9b9f788cc66cd3f88c8094040165 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 12 Mar 2019 11:26:58 +0100 Subject: [PATCH 087/665] #59 - Consider custom conversion in MappingR2dbcConverter. MappingR2dbcConverter now considers custom conversions for inbound and outbound conversion of top-level types (Row to Entity, Entity to OutboundRow) and on property level (e.g. convert an object to String and vice versa). Original pull request: #70. --- src/main/asciidoc/reference/mapping.adoc | 86 ++++++- .../r2dbc/dialect/R2dbcSimpleTypeHolder.java | 6 +- .../convert => domain}/OutboundRow.java | 2 +- .../convert => domain}/SettableValue.java | 60 ++++- .../data/r2dbc/domain/package-info.java | 7 + .../r2dbc/function/BindableOperation.java | 2 +- .../r2dbc/function/DefaultDatabaseClient.java | 16 +- .../DefaultReactiveDataAccessStrategy.java | 6 +- .../function/MapBindParameterSource.java | 4 +- .../function/ReactiveDataAccessStrategy.java | 4 +- .../convert/MappingR2dbcConverter.java | 128 +++++++++-- .../function/convert/R2dbcConverter.java | 1 + .../support/SimpleR2dbcRepository.java | 2 +- .../r2dbc/domain/SettableValueUnitTests.java | 56 +++++ ...ltReactiveDataAccessStrategyUnitTests.java | 2 +- .../MappingR2dbcConverterUnitTests.java | 212 +++++++++++++++++- 16 files changed, 544 insertions(+), 50 deletions(-) rename src/main/java/org/springframework/data/r2dbc/{function/convert => domain}/OutboundRow.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function/convert => domain}/SettableValue.java (59%) create mode 100644 src/main/java/org/springframework/data/r2dbc/domain/package-info.java create mode 100644 src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 138ac9a4..7cd2b17a 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -28,6 +28,47 @@ Public `JavaBean` properties are not used. Otherwise, the zero-argument constructor is used. If there is more than one non-zero-argument constructor, an exception will be thrown. +[[mapping-configuration]] +== Mapping Configuration + +Unless explicitly configured, an instance of `MappingR2dbcConverter` is created by default when you create a `DatabaseClient`. +You can create your own instance of the `MappingR2dbcConverter`. +By creating your own instance, you can register Spring converters to map specific classes to and from the database. + +You can configure the `MappingR2dbcConverter` as well as `DatabaseClient` and `ConnectionFactory` by using Java-based metadata. The following example uses Spring's Java-based configuration: + +.@Configuration class to configure R2DBC mapping support +==== +[source,java] +---- +@Configuration +public class MyAppConfig extends AbstractR2dbcConfiguration { + + public ConnectionFactory connectionFactory() { + return ConnectionFactories.get("r2dbc:…"); + } + + // the following are optional + + @Bean + @Override + public R2dbcCustomConversions r2dbcCustomConversions() { + + List> converterList = new ArrayList>(); + converterList.add(new org.springframework.data.r2dbc.test.PersonReadConverter()); + converterList.add(new org.springframework.data.r2dbc.test.PersonWriteConverter()); + return new R2dbcCustomConversions(getStoreConversions(), converterList); + } +} +---- +==== + +`AbstractR2dbcConfiguration` requires you to implement a method that defines a `ConnectionFactory`. + +You can add additional converters to the converter by overriding the `r2dbcCustomConversions` method. + +NOTE: `AbstractR2dbcConfiguration` creates a `DatabaseClient` instance and registers it with the container under the name `databaseClient`. + [[mapping-usage]] == Metadata-based Mapping @@ -52,7 +93,6 @@ public class Person { private String firstName; - @Indexed private String lastName; } ---- @@ -103,3 +143,47 @@ class OrderItem { ---- +[[mapping-explicit-converters]] +=== Overriding Mapping with Explicit Converters + +When storing and querying your objects, it is convenient to have a `R2dbcConverter` instance handle the mapping of all Java types to `OutboundRow` instances. +However, sometimes you may want the `R2dbcConverter` instances do most of the work but let you selectively handle the conversion for a particular type -- perhaps to optimize performance. + +To selectively handle the conversion yourself, register one or more one or more `org.springframework.core.convert.converter.Converter` instances with the `R2dbcConverter`. + +You can use the `r2dbcCustomConversions` method in `AbstractR2dbcConfiguration` to configure converters. The examples <> show how to perform the configuration using Java. + +NOTE: Custom top-level entity conversion requires asymmetric types for conversion. Inbound data is extracted from R2DBC's `Row`. +Outbound data (to be used with `INSERT`/`UPDATE` statements) is represented as `OutboundRow` and later assembled to a statement. + +The following example of a Spring Converter implementation converts from a `Row` to a `Person` POJO: + +[source,java] +---- +@ReadingConverter + public class PersonReadConverter implements Converter { + + public Person convert(Row source) { + Person p = new Person(source.get("id", String.class),source.get("name", String.class)); + p.setAge(source.get("age", Integer.class)); + return p; + } +} +---- + +The following example converts from a `Person` to a `OutboundRow`: + +[source,java] +---- +@WritingConverter +public class PersonWriteConverter implements Converter { + + public OutboundRow convert(Person source) { + OutboundRow row = new OutboundRow(); + row.put("_d", source.getId()); + row.put("name", source.getFirstName()); + row.put("age", source.getAge()); + return row; + } +} +---- diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java index d32d8156..a02b96e3 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java @@ -17,10 +17,13 @@ package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Row; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import java.util.Set; import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.r2dbc.domain.OutboundRow; /** * Simple constant holder for a {@link SimpleTypeHolder} enriched with R2DBC specific simple types. @@ -32,7 +35,8 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder { /** * Set of R2DBC simple types. */ - public static final Set> R2DBC_SIMPLE_TYPES = Collections.singleton(Row.class); + public static final Set> R2DBC_SIMPLE_TYPES = Collections + .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class))); public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java rename to src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java index 1ea2cc5e..c73127fc 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/OutboundRow.java +++ b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.domain; import io.r2dbc.spi.Row; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java similarity index 59% rename from src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java rename to src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java index edce1794..6c322464 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.domain; import java.util.Objects; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** * A database value that can be set in a statement. @@ -31,13 +32,7 @@ public class SettableValue { private final @Nullable Object value; private final Class type; - /** - * Create a {@link SettableValue}. - * - * @param value - * @param type - */ - public SettableValue(@Nullable Object value, Class type) { + private SettableValue(@Nullable Object value, Class type) { Assert.notNull(type, "Type must not be null"); @@ -45,6 +40,42 @@ public class SettableValue { this.type = type; } + /** + * Creates a new {@link SettableValue} from {@code value}. + * + * @param value must not be {@literal null}. + * @return the {@link SettableValue} value for {@code value}. + */ + public static SettableValue from(Object value) { + + Assert.notNull(value, "Value must not be null"); + + return new SettableValue(value, ClassUtils.getUserClass(value)); + } + + /** + * Creates a new {@link SettableValue} from {@code value} and {@code type}. + * + * @param value can be {@literal null}. + * @param type must not be {@literal null}. + * @return the {@link SettableValue} value for {@code value}. + */ + public static SettableValue fromOrEmpty(@Nullable Object value, Class type) { + return value == null ? empty(type) : new SettableValue(value, ClassUtils.getUserClass(value)); + } + + /** + * Creates a new empty {@link SettableValue} for {@code type}. + * + * @return the empty {@link SettableValue} value for {@code type}. + */ + public static SettableValue empty(Class type) { + + Assert.notNull(type, "Type must not be null"); + + return new SettableValue(null, type); + } + /** * Returns the column value. Can be {@literal null}. * @@ -74,6 +105,15 @@ public class SettableValue { return value != null; } + /** + * Returns whether this {@link SettableValue} has a empty. + * + * @return whether this {@link SettableValue} is empty. {@literal true} if {@link #getValue()} is {@literal null}. + */ + public boolean isEmpty() { + return value == null; + } + @Override public boolean equals(Object o) { if (this == o) @@ -92,8 +132,8 @@ public class SettableValue { @Override public String toString() { final StringBuffer sb = new StringBuffer(); - sb.append(getClass().getSimpleName()); - sb.append(" [value=").append(value); + sb.append("SettableValue"); + sb.append("[value=").append(value); sb.append(", type=").append(type); sb.append(']'); return sb.toString(); diff --git a/src/main/java/org/springframework/data/r2dbc/domain/package-info.java b/src/main/java/org/springframework/data/r2dbc/domain/package-info.java new file mode 100644 index 00000000..2ca5d1d8 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/domain/package-info.java @@ -0,0 +1,7 @@ +/** + * Domain objects for R2DBC. + */ +@NonNullApi +package org.springframework.data.r2dbc.domain; + +import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java index 8038c3b0..941b2cfd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java +++ b/src/main/java/org/springframework/data/r2dbc/function/BindableOperation.java @@ -2,7 +2,7 @@ package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Statement; -import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.data.r2dbc.domain.SettableValue; /** * Extension to {@link QueryOperation} for operations that allow parameter substitution by binding parameter values. 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 8da6d444..cb33631f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -52,10 +52,10 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.UncategorizedR2dbcException; +import org.springframework.data.r2dbc.domain.OutboundRow; +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.convert.OutboundRow; -import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.jdbc.core.SqlProvider; import org.springframework.lang.Nullable; @@ -370,7 +370,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index)); Map byIndex = new LinkedHashMap<>(this.byIndex); - byIndex.put(index, new SettableValue(value, value.getClass())); + byIndex.put(index, SettableValue.fromOrEmpty(value, value.getClass())); return createInstance(byIndex, this.byName, this.sqlSupplier); } @@ -378,7 +378,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public ExecuteSpecSupport bindNull(int index, Class type) { Map byIndex = new LinkedHashMap<>(this.byIndex); - byIndex.put(index, new SettableValue(null, type)); + byIndex.put(index, SettableValue.empty(type)); return createInstance(byIndex, this.byName, this.sqlSupplier); } @@ -390,7 +390,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { () -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name)); Map byName = new LinkedHashMap<>(this.byName); - byName.put(name, new SettableValue(value, value.getClass())); + byName.put(name, SettableValue.fromOrEmpty(value, value.getClass())); return createInstance(this.byIndex, byName, this.sqlSupplier); } @@ -400,7 +400,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.hasText(name, "Parameter name must not be null or empty!"); Map byName = new LinkedHashMap<>(this.byName); - byName.put(name, new SettableValue(null, type)); + byName.put(name, SettableValue.empty(type)); return createInstance(this.byIndex, byName, this.sqlSupplier); } @@ -842,7 +842,7 @@ 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, new SettableValue(value, value.getClass())); + byName.put(field, SettableValue.fromOrEmpty(value, value.getClass())); return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); } @@ -853,7 +853,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { Assert.notNull(field, "Field must not be null!"); Map byName = new LinkedHashMap<>(this.byName); - byName.put(field, new SettableValue(null, type)); + byName.put(field, SettableValue.empty(type)); return new DefaultGenericInsertSpec<>(this.table, byName, this.mappingFunction); } 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 ca482d1f..0793dec9 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -41,12 +41,12 @@ import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.Dialect; +import org.springframework.data.r2dbc.domain.OutboundRow; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.convert.EntityRowMapper; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; -import org.springframework.data.r2dbc.function.convert.OutboundRow; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; -import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.r2dbc.support.StatementRenderUtil; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -182,7 +182,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra "Dialect " + dialect.getClass().getName() + " does not support array columns"); } - return new SettableValue(converter.getArrayValue(arrayColumns, property, value.getValue()), + return SettableValue.fromOrEmpty(converter.getArrayValue(arrayColumns, property, value.getValue()), property.getActualType()); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java index a6d79f4d..21ca25ec 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java +++ b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java @@ -18,7 +18,7 @@ package org.springframework.data.r2dbc.function; import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.util.Assert; /** @@ -65,7 +65,7 @@ class MapBindParameterSource implements BindParameterSource { Assert.notNull(paramName, "Parameter name must not be null!"); Assert.notNull(value, "Value must not be null!"); - this.values.put(paramName, new SettableValue(value, value.getClass())); + this.values.put(paramName, SettableValue.fromOrEmpty(value, value.getClass())); return this; } diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index aff9f794..6c989fec 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -26,9 +26,9 @@ import java.util.function.BiFunction; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; -import org.springframework.data.r2dbc.function.convert.OutboundRow; +import org.springframework.data.r2dbc.domain.OutboundRow; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; -import org.springframework.data.r2dbc.function.convert.SettableValue; /** * Draft of a data access strategy that generalizes convenience operations using mapped entities. Typically used diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index f851534f..fb60411d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -25,6 +25,7 @@ import java.lang.reflect.Array; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Optional; import java.util.function.BiFunction; import org.springframework.core.convert.ConversionService; @@ -38,6 +39,8 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.r2dbc.dialect.ArrayColumns; +import org.springframework.data.r2dbc.domain.OutboundRow; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; @@ -135,13 +138,40 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R } Object value = row.get(prefix + property.getColumnName()); - return readValue(value, property.getTypeInformation()); + return getPotentiallyConvertedSimpleRead(value, property.getTypeInformation().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Could not read property %s from result set!", property), o_O); } } + /** + * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies + * {@link Enum} handling or returns the value as is. + * + * @param value + * @param target must not be {@literal null}. + * @return + */ + @Nullable + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { + + if (value == null || target == null || ClassUtils.isAssignableValue(target, value)) { + return value; + } + + if (getConversions().hasCustomReadTarget(value.getClass(), target)) { + return getConversionService().convert(value, target); + } + + if (Enum.class.isAssignableFrom(target)) { + return Enum.valueOf((Class) target, value.toString()); + } + + return getConversionService().convert(value, target); + } + private S readEntityFrom(Row row, PersistentProperty property) { String prefix = property.getName() + "_"; @@ -182,33 +212,101 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R public void write(Object source, OutboundRow sink) { Class userClass = ClassUtils.getUserClass(source); - RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass); - PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source); + Optional> customTarget = getConversions().getCustomWriteTarget(userClass, OutboundRow.class); + if (customTarget.isPresent()) { + + OutboundRow result = getConversionService().convert(source, OutboundRow.class); + sink.putAll(result); + return; + } + + writeInternal(source, sink, userClass); + } + + private void writeInternal(Object source, OutboundRow sink, Class userClass) { + + RelationalPersistentEntity entity = getRequiredPersistentEntity(userClass); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(source); + + writeProperties(sink, entity, propertyAccessor); + } + + private void writeProperties(OutboundRow sink, RelationalPersistentEntity entity, + PersistentPropertyAccessor accessor) { for (RelationalPersistentProperty property : entity) { - Object writeValue = getWriteValue(propertyAccessor, property); + if (!property.isWritable()) { + continue; + } - sink.put(property.getColumnName(), new SettableValue(writeValue, property.getType())); + Object value = accessor.getProperty(property); + + if (value == null) { + writeNullInternal(sink, property); + continue; + } + + if (!getConversions().isSimpleType(value.getClass())) { + + RelationalPersistentEntity nestedEntity = getMappingContext().getPersistentEntity(property.getActualType()); + if (nestedEntity != null) { + throw new InvalidDataAccessApiUsageException("Nested entities are not supported"); + } + } + + writeSimpleInternal(sink, value, property); } - } - @SuppressWarnings("unchecked") - private Object getWriteValue(PersistentPropertyAccessor propertyAccessor, RelationalPersistentProperty property) { + private void writeSimpleInternal(OutboundRow sink, Object value, RelationalPersistentProperty property) { + sink.put(property.getColumnName(), SettableValue.from(getPotentiallyConvertedSimpleWrite(value))); + } - TypeInformation type = property.getTypeInformation(); - Object value = propertyAccessor.getProperty(property); + private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) { - RelationalPersistentEntity nestedEntity = getMappingContext() - .getPersistentEntity(type.getRequiredActualType().getType()); + sink.put(property.getColumnName(), + SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType()))); + } + + private Class getPotentiallyConvertedSimpleNullType(Class type) { + + Optional> customTarget = getConversions().getCustomWriteTarget(type); + + if (customTarget.isPresent()) { + return customTarget.get(); - if (nestedEntity != null) { - throw new InvalidDataAccessApiUsageException("Nested entities are not supported"); } - return value; + if (type.isEnum()) { + return String.class; + } + + return type; + } + + /** + * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type. + * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. + * + * @param value + * @return + */ + @Nullable + private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value) { + + if (value == null) { + return null; + } + + Optional> customTarget = getConversions().getCustomWriteTarget(value.getClass()); + + if (customTarget.isPresent()) { + return getConversionService().convert(value, customTarget.get()); + } + + return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value; } public Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) { diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java index 6cf89316..f205913f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java @@ -25,6 +25,7 @@ import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityWriter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.ArrayColumns; +import org.springframework.data.r2dbc.domain.OutboundRow; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 4b28127c..c985a573 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -30,12 +30,12 @@ import org.reactivestreams.Publisher; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.BindIdOperation; import org.springframework.data.r2dbc.function.DatabaseClient; import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; -import org.springframework.data.r2dbc.function.convert.SettableValue; import org.springframework.data.relational.core.sql.Conditions; import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.Functions; diff --git a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java new file mode 100644 index 00000000..166deb7f --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java @@ -0,0 +1,56 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.domain; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Unit tests for {@link SettableValue}. + * + * @author Mark Paluch + */ +public class SettableValueUnitTests { + + @Test // gh-59 + public void shouldCreateSettableValue() { + + SettableValue value = SettableValue.from("foo"); + + assertThat(value.isEmpty()).isFalse(); + assertThat(value.hasValue()).isTrue(); + assertThat(value).isEqualTo(SettableValue.from("foo")); + } + + @Test // gh-59 + public void shouldCreateEmpty() { + + SettableValue value = SettableValue.empty(Object.class); + + assertThat(value.isEmpty()).isTrue(); + assertThat(value.hasValue()).isFalse(); + assertThat(value).isEqualTo(SettableValue.empty(Object.class)); + assertThat(value).isNotEqualTo(SettableValue.empty(String.class)); + } + + @Test // gh-59 + public void shouldCreatePotentiallyEmpty() { + + assertThat(SettableValue.fromOrEmpty("foo", Object.class).isEmpty()).isFalse(); + assertThat(SettableValue.fromOrEmpty(null, Object.class).isEmpty()).isTrue(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java index 59690d5b..2d6c2211 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java @@ -14,7 +14,7 @@ import java.util.Map; import org.junit.Test; import org.springframework.data.r2dbc.dialect.PostgresDialect; -import org.springframework.data.r2dbc.function.convert.SettableValue; +import org.springframework.data.r2dbc.domain.SettableValue; /** * Unit tests for {@link DefaultReactiveDataAccessStrategy}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java index 9fcbe4c0..1a292f52 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java @@ -21,9 +21,20 @@ import static org.mockito.Mockito.*; import io.r2dbc.spi.Row; import lombok.AllArgsConstructor; +import java.util.Arrays; +import java.util.Collections; +import java.util.Map; + +import org.junit.Before; import org.junit.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.domain.OutboundRow; +import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** @@ -33,7 +44,21 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext */ public class MappingR2dbcConverterUnitTests { - MappingR2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); + RelationalMappingContext mappingContext = new RelationalMappingContext(); + MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext); + + @Before + public void before() { + + R2dbcCustomConversions conversions = new R2dbcCustomConversions( + Arrays.asList(StringToMapConverter.INSTANCE, MapToStringConverter.INSTANCE, + CustomConversionPersonToOutboundRowConverter.INSTANCE, RowToCustomConversionPerson.INSTANCE)); + + mappingContext = new RelationalMappingContext(); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + + converter = new MappingR2dbcConverter(mappingContext, conversions); + } @Test // gh-61 public void shouldIncludeAllPropertiesInOutboundRow() { @@ -42,9 +67,9 @@ public class MappingR2dbcConverterUnitTests { converter.write(new Person("id", "Walter", "White"), row); - assertThat(row).containsEntry("id", new SettableValue("id", String.class)); - assertThat(row).containsEntry("firstname", new SettableValue("Walter", String.class)); - assertThat(row).containsEntry("lastname", new SettableValue("White", String.class)); + assertThat(row).containsEntry("id", SettableValue.fromOrEmpty("id", String.class)); + assertThat(row).containsEntry("firstname", SettableValue.fromOrEmpty("Walter", String.class)); + assertThat(row).containsEntry("lastname", SettableValue.fromOrEmpty("White", String.class)); } @Test // gh-41 @@ -68,9 +93,188 @@ public class MappingR2dbcConverterUnitTests { assertThat(result).isEqualTo(42); } + @Test // gh-59 + public void shouldFailOnUnsupportedEntity() { + + PersonWithConversions withMap = new PersonWithConversions(null, null, new NonMappableEntity()); + OutboundRow row = new OutboundRow(); + + assertThatThrownBy(() -> converter.write(withMap, row)).isInstanceOf(InvalidDataAccessApiUsageException.class); + } + + @Test // gh-59 + public void shouldConvertMapToString() { + + PersonWithConversions withMap = new PersonWithConversions("foo", Collections.singletonMap("map", "value"), null); + OutboundRow row = new OutboundRow(); + converter.write(withMap, row); + + assertThat(row).containsEntry("nested", SettableValue.from("map")); + } + + @Test // gh-59 + public void shouldReadMapFromString() { + + Row rowMock = mock(Row.class); + when(rowMock.get("nested")).thenReturn("map"); + + PersonWithConversions result = converter.read(PersonWithConversions.class, rowMock); + + assertThat(result.nested).isEqualTo(Collections.singletonMap("map", "map")); + } + + @Test // gh-59 + public void shouldConvertEnum() { + + WithEnum withMap = new WithEnum("foo", Condition.Mint); + OutboundRow row = new OutboundRow(); + converter.write(withMap, row); + + assertThat(row).containsEntry("condition", SettableValue.from("Mint")); + } + + @Test // gh-59 + public void shouldConvertNullEnum() { + + WithEnum withMap = new WithEnum("foo", null); + OutboundRow row = new OutboundRow(); + converter.write(withMap, row); + + assertThat(row).containsEntry("condition", SettableValue.fromOrEmpty(null, String.class)); + } + + @Test // gh-59 + public void shouldReadEnum() { + + Row rowMock = mock(Row.class); + when(rowMock.get("condition")).thenReturn("Mint"); + + WithEnum result = converter.read(WithEnum.class, rowMock); + + assertThat(result.condition).isEqualTo(Condition.Mint); + } + + @Test // gh-59 + public void shouldWriteTopLevelEntity() { + + CustomConversionPerson person = new CustomConversionPerson(); + person.entity = new NonMappableEntity(); + person.foo = "bar"; + + OutboundRow row = new OutboundRow(); + converter.write(person, row); + + assertThat(row).containsEntry("foo_column", SettableValue.from("bar")).containsEntry("entity", + SettableValue.from("nested_entity")); + } + + @Test // gh-59 + public void shouldReadTopLevelEntity() { + + Row rowMock = mock(Row.class); + when(rowMock.get("foo_column", String.class)).thenReturn("bar"); + when(rowMock.get("nested_entity")).thenReturn("map"); + + CustomConversionPerson result = converter.read(CustomConversionPerson.class, rowMock); + + assertThat(result.foo).isEqualTo("bar"); + assertThat(result.entity).isNotNull(); + } + @AllArgsConstructor static class Person { @Id String id; String firstname, lastname; } + + @AllArgsConstructor + static class WithEnum { + @Id String id; + Condition condition; + } + + enum Condition { + Mint, Used + } + + @AllArgsConstructor + static class PersonWithConversions { + @Id String id; + Map nested; + NonMappableEntity unsupported; + } + + static class CustomConversionPerson { + + String foo; + NonMappableEntity entity; + } + + static class NonMappableEntity {} + + @ReadingConverter + enum StringToMapConverter implements Converter> { + + INSTANCE; + + @Override + public Map convert(String source) { + + if (source != null) { + return Collections.singletonMap(source, source); + } + + return null; + } + } + + @WritingConverter + enum MapToStringConverter implements Converter, String> { + + INSTANCE; + + @Override + public String convert(Map source) { + + if (!source.isEmpty()) { + return source.keySet().iterator().next(); + } + + return null; + } + } + + @WritingConverter + enum CustomConversionPersonToOutboundRowConverter implements Converter { + + INSTANCE; + + @Override + public OutboundRow convert(CustomConversionPerson source) { + + OutboundRow row = new OutboundRow(); + row.put("foo_column", SettableValue.from(source.foo)); + row.put("entity", SettableValue.from("nested_entity")); + + return row; + } + } + + @ReadingConverter + enum RowToCustomConversionPerson implements Converter { + + INSTANCE; + + @Override + public CustomConversionPerson convert(Row source) { + + CustomConversionPerson person = new CustomConversionPerson(); + person.foo = source.get("foo_column", String.class); + + Object nested_entity = source.get("nested_entity"); + person.entity = nested_entity != null ? new NonMappableEntity() : null; + + return person; + } + } } From 6127a51090bc8cb1ba30da587c0de99a690271af Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Wed, 20 Mar 2019 07:50:56 -0700 Subject: [PATCH 088/665] #74 - URL Cleanup. This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed Success These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * http://maven.apache.org/xsd/maven-4.0.0.xsd with 1 occurrences migrated to: https://maven.apache.org/xsd/maven-4.0.0.xsd ([https](https://maven.apache.org/xsd/maven-4.0.0.xsd) result 200). # Ignored These URLs were intentionally ignored. * http://maven.apache.org/POM/4.0.0 with 2 occurrences * http://www.w3.org/2001/XMLSchema-instance with 1 occurrences --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index f50708c4..a977b39d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 From 37bb12cfef3c38a4c5d618b243e61481938e3bed Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Thu, 21 Mar 2019 01:43:30 -0700 Subject: [PATCH 089/665] #76 - URL Cleanup. This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed But Review Recommended These URLs were fixed, but the https status was not OK. However, the https status was the same as the http request or http redirected to an https URL, so they were migrated. Your review is recommended. * [ ] http://www.reactive-streams.org/reactive-streams- (404) with 1 occurrences migrated to: https://www.reactive-streams.org/reactive-streams- ([https](https://www.reactive-streams.org/reactive-streams-) result 404). ## Fixed Success These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * [ ] http://docs.spring.io/spring/docs/ with 7 occurrences migrated to: https://docs.spring.io/spring/docs/ ([https](https://docs.spring.io/spring/docs/) result 200). * [ ] http://github.com/spring-projects/spring-data-r2dbc with 1 occurrences migrated to: https://github.com/spring-projects/spring-data-r2dbc ([https](https://github.com/spring-projects/spring-data-r2dbc) result 200). * [ ] http://pivotal.io/ with 1 occurrences migrated to: https://pivotal.io/ ([https](https://pivotal.io/) result 200). * [ ] http://projects.spring.io/spring-data-r2dbc/ with 1 occurrences migrated to: https://projects.spring.io/spring-data-r2dbc/ ([https](https://projects.spring.io/spring-data-r2dbc/) result 200). * [ ] http://repo.spring.io/milestone/org/springframework/data/ with 1 occurrences migrated to: https://repo.spring.io/milestone/org/springframework/data/ ([https](https://repo.spring.io/milestone/org/springframework/data/) result 200). * [ ] http://spring.io with 1 occurrences migrated to: https://spring.io ([https](https://spring.io) result 200). * [ ] http://spring.io/blog with 2 occurrences migrated to: https://spring.io/blog ([https](https://spring.io/blog) result 200). * [ ] http://spring.io/docs with 1 occurrences migrated to: https://spring.io/docs ([https](https://spring.io/docs) result 200). * [ ] http://spring.io/projects/spring-data-r2dbc with 4 occurrences migrated to: https://spring.io/projects/spring-data-r2dbc ([https](https://spring.io/projects/spring-data-r2dbc) result 200). * [ ] http://stackoverflow.com/questions/tagged/spring-data with 2 occurrences migrated to: https://stackoverflow.com/questions/tagged/spring-data ([https](https://stackoverflow.com/questions/tagged/spring-data) result 200). * [ ] http://stackoverflow.com/questions/tagged/spring-data-r2dbc with 1 occurrences migrated to: https://stackoverflow.com/questions/tagged/spring-data-r2dbc ([https](https://stackoverflow.com/questions/tagged/spring-data-r2dbc) result 200). * [ ] http://twitter.com/SpringData with 1 occurrences migrated to: https://twitter.com/SpringData ([https](https://twitter.com/SpringData) result 200). * [ ] http://contributor-covenant.org with 1 occurrences migrated to: https://contributor-covenant.org ([https](https://contributor-covenant.org) result 301). * [ ] http://contributor-covenant.org/version/1/3/0/ with 1 occurrences migrated to: https://contributor-covenant.org/version/1/3/0/ ([https](https://contributor-covenant.org/version/1/3/0/) result 301). * [ ] http://help.github.com/forking/ with 1 occurrences migrated to: https://help.github.com/forking/ ([https](https://help.github.com/forking/) result 301). * [ ] http://projects.spring.io/spring-data with 1 occurrences migrated to: https://projects.spring.io/spring-data ([https](https://projects.spring.io/spring-data) result 301). * [ ] http://repo.spring.io/libs-milestone with 1 occurrences migrated to: https://repo.spring.io/libs-milestone ([https](https://repo.spring.io/libs-milestone) result 302). --- CODE_OF_CONDUCT.adoc | 2 +- README.adoc | 8 ++++---- docs/index.html | 8 ++++---- src/main/asciidoc/index.adoc | 2 +- src/main/asciidoc/preface.adoc | 28 +++++++++++++------------- src/main/asciidoc/reference/r2dbc.adoc | 6 +++--- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/CODE_OF_CONDUCT.adoc b/CODE_OF_CONDUCT.adoc index f64fb1b7..33ae7bc9 100644 --- a/CODE_OF_CONDUCT.adoc +++ b/CODE_OF_CONDUCT.adoc @@ -24,4 +24,4 @@ Instances of abusive, harassing, or otherwise unacceptable behavior may be repor All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. -This Code of Conduct is adapted from the http://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]. \ No newline at end of file +This Code of Conduct is adapted from the https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]. \ No newline at end of file diff --git a/README.adoc b/README.adoc index 1033deb7..01fc389a 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ = Spring Data R2DBC -The primary goal of the http://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use data access technologies. *Spring Data R2DBC* offers the popular Repository abstraction based on https://r2dbc.io[R2DBC]. +The primary goal of the https://projects.spring.io/spring-data[Spring Data] project is to make it easier to build Spring-powered applications that use data access technologies. *Spring Data R2DBC* offers the popular Repository abstraction based on https://r2dbc.io[R2DBC]. R2DBC is the abbreviation for https://github.com/r2dbc/[Reactive Relational Database Connectivity], an incubator to integrate relational databases using a reactive driver. @@ -188,9 +188,9 @@ _Also see link:CONTRIBUTING.adoc[CONTRIBUTING.adoc] if you wish to submit pull r Here are some ways for you to get involved in the community: -* Get involved with the Spring community by helping out on http://stackoverflow.com/questions/tagged/spring-data-r2dbc[Stackoverflow] by responding to questions and joining the debate. +* Get involved with the Spring community by helping out on https://stackoverflow.com/questions/tagged/spring-data-r2dbc[Stackoverflow] by responding to questions and joining the debate. * Create https://github.com/spring-projects/spring-data-r2dbc[GitHub] tickets for bugs and new features and comment and vote on the ones that you are interested in. -* Github is for social coding: if you want to write code, we encourage contributions through pull requests from http://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing. -* Watch for upcoming articles on Spring by http://spring.io/blog[subscribing] to spring.io. +* Github is for social coding: if you want to write code, we encourage contributions through pull requests from https://help.github.com/forking/[forks of this repository]. If you want to contribute code this way, please reference a JIRA ticket as well, covering the specific issue you are addressing. +* Watch for upcoming articles on Spring by https://spring.io/blog[subscribing] to spring.io. Before we accept a non-trivial patch or pull request we will need you to https://cla.pivotal.io/sign/spring[sign the Contributor License Agreement]. Signing the contributor’s agreement does not grant anyone commit rights to the main repository, but it does mean that we can accept your contributions, and you will get an author credit if we do. If you forget to do so, you'll be reminded when you submit a pull request. Active contributors might be asked to join the core team, and given the ability to merge pull requests. diff --git a/docs/index.html b/docs/index.html index 6c508887..7b9c255e 100644 --- a/docs/index.html +++ b/docs/index.html @@ -2,10 +2,10 @@ Redirecting… - - + +

Redirecting…

- Click here if you are not redirected. - + Click here if you are not redirected. + diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 89358348..d2994639 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -4,7 +4,7 @@ :revdate: {localdate} ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]] :spring-data-commons-docs: ../../../../spring-data-commons/src/main/asciidoc -:reactiveStreamsJavadoc: http://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc +:reactiveStreamsJavadoc: https://www.reactive-streams.org/reactive-streams-{reactiveStreamsVersion}-javadoc (C) 2018-2019 The original authors. diff --git a/src/main/asciidoc/preface.adoc b/src/main/asciidoc/preface.adoc index 8582eea1..670d109a 100644 --- a/src/main/asciidoc/preface.adoc +++ b/src/main/asciidoc/preface.adoc @@ -11,13 +11,13 @@ This section provides some basic introduction to Spring and databases. [[get-started:first-steps:spring]] == Learning Spring -Spring Data uses Spring framework's http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html[core] functionality, including: +Spring Data uses Spring framework's https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html[core] functionality, including: -* http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#beans[IoC] container -* http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#validation[type conversion system] -* http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#expressions[expression language] -* http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/integration.html#jmx[JMX integration] -* http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html#dao-exceptions[DAO exception hierarchy]. +* https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#beans[IoC] container +* https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#validation[type conversion system] +* https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/core.html#expressions[expression language] +* https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/integration.html#jmx[JMX integration] +* https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html#dao-exceptions[DAO exception hierarchy]. While you need not know the Spring APIs, understanding the concepts behind them is important. At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. @@ -28,7 +28,7 @@ To leverage all the features of Spring Data R2DBC, such as the repository suppor To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. There are a lot of articles, blog entries, and books on the subject. -See the Spring framework http://spring.io/docs[home page] for more information. +See the Spring framework https://spring.io/docs[home page] for more information. [[get-started:first-steps:what]] == What is R2DBC? @@ -92,29 +92,29 @@ In this section, we try to provide what we think is an easy-to-follow guide for However, if you encounter issues or you need advice, feel free to use one of the following links: [[get-started:help:community]] -Community Forum :: Spring Data on http://stackoverflow.com/questions/tagged/spring-data[Stack Overflow] is a tag for all Spring Data (not just R2DBC) users to share information and help each other. +Community Forum :: Spring Data on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow] is a tag for all Spring Data (not just R2DBC) users to share information and help each other. Note that registration is needed only for posting. [[get-started:help:professional]] -Professional Support :: Professional, from-the-source support, with guaranteed response time, is available from http://pivotal.io/[Pivotal Sofware, Inc.], the company behind Spring Data and Spring. +Professional Support :: Professional, from-the-source support, with guaranteed response time, is available from https://pivotal.io/[Pivotal Sofware, Inc.], the company behind Spring Data and Spring. [[get-started:up-to-date]] == Following Development -* For information on the Spring Data R2DBC source code repository, nightly builds, and snapshot artifacts, see the Spring Data R2DBC http://projects.spring.io/spring-data-r2dbc/[homepage]. +* For information on the Spring Data R2DBC source code repository, nightly builds, and snapshot artifacts, see the Spring Data R2DBC https://projects.spring.io/spring-data-r2dbc/[homepage]. -* You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on http://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. +* You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. * If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data R2DBC https://github.com/spring-projects/spring-data-r2dbc/issues[issue tracker]. -* To stay up to date with the latest news and announcements in the Spring ecosystem, subscribe to the Spring Community http://spring.io[Portal]. +* To stay up to date with the latest news and announcements in the Spring ecosystem, subscribe to the Spring Community https://spring.io[Portal]. -* You can also follow the Spring http://spring.io/blog[blog] or the Spring Data project team on Twitter (http://twitter.com/SpringData[SpringData]). +* You can also follow the Spring https://spring.io/blog[blog] or the Spring Data project team on Twitter (https://twitter.com/SpringData[SpringData]). [[project-metadata]] == Project Metadata -* Version control: http://github.com/spring-projects/spring-data-r2dbc +* Version control: https://github.com/spring-projects/spring-data-r2dbc * Bugtracker: https://github.com/spring-projects/spring-data-r2dbc/issues * Release repository: https://repo.spring.io/libs-release * Milestone repository: https://repo.spring.io/libs-milestone diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc index 55c737ca..27802ce5 100644 --- a/src/main/asciidoc/reference/r2dbc.adoc +++ b/src/main/asciidoc/reference/r2dbc.adoc @@ -55,12 +55,12 @@ An easy way to bootstrap setting up a working environment is to create a Spring- spring-milestone Spring Maven MILESTONE Repository - http://repo.spring.io/libs-milestone + https://repo.spring.io/libs-milestone ---- -The repository is also http://repo.spring.io/milestone/org/springframework/data/[browseable here]. +The repository is also https://repo.spring.io/milestone/org/springframework/data/[browseable here]. You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content: @@ -213,7 +213,7 @@ public class ApplicationConfiguration extends AbstractR2dbcConfiguration { ---- ==== -This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in http://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features]. +This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features]. `AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation. From 7e3bcceeda5082a90b86a820dc2c6cd75e006cf5 Mon Sep 17 00:00:00 2001 From: Spring Operator Date: Fri, 22 Mar 2019 02:14:40 -0700 Subject: [PATCH 090/665] #79 - URL Cleanup. This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener). # Fixed URLs ## Fixed Success These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended. * [ ] http://www.apache.org/licenses/ with 1 occurrences migrated to: https://www.apache.org/licenses/ ([https](https://www.apache.org/licenses/) result 200). * [ ] http://www.apache.org/licenses/LICENSE-2.0 with 93 occurrences migrated to: https://www.apache.org/licenses/LICENSE-2.0 ([https](https://www.apache.org/licenses/LICENSE-2.0) result 200). Original pull request: #79. --- .../springframework/data/r2dbc/BadSqlGrammarException.java | 2 +- .../data/r2dbc/InvalidResultAccessException.java | 2 +- .../data/r2dbc/UncategorizedR2dbcException.java | 2 +- .../data/r2dbc/config/AbstractR2dbcConfiguration.java | 2 +- .../data/r2dbc/dialect/R2dbcSimpleTypeHolder.java | 2 +- .../org/springframework/data/r2dbc/domain/OutboundRow.java | 2 +- .../org/springframework/data/r2dbc/domain/SettableValue.java | 2 +- .../data/r2dbc/function/BindParameterSource.java | 2 +- .../data/r2dbc/function/ConnectionAccessor.java | 2 +- .../springframework/data/r2dbc/function/DatabaseClient.java | 2 +- .../data/r2dbc/function/DefaultDatabaseClient.java | 2 +- .../data/r2dbc/function/DefaultDatabaseClientBuilder.java | 2 +- .../springframework/data/r2dbc/function/DefaultFetchSpec.java | 2 +- .../r2dbc/function/DefaultReactiveDataAccessStrategy.java | 2 +- .../springframework/data/r2dbc/function/DefaultSqlResult.java | 2 +- .../r2dbc/function/DefaultTransactionalDatabaseClient.java | 2 +- .../function/DefaultTransactionalDatabaseClientBuilder.java | 2 +- .../org/springframework/data/r2dbc/function/FetchSpec.java | 2 +- .../data/r2dbc/function/MapBindParameterSource.java | 2 +- .../data/r2dbc/function/NamedParameterExpander.java | 2 +- .../data/r2dbc/function/NamedParameterUtils.java | 2 +- .../org/springframework/data/r2dbc/function/ParsedSql.java | 2 +- .../data/r2dbc/function/ReactiveDataAccessStrategy.java | 2 +- .../springframework/data/r2dbc/function/RowsFetchSpec.java | 2 +- .../org/springframework/data/r2dbc/function/SqlResult.java | 2 +- .../data/r2dbc/function/TransactionalDatabaseClient.java | 2 +- .../data/r2dbc/function/UpdatedRowsFetchSpec.java | 2 +- .../function/connectionfactory/ConnectionFactoryUtils.java | 2 +- .../r2dbc/function/connectionfactory/ConnectionProxy.java | 2 +- .../connectionfactory/DefaultTransactionResources.java | 2 +- .../connectionfactory/ReactiveTransactionSynchronization.java | 2 +- .../connectionfactory/SingletonConnectionFactory.java | 2 +- .../function/connectionfactory/SmartConnectionFactory.java | 2 +- .../function/connectionfactory/TransactionResources.java | 2 +- .../data/r2dbc/function/convert/ColumnMapRowMapper.java | 2 +- .../data/r2dbc/function/convert/EntityRowMapper.java | 2 +- .../data/r2dbc/function/convert/IterableUtils.java | 2 +- .../data/r2dbc/function/convert/MappingR2dbcConverter.java | 2 +- .../data/r2dbc/function/convert/R2dbcConverter.java | 2 +- .../data/r2dbc/function/convert/R2dbcConverters.java | 2 +- .../data/r2dbc/repository/R2dbcRepository.java | 2 +- .../data/r2dbc/repository/config/EnableR2dbcRepositories.java | 2 +- .../r2dbc/repository/config/R2dbcRepositoriesRegistrar.java | 2 +- .../config/R2dbcRepositoryConfigurationExtension.java | 2 +- .../data/r2dbc/repository/query/AbstractR2dbcQuery.java | 2 +- .../data/r2dbc/repository/query/BindableQuery.java | 2 +- .../springframework/data/r2dbc/repository/query/Query.java | 2 +- .../data/r2dbc/repository/query/R2dbcParameterAccessor.java | 2 +- .../data/r2dbc/repository/query/R2dbcQueryExecution.java | 2 +- .../data/r2dbc/repository/query/R2dbcQueryMethod.java | 2 +- .../data/r2dbc/repository/query/StringBasedR2dbcQuery.java | 2 +- .../data/r2dbc/repository/support/R2dbcRepositoryFactory.java | 2 +- .../r2dbc/repository/support/R2dbcRepositoryFactoryBean.java | 2 +- .../data/r2dbc/repository/support/SimpleR2dbcRepository.java | 2 +- .../support/AbstractFallbackR2dbcExceptionTranslator.java | 2 +- .../data/r2dbc/support/R2dbcExceptionTranslator.java | 2 +- .../r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java | 2 +- .../data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java | 2 +- .../data/r2dbc/support/StatementRenderUtil.java | 2 +- .../data/r2dbc/function/DatabaseClientExtensions.kt | 2 +- .../data/r2dbc/function/RowsFetchSpecExtensions.kt | 2 +- src/main/resources/license.txt | 4 ++-- .../java/org/springframework/data/r2dbc/DependencyTests.java | 2 +- .../data/r2dbc/domain/SettableValueUnitTests.java | 2 +- .../function/AbstractDatabaseClientIntegrationTests.java | 2 +- .../AbstractTransactionalDatabaseClientIntegrationTests.java | 2 +- .../data/r2dbc/function/DefaultDatabaseClientUnitTests.java | 2 +- .../data/r2dbc/function/NamedParameterUtilsUnitTests.java | 2 +- .../function/PostgresDatabaseClientIntegrationTests.java | 2 +- .../data/r2dbc/function/PostgresIntegrationTests.java | 2 +- .../function/SqlServerDatabaseClientIntegrationTests.java | 2 +- .../connectionfactory/ConnectionFactoryUtilsUnitTests.java | 2 +- .../function/convert/MappingR2dbcConverterUnitTests.java | 2 +- .../data/r2dbc/function/convert/R2dbcConvertersUnitTests.java | 2 +- .../repository/AbstractR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/PostgresR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/SqlServerR2dbcRepositoryIntegrationTests.java | 2 +- .../springframework/data/r2dbc/repository/config/Person.java | 2 +- .../data/r2dbc/repository/config/PersonRepository.java | 2 +- .../repository/config/R2dbcRepositoriesRegistrarTests.java | 2 +- .../R2dbcRepositoryConfigurationExtensionUnitTests.java | 2 +- .../r2dbc/repository/query/R2dbcQueryMethodUnitTests.java | 2 +- .../repository/query/StringBasedR2dbcQueryUnitTests.java | 2 +- .../AbstractSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../PostgresSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../repository/support/R2dbcRepositoryFactoryUnitTests.java | 2 +- .../SqlServerSimpleR2dbcRepositoryIntegrationTests.java | 2 +- .../SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java | 2 +- .../support/SqlStateR2dbcExceptionTranslatorUnitTests.java | 2 +- .../springframework/data/r2dbc/testing/ExternalDatabase.java | 2 +- .../data/r2dbc/testing/R2dbcIntegrationTestSupport.java | 2 +- .../data/r2dbc/function/DatabaseClientExtensionsTests.kt | 2 +- .../data/r2dbc/function/RowsFetchSpecExtensionsTests.kt | 2 +- 93 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java index bb507a34..9e7e6f77 100644 --- a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java +++ b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java index 37a6ca9e..06917696 100644 --- a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java index fd161c3f..8e051424 100644 --- a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java +++ b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index f6570c6b..b700db91 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java index a02b96e3..469b1ec1 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java index c73127fc..0f32d896 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java +++ b/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java index 6c322464..ebb5dca2 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java index c89eae8b..ed709fa0 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java +++ b/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java index fac0d213..5a88dbf2 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, 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 fb4f48bc..7062fca5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, 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 cb33631f..b7c52268 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 224771d4..992cab5c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java index 4ddabc8f..66e2bbbf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, 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 0793dec9..d36ea69c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index d1acae5d..531090de 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java index 99f93129..19170f55 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java index 3c827e4a..444afd26 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java index 96a4a8b5..cf3080c6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java index 21ca25ec..2d031916 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java +++ b/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java index c904b123..8beeb507 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java index c012ed93..15c0b1b3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java index 706931ea..436aa80e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java index 6c989fec..2e82f367 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java index e7ab42de..6c96962c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java index 4132bdff..1c6cac09 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java index 3acc780c..0b4860e5 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java index 6e45f04a..2410018c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java index 3b320398..be9b9691 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index e02011e9..eda5f424 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java index ba761646..eb4fac0a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java index 7c819247..a476097d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java index b73597d2..6beb79d4 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java index 07514b35..39a3bbed 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java index b4c88832..c6721a10 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java index a5a2dcf2..4015f861 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java index dfb6e119..5e2b9f27 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java index f2544007..a1de338e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index fb60411d..356d02d3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java index f205913f..6f8083ac 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java index 62b3d412..dce488bc 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java index 264fd1bc..32311d51 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/R2dbcRepository.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java index bb7b0157..b0e0e10a 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java index 36615549..f5efa61d 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrar.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java index 4ca96a9c..55bed05b 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtension.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java index dc15e384..b3e17cf3 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java index 7ba29c27..bff01011 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java index 66b5bee8..4255fd93 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/Query.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index bf46b453..a40f8ecb 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index 67d0090f..701ff40c 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index ee73b6cf..eb9db8f5 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index bfa034d3..ad61dffb 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index 0c82c087..e2bdbeec 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 506f7ee5..61ffd297 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index c985a573..720c97ba 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java index cc4bf6c9..bb57526b 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java index f2a39448..655cc678 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionTranslator.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java index cb2ecb31..e9531df5 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslator.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java index f0d1c489..01b5a605 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslator.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java index 4ea19aaf..ae8c7dd8 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java +++ b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt index 7799e7ca..163805b8 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt index 8b11402e..88934c96 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/resources/license.txt b/src/main/resources/license.txt index 261eeb9e..20e4bd85 100644 --- a/src/main/resources/license.txt +++ b/src/main/resources/license.txt @@ -1,6 +1,6 @@ Apache License Version 2.0, January 2004 - http://www.apache.org/licenses/ + https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION @@ -192,7 +192,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/test/java/org/springframework/data/r2dbc/DependencyTests.java b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java index 3b6e75b0..fabcb358 100644 --- a/src/test/java/org/springframework/data/r2dbc/DependencyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/DependencyTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java index 166deb7f..08d9c887 100644 --- a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 8700e4a9..36818dfe 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index fc406a21..10264512 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java index ac1d7eaa..2e8ccdb2 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java index b4f6ca25..ed87827f 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java index 264e8dba..a0fb9a27 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java index 10d3b725..ba1d5346 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java index 296fff03..c9856a81 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java index 9580af4c..5793096e 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java index 1a292f52..69573c61 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java index bbbf931b..d26aebd4 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 6e880357..84e27861 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index 626cb40a..4107fce1 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java index 8661d9f0..719d188f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java index 8b022fd0..d8858c63 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/Person.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java index 6d59968b..6453479e 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/PersonRepository.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index db97506e..b84adfa0 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java index 81e17d11..90e58c11 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoryConfigurationExtensionUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java index d2a730df..7edf06a1 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethodUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index bd2aa716..de7037ee 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index 29e731e9..f1505648 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java index 8fc212e4..de9de557 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/PostgresSimpleR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index a1119355..0197149f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java index fa93e7fd..18bb8970 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/SqlServerSimpleR2dbcRepositoryIntegrationTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java index 6dab0d84..131d256f 100644 --- a/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlErrorCodeR2dbcExceptionTranslatorUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java index 86eb135e..4be7448d 100644 --- a/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/support/SqlStateR2dbcExceptionTranslatorUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index dca944b9..996abf9d 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java index f1467efe..aaf9da70 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/R2dbcIntegrationTestSupport.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt index 3ace37d4..4f0e1f5b 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt index 5f1d92e0..6278572d 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, From 7c9e777d000c5dd86dc915239a11c4cdd9ab2203 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 2 Apr 2019 16:01:41 +0200 Subject: [PATCH 091/665] #85 - Consider BigDecimal and BigInteger as simple types. We now consider BigDecimal and BigInteger as simple types and no longer as entities. We also no longer set properties whose value is null. --- .../r2dbc/dialect/R2dbcSimpleTypeHolder.java | 4 +- .../convert/MappingR2dbcConverter.java | 18 +- .../function/convert/R2dbcConverters.java | 70 ++++++ .../convert/R2dbcCustomConversions.java | 12 +- ...stgresReactiveDataAccessStrategyTests.java | 33 +++ ...ReactiveDataAccessStrategyTestSupport.java | 225 ++++++++++++++++++ ...ServerReactiveDataAccessStrategyTests.java | 33 +++ 7 files changed, 388 insertions(+), 7 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java index 469b1ec1..3b6aa93f 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java @@ -17,6 +17,8 @@ package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Row; +import java.math.BigDecimal; +import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -36,7 +38,7 @@ public class R2dbcSimpleTypeHolder extends SimpleTypeHolder { * Set of R2DBC simple types. */ public static final Set> R2DBC_SIMPLE_TYPES = Collections - .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class))); + .unmodifiableSet(new HashSet<>(Arrays.asList(OutboundRow.class, Row.class, BigInteger.class, BigDecimal.class))); public static final SimpleTypeHolder HOLDER = new R2dbcSimpleTypeHolder(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 356d02d3..25604dab 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -114,7 +114,11 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R continue; } - propertyAccessor.setProperty(property, readFrom(row, property, "")); + Object value = readFrom(row, property, ""); + + if (value != null) { + propertyAccessor.setProperty(property, value); + } } return result; @@ -266,8 +270,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R private void writeNullInternal(OutboundRow sink, RelationalPersistentProperty property) { - sink.put(property.getColumnName(), - SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType()))); + sink.put(property.getColumnName(), SettableValue.empty(getPotentiallyConvertedSimpleNullType(property.getType()))); } private Class getPotentiallyConvertedSimpleNullType(Class type) { @@ -419,7 +422,14 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R String column = prefix + property.getColumnName(); try { - return converter.getConversionService().convert(resultSet.get(column), parameter.getType().getType()); + + Object value = resultSet.get(column); + + if (value == null) { + return null; + } + + return converter.getConversionService().convert(value, parameter.getType().getType()); } catch (Exception o_O) { throw new MappingException(String.format("Couldn't read column %s from Row.", column), o_O); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java index dce488bc..8315fbdf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java @@ -30,6 +30,12 @@ import java.util.UUID; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.Jsr310Converters; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateConverterOverride; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateTimeConverterOverride; +import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalTimeConverterOverride; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; @@ -67,6 +73,22 @@ abstract class R2dbcConverters { return converters; } + /** + * @return A list of the registered converters to enforce JSR-310 type usage. + * @see CustomConversions#DEFAULT_CONVERTERS + * @see Jsr310Converters + */ + public static Collection getOverrideConvertersToRegister() { + + List converters = new ArrayList<>(); + + converters.add(LocalDateConverterOverride.INSTANCE); + converters.add(LocalDateTimeConverterOverride.INSTANCE); + converters.add(LocalTimeConverterOverride.INSTANCE); + + return converters; + } + /** * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. * @@ -229,5 +251,53 @@ abstract class R2dbcConverters { return row.get(0, ZonedDateTime.class); } } + + /** + * {@link Converter} override that forces {@link LocalDate} to stay on {@link LocalDate}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalDateConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalDate convert(LocalDate value) { + return value; + } + } + + /** + * {@link Converter} override that forces {@link LocalDateTime} to stay on {@link LocalDateTime}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalDateTimeConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalDateTime convert(LocalDateTime value) { + return value; + } + } + + /** + * {@link Converter} override that forces {@link LocalTime} to stay on {@link LocalTime}. + * + * @author Mark Paluch + */ + @WritingConverter + public enum LocalTimeConverterOverride implements Converter { + + INSTANCE; + + @Override + public LocalTime convert(LocalTime value) { + return value; + } + } } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java index da6be5fa..57bacf30 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java @@ -40,7 +40,7 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(Collection converters) { - super(STORE_CONVERSIONS, converters); + super(STORE_CONVERSIONS, appendOverriddes(converters)); } /** @@ -50,6 +50,14 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(StoreConversions storeConversions, Collection converters) { - super(storeConversions, converters); + super(storeConversions, appendOverriddes(converters)); + } + + private static Collection appendOverriddes(Collection converters) { + + List objects = new ArrayList<>(converters); + objects.addAll(R2dbcConverters.getOverrideConvertersToRegister()); + + return objects; } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java new file mode 100644 index 00000000..0231787b --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java @@ -0,0 +1,33 @@ +/* + * 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 org.springframework.data.r2dbc.dialect.PostgresDialect; + +/** + * {@link PostgresDialect} specific tests for {@link ReactiveDataAccessStrategy}. + * + * @author Mark Paluch + */ +public class PostgresReactiveDataAccessStrategyTests extends ReactiveDataAccessStrategyTestSupport { + + private final ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + + @Override + protected ReactiveDataAccessStrategy getStrategy() { + return strategy; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java b/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java new file mode 100644 index 00000000..f0ff0903 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java @@ -0,0 +1,225 @@ +/* + * 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 io.r2dbc.spi.Row; +import io.r2dbc.spi.RowMetadata; +import lombok.Data; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.Function; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.Dialect; +import org.springframework.data.r2dbc.domain.SettableValue; + +/** + * Abstract base class for {@link Dialect}-aware {@link DefaultReactiveDataAccessStrategy} tests. + * + * @author Mark Paluch + */ +public abstract class ReactiveDataAccessStrategyTestSupport { + + protected abstract ReactiveDataAccessStrategy getStrategy(); + + @Test // gh-85 + public void shouldReadAndWriteString() { + testType(PrimitiveTypes::setString, PrimitiveTypes::getString, "foo", "string"); + } + + @Test // gh-85 + public void shouldReadAndWriteCharacter() { + testType(PrimitiveTypes::setCharacter, PrimitiveTypes::getCharacter, 'f', "character"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoolean() { + testType(PrimitiveTypes::setBooleanValue, PrimitiveTypes::isBooleanValue, true, "boolean_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedBoolean() { + testType(PrimitiveTypes::setBoxedBooleanValue, PrimitiveTypes::getBoxedBooleanValue, true, "boxed_boolean_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteByte() { + testType(PrimitiveTypes::setByteValue, PrimitiveTypes::getByteValue, (byte) 123, "byte_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedByte() { + testType(PrimitiveTypes::setBoxedByteValue, PrimitiveTypes::getBoxedByteValue, (byte) 123, "boxed_byte_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteShort() { + testType(PrimitiveTypes::setShortValue, PrimitiveTypes::getShortValue, (short) 123, "short_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedShort() { + testType(PrimitiveTypes::setBoxedShortValue, PrimitiveTypes::getBoxedShortValue, (short) 123, "boxed_short_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteInteger() { + testType(PrimitiveTypes::setIntValue, PrimitiveTypes::getIntValue, 123, "int_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedInteger() { + testType(PrimitiveTypes::setBoxedIntegerValue, PrimitiveTypes::getBoxedIntegerValue, 123, "boxed_integer_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteLong() { + testType(PrimitiveTypes::setLongValue, PrimitiveTypes::getLongValue, 123L, "long_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedLong() { + testType(PrimitiveTypes::setBoxedLongValue, PrimitiveTypes::getBoxedLongValue, 123L, "boxed_long_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteFloat() { + testType(PrimitiveTypes::setFloatValue, PrimitiveTypes::getFloatValue, 0.1f, "float_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedFloat() { + testType(PrimitiveTypes::setBoxedFloatValue, PrimitiveTypes::getBoxedFloatValue, 0.1f, "boxed_float_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteDouble() { + testType(PrimitiveTypes::setDoubleValue, PrimitiveTypes::getDoubleValue, 0.1, "double_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBoxedDouble() { + testType(PrimitiveTypes::setBoxedDoubleValue, PrimitiveTypes::getBoxedDoubleValue, 0.1, "boxed_double_value"); + } + + @Test // gh-85 + public void shouldReadAndWriteBigInteger() { + testType(PrimitiveTypes::setBigInteger, PrimitiveTypes::getBigInteger, BigInteger.TEN, "big_integer"); + } + + @Test // gh-85 + public void shouldReadAndWriteBigDecimal() { + testType(PrimitiveTypes::setBigDecimal, PrimitiveTypes::getBigDecimal, new BigDecimal("100.123"), "big_decimal"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalDate() { + testType(PrimitiveTypes::setLocalDate, PrimitiveTypes::getLocalDate, LocalDate.now(), "local_date"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalTime() { + testType(PrimitiveTypes::setLocalTime, PrimitiveTypes::getLocalTime, LocalTime.now(), "local_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteLocalDateTime() { + testType(PrimitiveTypes::setLocalDateTime, PrimitiveTypes::getLocalDateTime, LocalDateTime.now(), + "local_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteZonedDateTime() { + testType(PrimitiveTypes::setZonedDateTime, PrimitiveTypes::getZonedDateTime, ZonedDateTime.now(), + "zoned_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteOffsetDateTime() { + testType(PrimitiveTypes::setOffsetDateTime, PrimitiveTypes::getOffsetDateTime, OffsetDateTime.now(), + "offset_date_time"); + } + + @Test // gh-85 + public void shouldReadAndWriteUuid() { + testType(PrimitiveTypes::setUuid, PrimitiveTypes::getUuid, UUID.randomUUID(), "uuid"); + } + + private void testType(BiConsumer setter, Function getter, T testValue, + String fieldname) { + + ReactiveDataAccessStrategy strategy = getStrategy(); + Row rowMock = mock(Row.class); + RowMetadata metadataMock = mock(RowMetadata.class); + + PrimitiveTypes toSave = new PrimitiveTypes(); + setter.accept(toSave, testValue); + + assertThat(strategy.getOutboundRow(toSave)).containsEntry(fieldname, SettableValue.from(testValue)); + + when(rowMock.get(fieldname)).thenReturn(testValue); + + PrimitiveTypes loaded = strategy.getRowMapper(PrimitiveTypes.class).apply(rowMock, metadataMock); + + assertThat(getter.apply(loaded)).isEqualTo(testValue); + } + + @Data + static class PrimitiveTypes { + + String string; + char character; + + boolean booleanValue; + byte byteValue; + short shortValue; + int intValue; + long longValue; + double doubleValue; + float floatValue; + + Boolean boxedBooleanValue; + Byte boxedByteValue; + Short boxedShortValue; + Integer boxedIntegerValue; + Long boxedLongValue; + Double boxedDoubleValue; + Float boxedFloatValue; + + BigInteger bigInteger; + BigDecimal bigDecimal; + + LocalDate localDate; + LocalTime localTime; + LocalDateTime localDateTime; + OffsetDateTime offsetDateTime; + ZonedDateTime zonedDateTime; + + UUID uuid; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java new file mode 100644 index 00000000..a1766d04 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java @@ -0,0 +1,33 @@ +/* + * 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 org.springframework.data.r2dbc.dialect.SqlServerDialect; + +/** + * {@link SqlServerDialect} specific tests for {@link ReactiveDataAccessStrategy}. + * + * @author Mark Paluch + */ +public class SqlServerReactiveDataAccessStrategyTests extends ReactiveDataAccessStrategyTestSupport { + + private final ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(SqlServerDialect.INSTANCE); + + @Override + protected ReactiveDataAccessStrategy getStrategy() { + return strategy; + } +} From 43a74cae317d9cae3b837a8bf38591c5417d8fea Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Wed, 3 Apr 2019 08:58:29 +0200 Subject: [PATCH 092/665] #86 - Add non-nullable variant to RowsFetchSpec extensions. This commit is a follow-up of gh-63. Original pull request: #86. --- .../r2dbc/function/RowsFetchSpecExtensions.kt | 27 +++++- .../function/RowsFetchSpecExtensionsTests.kt | 96 ++++++++++++++++++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt index 88934c96..cb1a1d05 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt @@ -16,22 +16,39 @@ package org.springframework.data.r2dbc.function import kotlinx.coroutines.reactive.awaitFirstOrNull +import kotlinx.coroutines.reactive.awaitSingle /** - * Coroutines variant of [RowsFetchSpec.one]. + * Non-nullable Coroutines variant of [RowsFetchSpec.one]. * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitOne(): T? +suspend fun RowsFetchSpec.awaitOne(): T + = one().awaitSingle() + +/** + * Nullable Coroutines variant of [RowsFetchSpec.one]. + * + * @author Sebastien Deleuze + */ +suspend fun RowsFetchSpec.awaitOneOrNull(): T? = one().awaitFirstOrNull() /** - * Coroutines variant of [RowsFetchSpec.first]. + * Non-nullable Coroutines variant of [RowsFetchSpec.first]. * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitFirst(): T? - = first().awaitFirstOrNull() +suspend fun RowsFetchSpec.awaitFirst(): T + = first().awaitSingle() + +/** + * Nullable Coroutines variant of [RowsFetchSpec.first]. + * + * @author Sebastien Deleuze + */ +suspend fun RowsFetchSpec.awaitFirstOrNull(): T? + = first().awaitFirstOrNull() // TODO Coroutines variant of [RowsFetchSpec.all], depends on [kotlinx.coroutines#254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). // suspend fun RowsFetchSpec.awaitAll() = all()... diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt index 6278572d..78596d4d 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt @@ -20,8 +20,10 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test import reactor.core.publisher.Mono +import java.lang.NullPointerException /** * Unit tests for [RowsFetchSpec] extensions. @@ -31,7 +33,7 @@ import reactor.core.publisher.Mono class RowsFetchSpecExtensionsTests { @Test // gh-63 - fun awaitOne() { + fun awaitOneWithValue() { val spec = mockk>() every { spec.one() } returns Mono.just("foo") @@ -46,7 +48,52 @@ class RowsFetchSpecExtensionsTests { } @Test // gh-63 - fun awaitFirst() { + fun awaitOneWithNull() { + + val spec = mockk>() + every { spec.one() } returns Mono.empty() + + assertThatExceptionOfType(NoSuchElementException::class.java).isThrownBy { + runBlocking { spec.awaitOne() } + } + + verify { + spec.one() + } + } + + @Test // gh-63 + fun awaitOneOrNullWithValue() { + + val spec = mockk>() + every { spec.one() } returns Mono.just("foo") + + runBlocking { + assertThat(spec.awaitOneOrNull()).isEqualTo("foo") + } + + verify { + spec.one() + } + } + + @Test // gh-63 + fun awaitOneOrNullWithNull() { + + val spec = mockk>() + every { spec.one() } returns Mono.empty() + + runBlocking { + assertThat(spec.awaitOneOrNull()).isNull() + } + + verify { + spec.one() + } + } + + @Test // gh-63 + fun awaitFirstWithValue() { val spec = mockk>() every { spec.first() } returns Mono.just("foo") @@ -59,4 +106,49 @@ class RowsFetchSpecExtensionsTests { spec.first() } } + + @Test // gh-63 + fun awaitFirstWithNull() { + + val spec = mockk>() + every { spec.first() } returns Mono.empty() + + assertThatExceptionOfType(NoSuchElementException::class.java).isThrownBy { + runBlocking { spec.awaitFirst() } + } + + verify { + spec.first() + } + } + + @Test // gh-63 + fun awaitFirstOrNullWithValue() { + + val spec = mockk>() + every { spec.first() } returns Mono.just("foo") + + runBlocking { + assertThat(spec.awaitFirstOrNull()).isEqualTo("foo") + } + + verify { + spec.first() + } + } + + @Test // gh-63 + fun awaitFirstOrNullWithNull() { + + val spec = mockk>() + every { spec.first() } returns Mono.empty() + + runBlocking { + assertThat(spec.awaitFirstOrNull()).isNull() + } + + verify { + spec.first() + } + } } From 372f5f02d42b5b90f9ec2bd8076d2fa291a8d804 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Tue, 2 Apr 2019 16:36:03 +0200 Subject: [PATCH 093/665] #86 - Polishing. Convert idents to tabs. Fix Kotlin extensions formatting. Original pull request: #86. --- .../function/DatabaseClientExtensions.kt | 22 +++++++++---------- .../r2dbc/function/RowsFetchSpecExtensions.kt | 16 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt index 163805b8..e1348935 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.reactive.awaitFirstOrNull * @author Sebastien Deleuze */ suspend fun DatabaseClient.GenericExecuteSpec.await() { - then().awaitFirstOrNull() + then().awaitFirstOrNull() } /** @@ -32,8 +32,8 @@ suspend fun DatabaseClient.GenericExecuteSpec.await() { * * @author Sebastien Deleuze */ -inline fun DatabaseClient.GenericExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec - = `as`(T::class.java) +inline fun DatabaseClient.GenericExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec = + `as`(T::class.java) /** * Extension for [DatabaseClient.GenericSelectSpec.as] providing a @@ -41,8 +41,8 @@ inline fun DatabaseClient.GenericExecuteSpec.asType(): Databas * * @author Sebastien Deleuze */ -inline fun DatabaseClient.GenericSelectSpec.asType(): DatabaseClient.TypedSelectSpec - = `as`(T::class.java) +inline fun DatabaseClient.GenericSelectSpec.asType(): DatabaseClient.TypedSelectSpec = + `as`(T::class.java) /** * Coroutines variant of [DatabaseClient.TypedExecuteSpec.then]. @@ -50,7 +50,7 @@ inline fun DatabaseClient.GenericSelectSpec.asType(): Database * @author Sebastien Deleuze */ suspend fun DatabaseClient.TypedExecuteSpec.await() { - then().awaitFirstOrNull() + then().awaitFirstOrNull() } /** @@ -59,8 +59,8 @@ suspend fun DatabaseClient.TypedExecuteSpec.await() { * * @author Sebastien Deleuze */ -inline fun DatabaseClient.TypedExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec - = `as`(T::class.java) +inline fun DatabaseClient.TypedExecuteSpec.asType(): DatabaseClient.TypedExecuteSpec = + `as`(T::class.java) /** * Coroutines variant of [DatabaseClient.InsertSpec.then]. @@ -68,7 +68,7 @@ inline fun DatabaseClient.TypedExecuteSpec.asType(): Databa * @author Sebastien Deleuze */ suspend fun DatabaseClient.InsertSpec.await() { - then().awaitFirstOrNull() + then().awaitFirstOrNull() } /** @@ -77,6 +77,6 @@ suspend fun DatabaseClient.InsertSpec.await() { * * @author Sebastien Deleuze */ -inline fun DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec - = into(T::class.java) +inline fun DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec = + into(T::class.java) diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt index cb1a1d05..34a09aef 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt @@ -23,32 +23,32 @@ import kotlinx.coroutines.reactive.awaitSingle * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitOne(): T - = one().awaitSingle() +suspend fun RowsFetchSpec.awaitOne(): T = + one().awaitSingle() /** * Nullable Coroutines variant of [RowsFetchSpec.one]. * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitOneOrNull(): T? - = one().awaitFirstOrNull() +suspend fun RowsFetchSpec.awaitOneOrNull(): T? = + one().awaitFirstOrNull() /** * Non-nullable Coroutines variant of [RowsFetchSpec.first]. * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitFirst(): T - = first().awaitSingle() +suspend fun RowsFetchSpec.awaitFirst(): T = + first().awaitSingle() /** * Nullable Coroutines variant of [RowsFetchSpec.first]. * * @author Sebastien Deleuze */ -suspend fun RowsFetchSpec.awaitFirstOrNull(): T? - = first().awaitFirstOrNull() +suspend fun RowsFetchSpec.awaitFirstOrNull(): T? = + first().awaitFirstOrNull() // TODO Coroutines variant of [RowsFetchSpec.all], depends on [kotlinx.coroutines#254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). // suspend fun RowsFetchSpec.awaitAll() = all()... From 591072f93c70b35201d8e14ba8c81d189c79b77a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 Apr 2019 09:42:09 +0200 Subject: [PATCH 094/665] #86 - Polishing Remove unused imports. Original pull request: #86. --- .../data/r2dbc/function/RowsFetchSpecExtensionsTests.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt index 78596d4d..da60d9a4 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt @@ -23,7 +23,6 @@ import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test import reactor.core.publisher.Mono -import java.lang.NullPointerException /** * Unit tests for [RowsFetchSpec] extensions. From 30fb4d57b98963356302f0fff31c3813b34da7e2 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Fri, 5 Apr 2019 16:17:46 +0200 Subject: [PATCH 095/665] #91 - Add RowsFetchSpec.flow() extension. Original pull request: #91. --- .../r2dbc/function/RowsFetchSpecExtensions.kt | 15 +++++++++++++-- .../function/RowsFetchSpecExtensionsTests.kt | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt index 34a09aef..ed147389 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt @@ -15,8 +15,11 @@ */ package org.springframework.data.r2dbc.function +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactive.awaitSingle +import kotlinx.coroutines.reactive.flow.asFlow /** * Non-nullable Coroutines variant of [RowsFetchSpec.one]. @@ -50,5 +53,13 @@ suspend fun RowsFetchSpec.awaitFirst(): T = suspend fun RowsFetchSpec.awaitFirstOrNull(): T? = first().awaitFirstOrNull() -// TODO Coroutines variant of [RowsFetchSpec.all], depends on [kotlinx.coroutines#254](https://github.com/Kotlin/kotlinx.coroutines/issues/254). -// suspend fun RowsFetchSpec.awaitAll() = all()... +/** + * Coroutines [Flow] variant of [RowsFetchSpec.all]. + * + * Backpressure is controlled by [batchSize] parameter that controls the size of in-flight elements + * and [org.reactivestreams.Subscription.request] size. + * + * @author Sebastien Deleuze + */ +@FlowPreview +fun RowsFetchSpec.flow(batchSize: Int = 1): Flow = all().asFlow(batchSize) diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt index da60d9a4..f6a432de 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt @@ -18,10 +18,13 @@ package org.springframework.data.r2dbc.function import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.toList import kotlinx.coroutines.runBlocking import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.Test +import reactor.core.publisher.Flux import reactor.core.publisher.Mono /** @@ -150,4 +153,20 @@ class RowsFetchSpecExtensionsTests { spec.first() } } + + @Test // gh-91 + @FlowPreview + fun allAsFlow() { + + val spec = mockk>() + every { spec.all() } returns Flux.just("foo", "bar", "baz") + + runBlocking { + assertThat(spec.flow().toList()).contains("foo", "bar", "baz") + } + + verify { + spec.all() + } + } } From 652facb14a821f35d6f77f36757521d305aba1af Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 15 Apr 2019 14:26:02 +0200 Subject: [PATCH 096/665] =?UTF-8?q?#90=20-=20Emit=20inserted=20object=20th?= =?UTF-8?q?rough=20SimpleR2dbcRepository.save(=E2=80=A6)=20with=20given=20?= =?UTF-8?q?Id.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SimpleR2dbcRepository.save(…) now falls back to the saved object as emitted result if the INSERT statement does not return generated keys. Adding H2 database engine for tests to simulate such behavior. Original pull request: #90. --- .../support/SimpleR2dbcRepository.java | 3 +- ...SimpleR2dbcRepositoryIntegrationTests.java | 24 +++++- ...SimpleR2dbcRepositoryIntegrationTests.java | 86 +++++++++++++++++++ .../data/r2dbc/testing/H2TestSupport.java | 71 +++++++++++++++ 4 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 720c97ba..2a9d7e02 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -75,7 +75,8 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository repository; - private JdbcTemplate jdbc; + SimpleR2dbcRepository repository; + JdbcTemplate jdbc; @Before public void before() { @@ -372,9 +373,26 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db @Table("legoset") @AllArgsConstructor @NoArgsConstructor - static class LegoSet { + static class LegoSet implements Persistable { @Id Integer id; String name; Integer manual; + + @Override + public boolean isNew() { + return id == null; + } + } + + static class AlwaysNewLegoSet extends LegoSet { + + AlwaysNewLegoSet(Integer id, String name, Integer manual) { + super(id, name, manual); + } + + @Override + public boolean isNew() { + return true; + } } } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..0c7f6217 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.repository.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.test.StepVerifier; + +import java.util.Map; + +import javax.sql.DataSource; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataAccessException; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.testing.H2TestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link SimpleR2dbcRepository} against H2. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class H2SimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbcRepositoryIntegrationTests { + + @Configuration + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + } + + @Override + protected DataSource createDataSource() { + return H2TestSupport.createDataSource(); + } + + @Override + protected String getCreateTableStatement() { + return H2TestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Test // gh-90 + public void shouldInsertNewObjectWithGivenId() { + + try { + this.jdbc.execute("DROP TABLE legoset"); + } catch (DataAccessException e) {} + + this.jdbc.execute(H2TestSupport.CREATE_TABLE_LEGOSET); + + AlwaysNewLegoSet legoSet = new AlwaysNewLegoSet(9999, "SCHAUFELRADBAGGER", 12); + + repository.save(legoSet) // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + + assertThat(actual.getId()).isEqualTo(9999); + }).verifyComplete(); + + Map map = jdbc.queryForMap("SELECT * FROM legoset"); + assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 12).containsKey("id"); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java new file mode 100644 index 00000000..7e986e38 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java @@ -0,0 +1,71 @@ +/* + * 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.testing; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +/** + * Utility class for testing against H2. + * + * @author Mark Paluch + */ +public class H2TestSupport { + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer CONSTRAINT id PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id serial CONSTRAINT id PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ");"; + + /** + * Creates a new {@link ConnectionFactory}. + */ + public static ConnectionFactory createConnectionFactory() { + + return new H2ConnectionFactory(H2ConnectionConfiguration.builder() // + .inMemory("r2dbc") // + .username("sa") // + .password("") // + .option("DB_CLOSE_DELAY=-1").build()); + } + + /** + * Creates a new {@link DataSource}. + */ + public static DataSource createDataSource() { + + DriverManagerDataSource dataSource = new DriverManagerDataSource(); + + dataSource.setUsername("sa"); + dataSource.setPassword(""); + dataSource.setUrl("jdbc:h2:mem:r2dbc;DB_CLOSE_DELAY=-1"); + + return dataSource; + } + +} From cca38b44803e1c6a2445c1562f5b2612c62dffe3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 15 Apr 2019 14:33:19 +0200 Subject: [PATCH 097/665] #90 - Add additional subclasses to test against H2. We now run all of our integration test also against H2. Original pull request: #90. --- .../H2DatabaseClientIntegrationTests.java | 51 ++++++++++ ...stractR2dbcRepositoryIntegrationTests.java | 6 +- .../H2R2dbcRepositoryIntegrationTests.java | 95 +++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java diff --git a/src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java new file mode 100644 index 00000000..0292ff1d --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.Ignore; + +import org.springframework.data.r2dbc.testing.H2TestSupport; + +/** + * Integration tests for {@link DatabaseClient} against H2. + * + * @author Mark Paluch + */ +public class H2DatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @Override + protected DataSource createDataSource() { + return H2TestSupport.createDataSource(); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + @Override + protected String getCreateTableStatement() { + return H2TestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + @Ignore("See https://github.com/r2dbc/r2dbc-h2/issues/66") + public void shouldTranslateDuplicateKeyException() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 84e27861..de75139e 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -188,16 +188,16 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg Flux> transactional = client.inTransaction(db -> { return transactionalRepository.save(legoSet1) // - .map(it -> jdbc.queryForMap("SELECT count(*) FROM legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) as count FROM legoset")); }); Mono> nonTransactional = transactionalRepository.save(legoSet2) // - .map(it -> jdbc.queryForMap("SELECT count(*) FROM legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) as count FROM legoset")); transactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 0L)).verifyComplete(); nonTransactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 2L)).verifyComplete(); - Map count = jdbc.queryForMap("SELECT count(*) FROM legoset"); + Map count = jdbc.queryForMap("SELECT count(*) as count FROM legoset"); assertThat(count).containsEntry("count", 2L); } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..96257352 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * 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.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.runner.RunWith; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.H2TestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against H2. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = H2LegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + } + + @Override + protected DataSource createDataSource() { + return H2TestSupport.createDataSource(); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + @Override + protected String getCreateTableStatement() { + return H2TestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return H2LegoSetRepository.class; + } + + interface H2LegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT * FROM legoset WHERE name like $1") + Flux findByNameContains(String name); + + @Override + @Query("SELECT * FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} From 2f5d11bd5636e733e373225182de5e260356647d Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 17 Apr 2019 12:59:15 +0200 Subject: [PATCH 098/665] #90 - Polishing. Original pull request: #90. --- .../r2dbc/repository/support/SimpleR2dbcRepository.java | 5 +---- .../AbstractR2dbcRepositoryIntegrationTests.java | 6 +++--- .../support/H2SimpleR2dbcRepositoryIntegrationTests.java | 7 +++---- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 2a9d7e02..19636168 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -88,10 +88,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecAdapter.create(exec); - columns.forEach((k, v) -> { - update.bind(wrapper, k, v); - - }); + columns.forEach((k, v) -> update.bind(wrapper, k, v)); update.bindId(wrapper, id); return wrapper.getBoundOperation().as(entity.getJavaType()) // diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index de75139e..677be99d 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -188,16 +188,16 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg Flux> transactional = client.inTransaction(db -> { return transactionalRepository.save(legoSet1) // - .map(it -> jdbc.queryForMap("SELECT count(*) as count FROM legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) AS count FROM legoset")); }); Mono> nonTransactional = transactionalRepository.save(legoSet2) // - .map(it -> jdbc.queryForMap("SELECT count(*) as count FROM legoset")); + .map(it -> jdbc.queryForMap("SELECT count(*) AS count FROM legoset")); transactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 0L)).verifyComplete(); nonTransactional.as(StepVerifier::create).expectNext(Collections.singletonMap("count", 2L)).verifyComplete(); - Map count = jdbc.queryForMap("SELECT count(*) as count FROM legoset"); + Map count = jdbc.queryForMap("SELECT count(*) AS count FROM legoset"); assertThat(count).containsEntry("count", 2L); } diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java index 0c7f6217..8536936f 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/H2SimpleR2dbcRepositoryIntegrationTests.java @@ -75,10 +75,9 @@ public class H2SimpleR2dbcRepositoryIntegrationTests extends AbstractSimpleR2dbc repository.save(legoSet) // .as(StepVerifier::create) // - .consumeNextWith(actual -> { - - assertThat(actual.getId()).isEqualTo(9999); - }).verifyComplete(); + .consumeNextWith( // + actual -> assertThat(actual.getId()).isEqualTo(9999) // + ).verifyComplete(); Map map = jdbc.queryForMap("SELECT * FROM legoset"); assertThat(map).containsEntry("name", "SCHAUFELRADBAGGER").containsEntry("manual", 12).containsKey("id"); From 08d91bdf84282b8016cc312dd6dca1f5847e75c9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Mar 2019 16:42:05 +0200 Subject: [PATCH 099/665] #75 - Add anonymous indexed bind markers. Anonymous indexed bind markers come with a static placeholder symbol for all parameter occurrences and they are bound by index. Original pull request: #84. --- .../r2dbc/dialect/AnonymousBindMarkers.java | 59 ++++++++++++++++++ .../r2dbc/dialect/BindMarkersFactory.java | 16 +++++ .../data/r2dbc/dialect/IndexedBindMarker.java | 60 +++++++++++++++++++ .../r2dbc/dialect/IndexedBindMarkers.java | 44 -------------- .../AnonymousBindMarkersUnitTests.java | 60 +++++++++++++++++++ 5 files changed, 195 insertions(+), 44 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java create mode 100644 src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java new file mode 100644 index 00000000..d2ef0b13 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java @@ -0,0 +1,59 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.dialect; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * Anonymous, index-based bind marker using a static placeholder. Instances are bound by the ordinal position ordered by + * the appearance of the placeholder. This implementation creates indexed bind markers using an anonymous placeholder + * that correlates with an index. + * + * @author Mark Paluch + */ +class AnonymousBindMarkers implements BindMarkers { + + private static final AtomicIntegerFieldUpdater COUNTER_INCREMENTER = AtomicIntegerFieldUpdater + .newUpdater(AnonymousBindMarkers.class, "counter"); + + // access via COUNTER_INCREMENTER + @SuppressWarnings("unused") private volatile int counter; + + private final String placeholder; + + /** + * Creates a new {@link AnonymousBindMarkers} instance given {@code placeholder}. + * + * @param placeholder parameter bind marker. + */ + AnonymousBindMarkers(String placeholder) { + this.counter = 0; + this.placeholder = placeholder; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.BindMarkers#next() + */ + @Override + public BindMarker next() { + + int index = COUNTER_INCREMENTER.getAndIncrement(this); + + return new IndexedBindMarker(placeholder, index); + } + +} diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarkersFactory.java b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarkersFactory.java index 751d8aed..10e3900a 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarkersFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarkersFactory.java @@ -43,6 +43,22 @@ public interface BindMarkersFactory { return () -> new IndexedBindMarkers(prefix, beginWith); } + /** + * Creates anonymous, index-based bind marker using a static placeholder. Instances are bound by the ordinal position + * ordered by the appearance of the placeholder. This implementation creates indexed bind markers using an anonymous + * placeholder that correlates with an index. + * + * @param placeholder parameter placeholder. + * @return a {@link BindMarkersFactory} using {@code placeholder}. + * @see io.r2dbc.spi.Statement#bindNull(int, Class) + * @see io.r2dbc.spi.Statement#bind(int, Object) + */ + static BindMarkersFactory anonymous(String placeholder) { + + Assert.hasText(placeholder, "Placeholder must not be empty!"); + return () -> new AnonymousBindMarkers(placeholder); + } + /** * Create named {@link BindMarkers} using identifiers to bind parameters. Named bind markers can support * {@link BindMarkers#next(String) name hints}. If no {@link BindMarkers#next(String) hint} is given, named bind diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java new file mode 100644 index 00000000..109d9295 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.dialect; + +import io.r2dbc.spi.Statement; + +/** + * A single indexed bind marker. + */ +class IndexedBindMarker implements BindMarker { + + private final String placeholder; + + private int index; + + IndexedBindMarker(String placeholder, int index) { + this.placeholder = placeholder; + this.index = index; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.BindMarker#getPlaceholder() + */ + @Override + public String getPlaceholder() { + return placeholder; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object) + */ + @Override + public void bind(Statement statement, Object value) { + statement.bind(this.index, value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class) + */ + @Override + public void bindNull(Statement statement, Class valueType) { + statement.bindNull(this.index, valueType); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java index 54b5e750..50a80c05 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkers.java @@ -1,7 +1,5 @@ package org.springframework.data.r2dbc.dialect; -import io.r2dbc.spi.Statement; - import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; /** @@ -45,46 +43,4 @@ class IndexedBindMarkers implements BindMarkers { return new IndexedBindMarker(prefix + "" + (index + offset), index); } - - /** - * A single indexed bind marker. - */ - static class IndexedBindMarker implements BindMarker { - - private final String placeholder; - - private int index; - - IndexedBindMarker(String placeholder, int index) { - this.placeholder = placeholder; - this.index = index; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.dialect.BindMarker#getPlaceholder() - */ - @Override - public String getPlaceholder() { - return placeholder; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.dialect.BindMarker#bindValue(io.r2dbc.spi.Statement, java.lang.Object) - */ - @Override - public void bind(Statement statement, Object value) { - statement.bind(this.index, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.dialect.BindMarker#bindNull(io.r2dbc.spi.Statement, java.lang.Class) - */ - @Override - public void bindNull(Statement statement, Class valueType) { - statement.bindNull(this.index, valueType); - } - } } diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java new file mode 100644 index 00000000..112f5b50 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.dialect; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Statement; + +import org.junit.Test; + +/** + * Unit tests for {@link AnonymousBindMarkers}. + * + * @author Mark Paluch + */ +public class AnonymousBindMarkersUnitTests { + + @Test // gh-75 + public void shouldCreateNewBindMarkers() { + + BindMarkersFactory factory = BindMarkersFactory.anonymous("?"); + + BindMarkers bindMarkers1 = factory.create(); + BindMarkers bindMarkers2 = factory.create(); + + assertThat(bindMarkers1.next().getPlaceholder()).isEqualTo("?"); + assertThat(bindMarkers2.next().getPlaceholder()).isEqualTo("?"); + } + + @Test // gh-75 + public void shouldBindByIndex() { + + Statement statement = mock(Statement.class); + + BindMarkers bindMarkers = BindMarkersFactory.anonymous("?").create(); + + BindMarker first = bindMarkers.next(); + BindMarker second = bindMarkers.next(); + + second.bind(statement, "foo"); + first.bindNull(statement, Object.class); + + verify(statement).bindNull(0, Object.class); + verify(statement).bind(1, "foo"); + } +} From 3ca32b89b7752e1cfb74d337d744d5d270bfe1cc Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 20 Mar 2019 16:49:38 +0200 Subject: [PATCH 100/665] #75 - Add support for MySQL using jasync-mysql. We now support MySQL through the jasync-mysql driver that exposes its asynchronous functionality through a R2DBC wrapper implementation. Jasync uses for now its own exceptions. Original pull request: #84. --- pom.xml | 23 ++- .../r2dbc/dialect/AnonymousBindMarkers.java | 2 +- .../data/r2dbc/dialect/Database.java | 12 ++ .../data/r2dbc/dialect/IndexedBindMarker.java | 2 +- .../data/r2dbc/dialect/MySqlDialect.java | 109 +++++++++++++ .../AnonymousBindMarkersUnitTests.java | 2 +- ...ctionalDatabaseClientIntegrationTests.java | 11 +- .../MySqlDatabaseClientIntegrationTests.java | 57 +++++++ ...ctionalDatabaseClientIntegrationTests.java | 63 ++++++++ .../MySqlR2dbcRepositoryIntegrationTests.java | 99 ++++++++++++ .../data/r2dbc/testing/MySqlTestSupport.java | 151 ++++++++++++++++++ 11 files changed, 525 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java diff --git a/pom.xml b/pom.xml index a977b39d..03f3665d 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,8 @@ 0.1.4 2.4.1 42.2.5 + 5.1.47 + 0.9.38 7.1.2.jre8-preview Arabba-M7 1.0.1 @@ -173,6 +175,13 @@ test + + mysql + mysql-connector-java + ${mysql.version} + test + + com.microsoft.sqlserver mssql-jdbc @@ -198,6 +207,13 @@ test + + com.github.jasync-sql + jasync-r2dbc-mysql + ${jasync.version} + test + + de.schauderhaft.degraph degraph-check @@ -295,7 +311,8 @@ ${querydsl} ${spring} ${r2dbc-spi.version} - ${reactive-streams.version} + ${reactive-streams.version} + ${releasetrain} true 3 @@ -416,6 +433,10 @@ spring-libs-snapshot https://repo.spring.io/libs-snapshot + + jcenter + https://jcenter.bintray.com/ + diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java index d2ef0b13..32b4cc10 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Database.java b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java index b59b9259..0eb27bf8 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Database.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Database.java @@ -53,6 +53,18 @@ public enum Database { public Dialect defaultDialect() { return H2Dialect.INSTANCE; } + }, + + MYSQL { + @Override + public String driverName() { + return "MySQL"; + } + + @Override + public Dialect defaultDialect() { + return MySqlDialect.INSTANCE; + } }; /** diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java index 109d9295..b7d84d65 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java new file mode 100644 index 00000000..d3cd260e --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/dialect/MySqlDialect.java @@ -0,0 +1,109 @@ +/* + * 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.dialect; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * An SQL dialect for MySQL. + * + * @author Mark Paluch + */ +public class MySqlDialect implements Dialect { + + private static final Set> SIMPLE_TYPES = new HashSet<>( + Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); + + /** + * Singleton instance. + */ + public static final MySqlDialect INSTANCE = new MySqlDialect(); + + private static final BindMarkersFactory ANONYMOUS = BindMarkersFactory.anonymous("?"); + + private static final LimitClause LIMIT_CLAUSE = new LimitClause() { + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long, long) + */ + @Override + public String getClause(long limit, long offset) { + return String.format("LIMIT %d,%d", limit, offset); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClause(long) + */ + @Override + public String getClause(long limit) { + return "LIMIT " + limit; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.LimitClause#getClausePosition() + */ + @Override + public Position getClausePosition() { + return Position.END; + } + }; + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getBindMarkersFactory() + */ + @Override + public BindMarkersFactory getBindMarkersFactory() { + return ANONYMOUS; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getSimpleTypesKeys() + */ + @Override + public Collection> getSimpleTypes() { + return SIMPLE_TYPES; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#limit() + */ + @Override + public LimitClause limit() { + return LIMIT_CLAUSE; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.dialect.Dialect#getArraySupport() + */ + @Override + public ArrayColumns getArraySupport() { + return ArrayColumns.Unsupported.INSTANCE; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java index 112f5b50..663af752 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index 10264512..628020f0 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -32,6 +32,7 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; @@ -201,15 +202,21 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(count).isEqualTo(0); } - @Test // gh-2 + @Test // gh-2, gh-75 public void emitTransactionIds() { TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); Flux transactionIds = databaseClient.inTransaction(db -> { + Mono insert = db.execute().sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated(); + Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0)).all(); - return txId.concatWith(txId); + return insert.thenMany(txId.concatWith(txId)); }); transactionIds.collectList().as(StepVerifier::create) // diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..3fc544c8 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; + +/** + * Integration tests for {@link DatabaseClient} against MySQL. + * + * @author Mark Paluch + */ +public class MySqlDatabaseClientIntegrationTests extends AbstractDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + @Ignore("Jasync currently uses its own exceptions, see jasync-sql/jasync-sql#106") + @Test + public void shouldTranslateDuplicateKeyException() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java new file mode 100644 index 00000000..615e29cf --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.ConnectionFactory; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; + +/** + * Integration tests for {@link TransactionalDatabaseClient} against MySQL. + * + * @author Mark Paluch + */ +public class MySqlTransactionalDatabaseClientIntegrationTests + extends AbstractTransactionalDatabaseClientIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET; + } + + @Override + protected String getCurrentTransactionIdStatement() { + return "SELECT tx.trx_id FROM information_schema.innodb_trx tx WHERE tx.trx_mysql_thread_id = connection_id()"; + } + + @Override + @Test + @Ignore("MySQL creates transactions only on interaction with transactional tables. BEGIN does not create a txid") + public void shouldManageUserTransaction() {} +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java new file mode 100644 index 00000000..5faf2cd4 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,99 @@ +/* + * 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.repository; + +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.sql.DataSource; + +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; +import org.springframework.data.r2dbc.testing.ExternalDatabase; +import org.springframework.data.r2dbc.testing.MySqlTestSupport; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory} against MySQL. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class MySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIntegrationTests { + + @ClassRule public static final ExternalDatabase database = MySqlTestSupport.database(); + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @Filter(classes = MySqlLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + } + + @Override + protected DataSource createDataSource() { + return MySqlTestSupport.createDataSource(database); + } + + @Override + protected ConnectionFactory createConnectionFactory() { + return MySqlTestSupport.createConnectionFactory(database); + } + + @Override + protected String getCreateTableStatement() { + return MySqlTestSupport.CREATE_TABLE_LEGOSET_WITH_ID_GENERATION; + } + + @Override + protected Class getRepositoryInterfaceType() { + return MySqlLegoSetRepository.class; + } + + interface MySqlLegoSetRepository extends LegoSetRepository { + + @Override + @Query("SELECT * FROM legoset WHERE name like ?") + Flux findByNameContains(String name); + + @Override + @Query("SELECT * FROM legoset") + Flux findAsProjection(); + + @Override + @Query("SELECT * FROM legoset WHERE manual = :manual") + Mono findByManual(int manual); + + @Override + @Query("SELECT id FROM legoset") + Flux findAllIds(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java new file mode 100644 index 00000000..8c13bda5 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java @@ -0,0 +1,151 @@ +/* + * 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.testing; + +import io.r2dbc.spi.ConnectionFactory; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import javax.sql.DataSource; + +import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; + +import org.testcontainers.containers.MySQLContainer; + +import com.github.jasync.r2dbc.mysql.JasyncConnectionFactory; +import com.github.jasync.sql.db.Configuration; +import com.github.jasync.sql.db.mysql.pool.MySQLConnectionFactory; +import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; + +/** + * Utility class for testing against MySQL. + * + * @author Mark Paluch + */ +public class MySqlTestSupport { + + private static ExternalDatabase testContainerDatabase; + + public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" // + + " id integer PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // + + " id integer AUTO_INCREMENT PRIMARY KEY,\n" // + + " name varchar(255) NOT NULL,\n" // + + " manual integer NULL\n" // + + ") ENGINE=InnoDB;"; + + /** + * Returns a database either hosted locally at {@code postgres:@localhost:5432/postgres} or running inside Docker. + * + * @return information about the database. Guaranteed to be not {@literal null}. + */ + public static ExternalDatabase database() { + + if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) { + + return getFirstWorkingDatabase( // + MySqlTestSupport::local, // + MySqlTestSupport::testContainer // + ); + } else { + + return getFirstWorkingDatabase( // + MySqlTestSupport::testContainer, // + MySqlTestSupport::local // + ); + } + } + + @SafeVarargs + private static ExternalDatabase getFirstWorkingDatabase(Supplier... suppliers) { + + return Stream.of(suppliers).map(Supplier::get) // + .filter(ExternalDatabase::checkValidity) // + .findFirst() // + .orElse(ExternalDatabase.unavailable()); + } + + /** + * Returns a locally provided database at {@code postgres:@localhost:5432/postgres}. + */ + private static ExternalDatabase local() { + + return ProvidedDatabase.builder() // + .hostname("localhost") // + .port(3306) // + .database("mysql") // + .username("root") // + .password("my-secret-pw").build(); + } + + /** + * Returns a database provided via Testcontainers. + */ + private static ExternalDatabase testContainer() { + + if (testContainerDatabase == null) { + + try { + MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.6.43"); + mySQLContainer.start(); + + testContainerDatabase = ProvidedDatabase.builder() // + .hostname("localhost") // + .port(mySQLContainer.getFirstMappedPort()) // + .database(mySQLContainer.getDatabaseName()) // + .username("root") // + .password(mySQLContainer.getPassword()).build(); + } catch (IllegalStateException ise) { + // docker not available. + testContainerDatabase = ExternalDatabase.unavailable(); + } + } + + return testContainerDatabase; + } + + /** + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + */ + public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { + + MySQLConnectionFactory jasync = new MySQLConnectionFactory(new Configuration(database.getUsername(), + database.getHostname(), database.getPort(), database.getPassword(), database.getDatabase())); + return new JasyncConnectionFactory(jasync); + } + + /** + * Creates a new {@link DataSource} configured from the {@link ExternalDatabase}. + */ + public static DataSource createDataSource(ExternalDatabase database) { + + MysqlDataSource dataSource = new MysqlDataSource(); + + dataSource.setUser(database.getUsername()); + dataSource.setPassword(database.getPassword()); + dataSource.setDatabaseName(database.getDatabase()); + dataSource.setServerName(database.getHostname()); + dataSource.setPortNumber(database.getPort()); + + return dataSource; + } + +} From c9435c879734e9c643997a42a5f3d8c74d8e5e69 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 27 Mar 2019 12:54:14 +0100 Subject: [PATCH 101/665] #75 - Polishing. Unify connection creation by providing JDBC URL. Obtain R2DBC ConnectionFactory using connection factory discovery. Original pull request: #84. --- .../data/r2dbc/testing/ConnectionUtils.java | 65 +++++++++ .../data/r2dbc/testing/ExternalDatabase.java | 124 +++++++++++++----- .../data/r2dbc/testing/MySqlTestSupport.java | 24 +--- .../r2dbc/testing/PostgresTestSupport.java | 29 +--- .../r2dbc/testing/SqlServerTestSupport.java | 16 +-- 5 files changed, 171 insertions(+), 87 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/testing/ConnectionUtils.java diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ConnectionUtils.java b/src/test/java/org/springframework/data/r2dbc/testing/ConnectionUtils.java new file mode 100644 index 00000000..a0725e8d --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/testing/ConnectionUtils.java @@ -0,0 +1,65 @@ +/* + * 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.testing; + +import static io.r2dbc.spi.ConnectionFactoryOptions.*; + +import io.r2dbc.spi.ConnectionFactories; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryOptions; + +import javax.sql.DataSource; + +/** + * Utility methods to configure {@link DataSource}/{@link ConnectionFactoryOptions}. + * + * @author Mark Paluch + */ +abstract class ConnectionUtils { + + /** + * Obtain a {@link ConnectionFactory} given {@link ExternalDatabase} and {@code driver}. + * + * @param driver + * @param configuration + * @return + */ + static ConnectionFactory getConnectionFactory(String driver, ExternalDatabase configuration) { + return ConnectionFactories.get(createOptions(driver, configuration)); + } + + /** + * Create {@link ConnectionFactoryOptions} from {@link ExternalDatabase} and {@code driver}. + * + * @param driver + * @param configuration + * @return + */ + private static ConnectionFactoryOptions createOptions(String driver, ExternalDatabase configuration) { + + return ConnectionFactoryOptions.builder().option(DRIVER, driver) // + .option(USER, configuration.getUsername()) // + .option(PASSWORD, configuration.getPassword()) // + .option(DATABASE, configuration.getDatabase()) // + .option(HOST, configuration.getHostname()) // + .option(PORT, configuration.getPort()) // + .build(); + } + + private ConnectionUtils() { + // utility constructor. + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java index 996abf9d..54c8cb17 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/ExternalDatabase.java @@ -26,6 +26,7 @@ import org.junit.AssumptionViolatedException; import org.junit.rules.ExternalResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.testcontainers.containers.JdbcDatabaseContainer; /** * {@link ExternalResource} wrapper to encapsulate {@link ProvidedDatabase} and @@ -47,15 +48,25 @@ public abstract class ExternalDatabase extends ExternalResource { return NoAvailableDatabase.INSTANCE; } + /** + * @return hostname on which the database service runs. + */ + public abstract String getHostname(); + /** * @return the post of the database service. */ public abstract int getPort(); /** - * @return hostname on which the database service runs. + * @return database user name. */ - public abstract String getHostname(); + public abstract String getUsername(); + + /** + * @return password for the database user. + */ + public abstract String getPassword(); /** * @return name of the database. @@ -63,9 +74,9 @@ public abstract class ExternalDatabase extends ExternalResource { public abstract String getDatabase(); /** - * @return database user name. + * @return JDBC URL for the endpoint. */ - public abstract String getUsername(); + public abstract String getJdbcUrl(); /** * Throws an {@link AssumptionViolatedException} if the database cannot be reached. @@ -98,22 +109,56 @@ public abstract class ExternalDatabase extends ExternalResource { return false; } - /** - * @return password for the database user. - */ - public abstract String getPassword(); - /** * Provided (unmanaged resource) database connection coordinates. */ @Builder public static class ProvidedDatabase extends ExternalDatabase { - private final int port; private final String hostname; - private final String database; + private final int port; private final String username; private final String password; + private final String database; + private final String jdbcUrl; + + public static ProvidedDatabaseBuilder builder() { + return new ProvidedDatabaseBuilder(); + } + + /** + * Create a {@link ProvidedDatabaseBuilder} initialized with {@link JdbcDatabaseContainer}. + * + * @param container + * @return + */ + public static ProvidedDatabaseBuilder builder(JdbcDatabaseContainer container) { + + return builder().hostname(container.getContainerIpAddress()) // + .port(container.getFirstMappedPort()) // + .username(container.getUsername()) // + .password(container.getPassword()) // + .database(container.getDatabaseName()) // + .jdbcUrl(container.getJdbcUrl()); + } + + /** + * Create a {@link ProvidedDatabase} from {@link JdbcDatabaseContainer}. + * + * @param container + * @return + */ + public static ProvidedDatabase from(JdbcDatabaseContainer container) { + return builder(container).build(); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname() + */ + @Override + public String getHostname() { + return hostname; + } /* (non-Javadoc) * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPort() @@ -123,22 +168,6 @@ public abstract class ExternalDatabase extends ExternalResource { return port; } - /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname() - */ - @Override - public String getHostname() { - return hostname; - } - - /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() - */ - @Override - public String getDatabase() { - return database; - } - /* (non-Javadoc) * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getUsername() */ @@ -154,6 +183,22 @@ public abstract class ExternalDatabase extends ExternalResource { public String getPassword() { return password; } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() + */ + @Override + public String getDatabase() { + return database; + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getJdbcUrl() + */ + @Override + public String getJdbcUrl() { + return jdbcUrl; + } } /** @@ -173,11 +218,6 @@ public abstract class ExternalDatabase extends ExternalResource { return false; } - @Override - public int getPort() { - throw new UnsupportedOperationException(getClass().getSimpleName()); - } - /* (non-Javadoc) * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getHostname() */ @@ -187,10 +227,10 @@ public abstract class ExternalDatabase extends ExternalResource { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getPort() */ @Override - public String getDatabase() { + public int getPort() { throw new UnsupportedOperationException(getClass().getSimpleName()); } @@ -209,5 +249,21 @@ public abstract class ExternalDatabase extends ExternalResource { public String getPassword() { throw new UnsupportedOperationException(getClass().getSimpleName()); } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getDatabase() + */ + @Override + public String getDatabase() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } + + /* (non-Javadoc) + * @see org.springframework.data.jdbc.core.function.ExternalDatabase#getJdbcUrl() + */ + @Override + public String getJdbcUrl() { + throw new UnsupportedOperationException(getClass().getSimpleName()); + } } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java index 8c13bda5..387dc401 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java @@ -26,9 +26,6 @@ import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; import org.testcontainers.containers.MySQLContainer; -import com.github.jasync.r2dbc.mysql.JasyncConnectionFactory; -import com.github.jasync.sql.db.Configuration; -import com.github.jasync.sql.db.mysql.pool.MySQLConnectionFactory; import com.mysql.jdbc.jdbc2.optional.MysqlDataSource; /** @@ -104,15 +101,12 @@ public class MySqlTestSupport { if (testContainerDatabase == null) { try { - MySQLContainer mySQLContainer = new MySQLContainer("mysql:5.6.43"); - mySQLContainer.start(); + MySQLContainer container = new MySQLContainer("mysql:5.6.43"); + container.start(); - testContainerDatabase = ProvidedDatabase.builder() // - .hostname("localhost") // - .port(mySQLContainer.getFirstMappedPort()) // - .database(mySQLContainer.getDatabaseName()) // + testContainerDatabase = ProvidedDatabase.builder(container) // .username("root") // - .password(mySQLContainer.getPassword()).build(); + .build(); } catch (IllegalStateException ise) { // docker not available. testContainerDatabase = ExternalDatabase.unavailable(); @@ -126,10 +120,7 @@ public class MySqlTestSupport { * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. */ public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { - - MySQLConnectionFactory jasync = new MySQLConnectionFactory(new Configuration(database.getUsername(), - database.getHostname(), database.getPort(), database.getPassword(), database.getDatabase())); - return new JasyncConnectionFactory(jasync); + return ConnectionUtils.getConnectionFactory("mysql", database); } /** @@ -141,11 +132,8 @@ public class MySqlTestSupport { dataSource.setUser(database.getUsername()); dataSource.setPassword(database.getPassword()); - dataSource.setDatabaseName(database.getDatabase()); - dataSource.setServerName(database.getHostname()); - dataSource.setPortNumber(database.getPort()); + dataSource.setURL(database.getJdbcUrl()); return dataSource; } - } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java index 834af652..31475a46 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -1,7 +1,5 @@ package org.springframework.data.r2dbc.testing; -import io.r2dbc.postgresql.PostgresqlConnectionConfiguration; -import io.r2dbc.postgresql.PostgresqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; import java.util.function.Supplier; @@ -10,7 +8,9 @@ import java.util.stream.Stream; import javax.sql.DataSource; import org.postgresql.ds.PGSimpleDataSource; + import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase; + import org.testcontainers.containers.PostgreSQLContainer; /** @@ -87,15 +87,10 @@ public class PostgresTestSupport { if (testContainerDatabase == null) { try { - PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer(); - postgreSQLContainer.start(); + PostgreSQLContainer container = new PostgreSQLContainer(); + container.start(); - testContainerDatabase = ProvidedDatabase.builder() // - .hostname("localhost") // - .port(postgreSQLContainer.getFirstMappedPort()) // - .database(postgreSQLContainer.getDatabaseName()) // - .username(postgreSQLContainer.getUsername()) // - .password(postgreSQLContainer.getPassword()).build(); + testContainerDatabase = ProvidedDatabase.from(container); } catch (IllegalStateException ise) { // docker not available. @@ -111,14 +106,7 @@ public class PostgresTestSupport { * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. */ public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { - - return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() // - .host(database.getHostname()) // - .database(database.getDatabase()) // - .port(database.getPort()) // - .username(database.getUsername()) // - .password(database.getPassword()) // - .build()); + return ConnectionUtils.getConnectionFactory("postgresql", database); } /** @@ -130,11 +118,8 @@ public class PostgresTestSupport { dataSource.setUser(database.getUsername()); dataSource.setPassword(database.getPassword()); - dataSource.setDatabaseName(database.getDatabase()); - dataSource.setServerName(database.getHostname()); - dataSource.setPortNumber(database.getPort()); + dataSource.setURL(database.getJdbcUrl()); return dataSource; } - } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java index d52c8476..fd12621e 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java @@ -1,7 +1,5 @@ package org.springframework.data.r2dbc.testing; -import io.r2dbc.mssql.MssqlConnectionConfiguration; -import io.r2dbc.mssql.MssqlConnectionFactory; import io.r2dbc.spi.ConnectionFactory; import javax.sql.DataSource; @@ -53,16 +51,10 @@ public class SqlServerTestSupport { } /** - * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}.. + * Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}. */ public static ConnectionFactory createConnectionFactory(ExternalDatabase database) { - - return new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host(database.getHostname()) // - .database(database.getDatabase()) // - .username(database.getUsername()) // - .password(database.getPassword()) // - .port(database.getPort()) // - .build()); + return ConnectionUtils.getConnectionFactory("mssql", database); } /** @@ -74,9 +66,7 @@ public class SqlServerTestSupport { dataSource.setUser(database.getUsername()); dataSource.setPassword(database.getPassword()); - dataSource.setDatabaseName(database.getDatabase()); - dataSource.setServerName(database.getHostname()); - dataSource.setPortNumber(database.getPort()); + dataSource.setURL(database.getJdbcUrl()); return dataSource; } From 6227d96341ae567fcbe59bf8e4d477f9e70020a6 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 17 Apr 2019 16:22:21 +0200 Subject: [PATCH 102/665] #75 - Polishing. Clarified code a little. Added a warning to `AnonymousBindMarkers`: Anonymous bind markers are problematic because the have to appear in generated SQL in the same order they get generated. This might cause challenges in the future with complex generate statements. For example those containing subselects which limit the freedom of arranging bind markers. Original pull request: #84. --- .../data/r2dbc/dialect/AnonymousBindMarkers.java | 6 ++++++ ...actTransactionalDatabaseClientIntegrationTests.java | 10 ++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java index 32b4cc10..c144245f 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkers.java @@ -21,6 +21,12 @@ import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; * Anonymous, index-based bind marker using a static placeholder. Instances are bound by the ordinal position ordered by * the appearance of the placeholder. This implementation creates indexed bind markers using an anonymous placeholder * that correlates with an index. + *

+ * Note: Anonymous bind markers are problematic because the have to appear in generated SQL in the same order they get generated. + * + * This might cause challenges in the future with complex generate statements. + * For example those containing subselects which limit the freedom of arranging bind markers. + *

* * @author Mark Paluch */ diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index 628020f0..deecc2e8 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -209,13 +209,19 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend Flux transactionIds = databaseClient.inTransaction(db -> { + // We have to execute a sql statement first. + // Otherwise some databases (MySql) don't have a transaction id. Mono insert = db.execute().sql(getInsertIntoLegosetStatement()) // .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // .bindNull(2, Integer.class) // .fetch().rowsUpdated(); - Flux txId = db.execute().sql(getCurrentTransactionIdStatement()).map((r, md) -> r.get(0)).all(); + Flux txId = db.execute() // + .sql(getCurrentTransactionIdStatement()) // + .map((row, md) -> row.get(0)) // + .all(); + return insert.thenMany(txId.concatWith(txId)); }); @@ -223,7 +229,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend .consumeNextWith(actual -> { assertThat(actual).hasSize(2); - assertThat(actual).containsExactly(actual.get(1), actual.get(0)); + assertThat(actual.get(0)).isEqualTo(actual.get(1)); }) // .verifyComplete(); } From 9c6142f9417455d886903905db80ef94ed44bca3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 15 Apr 2019 14:58:18 +0200 Subject: [PATCH 103/665] #95 - Use customized ConnectionFactory lookup to avoid AbstractR2dbcConfiguration proxying. We now attempt to lookup ConnectionFactory from ApplicationContext and fall back to a local method call if ConnectionFactory is not exposed as bean. Original pull request: #96. --- .../config/AbstractR2dbcConfiguration.java | 46 +++++- .../R2dbcConfigurationIntegrationTests.java | 147 ++++++++++++++++++ 2 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index b700db91..2f197d62 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -20,6 +20,9 @@ import io.r2dbc.spi.ConnectionFactory; import java.util.Collections; import java.util.Optional; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; @@ -37,6 +40,7 @@ import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslat import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,8 +52,21 @@ import org.springframework.util.Assert; * @see DatabaseClient * @see org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories */ -@Configuration -public abstract class AbstractR2dbcConfiguration { +@Configuration(proxyBeanMethods = false) +public abstract class AbstractR2dbcConfiguration implements ApplicationContextAware { + + private static final String CONNECTION_FACTORY_BEAN_NAME = "connectionFactory"; + + private @Nullable ApplicationContext context; + + /* + * (non-Javadoc) + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } /** * Return a R2DBC {@link ConnectionFactory}. Annotate with {@link Bean} in case you want to expose a @@ -91,7 +108,7 @@ public abstract class AbstractR2dbcConfiguration { Assert.notNull(exceptionTranslator, "ExceptionTranslator must not be null!"); return DatabaseClient.builder() // - .connectionFactory(connectionFactory()) // + .connectionFactory(lookupConnectionFactory()) // .dataAccessStrategy(dataAccessStrategy) // .exceptionTranslator(exceptionTranslator) // .build(); @@ -137,7 +154,7 @@ public abstract class AbstractR2dbcConfiguration { MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext, r2dbcCustomConversions); - return new DefaultReactiveDataAccessStrategy(getDialect(connectionFactory()), converter); + return new DefaultReactiveDataAccessStrategy(getDialect(lookupConnectionFactory()), converter); } /** @@ -160,7 +177,7 @@ public abstract class AbstractR2dbcConfiguration { */ protected StoreConversions getStoreConversions() { - Dialect dialect = getDialect(connectionFactory()); + Dialect dialect = getDialect(lookupConnectionFactory()); return StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS); } @@ -172,6 +189,23 @@ public abstract class AbstractR2dbcConfiguration { */ @Bean public R2dbcExceptionTranslator exceptionTranslator() { - return new SqlErrorCodeR2dbcExceptionTranslator(connectionFactory()); + return new SqlErrorCodeR2dbcExceptionTranslator(lookupConnectionFactory()); + } + + ConnectionFactory lookupConnectionFactory() { + + ApplicationContext context = this.context; + Assert.notNull(context, "ApplicationContext is not yet initialized"); + + String[] beanNamesForType = context.getBeanNamesForType(ConnectionFactory.class); + + for (String beanName : beanNamesForType) { + + if (beanName.equals(CONNECTION_FACTORY_BEAN_NAME)) { + return context.getBean(CONNECTION_FACTORY_BEAN_NAME, ConnectionFactory.class); + } + } + + return connectionFactory(); } } diff --git a/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java new file mode 100644 index 00000000..2295cbd7 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java @@ -0,0 +1,147 @@ +/* + * 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.config; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.h2.H2ConnectionConfiguration; +import io.r2dbc.h2.H2ConnectionFactory; +import io.r2dbc.spi.ConnectionFactory; + +import org.junit.Test; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.function.DatabaseClient; + +/** + * Tests for {@link AbstractR2dbcConfiguration}. + * + * @author Mark Paluch + */ +public class R2dbcConfigurationIntegrationTests { + + @Test // gh-95 + public void shouldLookupConnectionFactoryThroughLocalCall() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + NonBeanConnectionFactoryConfiguration.class); + + context.getBean(DatabaseClient.class); + + NonBeanConnectionFactoryConfiguration bean = context.getBean(NonBeanConnectionFactoryConfiguration.class); + + assertThat(context.getBeanNamesForType(ConnectionFactory.class)).isEmpty(); + assertThat(bean.callCounter).isGreaterThan(2); + + context.stop(); + } + + @Test // gh-95 + public void shouldLookupConnectionFactoryThroughLocalCallForExistingCustomBeans() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + CustomConnectionFactoryBeanNameConfiguration.class); + + context.getBean(DatabaseClient.class); + + CustomConnectionFactoryBeanNameConfiguration bean = context + .getBean(CustomConnectionFactoryBeanNameConfiguration.class); + + assertThat(context.getBeanNamesForType(ConnectionFactory.class)).hasSize(1).contains("myCustomBean"); + assertThat(bean.callCounter).isGreaterThan(2); + + ConnectionFactoryWrapper wrapper = context.getBean(ConnectionFactoryWrapper.class); + assertThat(wrapper.connectionFactory).isExactlyInstanceOf(H2ConnectionFactory.class); + + context.stop(); + } + + @Test // gh-95 + public void shouldRegisterConnectionFactory() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( + BeanConnectionFactoryConfiguration.class); + + context.getBean(DatabaseClient.class); + + BeanConnectionFactoryConfiguration bean = context.getBean(BeanConnectionFactoryConfiguration.class); + + assertThat(bean.callCounter).isEqualTo(1); + assertThat(context.getBeanNamesForType(ConnectionFactory.class)).hasSize(1); + + context.stop(); + } + + @Configuration(proxyBeanMethods = false) + static class NonBeanConnectionFactoryConfiguration extends AbstractR2dbcConfiguration { + + int callCounter; + + @Override + public ConnectionFactory connectionFactory() { + + callCounter++; + return new H2ConnectionFactory( + H2ConnectionConfiguration.builder().inMemory("foo").username("sa").password("").build()); + } + } + + @Configuration(proxyBeanMethods = false) + static class CustomConnectionFactoryBeanNameConfiguration extends AbstractR2dbcConfiguration { + + int callCounter; + + @Bean + public ConnectionFactory myCustomBean() { + return mock(ConnectionFactory.class); + } + + @Override + public ConnectionFactory connectionFactory() { + + callCounter++; + return new H2ConnectionFactory( + H2ConnectionConfiguration.builder().inMemory("foo").username("sa").password("").build()); + } + + @Bean + ConnectionFactoryWrapper wrapper() { + return new ConnectionFactoryWrapper(lookupConnectionFactory()); + } + } + + static class ConnectionFactoryWrapper { + ConnectionFactory connectionFactory; + + ConnectionFactoryWrapper(ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + } + + @Configuration(proxyBeanMethods = false) + static class BeanConnectionFactoryConfiguration extends NonBeanConnectionFactoryConfiguration { + + @Override + @Bean + public ConnectionFactory connectionFactory() { + return super.connectionFactory(); + } + } + +} From e161476d1c8456e4e90632be8fc6761f3a4a62fb Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Mar 2019 12:38:19 +0100 Subject: [PATCH 104/665] #57 - Add support for R2DBC subclass exception translation. We now use R2DBC's exception hierarchy to translate exceptions into Spring's DataAccessException hierarchy. Original pull request: #97. --- pom.xml | 3 +- .../config/AbstractR2dbcConfiguration.java | 7 +- .../DefaultDatabaseClientBuilder.java | 4 +- ...tractFallbackR2dbcExceptionTranslator.java | 5 +- .../R2dbcExceptionSubclassTranslator.java | 91 ++++++++++++++ ...bstractDatabaseClientIntegrationTests.java | 5 +- ...cExceptionSubclassTranslatorUnitTests.java | 114 ++++++++++++++++++ 7 files changed, 220 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java create mode 100644 src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java diff --git a/pom.xml b/pom.xml index 03f3665d..a577fa59 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 5.1.47 0.9.38 7.1.2.jre8-preview - Arabba-M7 + Arabba-BUILD-SNAPSHOT 1.0.1 1.10.1 @@ -111,6 +111,7 @@ org.springframework spring-jdbc + true diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 2f197d62..85043985 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -35,8 +35,9 @@ import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; +import org.springframework.data.r2dbc.support.SqlStateR2dbcExceptionTranslator; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -186,10 +187,12 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw * * @return must not be {@literal null}. * @see #connectionFactory() + * @see R2dbcExceptionSubclassTranslator + * @see SqlStateR2dbcExceptionTranslator */ @Bean public R2dbcExceptionTranslator exceptionTranslator() { - return new SqlErrorCodeR2dbcExceptionTranslator(lookupConnectionFactory()); + return new R2dbcExceptionSubclassTranslator(); } ConnectionFactory lookupConnectionFactory() { diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java index 992cab5c..a8d18b50 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java @@ -23,8 +23,8 @@ import java.util.function.Consumer; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.function.DatabaseClient.Builder; +import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; -import org.springframework.data.r2dbc.support.SqlErrorCodeR2dbcExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -114,7 +114,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { R2dbcExceptionTranslator exceptionTranslator = this.exceptionTranslator; if (exceptionTranslator == null) { - exceptionTranslator = new SqlErrorCodeR2dbcExceptionTranslator(connectionFactory); + exceptionTranslator = new R2dbcExceptionSubclassTranslator(); } ReactiveDataAccessStrategy accessStrategy = this.accessStrategy; diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java index bb57526b..139ea8b9 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -19,6 +19,7 @@ import io.r2dbc.spi.R2dbcException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.UncategorizedR2dbcException; import org.springframework.lang.NonNull; @@ -99,7 +100,7 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE protected abstract DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex); /** - * Build a message {@code String} for the given {@link java.sql.R2dbcException}. + * Build a message {@code String} for the given {@link R2dbcException}. *

* To be called by translator subclasses when creating an instance of a generic * {@link org.springframework.dao.DataAccessException} class. @@ -110,6 +111,6 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE * @return the message {@code String} to use. */ protected String buildMessage(String task, @Nullable String sql, R2dbcException ex) { - return task + "; " + (sql != null ? "SQL [" + sql : "]; " + "") + ex.getMessage(); + return task + "; " + (sql != null ? "SQL [" + sql + "]; " : "") + ex.getMessage(); } } diff --git a/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java new file mode 100644 index 00000000..16ed7a69 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslator.java @@ -0,0 +1,91 @@ +/* + * 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.support; + +import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.lang.Nullable; + +/** + * {@link R2dbcExceptionTranslator} implementation which analyzes the specific {@link R2dbcException} subclass thrown by + * the R2DBC driver. + *

+ * Falls back to a standard {@link SqlStateR2dbcExceptionTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslator extends AbstractFallbackR2dbcExceptionTranslator { + + public R2dbcExceptionSubclassTranslator() { + setFallbackTranslator(new SqlStateR2dbcExceptionTranslator()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.support.AbstractFallbackR2dbcExceptionTranslator#doTranslate(java.lang.String, java.lang.String, io.r2dbc.spi.R2dbcException) + */ + @Override + @Nullable + protected DataAccessException doTranslate(String task, @Nullable String sql, R2dbcException ex) { + + if (ex instanceof R2dbcTransientException) { + if (ex instanceof R2dbcTransientResourceException) { + return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcRollbackException) { + return new ConcurrencyFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcTimeoutException) { + return new QueryTimeoutException(buildMessage(task, sql, ex), ex); + } + } + + if (ex instanceof R2dbcNonTransientException) { + if (ex instanceof R2dbcNonTransientResourceException) { + return new DataAccessResourceFailureException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcDataIntegrityViolationException) { + return new DataIntegrityViolationException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcPermissionDeniedException) { + return new PermissionDeniedDataAccessException(buildMessage(task, sql, ex), ex); + } + if (ex instanceof R2dbcBadGrammarException) { + return new BadSqlGrammarException(task, (sql != null ? sql : ""), ex); + } + } + + // Fallback to Spring's own R2DBC state translation... + return null; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index 36818dfe..fb72765f 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -27,8 +27,9 @@ import javax.sql.DataSource; import org.junit.Before; import org.junit.Test; + import org.springframework.dao.DataAccessException; -import org.springframework.dao.DuplicateKeyException; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; @@ -125,7 +126,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .fetch().rowsUpdated() // .as(StepVerifier::create) // .expectErrorSatisfies(exception -> assertThat(exception) // - .isInstanceOf(DuplicateKeyException.class) // + .isInstanceOf(DataIntegrityViolationException.class) // .hasMessageContaining("execute; SQL [INSERT INTO legoset")) // .verify(); } diff --git a/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java new file mode 100644 index 00000000..c65f090c --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java @@ -0,0 +1,114 @@ +/* + * 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.support; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcDataIntegrityViolationException; +import io.r2dbc.spi.R2dbcException; +import io.r2dbc.spi.R2dbcNonTransientResourceException; +import io.r2dbc.spi.R2dbcPermissionDeniedException; +import io.r2dbc.spi.R2dbcRollbackException; +import io.r2dbc.spi.R2dbcTimeoutException; +import io.r2dbc.spi.R2dbcTransientResourceException; + +import org.junit.Test; + +import org.springframework.dao.ConcurrencyFailureException; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.dao.PermissionDeniedDataAccessException; +import org.springframework.dao.QueryTimeoutException; +import org.springframework.dao.TransientDataAccessResourceException; +import org.springframework.data.r2dbc.BadSqlGrammarException; +import org.springframework.data.r2dbc.UncategorizedR2dbcException; + +/** + * Unit tests for {@link R2dbcExceptionSubclassTranslator}. + * + * @author Mark Paluch + */ +public class R2dbcExceptionSubclassTranslatorUnitTests { + + R2dbcExceptionSubclassTranslator translator = new R2dbcExceptionSubclassTranslator(); + + @Test // gh-57 + public void shouldTranslateTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcTransientResourceException()); + + assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class); + } + + @Test // gh-57 + public void shouldTranslateRollbackException() { + + Exception exception = translator.translate("", "", new R2dbcRollbackException()); + + assertThat(exception).isInstanceOf(ConcurrencyFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateTimeoutException() { + + Exception exception = translator.translate("", "", new R2dbcTimeoutException()); + + assertThat(exception).isInstanceOf(QueryTimeoutException.class); + } + + @Test // gh-57 + public void shouldNotTranslateUnknownExceptions() { + + Exception exception = translator.translate("", "", new MyTransientExceptions()); + + assertThat(exception).isInstanceOf(UncategorizedR2dbcException.class); + } + + @Test // gh-57 + public void shouldTranslateNonTransientResourceException() { + + Exception exception = translator.translate("", "", new R2dbcNonTransientResourceException()); + + assertThat(exception).isInstanceOf(DataAccessResourceFailureException.class); + } + + @Test // gh-57 + public void shouldTranslateIntegrityViolationException() { + + Exception exception = translator.translate("", "", new R2dbcDataIntegrityViolationException()); + + assertThat(exception).isInstanceOf(DataIntegrityViolationException.class); + } + + @Test // gh-57 + public void shouldTranslatePermissionDeniedException() { + + Exception exception = translator.translate("", "", new R2dbcPermissionDeniedException()); + + assertThat(exception).isInstanceOf(PermissionDeniedDataAccessException.class); + } + + @Test // gh-57 + public void shouldTranslateBadSqlGrammarException() { + + Exception exception = translator.translate("", "", new R2dbcBadGrammarException()); + + assertThat(exception).isInstanceOf(BadSqlGrammarException.class); + } + + private static class MyTransientExceptions extends R2dbcException {} +} From f7d3124c6336fc39f338663ee1d4f5d44f9625db Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 23 Apr 2019 16:05:05 +0200 Subject: [PATCH 105/665] #57 - Added tests for the exception message. Original pull request: #97. --- ...tractFallbackR2dbcExceptionTranslator.java | 7 +++- ...cExceptionSubclassTranslatorUnitTests.java | 36 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java index 139ea8b9..9e12a7d8 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java +++ b/src/main/java/org/springframework/data/r2dbc/support/AbstractFallbackR2dbcExceptionTranslator.java @@ -111,6 +111,11 @@ public abstract class AbstractFallbackR2dbcExceptionTranslator implements R2dbcE * @return the message {@code String} to use. */ protected String buildMessage(String task, @Nullable String sql, R2dbcException ex) { - return task + "; " + (sql != null ? "SQL [" + sql + "]; " : "") + ex.getMessage(); + + return task + "; " + // + (sql != null // + ? "SQL [" + sql + "]; " // + : "" // + ) + ex.getMessage(); } } diff --git a/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java index c65f090c..246a1046 100644 --- a/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/support/R2dbcExceptionSubclassTranslatorUnitTests.java @@ -27,7 +27,6 @@ import io.r2dbc.spi.R2dbcTimeoutException; import io.r2dbc.spi.R2dbcTransientResourceException; import org.junit.Test; - import org.springframework.dao.ConcurrencyFailureException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -49,9 +48,10 @@ public class R2dbcExceptionSubclassTranslatorUnitTests { @Test // gh-57 public void shouldTranslateTransientResourceException() { - Exception exception = translator.translate("", "", new R2dbcTransientResourceException()); + Exception exception = translator.translate("", "", new R2dbcTransientResourceException("")); - assertThat(exception).isInstanceOf(TransientDataAccessResourceException.class); + assertThat(exception) + .isInstanceOf(TransientDataAccessResourceException.class); } @Test // gh-57 @@ -110,5 +110,35 @@ public class R2dbcExceptionSubclassTranslatorUnitTests { assertThat(exception).isInstanceOf(BadSqlGrammarException.class); } + @Test // gh-57 + public void messageGeneration() { + + Exception exception = translator.translate("TASK", "SOME-SQL", new R2dbcTransientResourceException("MESSAGE")); + + assertThat(exception) // + .isInstanceOf(TransientDataAccessResourceException.class) // + .hasMessage("TASK; SQL [SOME-SQL]; MESSAGE; nested exception is io.r2dbc.spi.R2dbcTransientResourceException: MESSAGE"); + } + + @Test // gh-57 + public void messageGenerationNullSQL() { + + Exception exception = translator.translate("TASK", null, new R2dbcTransientResourceException("MESSAGE")); + + assertThat(exception) // + .isInstanceOf(TransientDataAccessResourceException.class) // + .hasMessage("TASK; MESSAGE; nested exception is io.r2dbc.spi.R2dbcTransientResourceException: MESSAGE"); + } + + @Test // gh-57 + public void messageGenerationNullMessage() { + + Exception exception = translator.translate("TASK", "SOME-SQL", new R2dbcTransientResourceException()); + + assertThat(exception) // + .isInstanceOf(TransientDataAccessResourceException.class) // + .hasMessage("TASK; SQL [SOME-SQL]; null; nested exception is io.r2dbc.spi.R2dbcTransientResourceException"); + } + private static class MyTransientExceptions extends R2dbcException {} } From 72ffccbfbc5fa71dfc40732634116a227aa55121 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Apr 2019 14:40:54 +0200 Subject: [PATCH 106/665] #100 - Remove Spring JDBC as mandatory dependency. We now ship our own SqlProvider variant to not require Spring JDBC as mandatory dependency. Spring JDBC can be provided optionally to use SQL-code based exception translation. --- .../r2dbc/InvalidResultAccessException.java | 1 - .../r2dbc/function/DefaultDatabaseClient.java | 1 - .../data/r2dbc/function/DefaultFetchSpec.java | 8 ++-- .../data/r2dbc/function/DefaultSqlResult.java | 11 +++--- .../r2dbc/function/NamedParameterUtils.java | 2 +- .../data/r2dbc/function/ParsedSql.java | 2 +- .../data/r2dbc/function/SqlProvider.java | 39 +++++++++++++++++++ .../query/R2dbcParameterAccessor.java | 2 +- .../repository/query/R2dbcQueryExecution.java | 2 +- .../query/StringBasedR2dbcQuery.java | 2 +- 10 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java index 06917696..1900509c 100644 --- a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -18,7 +18,6 @@ package org.springframework.data.r2dbc; import io.r2dbc.spi.R2dbcException; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.lang.Nullable; /** 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 b7c52268..0ccfd573 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -57,7 +57,6 @@ 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.support.R2dbcExceptionTranslator; -import org.springframework.jdbc.core.SqlProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java index 66e2bbbf..a1e8556c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java @@ -38,7 +38,7 @@ class DefaultFetchSpec implements FetchSpec { private final Function> updatedRowsFunction; /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#one() + * @see org.springframework.data.r2dbc.function.FetchSpec#one() */ @Override public Mono one() { @@ -60,7 +60,7 @@ class DefaultFetchSpec implements FetchSpec { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#first() + * @see org.springframework.data.r2dbc.function.FetchSpec#first() */ @Override public Mono first() { @@ -68,7 +68,7 @@ class DefaultFetchSpec implements FetchSpec { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#all() + * @see org.springframework.data.r2dbc.function.FetchSpec#all() */ @Override public Flux all() { @@ -76,7 +76,7 @@ class DefaultFetchSpec implements FetchSpec { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#rowsUpdated() + * @see org.springframework.data.r2dbc.function.FetchSpec#rowsUpdated() */ @Override public Mono rowsUpdated() { diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java index 531090de..294e5ce1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java @@ -25,7 +25,6 @@ import reactor.core.publisher.Mono; import java.util.function.BiFunction; import java.util.function.Function; -import org.springframework.jdbc.core.SqlProvider; /** * Default {@link SqlResult} implementation. @@ -110,7 +109,7 @@ class DefaultSqlResult implements SqlResult { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.SqlResult#map(java.util.function.BiFunction) + * @see org.springframework.data.r2dbc.function.SqlResult#map(java.util.function.BiFunction) */ @Override public SqlResult map(BiFunction mappingFunction) { @@ -118,7 +117,7 @@ class DefaultSqlResult implements SqlResult { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#one() + * @see org.springframework.data.r2dbc.function.FetchSpec#one() */ @Override public Mono one() { @@ -126,7 +125,7 @@ class DefaultSqlResult implements SqlResult { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#first() + * @see org.springframework.data.r2dbc.function.FetchSpec#first() */ @Override public Mono first() { @@ -134,7 +133,7 @@ class DefaultSqlResult implements SqlResult { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#all() + * @see org.springframework.data.r2dbc.function.FetchSpec#all() */ @Override public Flux all() { @@ -142,7 +141,7 @@ class DefaultSqlResult implements SqlResult { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.core.function.FetchSpec#rowsUpdated() + * @see org.springframework.data.r2dbc.function.FetchSpec#rowsUpdated() */ @Override public Mono rowsUpdated() { diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java index 15c0b1b3..2ad61606 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java @@ -39,7 +39,7 @@ import org.springframework.util.Assert; * Only intended for internal use within Spring's Data's R2DBC framework. Partially extracted from Spring's JDBC named * parameter support. *

- * This is a subset of Spring Frameworks's {@code org.springframework.jdbc.core.namedparam.NamedParameterUtils}. + * This is a subset of Spring Frameworks's {@code org.springframework.r2dbc.namedparam.NamedParameterUtils}. * * @author Thomas Risberg * @author Juergen Hoeller diff --git a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java index 436aa80e..651be9c8 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java +++ b/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java @@ -21,7 +21,7 @@ import java.util.List; /** * Holds information about a parsed SQL statement. *

- * This is a copy of Spring Frameworks's {@code org.springframework.jdbc.core.namedparam.ParsedSql}. + * This is a copy of Spring Frameworks's {@code org.springframework.r2dbc.namedparam.ParsedSql}. * * @author Thomas Risberg * @author Juergen Hoeller diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java b/src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java new file mode 100644 index 00000000..83843249 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java @@ -0,0 +1,39 @@ +/* + * 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 org.springframework.lang.Nullable; + +/** + * Interface to be implemented by objects that can provide SQL strings. + *

+ * Typically implemented by objects that want to expose the SQL they use to create their statements, to allow for better + * contextual information in case of exceptions. + * + * @author Juergen Hoeller + * @author Mark Paluch + */ +public interface SqlProvider { + + /** + * Return the SQL string for this object, i.e. typically the SQL used for creating statements. + * + * @return the SQL string, or {@code null}. + */ + @Nullable + String getSql(); + +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java index a40f8ecb..c82404e0 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcParameterAccessor.java @@ -79,7 +79,7 @@ class R2dbcParameterAccessor extends RelationalParametersParameterAccessor { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.JdbcParametersParameterAccessor#getValues() + * @see org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor#getValues() */ @Override public Object[] getValues() { diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index 701ff40c..32ce93f4 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -49,7 +49,7 @@ interface R2dbcQueryExecution { private final @NonNull Converter converter; /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.R2dbcQueryExecution#execute(org.springframework.data.jdbc.core.function.FetchSpec, java.lang.Class, java.lang.String) + * @see org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution#execute(org.springframework.data.r2dbc.function.FetchSpec, java.lang.Class, java.lang.String) */ @Override public Object execute(FetchSpec query, Class type, String tableName) { diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index ad61dffb..833ee534 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -75,7 +75,7 @@ public class StringBasedR2dbcQuery extends AbstractR2dbcQuery { } /* (non-Javadoc) - * @see org.springframework.data.jdbc.repository.query.AbstractR2dbcQuery#createQuery(org.springframework.data.jdbc.repository.query.JdbcParameterAccessor) + * @see org.springframework.data.r2dbc.repository.query.AbstractR2dbcQuery#createQuery(org.springframework.data.relational.repository.query.RelationalParameterAccessor) */ @Override protected BindableQuery createQuery(RelationalParameterAccessor accessor) { From ef2d885b1e24a31c822900237e4de9a861f09cec Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Mar 2019 14:52:30 +0100 Subject: [PATCH 107/665] #73 - Introduce PreparedOperation. We now encapsulate prepared operations from the StatementFactory within PreparedOperation that renders SQL and provides binding values. StatementFactory supports SELECT/INSERT/UPDATE/DELETE statement creation considering Dialect-specific rendering. StatementFactory replaces String-based statement methods in ReactiveDataAccessStrategy. PreparedOperation operation = accessStrategy.getStatements().update(entity.getTableName(), binder -> { binder.bind("name", "updated value"); binder.filterBy("id", SettableValue.from(42)); }); databaseClient.execute().sql(operation).then(); Original pull request: #82. --- .../data/r2dbc/function/BindIdOperation.java | 32 -- .../data/r2dbc/function/DatabaseClient.java | 5 + .../r2dbc/function/DefaultDatabaseClient.java | 65 ++- .../DefaultReactiveDataAccessStrategy.java | 356 ++----------- .../function/DefaultStatementFactory.java | 474 ++++++++++++++++++ .../r2dbc/function/PreparedOperation.java | 48 ++ .../function/ReactiveDataAccessStrategy.java | 43 +- .../data/r2dbc/function/StatementFactory.java | 102 ++++ .../support/SimpleR2dbcRepository.java | 100 ++-- .../r2dbc/support/StatementRenderUtil.java | 1 + ...ltReactiveDataAccessStrategyUnitTests.java | 103 ---- .../function/StatementFactoryUnitTests.java | 247 +++++++++ 12 files changed, 991 insertions(+), 585 deletions(-) delete mode 100644 src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/PreparedOperation.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java delete mode 100644 src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/StatementFactoryUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java b/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java deleted file mode 100644 index 71f437ca..00000000 --- a/src/main/java/org/springframework/data/r2dbc/function/BindIdOperation.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.springframework.data.r2dbc.function; - -import io.r2dbc.spi.Statement; - -/** - * Extension to {@link BindableOperation} for operations that allow parameter substitution for a single {@code id} - * column that accepts either a single value or multiple values, depending on the underlying operation. - * - * @author Mark Paluch - * @see Statement#bind - * @see Statement#bindNull - */ -public interface BindIdOperation extends BindableOperation { - - /** - * Bind the given {@code value} to the {@link Statement} using the underlying binding strategy. - * - * @param statement the statement to bind the value to. - * @param value the actual value. Must not be {@literal null}. - * @see Statement#bind - */ - void bindId(Statement statement, Object value); - - /** - * Bind the given {@code values} to the {@link Statement} using the underlying binding strategy. - * - * @param statement the statement to bind the value to. - * @param values the actual values. - * @see Statement#bind - */ - void bindIds(Statement statement, Iterable values); -} 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 7062fca5..df95db78 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java @@ -26,6 +26,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import org.reactivestreams.Publisher; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; @@ -137,6 +138,9 @@ public interface DatabaseClient { * Contract for specifying a SQL call along with options leading to the exchange. The SQL string can contain either * native parameter bind markers (e.g. {@literal $1, $2} for Postgres, {@literal @P0, @P1} for SQL Server) or named * parameters (e.g. {@literal :foo, :bar}) when {@link NamedParameterExpander} is enabled. + *

+ * Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}. + *

* * @see NamedParameterExpander * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) @@ -156,6 +160,7 @@ public interface DatabaseClient { * * @param sqlSupplier must not be {@literal null}. * @return a new {@link GenericExecuteSpec}. + * @see PreparedOperation */ GenericExecuteSpec sql(Supplier sqlSupplier); } 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 0ccfd573..e8378167 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -37,7 +37,6 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiFunction; import java.util.function.Function; @@ -49,6 +48,7 @@ import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.UncategorizedR2dbcException; @@ -57,6 +57,7 @@ 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.support.R2dbcExceptionTranslator; +import org.springframework.data.relational.core.sql.Insert; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -336,9 +337,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { logger.debug("Executing SQL statement [" + sql + "]"); } + if (sqlSupplier instanceof PreparedOperation) { + return ((PreparedOperation) sqlSupplier).bind(it.createStatement(sql)); + } + BindableOperation operation = namedParameters.expand(sql, dataAccessStrategy.getBindMarkersFactory(), new MapBindParameterSource(byName)); + if (logger.isTraceEnabled()) { + logger.trace("Expanded SQL [" + operation.toQuery() + "]"); + } + Statement statement = it.createStatement(operation.toQuery()); byName.forEach((name, o) -> { @@ -366,6 +375,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public ExecuteSpecSupport bind(int index, Object value) { + assertNotPreparedOperation(); Assert.notNull(value, () -> String.format("Value at index %d must not be null. Use bindNull(…) instead.", index)); Map byIndex = new LinkedHashMap<>(this.byIndex); @@ -376,6 +386,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public ExecuteSpecSupport bindNull(int index, Class type) { + assertNotPreparedOperation(); + Map byIndex = new LinkedHashMap<>(this.byIndex); byIndex.put(index, SettableValue.empty(type)); @@ -384,6 +396,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public ExecuteSpecSupport bind(String name, Object value) { + assertNotPreparedOperation(); + Assert.hasText(name, "Parameter name must not be null or empty!"); Assert.notNull(value, () -> String.format("Value for parameter %s must not be null. Use bindNull(…) instead.", name)); @@ -396,6 +410,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { public ExecuteSpecSupport bindNull(String name, Class type) { + assertNotPreparedOperation(); Assert.hasText(name, "Parameter name must not be null or empty!"); Map byName = new LinkedHashMap<>(this.byName); @@ -404,6 +419,12 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return createInstance(this.byIndex, byName, this.sqlSupplier); } + private void assertNotPreparedOperation() { + if (sqlSupplier instanceof PreparedOperation) { + throw new InvalidDataAccessApiUsageException("Cannot add bindings to a PreparedOperation"); + } + } + protected ExecuteSpecSupport createInstance(Map byIndex, Map byName, Supplier sqlSupplier) { return new ExecuteSpecSupport(byIndex, byName, sqlSupplier); @@ -881,20 +902,19 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { throw new IllegalStateException("Insert fields is empty!"); } - BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, byName.keySet()); + PreparedOperation operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(), + it -> { + byName.forEach(it::bind); + }); - String sql = bindableInsert.toQuery(); + String sql = operation.toQuery(); Function insertFunction = it -> { if (logger.isDebugEnabled()) { logger.debug("Executing SQL statement [" + sql + "]"); } - Statement statement = it.createStatement(sql).returnGeneratedValues(); - - byName.forEach((k, v) -> bindableInsert.bind(statement, k, v)); - - return statement; + return operation.bind(it.createStatement(sql)); }; Function> resultFunction = it -> Flux.from(insertFunction.apply(it).execute()); @@ -998,18 +1018,17 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { OutboundRow outboundRow = dataAccessStrategy.getOutboundRow(toInsert); - Set columns = new LinkedHashSet<>(); + PreparedOperation operation = dataAccessStrategy.getStatements().insert(table, Collections.emptyList(), + it -> { + outboundRow.forEach((k, v) -> { - outboundRow.forEach((k, v) -> { + if (v.hasValue()) { + it.bind(k, v); + } + }); + }); - if (v.hasValue()) { - columns.add(k); - } - }); - - BindableOperation bindableInsert = dataAccessStrategy.insertAndReturnGeneratedKeys(table, columns); - - String sql = bindableInsert.toQuery(); + String sql = operation.toQuery(); Function insertFunction = it -> { @@ -1017,15 +1036,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { logger.debug("Executing SQL statement [" + sql + "]"); } - Statement statement = it.createStatement(sql).returnGeneratedValues(); - - outboundRow.forEach((k, v) -> { - if (v.hasValue()) { - bindableInsert.bind(statement, k, v); - } - }); - - return statement; + return operation.bind(it.createStatement(sql)); }; Function> resultFunction = it -> Flux.from(insertFunction.apply(it).execute()); 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 d36ea69c..17f3ddbd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java @@ -17,14 +17,11 @@ package org.springframework.data.r2dbc.function; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; -import io.r2dbc.spi.Statement; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.OptionalLong; import java.util.Set; import java.util.function.BiFunction; @@ -37,8 +34,6 @@ 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.BindMarker; -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; @@ -53,13 +48,17 @@ import org.springframework.data.relational.core.mapping.RelationalPersistentEnti import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.Expression; import org.springframework.data.relational.core.sql.OrderByField; +import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.SelectBuilder.SelectFromAndOrderBy; import org.springframework.data.relational.core.sql.StatementBuilder; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.render.NamingStrategies; +import org.springframework.data.relational.core.sql.render.RenderContext; +import org.springframework.data.relational.core.sql.render.RenderNamingStrategy; +import org.springframework.data.relational.core.sql.render.SelectRenderContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; /** * Default {@link ReactiveDataAccessStrategy} implementation. @@ -71,6 +70,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final Dialect dialect; private final R2dbcConverter converter; private final MappingContext, ? extends RelationalPersistentProperty> mappingContext; + private final StatementFactory statements; /** * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}. @@ -118,6 +118,30 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra this.mappingContext = (MappingContext, ? extends RelationalPersistentProperty>) this.converter .getMappingContext(); this.dialect = dialect; + + RenderContext renderContext = new RenderContext() { + @Override + public RenderNamingStrategy getNamingStrategy() { + return NamingStrategies.asIs(); + } + + @Override + public SelectRenderContext getSelect() { + return new SelectRenderContext() { + @Override + public Function afterSelectList() { + return it -> ""; + } + + @Override + public Function afterOrderBy(boolean hasOrderBy) { + return it -> ""; + } + }; + } + }; + + this.statements = new DefaultStatementFactory(this.dialect, renderContext); } /* @@ -218,7 +242,6 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getRowMapper(java.lang.Class) */ - @SuppressWarnings("unchecked") @Override public BiFunction getRowMapper(Class typeToRead) { return new EntityRowMapper<>(typeToRead, converter); @@ -233,6 +256,15 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return getRequiredPersistentEntity(type).getTableName(); } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getStatements() + */ + @Override + public StatementFactory getStatements() { + return this.statements; + } + /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#getBindMarkersFactory() @@ -251,15 +283,6 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return mappingContext.getPersistentEntity(typeToRead); } - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#insertAndReturnGeneratedKeys(java.lang.String, java.util.Set) - */ - @Override - public BindableOperation insertAndReturnGeneratedKeys(String table, Set columns) { - return new DefaultBindableInsert(dialect.getBindMarkersFactory().create(), table, columns); - } - /* * (non-Javadoc) * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#select(java.lang.String, java.util.Set, org.springframework.data.domain.Sort, org.springframework.data.domain.Pageable) @@ -290,6 +313,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra offset = OptionalLong.of(page.getOffset()); } + // See https://github.com/spring-projects/spring-data-r2dbc/issues/55 return StatementRenderUtil.render(selectBuilder.build(), limit, offset, this.dialect); } @@ -310,304 +334,4 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra return fields; } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#updateById(java.lang.String, java.util.Set, java.lang.String) - */ - @Override - public BindIdOperation updateById(String table, Set columns, String idColumn) { - return new DefaultBindableUpdate(dialect.getBindMarkersFactory().create(), table, columns, idColumn); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#deleteById(java.lang.String, java.lang.String) - */ - @Override - public BindIdOperation deleteById(String table, String idColumn) { - - return new DefaultBindIdOperation(dialect.getBindMarkersFactory().create(), - marker -> String.format("DELETE FROM %s WHERE %s = %s", table, idColumn, marker.getPlaceholder())); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy#deleteByIdIn(java.lang.String, java.lang.String) - */ - @Override - public BindIdOperation deleteByIdIn(String table, String idColumn) { - - String query = String.format("DELETE FROM %s", table); - return new DefaultBindIdIn(dialect.getBindMarkersFactory().create(), query, idColumn); - } - - /** - * Default {@link BindableOperation} implementation for a {@code INSERT} operation. - */ - static class DefaultBindableInsert implements BindableOperation { - - private final Map markers = new LinkedHashMap<>(); - private final String query; - - DefaultBindableInsert(BindMarkers bindMarkers, String table, Collection columns) { - - StringBuilder builder = new StringBuilder(); - List placeholders = new ArrayList<>(columns.size()); - - for (String column : columns) { - BindMarker marker = markers.computeIfAbsent(column, bindMarkers::next); - placeholders.add(marker.getPlaceholder()); - } - - String columnsString = StringUtils.collectionToDelimitedString(columns, ", "); - String placeholdersString = StringUtils.collectionToDelimitedString(placeholders, ", "); - - builder.append("INSERT INTO ").append(table).append(" (").append(columnsString).append(")").append(" VALUES(") - .append(placeholdersString).append(")"); - - this.query = builder.toString(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object) - */ - @Override - public void bind(Statement statement, String identifier, Object value) { - markers.get(identifier).bind(statement, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class) - */ - @Override - public void bindNull(Statement statement, String identifier, Class valueType) { - markers.get(identifier).bindNull(statement, valueType); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery() - */ - @Override - public String toQuery() { - return this.query; - } - } - - /** - * Default {@link BindIdOperation} implementation for a {@code UPDATE} operation using a single key. - */ - static class DefaultBindableUpdate implements BindIdOperation { - - private final Map markers = new LinkedHashMap<>(); - private final BindMarker idMarker; - private final String query; - - DefaultBindableUpdate(BindMarkers bindMarkers, String tableName, Set columns, String idColumnName) { - - this.idMarker = bindMarkers.next(); - - StringBuilder setClause = new StringBuilder(); - - for (String column : columns) { - - BindMarker marker = markers.computeIfAbsent(column, bindMarkers::next); - - if (setClause.length() != 0) { - setClause.append(", "); - } - - setClause.append(column).append(" = ").append(marker.getPlaceholder()); - } - - this.query = String.format("UPDATE %s SET %s WHERE %s = %s", tableName, setClause, idColumnName, - idMarker.getPlaceholder()); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object) - */ - @Override - public void bind(Statement statement, String identifier, Object value) { - markers.get(identifier).bind(statement, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class) - */ - @Override - public void bindNull(Statement statement, String identifier, Class valueType) { - markers.get(identifier).bindNull(statement, valueType); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object) - */ - @Override - public void bindId(Statement statement, Object value) { - idMarker.bind(statement, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable) - */ - @Override - public void bindIds(Statement statement, Iterable values) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery() - */ - @Override - public String toQuery() { - return this.query; - } - } - - /** - * Default {@link BindIdOperation} implementation for a {@code SELECT} or {@code DELETE} operation using a single key - * in the {@code WHERE} predicate. - */ - static class DefaultBindIdOperation implements BindIdOperation { - - private final BindMarker idMarker; - private final String query; - - DefaultBindIdOperation(BindMarkers bindMarkers, Function queryFunction) { - - this.idMarker = bindMarkers.next(); - this.query = queryFunction.apply(this.idMarker); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object) - */ - @Override - public void bind(Statement statement, String identifier, Object value) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class) - */ - @Override - public void bindNull(Statement statement, String identifier, Class valueType) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object) - */ - @Override - public void bindId(Statement statement, Object value) { - idMarker.bind(statement, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable) - */ - @Override - public void bindIds(Statement statement, Iterable values) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery() - */ - @Override - public String toQuery() { - return this.query; - } - } - - /** - * Default {@link BindIdOperation} implementation for a {@code SELECT … WHERE id IN (…)} or - * {@code DELETE … WHERE id IN (…)}. - */ - static class DefaultBindIdIn implements BindIdOperation { - - private final List markers = new ArrayList<>(); - private final BindMarkers bindMarkers; - private final String baseQuery; - private final String idColumnName; - - DefaultBindIdIn(BindMarkers bindMarkers, String baseQuery, String idColumnName) { - - this.bindMarkers = bindMarkers; - this.baseQuery = baseQuery; - this.idColumnName = idColumnName; - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bind(io.r2dbc.spi.Statement, java.lang.String, java.lang.Object) - */ - @Override - public void bind(Statement statement, String identifier, Object value) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindableOperation#bindNull(io.r2dbc.spi.Statement, java.lang.String, java.lang.Class) - */ - @Override - public void bindNull(Statement statement, String identifier, Class valueType) { - throw new UnsupportedOperationException(); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindId(io.r2dbc.spi.Statement, java.lang.Object) - */ - @Override - public void bindId(Statement statement, Object value) { - - BindMarker bindMarker = bindMarkers.next(); - markers.add(bindMarker.getPlaceholder()); - bindMarker.bind(statement, value); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.BindIdOperation#bindIds(io.r2dbc.spi.Statement, java.lang.Iterable) - */ - @Override - public void bindIds(Statement statement, Iterable values) { - - for (Object value : values) { - bindId(statement, value); - } - } - - /* - * (non-Javadoc) - * @see org.springframework.data.r2dbc.function.QueryOperation#toQuery() - */ - @Override - public String toQuery() { - - if (this.markers.isEmpty()) { - throw new UnsupportedOperationException(); - } - - String in = StringUtils.collectionToDelimitedString(this.markers, ", "); - - return String.format("%s WHERE %s IN (%s)", this.baseQuery, this.idColumnName, in); - } - } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java new file mode 100644 index 00000000..6028fd18 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java @@ -0,0 +1,474 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import io.r2dbc.spi.Statement; +import lombok.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.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.r2dbc.dialect.BindMarker; +import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.Dialect; +import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.relational.core.sql.AssignValue; +import org.springframework.data.relational.core.sql.Assignment; +import org.springframework.data.relational.core.sql.Column; +import org.springframework.data.relational.core.sql.Condition; +import org.springframework.data.relational.core.sql.Delete; +import org.springframework.data.relational.core.sql.DeleteBuilder; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Insert; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.SelectBuilder; +import org.springframework.data.relational.core.sql.StatementBuilder; +import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.Update; +import org.springframework.data.relational.core.sql.UpdateBuilder; +import org.springframework.data.relational.core.sql.render.RenderContext; +import org.springframework.data.relational.core.sql.render.SqlRenderer; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default {@link StatementFactory} implementation. + * + * @author Mark Paluch + */ +@RequiredArgsConstructor +class DefaultStatementFactory implements StatementFactory { + + 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, + Consumer binderConsumer); + + /** + * Creates a {@link Insert} statement. + * + * @param tableName must not be {@literal null} or empty. + * @param generatedKeysNames must not be {@literal null}. + * @param binderConsumer customizer for bindings. Supports only + * {@link StatementBinderBuilder#bind(String, SettableValue)} bindings. + * @return the {@link PreparedOperation} to update values in {@code tableName} assigning bound values. + */ + PreparedOperation insert(String tableName, Collection generatedKeysNames, + Consumer binderConsumer); + + /** + * Creates a {@link Update} statement. + * + * @param tableName must not be {@literal null} or empty. + * @param binderConsumer customizer for bindings. + * @return the {@link PreparedOperation} to update values in {@code tableName} assigning bound values. + */ + PreparedOperation update(String tableName, Consumer binderConsumer); + + /** + * Creates a {@link Delete} statement. + * + * @param tableName must not be {@literal null} or empty. + * @param binderConsumer customizer for bindings. Supports only + * {@link StatementBinderBuilder#filterBy(String, SettableValue)} bindings. + * @return the {@link PreparedOperation} to delete rows from {@code tableName}. + */ + PreparedOperation delete(String tableName, Consumer binderConsumer); + + /** + * Binder to specify parameter bindings by name. Bindings match to equals comparisons. + */ + interface StatementBinderBuilder { + + /** + * Bind the given Id {@code value} to this builder using the underlying binding strategy to express a filter + * condition. {@link Collection} type values translate to {@code IN} matching. + * + * @param identifier named identifier that is considered by the underlying binding strategy. + * @param settable must not be {@literal null}. Use {@link SettableValue#empty(Class)} for {@code NULL} values. + */ + void filterBy(String identifier, SettableValue settable); + + /** + * Bind the given {@code value} to this builder using the underlying binding strategy. + * + * @param identifier named identifier that is considered by the underlying binding strategy. + * @param settable must not be {@literal null}. Use {@link SettableValue#empty(Class)} for {@code NULL} values. + */ + void bind(String identifier, SettableValue settable); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 19636168..cda23d9a 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -20,29 +20,25 @@ import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashSet; -import java.util.List; import java.util.Map; import java.util.Set; import org.reactivestreams.Publisher; -import org.springframework.data.r2dbc.dialect.BindMarker; -import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.BindIdOperation; import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; +import org.springframework.data.r2dbc.function.PreparedOperation; import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.function.StatementFactory; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; -import org.springframework.data.relational.core.sql.Conditions; -import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.Delete; import org.springframework.data.relational.core.sql.Functions; -import org.springframework.data.relational.core.sql.SQL; import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.StatementBuilder; import org.springframework.data.relational.core.sql.Table; +import org.springframework.data.relational.core.sql.Update; import org.springframework.data.relational.core.sql.render.SqlRenderer; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.repository.reactive.ReactiveCrudRepository; @@ -83,15 +79,13 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = accessStrategy.getOutboundRow(objectToSave); columns.remove(getIdColumnName()); // do not update the Id column. String idColumnName = getIdColumnName(); - BindIdOperation update = accessStrategy.updateById(entity.getTableName(), columns.keySet(), idColumnName); - GenericExecuteSpec exec = databaseClient.execute().sql(update); + PreparedOperation operation = accessStrategy.getStatements().update(entity.getTableName(), binder -> { + columns.forEach(binder::bind); + binder.filterBy(idColumnName, SettableValue.from(id)); + }); - BindSpecAdapter wrapper = BindSpecAdapter.create(exec); - columns.forEach((k, v) -> update.bind(wrapper, k, v)); - update.bindId(wrapper, id); - - return wrapper.getBoundOperation().as(entity.getJavaType()) // + return databaseClient.execute().sql(operation).as(entity.getJavaType()) // .then() // .thenReturn(objectToSave); } @@ -129,18 +123,14 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository columns = new LinkedHashSet<>(accessStrategy.getAllColumns(entity.getJavaType())); String idColumnName = getIdColumnName(); - BindMarkers bindMarkers = accessStrategy.getBindMarkersFactory().create(); - BindMarker bindMarker = bindMarkers.next("id"); + StatementFactory statements; - Table table = Table.create(entity.getTableName()); - Select select = StatementBuilder // - .select(table.columns(columns)) // - .from(table) // - .where(Conditions.isEqual(table.column(idColumnName), SQL.bindMarker(bindMarker.getPlaceholder()))) // - .build(); + PreparedOperation operation = accessStrategy.getStatements().select(entity.getTableName(), columns, + binder -> { + binder.filterBy(idColumnName, SettableValue.from(ids)); + }); - List markers = new ArrayList<>(); - - for (int i = 0; i < ids.size(); i++) { - markers.add(SQL.bindMarker(bindMarkers.next("id").getPlaceholder())); - } - - Table table = Table.create(entity.getTableName()); - Select select = StatementBuilder.select(table.columns(columns)).from(table) - .where(Conditions.in(table.column(idColumnName), markers)).build(); - - GenericExecuteSpec executeSpec = databaseClient.execute().sql(SqlRenderer.toString(select)); - - for (int i = 0; i < ids.size(); i++) { - executeSpec = executeSpec.bind(i, ids.get(i)); - } - - return executeSpec.as(entity.getJavaType()).fetch().all(); + return databaseClient.execute().sql(operation).as(entity.getJavaType()).fetch().all(); }); } @@ -274,12 +245,11 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository wrapper = BindSpecAdapter.create(databaseClient.execute().sql(delete)); + PreparedOperation delete = accessStrategy.getStatements().delete(entity.getTableName(), binder -> { + binder.filterBy(getIdColumnName(), SettableValue.from(id)); + }); - delete.bindId(wrapper, id); - - return wrapper.getBoundOperation() // + return databaseClient.execute().sql(delete) // .fetch() // .rowsUpdated() // .then(); @@ -299,13 +269,11 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository delete = accessStrategy.getStatements().delete(entity.getTableName(), binder -> { + binder.filterBy(getIdColumnName(), SettableValue.from(ids)); + }); - BindSpecAdapter wrapper = BindSpecAdapter.create(databaseClient.execute().sql(delete)); - delete.bindIds(wrapper, ids); - - return wrapper.getBoundOperation().as(entity.getJavaType()).fetch().rowsUpdated(); + return databaseClient.execute().sql(delete).then(); }).then(); } diff --git a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java index ae8c7dd8..b0fb55ed 100644 --- a/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java +++ b/src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java @@ -43,6 +43,7 @@ public abstract class StatementRenderUtil { String sql = SqlRenderer.toString(select); // TODO: Replace with proper {@link Dialect} rendering for limit/offset. + // See https://github.com/spring-projects/spring-data-r2dbc/issues/55 if (limit.isPresent()) { LimitClause limitClause = dialect.limit(); diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java deleted file mode 100644 index 2d6c2211..00000000 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategyUnitTests.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.springframework.data.r2dbc.function; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; - -import io.r2dbc.spi.Statement; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; - -import org.junit.Test; - -import org.springframework.data.r2dbc.dialect.PostgresDialect; -import org.springframework.data.r2dbc.domain.SettableValue; - -/** - * Unit tests for {@link DefaultReactiveDataAccessStrategy}. - * - * @author Mark Paluch - */ -public class DefaultReactiveDataAccessStrategyUnitTests { - - DefaultReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); - - @Test // gh-20 - public void shouldRenderInsertAndReturnGeneratedKeysQuery() { - - BindableOperation operation = strategy.insertAndReturnGeneratedKeys("table", - new HashSet<>(Arrays.asList("firstname", "lastname"))); - - assertThat(operation.toQuery()).isEqualTo("INSERT INTO table (firstname, lastname) VALUES($1, $2)"); - } - - @Test // gh-20 - public void shouldRenderUpdateByIdQuery() { - - BindableOperation operation = strategy.updateById("table", new HashSet<>(Arrays.asList("firstname", "lastname")), - "id"); - - assertThat(operation.toQuery()).isEqualTo("UPDATE table SET firstname = $2, lastname = $3 WHERE id = $1"); - } - - @Test // gh-20 - public void shouldRenderDeleteByIdQuery() { - - BindableOperation operation = strategy.deleteById("table", "id"); - - assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id = $1"); - } - - @Test // gh-20 - public void shouldRenderDeleteByIdInQuery() { - - Statement statement = mock(Statement.class); - BindIdOperation operation = strategy.deleteByIdIn("table", "id"); - - operation.bindId(statement, Collections.singleton("foo")); - assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1)"); - - operation.bindId(statement, "bar"); - assertThat(operation.toQuery()).isEqualTo("DELETE FROM table WHERE id IN ($1, $2)"); - } - - @Test // gh-22 - public void shouldUpdateArray() { - - Map columnsToUpdate = strategy - .getOutboundRow(new WithCollectionTypes(new String[] { "one", "two" }, null)); - - Object stringArray = columnsToUpdate.get("string_array").getValue(); - - assertThat(stringArray).isInstanceOf(String[].class); - assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); - } - - @Test // gh-22 - public void shouldConvertListToArray() { - - Map columnsToUpdate = strategy - .getOutboundRow(new WithCollectionTypes(null, Arrays.asList("one", "two"))); - - Object stringArray = columnsToUpdate.get("string_collection").getValue(); - - assertThat(stringArray).isInstanceOf(String[].class); - assertThat((String[]) stringArray).hasSize(2).contains("one", "two"); - } - - static class WithCollectionTypes { - - String[] stringArray; - - List stringCollection; - - WithCollectionTypes(String[] stringArray, List stringCollection) { - - this.stringArray = stringArray; - this.stringCollection = stringCollection; - } - } -} diff --git a/src/test/java/org/springframework/data/r2dbc/function/StatementFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/StatementFactoryUnitTests.java new file mode 100644 index 00000000..b6587458 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/StatementFactoryUnitTests.java @@ -0,0 +1,247 @@ +/* + * 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 io.r2dbc.spi.Statement; + +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.relational.core.dialect.RenderContextFactory; +import org.springframework.data.relational.core.sql.Delete; +import org.springframework.data.relational.core.sql.Insert; +import org.springframework.data.relational.core.sql.Select; +import org.springframework.data.relational.core.sql.Update; + +/** + * Unit tests for {@link StatementFactory}. + * + * @author Mark Paluch + */ +public class StatementFactoryUnitTests { + + // See https://github.com/spring-projects/spring-data-r2dbc/issues/55 + DefaultStatementFactory statements = new DefaultStatementFactory(PostgresDialect.INSTANCE, + new RenderContextFactory(org.springframework.data.relational.core.dialect.PostgresDialect.INSTANCE) + .createRenderContext()); + + Statement statementMock = mock(Statement.class); + + @Test + public void shouldToQuerySimpleSelectWithoutBindings() { + + PreparedOperation select = statements.select("foo", Arrays.asList("bar", "baz"), it -> { + it.filterBy("doe", SettableValue.from("John")); + }); + + assertThat(select.getSource()).isInstanceOf(Select.class); + assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe = $1"); + + select.bind(statementMock); + verify(statementMock).bind(0, "John"); + verifyNoMoreInteractions(statementMock); + } + + @Test + public void shouldToQuerySimpleSelectWithMultipleFilters() { + + PreparedOperation select = statements.select("foo", Arrays.asList("bar", "baz"), it -> { + it.filterBy("doe", SettableValue.empty(String.class)); + }); + + assertThat(select.getSource()).isInstanceOf(Select.class); + assertThat(select.toQuery()).isEqualTo("SELECT foo.bar, foo.baz FROM foo WHERE foo.doe IS NULL"); + + select.bind(statementMock); + verifyZeroInteractions(statementMock); + } + + @Test + public void shouldToQuerySimpleSelectWithIterableFilter() { + + PreparedOperation operation = dataAccessStrategy.getStatements().select(table, this.projectedFields, + (t, configurer) -> { - return execute(select, mappingFunction); + configurer.withPageRequest(page).withSort(sort); + + if (criteria != null) { + + BoundCondition boundCondition = dataAccessStrategy.getMappedCriteria(criteria, t); + configurer.withWhere(boundCondition.getCondition()).withBindings(boundCondition.getBindings()); + } + + }); + + return execute(operation, mappingFunction); } @Override - protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Sort sort, - Pageable page) { - return new DefaultGenericSelectSpec(table, projectedFields, sort, page); + protected DefaultGenericSelectSpec createInstance(String table, List projectedFields, Criteria criteria, + Sort sort, Pageable page) { + return new DefaultGenericSelectSpec(table, projectedFields, criteria, sort, page); } } @@ -763,14 +790,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); } - DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, + DefaultTypedSelectSpec(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page, BiFunction mappingFunction) { - this(table, projectedFields, sort, page, null, mappingFunction); + this(table, projectedFields, criteria, sort, page, null, mappingFunction); } - DefaultTypedSelectSpec(String table, List projectedFields, Sort sort, Pageable page, Class typeToRead, - BiFunction mappingFunction) { - super(table, projectedFields, sort, page); + DefaultTypedSelectSpec(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page, + Class typeToRead, BiFunction mappingFunction) { + super(table, projectedFields, criteria, sort, page); this.typeToRead = typeToRead; this.mappingFunction = mappingFunction; } @@ -796,6 +823,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return (DefaultTypedSelectSpec) super.project(selectedFields); } + @Override + public DefaultTypedSelectSpec where(Criteria criteria) { + return (DefaultTypedSelectSpec) super.where(criteria); + } + @Override public DefaultTypedSelectSpec orderBy(Sort sort) { return (DefaultTypedSelectSpec) super.orderBy(sort); @@ -821,15 +853,31 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { columns = this.projectedFields; } - String select = dataAccessStrategy.select(table, new LinkedHashSet<>(columns), sort, page); + 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 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)); diff --git a/src/test/java/org/springframework/data/r2dbc/domain/BindingsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/domain/BindingsUnitTests.java new file mode 100644 index 00000000..efc8b129 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/domain/BindingsUnitTests.java @@ -0,0 +1,156 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.domain; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Statement; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.BindMarker; +import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; + +/** + * Unit tests for {@link Bindings}. + * + * @author Mark Paluch + */ +public class BindingsUnitTests { + + BindMarkersFactory markersFactory = BindMarkersFactory.indexed("$", 1); + Statement statementMock = mock(Statement.class); + + @Test // gh-64 + public void shouldCreateBindings() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + bindings.bind(bindings.nextMarker(), "foo"); + bindings.bindNull(bindings.nextMarker(), String.class); + + assertThat(bindings.stream()).hasSize(2); + } + + @Test // gh-64 + public void shouldApplyValueBinding() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + bindings.bind(bindings.nextMarker(), "foo"); + bindings.apply(statementMock); + + verify(statementMock).bind(0, "foo"); + } + + @Test // gh-64 + public void shouldApplySimpleValueBinding() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + BindMarker marker = bindings.bind("foo"); + bindings.apply(statementMock); + + assertThat(marker.getPlaceholder()).isEqualTo("$1"); + verify(statementMock).bind(0, "foo"); + } + + @Test // gh-64 + public void shouldApplyNullBinding() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + bindings.bindNull(bindings.nextMarker(), String.class); + + bindings.apply(statementMock); + + verify(statementMock).bindNull(0, String.class); + } + + @Test // gh-64 + public void shouldApplySimpleNullBinding() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + BindMarker marker = bindings.bindNull(String.class); + bindings.apply(statementMock); + + assertThat(marker.getPlaceholder()).isEqualTo("$1"); + verify(statementMock).bindNull(0, String.class); + } + + @Test // gh-64 + public void shouldConsumeBindings() { + + MutableBindings bindings = new MutableBindings(markersFactory.create()); + + bindings.bind(bindings.nextMarker(), "foo"); + bindings.bindNull(bindings.nextMarker(), String.class); + + AtomicInteger counter = new AtomicInteger(); + + bindings.forEach(binding -> { + + if (binding.hasValue()) { + counter.incrementAndGet(); + assertThat(binding.getValue()).isEqualTo("foo"); + assertThat(binding.getBindMarker().getPlaceholder()).isEqualTo("$1"); + } + + if (binding.isNull()) { + counter.incrementAndGet(); + + assertThat(((Bindings.NullBinding) binding).getValueType()).isEqualTo(String.class); + assertThat(binding.getBindMarker().getPlaceholder()).isEqualTo("$2"); + } + }); + + assertThat(counter).hasValue(2); + } + + @Test // gh-64 + public void shouldMergeBindings() { + + BindMarkers markers = markersFactory.create(); + + BindMarker shared = markers.next(); + BindMarker leftMarker = markers.next(); + List left = new ArrayList<>(); + left.add(new Bindings.NullBinding(shared, String.class)); + left.add(new Bindings.ValueBinding(leftMarker, "left")); + + BindMarker rightMarker = markers.next(); + List right = new ArrayList<>(); + left.add(new Bindings.ValueBinding(shared, "override")); + left.add(new Bindings.ValueBinding(rightMarker, "right")); + + Bindings merged = Bindings.merge(new Bindings(left), new Bindings(right)); + + assertThat(merged).hasSize(3); + + merged.apply(statementMock); + verify(statementMock).bind(0, "override"); + verify(statementMock).bind(1, "left"); + verify(statementMock).bind(2, "right"); + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index fb72765f..c22ae3f1 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -32,6 +32,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.function.query.Criteria; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; @@ -204,6 +205,43 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).containsEntry("id", 42055); } + @Test // gh-64 + public void deleteUntyped() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.delete() // + .from("legoset") // + .where(Criteria.of("id").is(42055)) // + .fetch() // + .rowsUpdated() // + .as(StepVerifier::create) // + .expectNext(1).verifyComplete(); + + assertThat(jdbc.queryForList("SELECT id AS count FROM legoset")).hasSize(1); + } + + @Test // gh-64 + public void deleteTyped() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.delete() // + .from(LegoSet.class) // + .where(Criteria.of("id").is(42055)) // + .then() // + .as(StepVerifier::create) // + .verifyComplete(); + + assertThat(jdbc.queryForList("SELECT id AS count FROM legoset")).hasSize(1); + } + @Test // gh-2 public void selectAsMap() { @@ -241,6 +279,44 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .verifyComplete(); } + @Test // gh-8 + public void selectWithCriteria() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from("legoset") // + .project("id", "name", "manual") // + .orderBy(Sort.by("id")) // + .where(Criteria.of("id").greaterThanOrEquals(42055).and("id").lessThanOrEquals(42055)) + .map((r, md) -> r.get("id", Integer.class)) // + .all() // + .as(StepVerifier::create) // + .expectNext(42055) // + .verifyComplete(); + } + + @Test // gh-64 + public void selectWithCriteriaIn() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42064, 'FORSCHUNGSSCHIFF', 13)"); + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42068, 'FLUGHAFEN-LÖSCHFAHRZEUG', 13)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.select().from(LegoSet.class) // + .orderBy(Sort.by("id")) // + .where(Criteria.of("id").in(42055, 42064)) // + .map((r, md) -> r.get("id", Integer.class)) // + .all() // + .as(StepVerifier::create) // + .expectNext(42055) // + .expectNext(42064) // + .verifyComplete(); + } + @Test // gh-2 public void selectOrderByIdDesc() { diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java index 2e8ccdb2..7fdba539 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java @@ -30,6 +30,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; + import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java index 351f969f..784a67ba 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java @@ -27,6 +27,7 @@ import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.dialect.SqlServerDialect; import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.BindableOperation; /** * Unit tests for {@link NamedParameterUtils}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java new file mode 100644 index 00000000..e47f9c11 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java @@ -0,0 +1,218 @@ +/* + * 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 io.r2dbc.spi.Statement; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +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.Table; + +/** + * Unit tests for {@link CriteriaMapper}. + * + * @author Mark Paluch + */ +public class CriteriaMapperUnitTests { + + R2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); + CriteriaMapper mapper = new CriteriaMapper(converter); + Statement statementMock = mock(Statement.class); + + @Test // gh-64 + public void shouldMapSimpleCriteria() { + + Criteria criteria = Criteria.of("name").is("foo"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name = ?[$1]"); + + bindings.getBindings().apply(statementMock); + verify(statementMock).bind(0, "foo"); + } + + @Test // gh-64 + public void shouldConsiderColumnName() { + + Criteria criteria = Criteria.of("alternative").is("foo"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.another_name = ?[$1]"); + } + + @Test // gh-64 + public void shouldMapAndCriteria() { + + Criteria criteria = Criteria.of("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"); + } + + @Test // gh-64 + public void shouldMapOrCriteria() { + + Criteria criteria = Criteria.of("name").is("foo").or("bar").is("baz"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name = ?[$1] OR person.bar = ?[$2]"); + } + + @Test // gh-64 + public void shouldMapAndOrCriteria() { + + Criteria criteria = Criteria.of("name").is("foo") // + .and("name").isNotNull() // + .or("bar").is("baz") // + .and("anotherOne").is("alternative"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo( + "person.name = ?[$1] AND person.name IS NOT NULL OR person.bar = ?[$2] AND person.anotherOne = ?[$3]"); + } + + @Test // gh-64 + public void shouldMapNeq() { + + Criteria criteria = Criteria.of("name").not("foo"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name != ?[$1]"); + } + + @Test // gh-64 + public void shouldMapIsNull() { + + Criteria criteria = Criteria.of("name").isNull(); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name IS NULL"); + } + + @Test // gh-64 + public void shouldMapIsNotNull() { + + Criteria criteria = Criteria.of("name").isNotNull(); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name IS NOT NULL"); + } + + @Test // gh-64 + public void shouldMapIsIn() { + + Criteria criteria = Criteria.of("name").in("a", "b", "c"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name IN (?[$1], ?[$2], ?[$3])"); + } + + @Test // gh-64 + public void shouldMapIsNotIn() { + + Criteria criteria = Criteria.of("name").notIn("a", "b", "c"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("NOT person.name IN (?[$1], ?[$2], ?[$3])"); + } + + @Test // gh-64 + public void shouldMapIsGt() { + + Criteria criteria = Criteria.of("name").greaterThan("a"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name > ?[$1]"); + } + + @Test // gh-64 + public void shouldMapIsGte() { + + Criteria criteria = Criteria.of("name").greaterThanOrEquals("a"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name >= ?[$1]"); + } + + @Test // gh-64 + public void shouldMapIsLt() { + + Criteria criteria = Criteria.of("name").lessThan("a"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name < ?[$1]"); + } + + @Test // gh-64 + public void shouldMapIsLte() { + + Criteria criteria = Criteria.of("name").lessThanOrEquals("a"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name <= ?[$1]"); + } + + @Test // gh-64 + public void shouldMapIsLike() { + + Criteria criteria = Criteria.of("name").like("a"); + + BoundCondition bindings = map(criteria); + + assertThat(bindings.getCondition().toString()).isEqualTo("person.name LIKE ?[$1]"); + } + + @SuppressWarnings("unchecked") + private BoundCondition map(Criteria criteria) { + + BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1); + + return mapper.getMappedObject(markers.create(), criteria, Table.create("person"), + converter.getMappingContext().getRequiredPersistentEntity(Person.class)); + } + + static class Person { + + String name; + @Column("another_name") String alternative; + } +} 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 new file mode 100644 index 00000000..c6ac54cd --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java @@ -0,0 +1,173 @@ +/* + * 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.springframework.data.r2dbc.function.query.Criteria.*; + +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.data.r2dbc.function.query.Criteria.*; + +/** + * Unit tests for {@link Criteria}. + * + * @author Mark Paluch + */ +public class CriteriaUnitTests { + + @Test // gh-64 + public void andChainedCriteria() { + + Criteria criteria = of("foo").is("bar").and("baz").isNotNull(); + + assertThat(criteria.getProperty()).isEqualTo("baz"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); + assertThat(criteria.getValue()).isNull(); + assertThat(criteria.getPrevious()).isNotNull(); + assertThat(criteria.getCombinator()).isEqualTo(Combinator.AND); + + criteria = criteria.getPrevious(); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // gh-64 + public void orChainedCriteria() { + + Criteria criteria = of("foo").is("bar").or("baz").isNotNull(); + + assertThat(criteria.getProperty()).isEqualTo("baz"); + assertThat(criteria.getCombinator()).isEqualTo(Combinator.OR); + + criteria = criteria.getPrevious(); + + assertThat(criteria.getPrevious()).isNull(); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // gh-64 + public void shouldBuildEqualsCriteria() { + + Criteria criteria = of("foo").is("bar"); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.EQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // gh-64 + public void shouldBuildNotEqualsCriteria() { + + Criteria criteria = of("foo").not("bar"); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.NEQ); + assertThat(criteria.getValue()).isEqualTo("bar"); + } + + @Test // gh-64 + public void shouldBuildInCriteria() { + + Criteria criteria = of("foo").in("bar", "baz"); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.IN); + assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); + } + + @Test // gh-64 + public void shouldBuildNotInCriteria() { + + Criteria criteria = of("foo").notIn("bar", "baz"); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.NOT_IN); + assertThat(criteria.getValue()).isEqualTo(Arrays.asList("bar", "baz")); + } + + @Test // gh-64 + public void shouldBuildGtCriteria() { + + Criteria criteria = of("foo").greaterThan(1); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.GT); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // gh-64 + public void shouldBuildGteCriteria() { + + Criteria criteria = of("foo").greaterThanOrEquals(1); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.GTE); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // gh-64 + public void shouldBuildLtCriteria() { + + Criteria criteria = of("foo").lessThan(1); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.LT); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // gh-64 + public void shouldBuildLteCriteria() { + + Criteria criteria = of("foo").lessThanOrEquals(1); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.LTE); + assertThat(criteria.getValue()).isEqualTo(1); + } + + @Test // gh-64 + public void shouldBuildLikeCriteria() { + + Criteria criteria = of("foo").like("hello%"); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.LIKE); + assertThat(criteria.getValue()).isEqualTo("hello%"); + } + + @Test // gh-64 + public void shouldBuildIsNullCriteria() { + + Criteria criteria = of("foo").isNull(); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NULL); + } + + @Test // gh-64 + public void shouldBuildIsNotNullCriteria() { + + Criteria criteria = of("foo").isNotNull(); + + assertThat(criteria.getProperty()).isEqualTo("foo"); + assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); + } +} From ff69a41162cd78d1f8bd564205ecac4e947f2474 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 30 Apr 2019 10:10:22 +0200 Subject: [PATCH 111/665] #64 - Split documentation for R2DBC core into parts. Original pull request: #106. --- src/main/asciidoc/reference/r2dbc-core.adoc | 241 ++++++++ .../reference/r2dbc-databaseclient.adoc | 111 ++++ src/main/asciidoc/reference/r2dbc-sql.adoc | 163 ++++++ .../reference/r2dbc-transactions.adoc | 29 + src/main/asciidoc/reference/r2dbc.adoc | 534 +----------------- 5 files changed, 548 insertions(+), 530 deletions(-) create mode 100644 src/main/asciidoc/reference/r2dbc-core.adoc create mode 100644 src/main/asciidoc/reference/r2dbc-databaseclient.adoc create mode 100644 src/main/asciidoc/reference/r2dbc-sql.adoc create mode 100644 src/main/asciidoc/reference/r2dbc-transactions.adoc diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc new file mode 100644 index 00000000..aba9b628 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -0,0 +1,241 @@ +The R2DBC support contains a wide range of features: + +* Spring configuration support with Java-based `@Configuration` classes for an R2DBC driver instance. +* `DatabaseClient` helper class that increases productivity when performing common R2DBC operations with integrated object mapping between rows and POJOs. +* Exception translation into Spring's portable Data Access Exception hierarchy. +* Feature-rich Object Mapping integrated with Spring's Conversion Service. +* Annotation-based mapping metadata that is extensible to support other metadata formats. +* Automatic implementation of Repository interfaces, including support for custom query methods. + +For most tasks, you should use `DatabaseClient` or the Repository support, which both leverage the rich mapping functionality. +`DatabaseClient` is the place to look for accessing functionality such as ad-hoc CRUD operations. + +[[r2dbc.getting-started]] +== Getting Started + +An easy way to bootstrap setting up a working environment is to create a Spring-based project through https://start.spring.io[start.spring.io]. + +. Add the following to the pom.xml files `dependencies` element: ++ +[source,xml,subs="+attributes"] +---- + + + + io.r2dbc + r2dbc-bom + ${r2dbc-releasetrain.version} + pom + import + + + + + + + + + + org.springframework.data + spring-data-r2dbc + {version} + + + + + io.r2dbc + r2dbc-h2 + {r2dbcVersion} + + + +---- +. Change the version of Spring in the pom.xml to be ++ +[source,xml,subs="+attributes"] +---- +{springVersion} +---- +. Add the following location of the Spring Milestone repository for Maven to your `pom.xml` such that it is at the same level of your `` element: ++ +[source,xml] +---- + + + spring-milestone + Spring Maven MILESTONE Repository + https://repo.spring.io/libs-milestone + + +---- + +The repository is also https://repo.spring.io/milestone/org/springframework/data/[browseable here]. + +You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content: + +[source] +---- +logging.level.org.springframework.data.r2dbc=DEBUG +---- + +Then you can create a `Person` class to persist: + +[source,java] +---- +package org.spring.r2dbc.example; + +public class Person { + + private String id; + private String name; + private int age; + + public Person(String id, String name, int age) { + this.id = id; + this.name = name; + this.age = age; + } + + public String getId() { + return id; + } + public String getName() { + return name; + } + public int getAge() { + return age; + } + + @Override + public String toString() { + return "Person [id=" + id + ", name=" + name + ", age=" + age + "]"; + } +} +---- + +Next, you need to create a table structure in your database: + +[source,sql] +---- +CREATE TABLE person + (id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + age INT); +---- + +You also need a main application to run: + +[source,java] +---- +package org.spring.r2dbc.example; + +public class R2dbcApp { + + private static final Log log = LogFactory.getLog(R2dbcApp.class); + + public static void main(String[] args) throws Exception { + + ConnectionFactory connectionFactory = ConnectionFactories.get("rdbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); + + DatabaseClient client = DatabaseClient.create(connectionFactory); + + client.execute() + .sql("CREATE TABLE person" + + "(id VARCHAR(255) PRIMARY KEY," + + "name VARCHAR(255)," + + "age INT)") + .fetch() + .rowsUpdated() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + + client.insert() + .into(Person.class) + .using(new Person("joe", "Joe", 34)) + .then() + .as(StepVerifier::create) + .verifyComplete(); + + client.select() + .from(Person.class) + .fetch() + .first() + .doOnNext(it -> log.info(it)) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } +} +---- + +When you run the main program, the preceding examples produce output similar to the following: + +[source] +---- +2018-11-28 10:47:03,893 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person + (id VARCHAR(255) PRIMARY KEY, + name VARCHAR(255), + age INT)] +2018-11-28 10:47:04,074 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)] +2018-11-28 10:47:04,092 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person] +2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34] +---- + +Even in this simple example, there are few things to notice: + +* You can create an instance of the central helper class in Spring Data R2DBC, <>, by using a standard `io.r2dbc.spi.ConnectionFactory` object. +* The mapper works against standard POJO objects without the need for any additional metadata (though you can optionally provide that information. See <>.). +* Mapping conventions can use field access. Notice that the `Person` class has only getters. +* If the constructor argument names match the column names of the stored row, they are used to instantiate the object. + +[[r2dbc.examples-repo]] +== Examples Repository + +There is a https://github.com/spring-projects/spring-data-examples[GitHub repository with several examples] that you can download and play around with to get a feel for how the library works. + +[[r2dbc.drivers]] +== Connecting to a Relational Database with Spring + +One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object using the IoC container. The following example explains Java-based configuration. + +[[r2dbc.connectionfactory]] +=== Registering a `ConnectionFactory` Instance using Java-based Metadata + +The following example shows an example of using Java-based bean metadata to register an instance of a `io.r2dbc.spi.ConnectionFactory`: + +.Registering a `io.r2dbc.spi.ConnectionFactory` object using Java-based bean metadata +==== +[source,java] +---- +@Configuration +public class ApplicationConfiguration extends AbstractR2dbcConfiguration { + + @Override + @Bean + public ConnectionFactory connectionFactory() { + return …; + } +} +---- +==== + +This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features]. + +`AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation. + +[[r2dbc.drivers]] +=== R2DBC Drivers + +Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC. +R2DBC is a relatively young initiative that gains significance by maturing through adoption. +As of writing the following 3 drivers are available: + +* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) +* https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) +* https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`) +* https://github.com/jasync-sql/jasync-sql[Microsoft SQL Server] (`com.github.jasync-sql:jasync-r2dbc-mysql`) + +Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect. +You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC. + diff --git a/src/main/asciidoc/reference/r2dbc-databaseclient.adoc b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc new file mode 100644 index 00000000..5cbb8652 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc @@ -0,0 +1,111 @@ +[[r2dbc.datbaseclient]] += Introduction to `DatabaseClient` + +Spring Data R2DBC includes a reactive, non-blocking `DatabaseClient` for database interaction. The client has a functional, fluent API with reactive types for declarative composition. +`DatabaseClient` encapsulates resource handling such as opening and closing connections so your application code can make use of executing SQL queries or calling higher-level functionality such as inserting or selecting data. + +NOTE: `DatabaseClient` is a young application component providing a minimal set of convenience methods that is likely to be extended through time. + +NOTE: Once configured, `DatabaseClient` is thread-safe and can be reused across multiple instances. + +Another central feature of `DatabaseClient` is translation of exceptions thrown by R2DBC drivers into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. + +The next section contains an example of how to work with the `DatabaseClient` in the context of the Spring container. + +[[r2dbc.datbaseclient.create]] +== Creating `DatabaseClient` + +The simplest way to create a `DatabaseClient` is through a static factory method: + +[source,java] +---- +DatabaseClient.create(ConnectionFactory connectionFactory) +---- + +The above method creates a `DatabaseClient` with default settings. + +You can also use `DatabaseClient.builder()` with further options to customize the client: + +* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. +* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped. + +Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows: + +[source,java] +---- +DatabaseClient client1 = DatabaseClient.builder() + .exceptionTranslator(exceptionTranslatorA).build(); + +DatabaseClient client2 = client1.mutate() + .exceptionTranslator(exceptionTranslatorB).build(); +---- + +== Controlling Database Connections + +Spring Data R2DBC obtains a connection to the database through a `ConnectionFactory`. +A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory. +It lets a container or a framework hide connection pooling and transaction management issues from the application code. + +When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your R2DBC driver. +`ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling. +`DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations. + +[[r2dbc.exception]] += Exception Translation + +The Spring framework provides exception translation for a wide variety of database and mapping technologies. +The Spring support for R2DBC extends this feature by providing implementations of the `R2dbcExceptionTranslator` interface. + +`R2dbcExceptionTranslator` is an interface to be implemented by classes that can translate between `R2dbcException` and Spring’s own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy. +Implementations can be generic (for example, using SQLState codes) or proprietary (for example, using Postgres error codes) for greater precision. + +`R2dbcExceptionSubclassTranslator` is the implementation of `R2dbcExceptionTranslator` that is used by default. +It considers R2DBC's categorized exception hierarchy to translate these into Spring's consistent exception hierarchy. +`R2dbcExceptionSubclassTranslator` uses `SqlStateR2dbcExceptionTranslator` as fallback if it is not able to translate an exception. + +`SqlErrorCodeR2dbcExceptionTranslator` uses specific vendor codes using Spring JDBC's `SQLErrorCodes`. +It is more precise than the SQLState implementation. +The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`. +This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module. +This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`. +The codes for the actual database you are using are used. + +The `SqlErrorCodeR2dbcExceptionTranslator` applies matching rules in the following sequence: + +1. Any custom translation implemented by a subclass. Normally, the provided concrete `SqlErrorCodeR2dbcExceptionTranslator` is used, so this rule does not apply. It applies only if you have actually provided a subclass implementation. +2. Any custom implementation of the `SQLExceptionTranslator` interface that is provided as the `customSqlExceptionTranslator` property of the `SQLErrorCodes` class. +3. Error code matching is applied. +4. Use a fallback translator. + +NOTE: The `SQLErrorCodesFactory` is used by default to define Error codes and custom exception translations. They are looked up in a file named `sql-error-codes.xml` from the classpath, and the matching `SQLErrorCodes` instance is located based on the database name from the database metadata of the database in use. `SQLErrorCodesFactory` requires Spring JDBC. + +You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example shows: + +[source,java] +---- +public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator { + + protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) { + if (sqlex.getErrorCode() == -12345) { + return new DeadlockLoserDataAccessException(task, r2dbcex); + } + return null; + } +} +---- + +In the preceding example, the specific error code (`-12345`) is translated, while other errors are left to be translated by the default translator implementation. +To use this custom translator, you must configure `DatabaseClient` through the builder method `exceptionTranslator`, and you must use this `DatabaseClient` for all of the data access processing where this translator is needed. +The following example shows how you can use this custom translator: + +[source,java] +---- +ConnectionFactory connectionFactory = …; + +CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator(); + +DatabaseClient client = DatabaseClient.builder() + .connectionFactory(connectionFactory) + .exceptionTranslator(exceptionTranslator) + .build(); +---- diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc new file mode 100644 index 00000000..d71034e5 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -0,0 +1,163 @@ +[[r2dbc.datbaseclient.statements]] += Running 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: + +[source,java] +---- +Mono completion = client.execute() + .sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") + .then(); +---- + +`DatabaseClient` is designed for a convenient fluent usage. +It exposes intermediate, continuation, and terminal methods at each stage of the execution specification. +The example above uses `then()` to return a completion `Publisher` that completes as soon as the query (or queries, if the SQL query contains multiple statements) completes. + +NOTE: `execute().sql(…)` accepts either the SQL query string or a query `Supplier` to defer the actual query creation until execution. + +[[r2dbc.datbaseclient.queries]] +== Running Queries + +SQL queries can return values or the number of affected rows. +`DatabaseClient` can return the number of updated rows or the rows themselves, depending on the issued query. + +The following example shows an `UPDATE` statement that returns the number of updated rows: + +[source,java] +---- +Mono affectedRows = client.execute() + .sql("UPDATE person SET name = 'Joe'") + .fetch().rowsUpdated(); +---- + +Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`. +You might have noticed the use of `fetch()` in the previous example. +`fetch()` is a continuation operator that allows you to specify how much data you want to consume. + +[source,java] +---- +Mono> first = client.execute() + .sql("SELECT id, name FROM person") + .fetch().first(); +---- + +Calling `first()` returns the first row from the result and discards remaining rows. +You can consume data with the following operators: + +* `first()` return the first row of the entire result +* `one()` returns exactly one result and fails if the result contains more rows. +* `all()` returns all rows of the result +* `rowsUpdated()` returns the number of affected rows (`INSERT` count, `UPDATE` count) + +`DatabaseClient` queries return their results by default as `Map` of column name to value. You can customize type mapping by applying an `as(Class)` operator. + +[source,java] +---- +Flux all = client.execute() + .sql("SELECT id, name FROM mytable") + .as(Person.class) + .fetch().all(); +---- + +`as(…)` applies <> and maps the resulting columns to your POJO. + +[[r2dbc.datbaseclient.mapping]] +== Mapping Results + +You can customize result extraction beyond `Map` and POJO result extraction by providing an extractor `BiFunction`. +The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects). + +The following example extracts the `id` column and emits its value: + +[source,java] +---- +Flux names= client.execute() + .sql("SELECT name FROM person") + .map((row, rowMetadata) -> row.get("id", String.class)) + .all(); +---- + +[[r2dbc.datbaseclient.mapping.null]] +.What about `null`? +**** +Relational database results may contain `null` values. +Reactive Streams forbids emission of `null` values which requires a proper `null` handling in the extractor function. +While you can obtain `null` values from a `Row`, you must not emit a `null` value. +You must wrap any `null` values in an object (e.g. `Optional` for singular values) to make sure a `null` value is never returned directly by your extractor function. +**** + +[[r2dbc.datbaseclient.binding]] +== Binding Values to Queries + +A typical application requires parameterized SQL statements to select or update rows according to some input. +These are typically `SELECT` statements constrained by a `WHERE` clause or `INSERT`/`UPDATE` statements accepting input parameters. +Parameterized statements bear the risk of SQL injection if parameters are not escaped properly. +`DatabaseClient` leverages R2DBC's Bind API to eliminate the risk of SQL injection for query parameters. +You can provide a parameterized SQL statement with the `sql(…)` operator and bind parameters to the actual `Statement`. +Your R2DBC driver then executes the statement using prepared statements and parameter substitution. + +Parameter binding supports various binding strategies: + +* By Index using zero-based parameter indexes. +* By Name using the placeholder name. + +The following example shows parameter binding for a query: + +[source,java] +---- +db.execute() + .sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34); +---- + +.R2DBC Native Bind Markers +**** +R2DBC uses database-native bind markers that depend on the actual database vendor. +As an example, Postgres uses indexed markers such as `$1`, `$2`, `$n`. +Another example is SQL Server that uses named bind markers prefixed with `@` (at). + +This is different from JDBC which requires `?` (question mark) as bind markers. +In JDBC, the actual drivers translate question mark bind markers to database-native markers as part of their statement execution. + +Spring Data R2DBC allows you to use native bind markers or named bind markers with the `:name` syntax. + +Named parameter support leverages ``Dialect``s to expand named parameters to native bind markers at the time of query execution which gives you a certain degree of query portability across various database vendors. +**** + +The query-preprocessor unrolls named `Collection` parameters into a series of bind markers to remove the need of dynamic query creation based on the number of arguments. +Nested object arrays are expanded to allow usage of e.g. select lists. + +Consider the following query: + +[source,sql] +---- +SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) +---- + +This query can be parametrized and executed as: + +[source,java] +---- +List tuples = new ArrayList<>(); +tuples.add(new Object[] {"John", 35}); +tuples.add(new Object[] {"Ann", 50}); + +db.execute() + .sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") + .bind("tuples", tuples); +---- + +NOTE: Usage of select lists is vendor-dependent. + +A simpler variant using `IN` predicates: + +[source,java] +---- +db.execute() + .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") + .bind("ages", Arrays.asList(35, 50)); +---- diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc new file mode 100644 index 00000000..2e4fe3b0 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -0,0 +1,29 @@ +[[r2dbc.datbaseclient.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. +Using different connections hence results in utilizing different transactions. +Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction. +`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods. + +You can run multiple statements within a transaction using the `inTransaction(Function)` closure: + +[source,java] +---- +TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + +Flux completion = databaseClient.inTransaction(db -> { + + return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34) + .fetch().rowsUpdated() + .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .bind("id", "joe") + .bind("name", "Joe") + .fetch().rowsUpdated()) + .then(); +}); +---- diff --git a/src/main/asciidoc/reference/r2dbc.adoc b/src/main/asciidoc/reference/r2dbc.adoc index 27802ce5..4c0b55d4 100644 --- a/src/main/asciidoc/reference/r2dbc.adoc +++ b/src/main/asciidoc/reference/r2dbc.adoc @@ -1,536 +1,10 @@ [[r2dbc.core]] = R2DBC support -The R2DBC support contains a wide range of features: +include::r2dbc-core.adoc[] -* Spring configuration support with Java-based `@Configuration` classes for an R2DBC driver instance. -* `DatabaseClient` helper class that increases productivity when performing common R2DBC operations with integrated object mapping between rows and POJOs. -* Exception translation into Spring's portable Data Access Exception hierarchy. -* Feature-rich Object Mapping integrated with Spring's Conversion Service. -* Annotation-based mapping metadata that is extensible to support other metadata formats. -* Automatic implementation of Repository interfaces, including support for custom query methods. +include::r2dbc-databaseclient.adoc[leveloffset=+1] -For most tasks, you should use `DatabaseClient` or the Repository support, which both leverage the rich mapping functionality. -`DatabaseClient` is the place to look for accessing functionality such as ad-hoc CRUD operations. +include::r2dbc-sql.adoc[leveloffset=+1] -[[r2dbc.getting-started]] -== Getting Started - -An easy way to bootstrap setting up a working environment is to create a Spring-based project through https://start.spring.io[start.spring.io]. - -. Add the following to the pom.xml files `dependencies` element: -+ -[source,xml,subs="+attributes"] ----- - - - - - - org.springframework.data - spring-data-r2dbc - {version} - - - - - io.r2dbc - r2dbc-h2 - {r2dbcVersion} - - - ----- -. Change the version of Spring in the pom.xml to be -+ -[source,xml,subs="+attributes"] ----- -{springVersion} ----- -. Add the following location of the Spring Milestone repository for Maven to your `pom.xml` such that it is at the same level of your `` element: -+ -[source,xml] ----- - - - spring-milestone - Spring Maven MILESTONE Repository - https://repo.spring.io/libs-milestone - - ----- - -The repository is also https://repo.spring.io/milestone/org/springframework/data/[browseable here]. - -You may also want to set the logging level to `DEBUG` to see some additional information. To do so, edit the `application.properties` file to have the following content: - -[source] ----- -logging.level.org.springframework.data.r2dbc=DEBUG ----- - -Then you can create a `Person` class to persist: - -[source,java] ----- -package org.spring.r2dbc.example; - -public class Person { - - private String id; - private String name; - private int age; - - public Person(String id, String name, int age) { - this.id = id; - this.name = name; - this.age = age; - } - - public String getId() { - return id; - } - public String getName() { - return name; - } - public int getAge() { - return age; - } - - @Override - public String toString() { - return "Person [id=" + id + ", name=" + name + ", age=" + age + "]"; - } -} ----- - -Next, you need to create a table structure in your database: - -[source,sql] ----- -CREATE TABLE person - (id VARCHAR(255) PRIMARY KEY, - name VARCHAR(255), - age INT); ----- - -You also need a main application to run: - -[source,java] ----- -package org.spring.r2dbc.example; - -public class R2dbcApp { - - private static final Log log = LogFactory.getLog(R2dbcApp.class); - - public static void main(String[] args) throws Exception { - - ConnectionFactory connectionFactory = new H2ConnectionFactory(H2ConnectionConfiguration.builder() - .url("mem:test;DB_CLOSE_DELAY=10") - .build()); - - DatabaseClient client = DatabaseClient.create(connectionFactory); - - client.execute() - .sql("CREATE TABLE person" + - "(id VARCHAR(255) PRIMARY KEY," + - "name VARCHAR(255)," + - "age INT)") - .fetch() - .rowsUpdated() - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); - - client.insert() - .into(Person.class) - .using(new Person("joe", "Joe", 34)) - .then() - .as(StepVerifier::create) - .verifyComplete(); - - client.select() - .from(Person.class) - .fetch() - .first() - .doOnNext(it -> log.info(it)) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); - } -} ----- - -When you run the main program, the preceding examples produce output similar to the following: - -[source] ----- -2018-11-28 10:47:03,893 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 310 - Executing SQL statement [CREATE TABLE person - (id VARCHAR(255) PRIMARY KEY, - name VARCHAR(255), - age INT)] -2018-11-28 10:47:04,074 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 908 - Executing SQL statement [INSERT INTO person (id, name, age) VALUES($1, $2, $3)] -2018-11-28 10:47:04,092 DEBUG ata.r2dbc.function.DefaultDatabaseClient: 575 - Executing SQL statement [SELECT id, name, age FROM person] -2018-11-28 10:47:04,436 INFO org.spring.r2dbc.example.R2dbcApp: 43 - Person [id='joe', name='Joe', age=34] ----- - -Even in this simple example, there are few things to notice: - -* You can create an instance of the central helper class in Spring Data R2DBC, <>, by using a standard `io.r2dbc.spi.ConnectionFactory` object. -* The mapper works against standard POJO objects without the need for any additional metadata (though you can optionally provide that information. See <>.). -* Mapping conventions can use field access. Notice that the `Person` class has only getters. -* If the constructor argument names match the column names of the stored row, they are used to instantiate the object. - -[[r2dbc.examples-repo]] -== Examples Repository - -There is a https://github.com/spring-projects/spring-data-examples[GitHub repository with several examples] that you can download and play around with to get a feel for how the library works. - -[[r2dbc.drivers]] -== Connecting to a Relational Database with Spring - -One of the first tasks when using relational databases and Spring is to create a `io.r2dbc.spi.ConnectionFactory` object using the IoC container. The following example explains Java-based configuration. - -[[r2dbc.connectionfactory]] -=== Registering a `ConnectionFactory` Instance using Java-based Metadata - -The following example shows an example of using Java-based bean metadata to register an instance of a `io.r2dbc.spi.ConnectionFactory`: - -.Registering a `io.r2dbc.spi.ConnectionFactory` object using Java-based bean metadata -==== -[source,java] ----- -@Configuration -public class ApplicationConfiguration extends AbstractR2dbcConfiguration { - - @Override - @Bean - public ConnectionFactory connectionFactory() { - return …; - } -} ----- -==== - -This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instance, with the container using Spring's `AbstractR2dbcConfiguration`. As compared to registering a `ConnectionFactory` instance directly, the configuration support has the added advantage of also providing the container with an `ExceptionTranslator` implementation that translates R2DBC exceptions to exceptions in Spring's portable `DataAccessException` hierarchy for data access classes annotated with the `@Repository` annotation. This hierarchy and the use of `@Repository` is described in https://docs.spring.io/spring/docs/{springVersion}/spring-framework-reference/data-access.html[Spring's DAO support features]. - -`AbstractR2dbcConfiguration` registers also `DatabaseClient` that is required for database interaction and for Repository implementation. - -[[r2dbc.drivers]] -=== R2DBC Drivers - -Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC. -R2DBC is a relatively young initiative that gains significance by maturing through adoption. -As of writing the following 3 drivers are available: - -* https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) -* https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) -* https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`) - -Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect. -You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC. - -[[r2dbc.datbaseclient]] -== Introduction to `DatabaseClient` - -Spring Data R2DBC includes a reactive, non-blocking `DatabaseClient` for database interaction. The client has a functional, fluent API with reactive types for declarative composition. -`DatabaseClient` encapsulates resource handling such as opening and closing connections so your application code can make use of executing SQL queries or calling higher-level functionality such as inserting or selecting data. - -NOTE: `DatabaseClient` is a young application component providing a minimal set of convenience methods that is likely to be extended through time. - -NOTE: Once configured, `DatabaseClient` is thread-safe and can be reused across multiple instances. - -Another central feature of `DatabaseClient` is translation of exceptions thrown by R2DBC drivers into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. - -The next section contains an example of how to work with the `DatabaseClient` in the context of the Spring container. - -[[r2dbc.datbaseclient.create]] -=== Creating `DatabaseClient` - -The simplest way to create a `DatabaseClient` is through a static factory method: - -[source,java] ----- -DatabaseClient.create(ConnectionFactory connectionFactory) ----- - -The above method creates a `DatabaseClient` with default settings. - -You can also use `DatabaseClient.builder()` with further options to customize the client: - -* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. -* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped. - -Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows: - -[source,java] ----- -DatabaseClient client1 = DatabaseClient.builder() - .exceptionTranslator(exceptionTranslatorA).build(); - -DatabaseClient client2 = client1.mutate() - .exceptionTranslator(exceptionTranslatorB).build(); ----- - -=== Controlling Database Connections - -Spring Data R2DBC obtains a connection to the database through a `ConnectionFactory`. -A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory. -It lets a container or a framework hide connection pooling and transaction management issues from the application code. - -When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your R2DBC driver. -`ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling. -`DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations. - -[[r2dbc.exception]] -== Exception Translation - -The Spring framework provides exception translation for a wide variety of database and mapping technologies. -This has traditionally been for JDBC and JPA. The Spring support for R2DBC extends this feature by providing implementations of the `R2dbcExceptionTranslator` interface. - -`R2dbcExceptionTranslator` is an interface to be implemented by classes that can translate between `R2dbcException` and Spring’s own `org.springframework.dao.DataAccessException`, which is agnostic in regard to data access strategy. -Implementations can be generic (for example, using SQLState codes) or proprietary (for example, using Postgres error codes) for greater precision. - -`SqlErrorCodeR2dbcExceptionTranslator` is the implementation of `R2dbcExceptionTranslator` that is used by default. -This implementation uses specific vendor codes. -It is more precise than the SQLState implementation. -The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`. -This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module. -This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`. -The codes for the actual database you are using are used. - -The `SqlErrorCodeR2dbcExceptionTranslator` applies matching rules in the following sequence: - -1. Any custom translation implemented by a subclass. Normally, the provided concrete `SqlErrorCodeR2dbcExceptionTranslator` is used, so this rule does not apply. It applies only if you have actually provided a subclass implementation. -2. Any custom implementation of the `SQLExceptionTranslator` interface that is provided as the `customSqlExceptionTranslator` property of the `SQLErrorCodes` class. -3. Error code matching is applied. -4. Use a fallback translator. - - -NOTE: The `SQLErrorCodesFactory` is used by default to define Error codes and custom exception translations. They are looked up in a file named `sql-error-codes.xml` from the classpath, and the matching `SQLErrorCodes` instance is located based on the database name from the database metadata of the database in use. `SQLErrorCodesFactory` is as of now part of Spring JDBC. Spring Data R2DBC reuses existing translation configurations. - -You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example shows: - -[source,java] ----- -public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator { - - protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) { - if (sqlex.getErrorCode() == -12345) { - return new DeadlockLoserDataAccessException(task, r2dbcex); - } - return null; - } -} ----- - -In the preceding example, the specific error code (`-12345`) is translated, while other errors are left to be translated by the default translator implementation. -To use this custom translator, you must configure `DatabaseClient` through the builder method `exceptionTranslator`, and you must use this `DatabaseClient` for all of the data access processing where this translator is needed. -The following example shows how you can use this custom translator: - -[source,java] ----- -ConnectionFactory connectionFactory = …; - -CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator(); - -DatabaseClient client = DatabaseClient.builder() - .connectionFactory(connectionFactory) - .exceptionTranslator(exceptionTranslator) - .build(); ----- - -[[r2dbc.datbaseclient.statements]] -=== Running 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: - -[source,java] ----- -Mono completion = client.execute() - .sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") - .then(); ----- - -`DatabaseClient` is designed for a convenient fluent usage. -It exposes intermediate, continuation, and terminal methods at each stage of the execution specification. -The example above uses `then()` to return a completion `Publisher` that completes as soon as the query (or queries, if the SQL query contains multiple statements) completes. - -NOTE: `execute().sql(…)` accepts either the SQL query string or a query `Supplier` to defer the actual query creation until execution. - -[[r2dbc.datbaseclient.queries]] -=== Running Queries - -SQL queries can return values or the number of affected rows. -`DatabaseClient` can return the number of updated rows or the rows themselves, depending on the issued query. - -The following example shows an `UPDATE` statement that returns the number of updated rows: - -[source,java] ----- -Mono affectedRows = client.execute() - .sql("UPDATE person SET name = 'Joe'") - .fetch().rowsUpdated(); ----- - -Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`. -You might have noticed the use of `fetch()` in the previous example. -`fetch()` is a continuation operator that allows you to specify how much data you want to consume. - -[source,java] ----- -Mono> first = client.execute() - .sql("SELECT id, name FROM person") - .fetch().first(); ----- - -Calling `first()` returns the first row from the result and discards remaining rows. -You can consume data with the following operators: - -* `first()` return the first row of the entire result -* `one()` returns exactly one result and fails if the result contains more rows. -* `all()` returns all rows of the result -* `rowsUpdated()` returns the number of affected rows (`INSERT` count, `UPDATE` count) - -`DatabaseClient` queries return their results by default as `Map` of column name to value. You can customize type mapping by applying an `as(Class)` operator. - -[source,java] ----- -Flux all = client.execute() - .sql("SELECT id, name FROM mytable") - .as(Person.class) - .fetch().all(); ----- - -`as(…)` applies <> and maps the resulting columns to your POJO. - -[[r2dbc.datbaseclient.mapping]] -=== Mapping Results - -You can customize result extraction beyond `Map` and POJO result extraction by providing an extractor `BiFunction`. -The extractor function interacts directly with R2DBC's `Row` and `RowMetadata` objects and can return arbitrary values (singular values, collections/maps, objects). - -The following example extracts the `id` column and emits its value: - -[source,java] ----- -Flux names= client.execute() - .sql("SELECT name FROM person") - .map((row, rowMetadata) -> row.get("id", String.class)) - .all(); ----- - -[[r2dbc.datbaseclient.mapping.null]] -.What about `null`? -**** -Relational database results may contain `null` values. -Reactive Streams forbids emission of `null` values which requires a proper `null` handling in the extractor function. -While you can obtain `null` values from a `Row`, you must not emit a `null` value. -You must wrap any `null` values in an object (e.g. `Optional` for singular values) to make sure a `null` value is never returned directly by your extractor function. -**** - -[[r2dbc.datbaseclient.binding]] -=== Binding Values to Queries - -A typical application requires parameterized SQL statements to select or update rows according to some input. -These are typically `SELECT` statements constrained by a `WHERE` clause or `INSERT`/`UPDATE` statements accepting input parameters. -Parameterized statements bear the risk of SQL injection if parameters are not escaped properly. -`DatabaseClient` leverages R2DBC's Bind API to eliminate the risk of SQL injection for query parameters. -You can provide a parameterized SQL statement with the `sql(…)` operator and bind parameters to the actual `Statement`. -Your R2DBC driver then executes the statement using prepared statements and parameter substitution. - -Parameter binding supports various binding strategies: - -* By Index using zero-based parameter indexes. -* By Name using the placeholder name. - -The following example shows parameter binding for a query: - -[source,java] ----- -db.execute() - .sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") - .bind("age", 34); ----- - -.R2DBC Native Bind Markers -**** -R2DBC uses database-native bind markers that depend on the actual database vendor. -As an example, Postgres uses indexed markers such as `$1`, `$2`, `$n`. -Another example is SQL Server that uses named bind markers prefixed with `@` (at). - -This is different from JDBC which requires `?` (question mark) as bind markers. -In JDBC, the actual drivers translate question mark bind markers to database-native markers as part of their statement execution. - -Spring Data R2DBC allows you to use native bind markers or named bind markers with the `:name` syntax. - -Named parameter support leverages ``Dialect``s to expand named parameters to native bind markers at the time of query execution which gives you a certain degree of query portability across various database vendors. -**** - -The query-preprocessor unrolls named `Collection` parameters into a series of bind markers to remove the need of dynamic query creation based on the number of arguments. -Nested object arrays are expanded to allow usage of e.g. select lists. - -Consider the following query: - -[source,sql] ----- -SELECT id, name, state FROM table WHERE (name, age) IN (('John', 35), ('Ann', 50)) ----- - -This query can be parametrized and executed as: - -[source,java] ----- -List tuples = new ArrayList<>(); -tuples.add(new Object[] {"John", 35}); -tuples.add(new Object[] {"Ann", 50}); - -db.execute() - .sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") - .bind("tuples", tuples); ----- - -NOTE: Usage of select lists is vendor-dependent. - -A simpler variant using `IN` predicates: - -[source,java] ----- -db.execute() - .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") - .bind("ages", Arrays.asList(35, 50)); ----- - -[[r2dbc.datbaseclient.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. -Using different connections hence results in utilizing different transactions. -Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction. -`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods. - -You can run multiple statements within a transaction using the `inTransaction(Function)` closure: - -[source,java] ----- -TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); - -Flux completion = databaseClient.inTransaction(db -> { - - return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") - .bind("age", 34) - .fetch().rowsUpdated() - .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") - .bind("id", "joe") - .bind("name", "Joe") - .fetch().rowsUpdated()) - .then(); -}); ----- +include::r2dbc-transactions.adoc[leveloffset=+1] From 774d2e8b09a22cc0c5fe27c15a0c60e21c4d9692 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 6 May 2019 14:13:21 +0200 Subject: [PATCH 112/665] #64 - API polishing. Document fluent API. Add fluent API for update. Introduce StatementMapper. Migrate Insert to StatementMapper. Refactoring and cleanup. Migrate Select to StatementMapper. Original pull request: #106. --- src/main/asciidoc/reference/r2dbc-fluent.adoc | 239 +++++++ src/main/asciidoc/reference/r2dbc-sql.adoc | 2 +- .../reference/r2dbc-transactions.adoc | 2 +- src/main/asciidoc/reference/r2dbc.adoc | 2 + .../r2dbc/{domain => dialect}/Bindings.java | 63 +- .../{domain => dialect}/MutableBindings.java | 4 +- .../data/r2dbc/function/DatabaseClient.java | 170 ++++- .../r2dbc/function/DefaultDatabaseClient.java | 431 ++++++++---- .../DefaultReactiveDataAccessStrategy.java | 120 ++-- .../function/DefaultStatementFactory.java | 661 +----------------- .../function/DefaultStatementMapper.java | 385 ++++++++++ .../function/ReactiveDataAccessStrategy.java | 57 +- .../data/r2dbc/function/StatementFactory.java | 244 ------- .../data/r2dbc/function/StatementMapper.java | 377 ++++++++++ .../function/query/BoundAssignments.java | 51 ++ .../r2dbc/function/query/BoundCondition.java | 6 +- .../data/r2dbc/function/query/Criteria.java | 84 +-- .../{CriteriaMapper.java => QueryMapper.java} | 181 ++--- .../data/r2dbc/function/query/Update.java | 86 +++ .../r2dbc/function/query/UpdateMapper.java | 139 ++++ .../support/SimpleR2dbcRepository.java | 115 ++- .../BindingsUnitTests.java | 34 +- ...bstractDatabaseClientIntegrationTests.java | 79 ++- .../function/StatementFactoryUnitTests.java | 264 ------- .../function/StatementMapperUnitTests.java | 69 ++ .../function/query/CriteriaUnitTests.java | 54 +- ...itTests.java => QueryMapperUnitTests.java} | 78 ++- .../function/query/UpdateMapperUnitTests.java | 106 +++ 28 files changed, 2369 insertions(+), 1734 deletions(-) create mode 100644 src/main/asciidoc/reference/r2dbc-fluent.adoc rename src/main/java/org/springframework/data/r2dbc/{domain => dialect}/Bindings.java (80%) rename src/main/java/org/springframework/data/r2dbc/{domain => dialect}/MutableBindings.java (95%) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java delete mode 100644 src/main/java/org/springframework/data/r2dbc/function/StatementFactory.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java rename src/main/java/org/springframework/data/r2dbc/function/query/{CriteriaMapper.java => QueryMapper.java} (70%) create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/Update.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java rename src/test/java/org/springframework/data/r2dbc/{domain => dialect}/BindingsUnitTests.java (82%) delete mode 100644 src/test/java/org/springframework/data/r2dbc/function/StatementFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java rename src/test/java/org/springframework/data/r2dbc/function/query/{CriteriaMapperUnitTests.java => QueryMapperUnitTests.java} (67%) create mode 100644 src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java diff --git a/src/main/asciidoc/reference/r2dbc-fluent.adoc b/src/main/asciidoc/reference/r2dbc-fluent.adoc new file mode 100644 index 00000000..faf9943e --- /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 d71034e5..56bbfe92 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 2e4fe3b0..f85db46f 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 4c0b55d4..27e5f3e1 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 89c1edf1..40242b82 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 739aa812..c0cb0ce5 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 4c9a965d..39f7c8bf 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 7f99ccfd..4db374c0 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 6089ce3f..a7b7af1b 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 bfb4cca6..5e713db9 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 00000000..bd5ed805 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.PostgresDialect; +import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.PreparedOperation; +import org.springframework.data.r2dbc.function.StatementMapper.UpdateSpec; +import org.springframework.data.r2dbc.function.query.Criteria; +import org.springframework.data.r2dbc.function.query.Update; + +/** + * Unit tests for {@link DefaultStatementMapper}. + * + * @author Mark Paluch + */ +public class StatementMapperUnitTests { + + ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE); + StatementMapper mapper = strategy.getStatementMapper(); + + BindTarget bindTarget = mock(BindTarget.class); + + @Test // gh-64 + public void shouldMapUpdate() { + + UpdateSpec updateSpec = mapper.createUpdate("foo", Update.update("column", "value")); + + PreparedOperation preparedOperation = mapper.getMappedObject(updateSpec); + + assertThat(preparedOperation.toQuery()).isEqualTo("UPDATE foo SET column = $1"); + + preparedOperation.bindTo(bindTarget); + verify(bindTarget).bind(0, "value"); + } + + @Test // gh-64 + public void shouldMapUpdateWithCriteria() { + + UpdateSpec updateSpec = mapper.createUpdate("foo", Update.update("column", "value")) + .withCriteria(Criteria.where("foo").is("bar")); + + PreparedOperation preparedOperation = mapper.getMappedObject(updateSpec); + + assertThat(preparedOperation.toQuery()).isEqualTo("UPDATE foo SET column = $1 WHERE foo.foo = $2"); + + preparedOperation.bindTo(bindTarget); + verify(bindTarget).bind(0, "value"); + verify(bindTarget).bind(1, "bar"); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java index c6ac54cd..32d190c5 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java @@ -34,9 +34,9 @@ public class CriteriaUnitTests { @Test // gh-64 public void andChainedCriteria() { - Criteria criteria = of("foo").is("bar").and("baz").isNotNull(); + Criteria criteria = where("foo").is("bar").and("baz").isNotNull(); - assertThat(criteria.getProperty()).isEqualTo("baz"); + assertThat(criteria.getColumn()).isEqualTo("baz"); assertThat(criteria.getComparator()).isEqualTo(Comparator.IS_NOT_NULL); assertThat(criteria.getValue()).isNull(); assertThat(criteria.getPrevious()).isNotNull(); @@ -44,7 +44,7 @@ public 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 e47f9c11..966ff4a9 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java @@ -17,12 +17,14 @@ 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 00000000..9c239303 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Map; +import java.util.stream.Collectors; + +import org.junit.Test; + +import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +import org.springframework.data.r2dbc.domain.BindTarget; +import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.AssignValue; +import org.springframework.data.relational.core.sql.Expression; +import org.springframework.data.relational.core.sql.SQL; +import org.springframework.data.relational.core.sql.Table; + +/** + * Unit tests for {@link UpdateMapper}. + * + * @author Mark Paluch + */ +public class UpdateMapperUnitTests { + + R2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext()); + UpdateMapper mapper = new UpdateMapper(converter); + BindTarget bindTarget = mock(BindTarget.class); + + @Test // gh-64 + public void shouldMapFieldNamesInUpdate() { + + Update update = Update.update("alternative", "foo"); + + BoundAssignments mapped = map(update); + + Map assignments = mapped.getAssignments().stream().map(it -> (AssignValue) it) + .collect(Collectors.toMap(k -> k.getColumn().getName(), AssignValue::getValue)); + + assertThat(assignments).containsEntry("another_name", SQL.bindMarker("$1")); + } + + @Test // gh-64 + public void shouldUpdateToSettableValue() { + + Update update = Update.update("alternative", SettableValue.empty(String.class)); + + BoundAssignments mapped = map(update); + + Map assignments = mapped.getAssignments().stream().map(it -> (AssignValue) it) + .collect(Collectors.toMap(k -> k.getColumn().getName(), AssignValue::getValue)); + + assertThat(assignments).containsEntry("another_name", SQL.bindMarker("$1")); + + mapped.getBindings().apply(bindTarget); + verify(bindTarget).bindNull(0, String.class); + } + + @Test // gh-64 + public void shouldUpdateToNull() { + + Update update = Update.update("alternative", null); + + BoundAssignments mapped = map(update); + + assertThat(mapped.getAssignments()).hasSize(1); + assertThat(mapped.getAssignments().get(0).toString()).isEqualTo("person.another_name = NULL"); + + mapped.getBindings().apply(bindTarget); + verifyZeroInteractions(bindTarget); + } + + @SuppressWarnings("unchecked") + private BoundAssignments map(Update update) { + + BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1); + + return mapper.getMappedObject(markers.create(), update, Table.create("person"), + converter.getMappingContext().getRequiredPersistentEntity(Person.class)); + } + + static class Person { + + String name; + @Column("another_name") String alternative; + } +} From 361d801b14ededf940599a162dbea1c414131410 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 7 May 2019 15:12:31 +0200 Subject: [PATCH 113/665] #64 - Polishing. Fixed typos, formatting and minor errors in documentation. Original pull request: #106. --- src/main/asciidoc/reference/r2dbc-core.adoc | 2 +- src/main/asciidoc/reference/r2dbc-fluent.adoc | 6 ++-- src/main/asciidoc/reference/r2dbc-sql.adoc | 4 +-- .../data/r2dbc/dialect/Bindings.java | 2 +- .../data/r2dbc/dialect/MutableBindings.java | 2 +- .../data/r2dbc/function/query/Criteria.java | 29 +++++++++---------- .../r2dbc/function/query/QueryMapper.java | 2 ++ .../r2dbc/function/query/UpdateMapper.java | 1 + 8 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index aba9b628..87466e3e 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -229,7 +229,7 @@ This approach lets you use the standard `io.r2dbc.spi.ConnectionFactory` instanc Spring Data R2DBC supports drivers by R2DBC's pluggable SPI mechanism. Any driver implementing the R2DBC spec can be used with Spring Data R2DBC. R2DBC is a relatively young initiative that gains significance by maturing through adoption. -As of writing the following 3 drivers are available: +As of writing the following drivers are available: * https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) * https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) diff --git a/src/main/asciidoc/reference/r2dbc-fluent.adoc b/src/main/asciidoc/reference/r2dbc-fluent.adoc index faf9943e..7ac0eadd 100644 --- a/src/main/asciidoc/reference/r2dbc-fluent.adoc +++ b/src/main/asciidoc/reference/r2dbc-fluent.adoc @@ -192,10 +192,10 @@ Mono update = databaseClient.update() <4> Use `then()` to just update rows an object without consuming further details. Modifying statements allow also consumption of the number of affected rows. ==== -[r2dbc.datbaseclient.fluent-api.delete.methods]] -==== Methods for DELETE operations +[r2dbc.datbaseclient.fluent-api.update.methods]] +==== Methods for UPDATE operations -The `delete()` entry point exposes some additional methods that provide options for the operation: +The `update()` entry point exposes some additional methods that provide options for the operation: * *table* `(Class)` used to specify the target table using a mapped object. Returns results by default as `T`. * *table* `(String)` used to specify the target table name. Returns results by default as `Map`. diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc index 56bbfe92..eb824d08 100644 --- a/src/main/asciidoc/reference/r2dbc-sql.adoc +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -2,7 +2,7 @@ = Executing Statements Running a statement is the basic functionality that is covered by `DatabaseClient`. -The following example shows what you need to include for a minimal but fully functional class that creates a new table: +The following example shows what you need to include for minimal but fully functional code that creates a new table: [source,java] ---- @@ -32,7 +32,7 @@ Mono affectedRows = client.execute() .fetch().rowsUpdated(); ---- -Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumes by streaming each `Row`. +Running a `SELECT` query returns a different type of result, in particular tabular results. Tabular data is typically consumed by streaming each `Row`. You might have noticed the use of `fetch()` in the previous example. `fetch()` is a continuation operator that allows you to specify how much data you want to consume. diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java index 40242b82..94fb90e0 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java @@ -33,7 +33,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** - * Value object representing value and {@code NULL} bindings for a {@link Statement} using {@link BindMarkers}. Bindings + * Value object representing value and {@code null} bindings for a {@link Statement} using {@link BindMarkers}. Bindings * are typically immutable. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java index c0cb0ce5..dd2abda5 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/MutableBindings.java @@ -22,7 +22,7 @@ import java.util.LinkedHashMap; import org.springframework.util.Assert; /** - * Mutable extension to {@link Bindings} for Value and {@code NULL} bindings for a {@link Statement} using + * Mutable extension to {@link Bindings} for Value and {@code null} bindings for a {@link Statement} using * {@link BindMarkers}. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java index 4e4153f1..30fa6a11 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java @@ -46,6 +46,7 @@ public class Criteria { private Criteria(@Nullable Criteria previous, Combinator combinator, String column, Comparator comparator, @Nullable Object value) { + this.previous = previous; this.combinator = combinator; this.column = column; @@ -56,7 +57,7 @@ public class Criteria { /** * Static factory method to create a Criteria using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the first {@link Criteria}. */ public static CriteriaStep where(String column) { @@ -69,7 +70,7 @@ public class Criteria { /** * Create a new {@link Criteria} and combine it with {@code AND} using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. */ public CriteriaStep and(String column) { @@ -87,7 +88,7 @@ public class Criteria { /** * Create a new {@link Criteria} and combine it with {@code OR} using the provided {@code column} name. * - * @param column + * @param column Must not be {@literal null} or empty. * @return a new {@link CriteriaStep} object to complete the next {@link Criteria}. */ public CriteriaStep or(String column) { @@ -179,7 +180,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IN}. * - * @param value + * @param values * @return */ Criteria in(Object... values); @@ -187,7 +188,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IN}. * - * @param value + * @param values * @return */ Criteria in(Collection values); @@ -195,7 +196,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param value + * @param values * @return */ Criteria notIn(Object... values); @@ -203,7 +204,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param value + * @param values * @return */ Criteria notIn(Collection values); @@ -251,7 +252,6 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IS NULL}. * - * @param value * @return */ Criteria isNull(); @@ -259,7 +259,6 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IS NOT NULL}. * - * @param value * @return */ Criteria isNotNull(); @@ -314,9 +313,9 @@ public class Criteria { return createCriteria(Comparator.IN, Arrays.asList(values)); } - /** - * @param values - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#in(java.util.Collection) */ @Override public Criteria in(Collection values) { @@ -343,9 +342,9 @@ public class Criteria { return createCriteria(Comparator.NOT_IN, Arrays.asList(values)); } - /** - * @param values - * @return + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.query.Criteria.CriteriaStep#notIn(java.util.Collection) */ @Override public Criteria notIn(Collection values) { diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java index 95b06533..18229f36 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java @@ -166,6 +166,7 @@ public class QueryMapper { typeHint = getTypeHint(mappedValue, actualType.getType(), settableValue); } else { + mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint()); typeHint = actualType.getType(); } @@ -227,6 +228,7 @@ public class QueryMapper { condition = column.in(expressions.toArray(new Expression[0])); } else { + BindMarker bindMarker = bindings.nextMarker(column.getName()); Expression expression = bind(mappedValue, valueType, bindings, bindMarker); diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java index 400a131f..b28a7abf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java @@ -21,6 +21,7 @@ import java.util.Map; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.MutableBindings; import org.springframework.data.r2dbc.domain.SettableValue; import org.springframework.data.r2dbc.function.convert.R2dbcConverter; From da53a9a934a8be52d116da8ddfc585c0accf2047 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 8 May 2019 10:36:21 +0200 Subject: [PATCH 114/665] #64 - Polishing. Incorporated feedback from review. Polished documentation and Javadoc. Minor code improvements restructuring for better readability. Removed unused methods types. Some polishing for compiler warnings. Original pull request: #106. --- src/main/asciidoc/reference/r2dbc-core.adoc | 4 +- .../reference/r2dbc-databaseclient.adoc | 21 ++++++++-- .../data/r2dbc/BadSqlGrammarException.java | 2 + .../r2dbc/UncategorizedR2dbcException.java | 2 + .../r2dbc/function/DefaultDatabaseClient.java | 42 ++++++------------- .../function/DefaultStatementFactory.java | 35 ---------------- .../convert/MappingR2dbcConverter.java | 40 +++++++++--------- .../data/r2dbc/function/query/Criteria.java | 27 +++++++----- .../r2dbc/function/query/QueryMapper.java | 18 ++++---- .../data/r2dbc/function/query/Update.java | 33 +++++++-------- .../repository/query/R2dbcQueryMethod.java | 3 ++ .../function/query/CriteriaUnitTests.java | 4 +- .../function/query/QueryMapperUnitTests.java | 2 - .../function/query/UpdateMapperUnitTests.java | 2 - 14 files changed, 100 insertions(+), 135 deletions(-) delete mode 100644 src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index 87466e3e..249c4cb5 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -236,6 +236,6 @@ As of writing the following drivers are available: * https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`) * https://github.com/jasync-sql/jasync-sql[Microsoft SQL Server] (`com.github.jasync-sql:jasync-r2dbc-mysql`) -Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` and selects the appropriate database dialect. -You can configure an own `Dialect` if the used driver is not yet known to Spring Data R2DBC. +Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` exposed by the `ConnectionFactory` and selects the appropriate database dialect accordingly. +You can configure an own https://docs.spring.io/spring-data/r2dbc/docs/{version}/api/org/springframework/data/r2dbc/dialect/Dialect.html[`Dialect`] if the used driver is not yet known to Spring Data R2DBC. diff --git a/src/main/asciidoc/reference/r2dbc-databaseclient.adoc b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc index 5cbb8652..2809ac18 100644 --- a/src/main/asciidoc/reference/r2dbc-databaseclient.adoc +++ b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc @@ -24,10 +24,10 @@ DatabaseClient.create(ConnectionFactory connectionFactory) The above method creates a `DatabaseClient` with default settings. -You can also use `DatabaseClient.builder()` with further options to customize the client: +You can also obtain a `Builder` instance via `DatabaseClient.builder()` with further options to customize the client by calling the following methods: -* `exceptionTranslator`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. -* `dataAccessStrategy`: Strategy how SQL queries are generated and how objects are mapped. +* `….exceptionTranslator(…)`: Supply a specific `R2dbcExceptionTranslator` to customize how R2DBC exceptions are translated into Spring's portable Data Access Exception hierarchy. See "`<>`" for more information. +* `….dataAccessStrategy(…)`: Strategy how SQL queries are generated and how objects are mapped. Once built, a `DatabaseClient` instance is immutable. However, you can clone it and build a modified copy without affecting the original instance, as the following example shows: @@ -50,6 +50,19 @@ When you use Spring Data R2DBC, you can create a `ConnectionFactory` using your `ConnectionFactory` implementations can either return the same connection, different connections or provide connection pooling. `DatabaseClient` uses `ConnectionFactory` to create and release connections per operation without affinity to a particular connection across multiple operations. +Assuming you'd be using H2 as a database, a typical programmatic setup looks something like this: + +[source, java] +---- +H2ConnectionConfiguration config = … <1> +ConnectionFactory factory = new H2ConnectionFactory(config); <2> + +DatabaseClient client = DatabaseClient.create(factory); <3> +---- +<1> Prepare the database specific configuration (host, port, credentials etc.) +<2> Create a connection factory using that configuration. +<3> Create a `DatabaseClient` to use that connection factory. + [[r2dbc.exception]] = Exception Translation @@ -66,7 +79,7 @@ It considers R2DBC's categorized exception hierarchy to translate these into Spr `SqlErrorCodeR2dbcExceptionTranslator` uses specific vendor codes using Spring JDBC's `SQLErrorCodes`. It is more precise than the SQLState implementation. The error code translations are based on codes held in a JavaBean type class called `SQLErrorCodes`. -This class is created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module. +Instances of this class are created and populated by an `SQLErrorCodesFactory`, which (as the name suggests) is a factory for creating SQLErrorCodes based on the contents of a configuration file named `sql-error-codes.xml` from Spring's Data Access module. This file is populated with vendor codes and based on the `ConnectionFactoryName` taken from `ConnectionFactoryMetadata`. The codes for the actual database you are using are used. diff --git a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java index 9e7e6f77..becd75b0 100644 --- a/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java +++ b/src/main/java/org/springframework/data/r2dbc/BadSqlGrammarException.java @@ -31,6 +31,8 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; */ public class BadSqlGrammarException extends InvalidDataAccessResourceUsageException { + private static final long serialVersionUID = 3814579246913482054L; + private final String sql; /** diff --git a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java index 8e051424..8833775f 100644 --- a/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java +++ b/src/main/java/org/springframework/data/r2dbc/UncategorizedR2dbcException.java @@ -27,6 +27,8 @@ import org.springframework.lang.Nullable; */ public class UncategorizedR2dbcException extends UncategorizedDataAccessException { + private static final long serialVersionUID = 361587356435210266L; + /** * SQL that led to the problem */ 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 4db374c0..51383c67 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -45,7 +45,6 @@ import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; - import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Pageable; @@ -232,7 +231,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { protected DataAccessException translateException(String task, @Nullable String sql, R2dbcException ex) { DataAccessException dae = this.exceptionTranslator.translate(task, sql, ex); - return (dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex)); + return dae != null ? dae : new UncategorizedR2dbcException(task, sql, ex); } /** @@ -267,25 +266,6 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return new DefaultGenericExecuteSpec(sqlSupplier); } - private static void doBind(Statement statement, Map byName, - Map byIndex) { - - bindByIndex(statement, byIndex); - bindByName(statement, byName); - } - - private static void bindByName(Statement statement, Map byName) { - - byName.forEach((name, o) -> { - - if (o.getValue() != null) { - statement.bind(name, o.getValue()); - } else { - statement.bindNull(name, o.getType()); - } - }); - } - private static void bindByIndex(Statement statement, Map byIndex) { byIndex.forEach((i, o) -> { @@ -591,7 +571,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @Override public DefaultTypedExecuteSpec bind(String name, Object value) { - return (DefaultTypedExecuteSpec) super.bind(name, value); + return (DefaultTypedExecuteSpec) super.bind(name, value); } @Override @@ -789,14 +769,11 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { this.mappingFunction = dataAccessStrategy.getRowMapper(typeToRead); } - DefaultTypedSelectSpec(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page, - BiFunction mappingFunction) { - this(table, projectedFields, criteria, sort, page, null, mappingFunction); - } - DefaultTypedSelectSpec(String table, List projectedFields, Criteria criteria, Sort sort, Pageable page, Class typeToRead, BiFunction mappingFunction) { + super(table, projectedFields, criteria, sort, page); + this.typeToRead = typeToRead; this.mappingFunction = mappingFunction; } @@ -900,13 +877,14 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { private final BiFunction mappingFunction; @Override - public GenericInsertSpec value(String field, Object value) { + public GenericInsertSpec value(String field, Object value) { Assert.notNull(field, "Field must not be null!"); Assert.notNull(value, () -> String.format("Value for field %s must not be null. Use nullValue(…) instead.", field)); Map byName = new LinkedHashMap<>(this.byName); + if (value instanceof SettableValue) { byName.put(field, (SettableValue) value); } else { @@ -917,7 +895,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override - public GenericInsertSpec nullValue(String field) { + public GenericInsertSpec nullValue(String field) { Assert.notNull(field, "Field must not be null!"); @@ -991,6 +969,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) public InsertSpec using(T objectToInsert) { Assert.notNull(objectToInsert, "Object to insert must not be null!"); @@ -1000,6 +979,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { } @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) public InsertSpec using(Publisher objectToInsert) { Assert.notNull(objectToInsert, "Publisher to insert must not be null!"); @@ -1417,7 +1397,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { if (method.getName().equals("equals")) { // Only consider equal when proxies are identical. - return (proxy == args[0]); + return proxy == args[0]; } else if (method.getName().equals("hashCode")) { // Use hashCode of PersistenceManager proxy. return System.identityHashCode(proxy); @@ -1454,6 +1434,8 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { @RequiredArgsConstructor static class ConnectionCloseHolder extends AtomicBoolean { + private static final long serialVersionUID = -8994138383301201380L; + final Connection connection; final Function> closeFunction; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java deleted file mode 100644 index 5e713db9..00000000 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementFactory.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.r2dbc.function; - -import lombok.RequiredArgsConstructor; - -import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.relational.core.sql.render.RenderContext; - -/** - * Default {@link StatementFactory} implementation. - * - * @author Mark Paluch - */ -// TODO: Move DefaultPreparedOperation et al to a better place. Probably StatementMapper. -@RequiredArgsConstructor -class DefaultStatementFactory { - - private final Dialect dialect; - private final RenderContext renderContext; - -} diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java index 25604dab..003d3273 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java @@ -55,6 +55,7 @@ import org.springframework.util.ClassUtils; * Converter for R2DBC. * * @author Mark Paluch + * @author Oliver Drotbohm */ public class MappingR2dbcConverter extends BasicRelationalConverter implements R2dbcConverter { @@ -176,21 +177,21 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R return getConversionService().convert(value, target); } + @SuppressWarnings("unchecked") private S readEntityFrom(Row row, PersistentProperty property) { String prefix = property.getName() + "_"; - RelationalPersistentEntity entity = (RelationalPersistentEntity) getMappingContext() - .getRequiredPersistentEntity(property.getActualType()); + RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType()); if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) { return null; } - S instance = createInstance(row, prefix, entity); + Object instance = createInstance(row, prefix, entity); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); - ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor, getConversionService()); + PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); + ConvertingPropertyAccessor propertyAccessor = new ConvertingPropertyAccessor<>(accessor, getConversionService()); for (RelationalPersistentProperty p : entity) { if (!entity.isConstructorArgument(property)) { @@ -198,7 +199,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R } } - return instance; + return (S) instance; } private S createInstance(Row row, String prefix, RelationalPersistentEntity entity) { @@ -346,17 +347,16 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R return (row, metadata) -> { - PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(object); RelationalPersistentProperty idProperty = entity.getRequiredIdProperty(); - if (propertyAccessor.getProperty(idProperty) == null) { - - if (potentiallySetId(row, metadata, propertyAccessor, idProperty)) { - return (T) propertyAccessor.getBean(); - } + if (propertyAccessor.getProperty(idProperty) != null) { + return object; } - return object; + return potentiallySetId(row, metadata, propertyAccessor, idProperty) // + ? (T) propertyAccessor.getBean() // + : object; }; } @@ -376,19 +376,19 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R generatedIdValue = row.get(key); } - if (generatedIdValue != null) { - - ConversionService conversionService = getConversionService(); - propertyAccessor.setProperty(idProperty, conversionService.convert(generatedIdValue, idProperty.getType())); - return true; + if (generatedIdValue == null) { + return false; } - return false; + ConversionService conversionService = getConversionService(); + propertyAccessor.setProperty(idProperty, conversionService.convert(generatedIdValue, idProperty.getType())); + + return true; } @SuppressWarnings("unchecked") private RelationalPersistentEntity getRequiredPersistentEntity(Class type) { - return (RelationalPersistentEntity) getMappingContext().getRequiredPersistentEntity(type); + return (RelationalPersistentEntity) getMappingContext().getRequiredPersistentEntity(type); } private static Map createMetadataMap(RowMetadata metadata) { diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java index 30fa6a11..be48f741 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java @@ -30,6 +30,7 @@ import org.springframework.util.Assert; * {@code where(property(…).is(…)}. * * @author Mark Paluch + * @author Oliver Drotbohm */ public class Criteria { @@ -164,7 +165,7 @@ public class Criteria { /** * Creates a {@link Criteria} using equality. * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria is(Object value); @@ -172,7 +173,7 @@ public class Criteria { /** * Creates a {@link Criteria} using equality (is not). * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria not(Object value); @@ -180,7 +181,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IN}. * - * @param values + * @param values must not be {@literal null}. * @return */ Criteria in(Object... values); @@ -188,7 +189,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code IN}. * - * @param values + * @param values must not be {@literal null}. * @return */ Criteria in(Collection values); @@ -196,7 +197,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param values + * @param values must not be {@literal null}. * @return */ Criteria notIn(Object... values); @@ -204,7 +205,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code NOT IN}. * - * @param values + * @param values must not be {@literal null}. * @return */ Criteria notIn(Collection values); @@ -212,7 +213,7 @@ public class Criteria { /** * Creates a {@link Criteria} using less-than ({@literal <}). * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria lessThan(Object value); @@ -220,7 +221,7 @@ public class Criteria { /** * Creates a {@link Criteria} using less-than or equal to ({@literal <=}). * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria lessThanOrEquals(Object value); @@ -228,7 +229,7 @@ public class Criteria { /** * Creates a {@link Criteria} using greater-than({@literal >}). * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria greaterThan(Object value); @@ -236,7 +237,7 @@ public class Criteria { /** * Creates a {@link Criteria} using greater-than or equal to ({@literal >=}). * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria greaterThanOrEquals(Object value); @@ -244,7 +245,7 @@ public class Criteria { /** * Creates a {@link Criteria} using {@code LIKE}. * - * @param value + * @param value must not be {@literal null}. * @return */ Criteria like(Object value); @@ -304,6 +305,7 @@ public class Criteria { public Criteria in(Object... values) { Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values, "Values must not contain a null value!"); if (values.length > 1 && values[1] instanceof Collection) { throw new InvalidDataAccessApiUsageException( @@ -321,6 +323,7 @@ public class Criteria { public Criteria in(Collection values) { Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values.toArray(), "Values must not contain a null value!"); return createCriteria(Comparator.IN, values); } @@ -333,6 +336,7 @@ public class Criteria { public Criteria notIn(Object... values) { Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values, "Values must not contain a null value!"); if (values.length > 1 && values[1] instanceof Collection) { throw new InvalidDataAccessApiUsageException( @@ -350,6 +354,7 @@ public class Criteria { public Criteria notIn(Collection values) { Assert.notNull(values, "Values must not be null!"); + Assert.noNullElements(values.toArray(), "Values must not contain a null value!"); return createCriteria(Comparator.NOT_IN, values); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java index 18229f36..752a1d0c 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java @@ -204,20 +204,22 @@ public class QueryMapper { private Condition createCondition(Column column, @Nullable Object mappedValue, Class valueType, MutableBindings bindings, Comparator comparator) { - switch (comparator) { - case IS_NULL: - return column.isNull(); - case IS_NOT_NULL: - return column.isNotNull(); + if (comparator.equals(Comparator.IS_NULL)) { + return column.isNull(); + } + + if (comparator.equals(Comparator.IS_NOT_NULL)) { + return column.isNotNull(); } if (comparator == Comparator.NOT_IN || comparator == Comparator.IN) { Condition condition; + if (mappedValue instanceof Iterable) { List expressions = new ArrayList<>( - mappedValue instanceof Collection ? ((Collection) mappedValue).size() : 10); + mappedValue instanceof Collection ? ((Collection) mappedValue).size() : 10); for (Object o : (Iterable) mappedValue) { @@ -260,9 +262,9 @@ public class QueryMapper { return column.isGreaterOrEqualTo(expression); case LIKE: return column.like(expression); + default: + throw new UnsupportedOperationException("Comparator " + comparator + " not supported"); } - - throw new UnsupportedOperationException("Comparator " + comparator + " not supported"); } Field createPropertyField(@Nullable RelationalPersistentEntity entity, String key, diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Update.java b/src/main/java/org/springframework/data/r2dbc/function/query/Update.java index be2f53b6..6d590406 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Update.java +++ b/src/main/java/org/springframework/data/r2dbc/function/query/Update.java @@ -18,7 +18,6 @@ package org.springframework.data.r2dbc.function.query; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; -import java.util.function.BiConsumer; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -27,6 +26,7 @@ import org.springframework.util.Assert; * Class to easily construct SQL update assignments. * * @author Mark Paluch + * @author Oliver Drotbohm */ public class Update { @@ -41,8 +41,8 @@ public class Update { /** * Static factory method to create an {@link Update} using the provided column. * - * @param column - * @param value + * @param column must not be {@literal null}. + * @param value can be {@literal null}. * @return */ public static Update update(String column, @Nullable Object value) { @@ -52,14 +52,23 @@ public class Update { /** * Update a column by assigning a value. * - * @param column - * @param value + * @param column must not be {@literal null}. + * @param value can be {@literal null}. * @return */ public Update set(String column, @Nullable Object value) { return addMultiFieldOperation(column, value); } + /** + * Returns all assignments. + * + * @return + */ + public Map getAssignments() { + return Collections.unmodifiableMap(this.columnsToUpdate); + } + private Update addMultiFieldOperation(String key, Object value) { Assert.hasText(key, "Column for update must not be null or blank"); @@ -69,18 +78,4 @@ public class Update { return new Update(updates); } - - /** - * Performs the given action for each column-value tuple in this {@link Update object} until all entries have been - * processed or the action throws an exception. - * - * @param action must not be {@literal null}. - */ - void forEachColumn(BiConsumer action) { - this.columnsToUpdate.forEach(action); - } - - public Map getAssignments() { - return Collections.unmodifiableMap(this.columnsToUpdate); - } } diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java index eb9db8f5..3f337483 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryMethod.java @@ -51,7 +51,10 @@ import org.springframework.util.ClassUtils; */ public class R2dbcQueryMethod extends QueryMethod { + @SuppressWarnings("rawtypes") // private static final ClassTypeInformation PAGE_TYPE = ClassTypeInformation.from(Page.class); + + @SuppressWarnings("rawtypes") // private static final ClassTypeInformation SLICE_TYPE = ClassTypeInformation.from(Slice.class); private final Method method; 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 32d190c5..d001e433 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 @@ -21,8 +21,8 @@ import static org.springframework.data.r2dbc.function.query.Criteria.*; import java.util.Arrays; import org.junit.Test; - -import org.springframework.data.r2dbc.function.query.Criteria.*; +import org.springframework.data.r2dbc.function.query.Criteria.Combinator; +import org.springframework.data.r2dbc.function.query.Criteria.Comparator; /** * Unit tests for {@link Criteria}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java index 966ff4a9..acef4b8f 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java @@ -20,7 +20,6 @@ import static org.mockito.Mockito.*; 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; @@ -227,7 +226,6 @@ public class QueryMapperUnitTests { assertThat(mapped.getOrderFor("alternative")).isNull(); } - @SuppressWarnings("unchecked") private BoundCondition map(Criteria criteria) { BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1); 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 index 9c239303..83c5e4ff 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java @@ -22,7 +22,6 @@ 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; @@ -89,7 +88,6 @@ public class UpdateMapperUnitTests { verifyZeroInteractions(bindTarget); } - @SuppressWarnings("unchecked") private BoundAssignments map(Update update) { BindMarkersFactory markers = BindMarkersFactory.indexed("$", 1); From 79e32941b51fb74d5356a050a0a66717e3e5da1c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 18 Mar 2019 08:19:05 +0100 Subject: [PATCH 115/665] #107 - Add support for ConnectionFactoryTransactionManager.ConnectionFactoryTransactionManager. We now support R2DBC transaction management through ConnectionFactoryTransactionManager which is a ReactiveTransactionManager implementation to be used with TransactionalOperator and Spring's declarative transaction management. ConnectionFactoryTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory); TransactionalOperator operator = TransactionalOperator.create(tm); DatabaseClient db = DatabaseClient.create(connectionFactory); Mono atomicOperation = db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34) .fetch().rowsUpdated() .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") .bind("id", "joe") .bind("name", "Joe") .fetch().rowsUpdated()) .then() .as(operator::transactional); Original Pull Request: #107 --- src/main/asciidoc/index.adoc | 1 + .../asciidoc/reference/r2dbc-connections.adoc | 71 +++ .../reference/r2dbc-transactions.adoc | 54 +- .../r2dbc/function/DefaultDatabaseClient.java | 8 +- .../DefaultTransactionalDatabaseClient.java | 19 +- .../function/TransactionalDatabaseClient.java | 4 + .../ConnectionFactoryTransactionManager.java | 493 ++++++++++++++++++ .../ConnectionFactoryUtils.java | 327 +++++++++++- .../connectionfactory/ConnectionHandle.java | 45 ++ .../connectionfactory/ConnectionHolder.java | 164 ++++++ .../connectionfactory/ConnectionProxy.java | 9 +- .../DefaultTransactionResources.java | 6 +- .../DelegatingConnectionFactory.java | 87 ++++ .../R2dbcTransactionObjectSupport.java | 67 +++ .../SimpleConnectionHandle.java | 54 ++ .../SingletonConnectionFactory.java | 10 +- ...ransactionAwareConnectionFactoryProxy.java | 175 +++++++ .../TransactionResources.java | 3 + ...bstractDatabaseClientIntegrationTests.java | 3 - ...ctionalDatabaseClientIntegrationTests.java | 38 +- .../MySqlDatabaseClientIntegrationTests.java | 1 + ...ctionalDatabaseClientIntegrationTests.java | 7 + ...ionFactoryTransactionManagerUnitTests.java | 362 +++++++++++++ .../ConnectionFactoryUtilsUnitTests.java | 13 +- .../DelegatingConnectionFactoryUnitTests.java | 60 +++ ...nAwareConnectionFactoryProxyUnitTests.java | 87 ++++ ...stractR2dbcRepositoryIntegrationTests.java | 3 - ...SimpleR2dbcRepositoryIntegrationTests.java | 3 - 28 files changed, 2078 insertions(+), 96 deletions(-) create mode 100644 src/main/asciidoc/reference/r2dbc-connections.adoc create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java create mode 100644 src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java create mode 100644 src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index d2994639..336662c9 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -24,4 +24,5 @@ include::{spring-data-commons-docs}/repositories.adoc[leveloffset=+1] include::reference/introduction.adoc[leveloffset=+1] include::reference/r2dbc.adoc[leveloffset=+1] include::reference/r2dbc-repositories.adoc[leveloffset=+1] +include::reference/r2dbc-connections.adoc[leveloffset=+1] include::reference/mapping.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/reference/r2dbc-connections.adoc b/src/main/asciidoc/reference/r2dbc-connections.adoc new file mode 100644 index 00000000..1e7de9a7 --- /dev/null +++ b/src/main/asciidoc/reference/r2dbc-connections.adoc @@ -0,0 +1,71 @@ +[[r2dbc.connections]] += Controlling Database Connections + +This section covers: + +* <> +* <> +* <> +* <> +* <> + +[[r2dbc.connections.connectionfactory]] +== Using `ConnectionFactory` + +Spring obtains an R2DBC connection to the database through a `ConnectionFactory`. +A `ConnectionFactory` is part of the R2DBC specification and is a generalized connection factory. +It lets a container or a framework hide connection pooling and transaction management issues from the application code. +As a developer, you need not know details about how to connect to the database. +That is the responsibility of the administrator who sets up the `ConnectionFactory`. +You most likely fill both roles as you develop and test code, but you do not necessarily have to know how the production data source is configured. + +When you use Spring's R2DBC layer, you can can configure your own with a connection pool implementation provided by a third party. +A popular implementation is R2DBC Pool. +Implementations in the Spring distribution are meant only for testing purposes and do not provide pooling. + +To configure a ``ConnectionFactory``: + +. Obtain a connection with `ConnectionFactory` as you typically obtain an R2DBC + `ConnectionFactory`. +. Provide an R2DBC URL. (See the documentation for your driver + for the correct value.) + +The following example shows how to configure a `ConnectionFactory` in Java: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + ConnectionFactory factory = ConnectionFactories.get("rdbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE"); +---- + +[[r2dbc.connections.ConnectionFactoryUtils]] +== Using `ConnectionFactoryUtils` + +The `ConnectionFactoryUtils` class is a convenient and powerful helper class that provides `static` methods to obtain connections from `ConnectionFactory` and close connections if necessary. +It supports subscriber ``Context``-bound connections with, for example `ConnectionFactoryTransactionManager`. + +[[r2dbc.connections.SmartConnectionFactory]] +== Implementing `SmartConnectionFactory` + +The `SmartConnectionFactory` interface should be implemented by classes that can provide a connection to a relational database. +It extends the `ConnectionFactory` interface to let classes that use it query whether the connection should be closed after a given operation. +This usage is efficient when you know that you need to reuse a connection. + + +[[r2dbc.connections.TransactionAwareConnectionFactoryProxy]] +== Using `TransactionAwareConnectionFactoryProxy` + +`TransactionAwareConnectionFactoryProxy` is a proxy for a target `ConnectionFactory`. +The proxy wraps that target `ConnectionFactory` to add awareness of Spring-managed transactions. + +[[r2dbc.connections.ConnectionFactoryTransactionManager]] +== Using `ConnectionFactoryTransactionManager` + +The `ConnectionFactoryTransactionManager` class is a `ReactiveTransactionManager` implementation for single R2DBC datasources. +It binds an R2DBC connection from the specified data source to the subscriber `Context`, potentially allowing for one subscriber connection per data source. + +Application code is required to retrieve the R2DBC connection through `ConnectionFactoryUtils.getConnection(ConnectionFactory)` instead of R2DBC's standard `ConnectionFactory.create()`. +All framework classes (such as `DatabaseClient`) use this strategy implicitly. +If not used with this transaction manager, the lookup strategy behaves exactly like the common one. Thus, it can be used in any case. + +The `ConnectionFactoryTransactionManager` class supports custom isolation levels that get applied to the connection. diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc index f85db46f..8917eb89 100644 --- a/src/main/asciidoc/reference/r2dbc-transactions.adoc +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -4,18 +4,19 @@ 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. Using different connections hence results in utilizing different transactions. -Spring Data R2DBC includes a transactional `DatabaseClient` implementation with `TransactionalDatabaseClient` that allows you to group multiple statements within the same transaction. -`TransactionalDatabaseClient` is a extension of `DatabaseClient` that exposes the same functionality as `DatabaseClient` and adds transaction-management methods. - -You can run multiple statements within a transaction using the `inTransaction(Function)` closure: +Spring Data R2DBC includes transaction-awareness in `DatabaseClient` that allows you to group multiple statements within the same transaction using https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction[Spring's Transaction Management]. +Spring Data R2DBC provides a implementation for `ReactiveTransactionManager` with `ConnectionFactoryTransactionManager`. +See <> for further details. +.Programmatic Transaction Management +==== [source,java] ---- -TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); +ConnectionFactoryTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory); +TransactionalOperator operator = TransactionalOperator.create(tm); +DatabaseClient db = DatabaseClient.create(connectionFactory); -Flux completion = databaseClient.inTransaction(db -> { - - return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") +Mono atomicOperation = db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34) @@ -24,6 +25,41 @@ Flux completion = databaseClient.inTransaction(db -> { .bind("id", "joe") .bind("name", "Joe") .fetch().rowsUpdated()) - .then(); + .then() + .as(operator::transactional); }); ---- +==== + +https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[Spring's declarative Transaction Management] is a less invasive, annotation-based approach to transaction demarcation. + +.Declarative Transaction Management +==== +[source,java] +---- +class MyService { + + private final DatabaseClient db; + + MyService(DatabaseClient db) { + this.db = db; + } + + + @Transactional + public Mono insertPerson() { + + return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34) + .fetch().rowsUpdated() + .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .bind("id", "joe") + .bind("name", "Joe") + .fetch().rowsUpdated()) + .then(); + } +} +---- +==== 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 51383c67..b0b77440 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java @@ -25,6 +25,7 @@ import io.r2dbc.spi.Statement; import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; @@ -55,6 +56,7 @@ import org.springframework.data.r2dbc.domain.BindableOperation; import org.springframework.data.r2dbc.domain.OutboundRow; import org.springframework.data.r2dbc.domain.PreparedOperation; import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; import org.springframework.data.r2dbc.function.query.Criteria; @@ -186,7 +188,7 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * @return a {@link Mono} able to emit a {@link Connection}. */ protected Mono getConnection() { - return Mono.from(obtainConnectionFactory().create()); + return ConnectionFactoryUtils.getConnection(obtainConnectionFactory()).map(Tuple2::getT1); } /** @@ -196,7 +198,9 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { * @return a {@link Publisher} that completes successfully when the connection is closed. */ protected Publisher closeConnection(Connection connection) { - return connection.close(); + + return ConnectionFactoryUtils.currentConnectionFactory(obtainConnectionFactory()).then() + .onErrorResume(Exception.class, e -> Mono.from(connection.close())); } /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java index 19170f55..3d78303d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java @@ -25,6 +25,7 @@ import reactor.util.function.Tuple2; import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils; import org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization; import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; @@ -106,24 +107,6 @@ class DefaultTransactionalDatabaseClient extends DefaultDatabaseClient implement return ConnectionFactoryUtils.getConnection(obtainConnectionFactory()).map(Tuple2::getT1); } - /* (non-Javadoc) - * @see org.springframework.data.r2dbc.function.DefaultDatabaseClient#closeConnection(io.r2dbc.spi.Connection) - */ - @Override - protected Publisher closeConnection(Connection connection) { - - return Mono.subscriberContext().flatMap(context -> { - - if (context.hasKey(ReactiveTransactionSynchronization.class)) { - - return ConnectionFactoryUtils.currentConnectionFactory() - .flatMap(it -> ConnectionFactoryUtils.releaseConnection(connection, it)); - } - - return Mono.from(connection.close()); - }); - } - /** * Execute a transactional cleanup. Also, deregister the current {@link TransactionResources synchronization} element. */ diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java index 0b4860e5..f1c6a0bf 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java @@ -23,8 +23,10 @@ import java.util.function.Consumer; import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; +import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; /** @@ -77,7 +79,9 @@ import org.springframework.util.Assert; * @see org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization * @see TransactionResources * @see org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils + * @deprecated Use {@link DatabaseClient} in combination with {@link TransactionalOperator}. */ +@Deprecated public interface TransactionalDatabaseClient extends DatabaseClient { /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java new file mode 100644 index 00000000..a5a0ea14 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java @@ -0,0 +1,493 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Result; +import reactor.core.publisher.Mono; + +import java.time.Duration; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.lang.Nullable; +import org.springframework.transaction.CannotCreateTransactionException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionException; +import org.springframework.transaction.TransactionSystemException; +import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; +import org.springframework.transaction.reactive.GenericReactiveTransaction; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.transaction.ReactiveTransactionManager} implementation for a single R2DBC + * {@link ConnectionFactory}. This class is capable of working in any environment with any R2DBC driver, as long as the + * setup uses a {@link ConnectionFactory} as its {@link Connection} factory mechanism. Binds a R2DBC {@link Connection} + * from the specified {@link ConnectionFactory} to the current subscriber context, potentially allowing for one + * context-bound {@link Connection} per {@link ConnectionFactory}. + *

+ * Note: The {@link ConnectionFactory} that this transaction manager operates on needs to return independent + * {@link Connection}s. The {@link Connection}s may come from a pool (the typical case), but the + * {@link ConnectionFactory} must not return scoped scoped {@link Connection}s or the like. This transaction manager + * will associate {@link Connection} with context-bound transactions itself, according to the specified propagation + * behavior. It assumes that a separate, independent {@link Connection} can be obtained even during an ongoing + * transaction. + *

+ * Application code is required to retrieve the R2DBC Connection via + * {@link ConnectionFactoryUtils#getConnection(ConnectionFactory)} instead of a standard R2DBC-style + * {@link ConnectionFactory#create()} call. Spring classes such as {@link DatabaseClient} use this strategy implicitly. + * If not used in combination with this transaction manager, the {@link ConnectionFactoryUtils} lookup strategy behaves + * exactly like the native {@link ConnectionFactory} lookup; it can thus be used in a portable fashion. + *

+ * Alternatively, you can allow application code to work with the standard R2DBC lookup pattern + * {@link ConnectionFactory#create()}, for example for code that is not aware of Spring at all. In that case, define a + * {@link TransactionAwareConnectionFactoryProxy} for your target {@link ConnectionFactory}, and pass that proxy + * {@link ConnectionFactory} to your DAOs, which will automatically participate in Spring-managed transactions when + * accessing it. + *

+ * This transaction manager triggers flush callbacks on registered transaction synchronizations (if synchronization is + * generally active), assuming resources operating on the underlying R2DBC {@link Connection}. + * + * @author Mark Paluch + * @see ConnectionFactoryUtils#getConnection(ConnectionFactory) + * @see ConnectionFactoryUtils#releaseConnection + * @see TransactionAwareConnectionFactoryProxy + * @see DatabaseClient + */ +public class ConnectionFactoryTransactionManager extends AbstractReactiveTransactionManager + implements InitializingBean { + + private ConnectionFactory connectionFactory; + + private boolean enforceReadOnly = false; + + /** + * Create a new @link ConnectionFactoryTransactionManager} instance. A ConnectionFactory has to be set to be able to + * use it. + * + * @see #setConnectionFactory + */ + public ConnectionFactoryTransactionManager() {} + + /** + * Create a new {@link ConnectionFactoryTransactionManager} instance. + * + * @param connectionFactory the R2DBC ConnectionFactory to manage transactions for + */ + public ConnectionFactoryTransactionManager(ConnectionFactory connectionFactory) { + this(); + setConnectionFactory(connectionFactory); + afterPropertiesSet(); + } + + /** + * Set the R2DBC {@link ConnectionFactory} that this instance should manage transactions for. + *

+ * This will typically be a locally defined {@link ConnectionFactory}, for example an connection pool. + *

+ * The {@link ConnectionFactory} specified here should be the target {@link ConnectionFactory} to manage transactions + * for, not a TransactionAwareConnectionFactoryProxy. Only data access code may work with + * TransactionAwareConnectionFactoryProxy, while the transaction manager needs to work on the underlying target + * {@link ConnectionFactory}. If there's nevertheless a TransactionAwareConnectionFactoryProxy passed in, it will be + * unwrapped to extract its target {@link ConnectionFactory}. + *

+ * The {@link ConnectionFactory} passed in here needs to return independent {@link Connection}s. The + * {@link Connection}s may come from a pool (the typical case), but the {@link ConnectionFactory} must not return + * scoped {@link Connection} or the like. + * + * @see TransactionAwareConnectionFactoryProxy + */ + public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * Return the R2DBC {@link ConnectionFactory} that this instance manages transactions for. + */ + @Nullable + public ConnectionFactory getConnectionFactory() { + return this.connectionFactory; + } + + /** + * Obtain the {@link ConnectionFactory} for actual use. + * + * @return the {@link ConnectionFactory} (never {@code null}) + * @throws IllegalStateException in case of no ConnectionFactory set + */ + protected ConnectionFactory obtainConnectionFactory() { + ConnectionFactory connectionFactory = getConnectionFactory(); + Assert.state(connectionFactory != null, "No ConnectionFactory set"); + return connectionFactory; + } + + /** + * Specify whether to enforce the read-only nature of a transaction (as indicated by + * {@link TransactionDefinition#isReadOnly()} through an explicit statement on the transactional connection: "SET + * TRANSACTION READ ONLY" as understood by Oracle, MySQL and Postgres. + *

+ * The exact treatment, including any SQL statement executed on the connection, can be customized through through + * {@link #prepareTransactionalConnection}. + * + * @see #prepareTransactionalConnection + */ + public void setEnforceReadOnly(boolean enforceReadOnly) { + this.enforceReadOnly = enforceReadOnly; + } + + /** + * Return whether to enforce the read-only nature of a transaction through an explicit statement on the transactional + * connection. + * + * @see #setEnforceReadOnly + */ + public boolean isEnforceReadOnly() { + return this.enforceReadOnly; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() + */ + @Override + public void afterPropertiesSet() { + if (getConnectionFactory() == null) { + throw new IllegalArgumentException("Property 'connectionFactory' is required"); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doGetTransaction(org.springframework.transaction.reactive.TransactionSynchronizationManager) + */ + @Override + protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager) + throws TransactionException { + + ConnectionFactoryTransactionObject txObject = new ConnectionFactoryTransactionObject(); + ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(obtainConnectionFactory()); + txObject.setConnectionHolder(conHolder, false); + return txObject; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#isExistingTransaction(java.lang.Object) + */ + @Override + protected boolean isExistingTransaction(Object transaction) { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; + return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doBegin(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, org.springframework.transaction.TransactionDefinition) + */ + @Override + protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction, + TransactionDefinition definition) throws TransactionException { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; + + return Mono.defer(() -> { + + Mono connection = null; + + if (!txObject.hasConnectionHolder() || txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + Mono newCon = Mono.from(obtainConnectionFactory().create()); + + connection = newCon.doOnNext(it -> { + + if (this.logger.isDebugEnabled()) { + this.logger.debug("Acquired Connection [" + newCon + "] for R2DBC transaction"); + } + txObject.setConnectionHolder(new ConnectionHolder(it), true); + }); + } else { + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + connection = Mono.just(txObject.getConnectionHolder().getConnection()); + } + + return connection.flatMap(con -> { + + return prepareTransactionalConnection(con, definition).then(doBegin(con, definition)).then().doOnSuccess(v -> { + txObject.getConnectionHolder().setTransactionActive(true); + + Duration timeout = determineTimeout(definition); + if (!timeout.isNegative() && !timeout.isZero()) { + txObject.getConnectionHolder().setTimeoutInMillis(timeout.toMillis()); + } + + // Bind the connection holder to the thread. + if (txObject.isNewConnectionHolder()) { + synchronizationManager.bindResource(obtainConnectionFactory(), txObject.getConnectionHolder()); + } + }).thenReturn(con).onErrorResume(e -> { + + CannotCreateTransactionException ex = new CannotCreateTransactionException( + "Could not open R2DBC Connection for transaction", e); + + if (txObject.isNewConnectionHolder()) { + return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()).doOnTerminate(() -> { + + txObject.setConnectionHolder(null, false); + }).then(Mono.error(ex)); + } + return Mono.error(ex); + }); + }); + }).then(); + } + + private Mono doBegin(Connection con, TransactionDefinition definition) { + + Mono doBegin = Mono.from(con.beginTransaction()); + + if (definition != null && definition.getIsolationLevel() != -1) { + + IsolationLevel isolationLevel = resolveIsolationLevel(definition.getIsolationLevel()); + + if (isolationLevel != null) { + if (this.logger.isDebugEnabled()) { + this.logger + .debug("Changing isolation level of R2DBC Connection [" + con + "] to " + definition.getIsolationLevel()); + } + doBegin = doBegin.then(Mono.from(con.setTransactionIsolationLevel(isolationLevel))); + } + } + + return doBegin; + } + + /** + * Determine the actual timeout to use for the given definition. Will fall back to this manager's default timeout if + * the transaction definition doesn't specify a non-default value. + * + * @param definition the transaction definition + * @return the actual timeout to use + * @see org.springframework.transaction.TransactionDefinition#getTimeout() + */ + protected Duration determineTimeout(TransactionDefinition definition) { + if (definition.getTimeout() != TransactionDefinition.TIMEOUT_DEFAULT) { + return Duration.ofSeconds(definition.getTimeout()); + } + return Duration.ZERO; + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSuspend(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction) + throws TransactionException { + + return Mono.defer(() -> { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; + txObject.setConnectionHolder(null); + return Mono.justOrEmpty(synchronizationManager.unbindResource(obtainConnectionFactory())); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doResume(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object, java.lang.Object) + */ + @Override + protected Mono doResume(TransactionSynchronizationManager synchronizationManager, Object transaction, + Object suspendedResources) throws TransactionException { + + return Mono.defer(() -> { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; + txObject.setConnectionHolder(null); + synchronizationManager.bindResource(obtainConnectionFactory(), suspendedResources); + + return Mono.empty(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCommit(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); + Connection connection = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + this.logger.debug("Committing R2DBC transaction on Connection [" + connection + "]"); + } + + return Mono.from(connection.commitTransaction()) + .onErrorMap(ex -> new TransactionSystemException("Could not commit R2DBC transaction", ex)); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doRollback(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doRollback(TransactionSynchronizationManager TransactionSynchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); + Connection connection = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + this.logger.debug("Rolling back R2DBC transaction on Connection [" + connection + "]"); + } + + return Mono.from(connection.rollbackTransaction()) + .onErrorMap(ex -> new TransactionSystemException("Could not roll back R2DBC transaction", ex)); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doSetRollbackOnly(org.springframework.transaction.reactive.TransactionSynchronizationManager, org.springframework.transaction.reactive.GenericReactiveTransaction) + */ + @Override + protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager, + GenericReactiveTransaction status) throws TransactionException { + + return Mono.fromRunnable(() -> { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); + + if (status.isDebug()) { + this.logger + .debug("Setting R2DBC transaction [" + txObject.getConnectionHolder().getConnection() + "] rollback-only"); + } + txObject.setRollbackOnly(); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.reactive.AbstractReactiveTransactionManager#doCleanupAfterCompletion(org.springframework.transaction.reactive.TransactionSynchronizationManager, java.lang.Object) + */ + @Override + protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager, + Object transaction) { + + return Mono.defer(() -> { + + ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) transaction; + + // Remove the connection holder from the context, if exposed. + if (txObject.isNewConnectionHolder()) { + synchronizationManager.unbindResource(obtainConnectionFactory()); + } + + // Reset connection. + Connection con = txObject.getConnectionHolder().getConnection(); + + try { + if (txObject.isNewConnectionHolder()) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Releasing R2DBC Connection [" + con + "] after transaction"); + } + return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()); + } + } finally { + txObject.getConnectionHolder().clear(); + } + + return Mono.empty(); + }); + } + + /** + * Prepare the transactional {@link Connection} right after transaction begin. + *

+ * The default implementation executes a "SET TRANSACTION READ ONLY" statement if the {@link #setEnforceReadOnly + * "enforceReadOnly"} flag is set to {@code true} and the transaction definition indicates a read-only transaction. + *

+ * The "SET TRANSACTION READ ONLY" is understood by Oracle, MySQL and Postgres and may work with other databases as + * well. If you'd like to adapt this treatment, override this method accordingly. + * + * @param con the transactional R2DBC Connection + * @param definition the current transaction definition + * @see #setEnforceReadOnly + */ + protected Mono prepareTransactionalConnection(Connection con, TransactionDefinition definition) { + + if (isEnforceReadOnly() && definition.isReadOnly()) { + + return Mono.from(con.createStatement("SET TRANSACTION READ ONLY").execute()) // + .flatMapMany(Result::getRowsUpdated) // + .then(); + } + + return Mono.empty(); + } + + /** + * Resolve the {@link TransactionDefinition#getIsolationLevel() isolation level constant} to a R2DBC + * {@link IsolationLevel}. If you'd like to extend isolation level translation for vendor-specific + * {@link IsolationLevel}s, override this method accordingly. + * + * @param isolationLevel the isolation level to translate. + * @return the resolved isolation level. Can be {@literal null} if not resolvable or the isolation level should remain + * {@link TransactionDefinition#ISOLATION_DEFAULT default}. + * @see TransactionDefinition#getIsolationLevel() + */ + @Nullable + protected IsolationLevel resolveIsolationLevel(int isolationLevel) { + + switch (isolationLevel) { + case TransactionDefinition.ISOLATION_READ_COMMITTED: + return IsolationLevel.READ_COMMITTED; + case TransactionDefinition.ISOLATION_READ_UNCOMMITTED: + return IsolationLevel.READ_UNCOMMITTED; + case TransactionDefinition.ISOLATION_REPEATABLE_READ: + return IsolationLevel.REPEATABLE_READ; + case TransactionDefinition.ISOLATION_SERIALIZABLE: + return IsolationLevel.SERIALIZABLE; + } + + return null; + } + + /** + * ConnectionFactory transaction object, representing a ConnectionHolder. Used as transaction object by + * ConnectionFactoryTransactionManager. + */ + private static class ConnectionFactoryTransactionObject extends R2dbcTransactionObjectSupport { + + private boolean newConnectionHolder; + + void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) { + super.setConnectionHolder(connectionHolder); + this.newConnectionHolder = newConnectionHolder; + } + + boolean isNewConnectionHolder() { + return this.newConnectionHolder; + } + + void setRollbackOnly() { + getConnectionHolder().setRollbackOnly(); + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java index be9b9691..c0172221 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java @@ -23,9 +23,14 @@ import reactor.util.function.Tuples; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; + +import org.springframework.core.Ordered; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.lang.Nullable; import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.util.Assert; /** @@ -37,10 +42,19 @@ import org.springframework.util.Assert; * * @author Mark Paluch */ -public class ConnectionFactoryUtils { +public abstract class ConnectionFactoryUtils { + + /** + * Order value for ReactiveTransactionSynchronization objects that clean up R2DBC Connections. + */ + public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000; private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class); + private ConnectionFactoryUtils() { + + } + /** * Obtain a {@link io.r2dbc.spi.Connection} from the given {@link io.r2dbc.spi.ConnectionFactory}. Translates * exceptions into the Spring hierarchy of unchecked generic data access exceptions, simplifying calling code and @@ -73,18 +87,70 @@ public class ConnectionFactoryUtils { Assert.notNull(connectionFactory, "ConnectionFactory must not be null!"); - return Mono.subscriberContext().flatMap(it -> { + return TransactionSynchronizationManager.currentTransaction().flatMap(synchronizationManager -> { - if (it.hasKey(ReactiveTransactionSynchronization.class)) { - - ReactiveTransactionSynchronization synchronization = it.get(ReactiveTransactionSynchronization.class); - - return obtainConnection(synchronization, connectionFactory); + ConnectionHolder conHolder = (ConnectionHolder) synchronizationManager.getResource(connectionFactory); + if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { + conHolder.requested(); + if (!conHolder.hasConnection()) { + logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); + return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection); + } + return Mono.just(conHolder.getConnection()); } - return Mono.empty(); - }).switchIfEmpty(Mono.defer(() -> { - return Mono.from(connectionFactory.create()).map(it -> Tuples.of(it, connectionFactory)); - })); + // Else we either got no holder or an empty thread-bound holder here. + + logger.debug("Fetching R2DBC Connection from ConnectionFactory"); + Mono con = fetchConnection(connectionFactory); + + if (synchronizationManager.isSynchronizationActive()) { + + return con.flatMap(it -> { + + return Mono.just(it).doOnNext(conn -> { + + // Use same Connection for further R2DBC actions within the transaction. + // Thread-bound object will get removed by synchronization at transaction completion. + ConnectionHolder holderToUse = conHolder; + if (holderToUse == null) { + holderToUse = new ConnectionHolder(conn); + } else { + holderToUse.setConnection(conn); + } + holderToUse.requested(); + synchronizationManager + .registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory)); + holderToUse.setSynchronizedWithTransaction(true); + if (holderToUse != conHolder) { + synchronizationManager.bindResource(connectionFactory, holderToUse); + } + + }).onErrorResume(e -> { + // Unexpected exception from external delegation call -> close Connection and rethrow. + return releaseConnection(it, connectionFactory).then(Mono.error(e)); + + }); + }); + } + + return con; + }) // + .map(conn -> Tuples.of(conn, connectionFactory)) // + .onErrorResume(NoTransactionException.class, e -> { + + return Mono.subscriberContext().flatMap(it -> { + + if (it.hasKey(ReactiveTransactionSynchronization.class)) { + + ReactiveTransactionSynchronization synchronization = it.get(ReactiveTransactionSynchronization.class); + + return obtainConnection(synchronization, connectionFactory); + } + return Mono.empty(); + }).switchIfEmpty(Mono.defer(() -> { + return Mono.from(connectionFactory.create()).map(it -> Tuples.of(it, connectionFactory)); + })); + }); } private static Mono> obtainConnection( @@ -121,6 +187,24 @@ public class ConnectionFactoryUtils { return Mono.empty(); } + /** + * Actually fetch a {@link Connection} from the given {@link ConnectionFactory}, defensively turning an unexpected + * {@code null} return value from {@link ConnectionFactory#create()} into an {@link IllegalStateException}. + * + * @param connectionFactory the {@link ConnectionFactory} to obtain {@link Connection}s from + * @return a R2DBC {@link Connection} from the given {@link ConnectionFactory} (never {@code null}) + * @throws IllegalStateException if the {@link ConnectionFactory} returned a {@literal null} value. + * @see ConnectionFactory#create() + */ + private static Mono fetchConnection(ConnectionFactory connectionFactory) { + + Publisher con = connectionFactory.create(); + if (con == null) { + throw new IllegalStateException("ConnectionFactory returned null from getConnection(): " + connectionFactory); + } + return Mono.from(con); + } + /** * Close the given {@link io.r2dbc.spi.Connection}, obtained from the given {@link ConnectionFactory}, if it is not * managed externally (that is, not bound to the thread). @@ -149,18 +233,29 @@ public class ConnectionFactoryUtils { public static Mono doReleaseConnection(@Nullable io.r2dbc.spi.Connection con, @Nullable ConnectionFactory connectionFactory) { - if (connectionFactory instanceof SingletonConnectionFactory) { + return TransactionSynchronizationManager.currentTransaction().flatMap(it -> { - SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory; + ConnectionHolder conHolder = (ConnectionHolder) it.getResource(connectionFactory); + if (conHolder != null && connectionEquals(conHolder, con)) { + // It's the transactional Connection: Don't close it. + conHolder.released(); + } + return Mono.from(con.close()); + }).onErrorResume(NoTransactionException.class, e -> { - logger.debug("Releasing R2DBC Connection"); + if (connectionFactory instanceof SingletonConnectionFactory) { - return factory.close(con); - } + SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory; - logger.debug("Closing R2DBC Connection"); + logger.debug("Releasing R2DBC Connection"); - return Mono.from(con.close()); + return factory.close(con); + } + + logger.debug("Closing R2DBC Connection"); + + return Mono.from(con.close()); + }); } /** @@ -236,14 +331,196 @@ public class ConnectionFactoryUtils { * @see ReactiveTransactionSynchronization * @see TransactionResources */ - public static Mono currentConnectionFactory() { + public static Mono currentConnectionFactory(ConnectionFactory connectionFactory) { - return currentActiveReactiveTransactionSynchronization() // - .map(synchronization -> { + return TransactionSynchronizationManager.currentTransaction() + .filter(TransactionSynchronizationManager::isSynchronizationActive).filter(it -> { - TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); - return currentSynchronization.getResource(ConnectionFactory.class); - }).switchIfEmpty(Mono.error(new DataAccessResourceFailureException( - "Cannot extract ConnectionFactory from current TransactionContext!"))); + ConnectionHolder conHolder = (ConnectionHolder) it.getResource(connectionFactory); + if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { + return true; + } + return false; + }).map(it -> connectionFactory).onErrorResume(NoTransactionException.class, e -> { + + return currentActiveReactiveTransactionSynchronization().map(synchronization -> { + + TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); + return currentSynchronization.getResource(ConnectionFactory.class); + }).switchIfEmpty(Mono.error(new DataAccessResourceFailureException( + "Cannot extract ConnectionFactory from current TransactionContext!"))); + + }); } + + /** + * Determine whether the given two {@link Connection}s are equal, asking the target {@link Connection} in case of a + * proxy. Used to detect equality even if the user passed in a raw target Connection while the held one is a proxy. + * + * @param conHolder the {@link ConnectionHolder} for the held Connection (potentially a proxy) + * @param passedInCon the {@link Connection} passed-in by the user (potentially a target {@link Connection} without + * proxy) + * @return whether the given Connections are equal + * @see #getTargetConnection + */ + private static boolean connectionEquals(ConnectionHolder conHolder, Connection passedInCon) { + + if (!conHolder.hasConnection()) { + return false; + } + Connection heldCon = conHolder.getConnection(); + // Explicitly check for identity too: for Connection handles that do not implement + // "equals" properly). + return (heldCon == passedInCon || heldCon.equals(passedInCon) || getTargetConnection(heldCon).equals(passedInCon)); + } + + /** + * Return the innermost target {@link Connection} of the given {@link Connection}. If the given {@link Connection} is + * a proxy, it will be unwrapped until a non-proxy {@link Connection} is found. Otherwise, the passed-in Connection + * will be returned as-is. + * + * @param con the {@link Connection} proxy to unwrap + * @return the innermost target Connection, or the passed-in one if no proxy + * @see ConnectionProxy#getTargetConnection() + */ + public static Connection getTargetConnection(Connection con) { + + Connection conToUse = con; + while (conToUse instanceof ConnectionProxy) { + conToUse = ((ConnectionProxy) conToUse).getTargetConnection(); + } + return conToUse; + } + + /** + * Determine the connection synchronization order to use for the given {@link ConnectionFactory}. Decreased for every + * level of nesting that a {@link ConnectionFactory} has, checked through the level of + * {@link DelegatingConnectionFactory} nesting. + * + * @param connectionFactory the {@link ConnectionFactory} to check. + * @return the connection synchronization order to use. + * @see #CONNECTION_SYNCHRONIZATION_ORDER + */ + private static int getConnectionSynchronizationOrder(ConnectionFactory connectionFactory) { + int order = CONNECTION_SYNCHRONIZATION_ORDER; + ConnectionFactory current = connectionFactory; + while (current instanceof DelegatingConnectionFactory) { + order--; + current = ((DelegatingConnectionFactory) current).getTargetConnectionFactory(); + } + return order; + } + + /** + * Callback for resource cleanup at the end of a non-native R2DBC transaction. + */ + private static class ConnectionSynchronization implements TransactionSynchronization, Ordered { + + private final ConnectionHolder connectionHolder; + + private final ConnectionFactory connectionFactory; + + private int order; + + private boolean holderActive = true; + + ConnectionSynchronization(ConnectionHolder connectionHolder, ConnectionFactory connectionFactory) { + this.connectionHolder = connectionHolder; + this.connectionFactory = connectionFactory; + this.order = getConnectionSynchronizationOrder(connectionFactory); + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public Mono suspend() { + if (this.holderActive) { + + return TransactionSynchronizationManager.currentTransaction().flatMap(it -> { + + it.unbindResource(this.connectionFactory); + if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) { + // Release Connection on suspend if the application doesn't keep + // a handle to it anymore. We will fetch a fresh Connection if the + // application accesses the ConnectionHolder again after resume, + // assuming that it will participate in the same transaction. + return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory) + .doOnTerminate(() -> this.connectionHolder.setConnection(null)); + } + return Mono.empty(); + }); + } + + return Mono.empty(); + } + + @Override + public Mono resume() { + if (this.holderActive) { + return TransactionSynchronizationManager.currentTransaction().doOnNext(it -> { + it.bindResource(this.connectionFactory, this.connectionHolder); + }).then(); + } + return Mono.empty(); + + } + + @Override + public Mono beforeCompletion() { + + // Release Connection early if the holder is not open anymore + // (that is, not used by another resource + // that has its own cleanup via transaction synchronization), + // to avoid issues with strict transaction implementations that expect + // the close call before transaction completion. + if (!this.connectionHolder.isOpen()) { + return TransactionSynchronizationManager.currentTransaction().flatMap(it -> { + + it.unbindResource(this.connectionFactory); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory); + } + return Mono.empty(); + }); + } + + return Mono.empty(); + } + + @Override + public Mono afterCompletion(int status) { + + // If we haven't closed the Connection in beforeCompletion, + // close it now. The holder might have been used for other + // cleanup in the meantime, for example by a Hibernate Session. + if (this.holderActive) { + // The thread-bound ConnectionHolder might not be available anymore, + // since afterCompletion might get called from a different thread. + return TransactionSynchronizationManager.currentTransaction().flatMap(it -> { + + it.unbindResourceIfPossible(this.connectionFactory); + this.holderActive = false; + if (this.connectionHolder.hasConnection()) { + return releaseConnection(this.connectionHolder.getConnection(), this.connectionFactory) + .doOnTerminate(() -> { + // Reset the ConnectionHolder: It might remain bound to the context. + this.connectionHolder.setConnection(null); + }); + + } + + return Mono.empty(); + }); + + } + + this.connectionHolder.reset(); + return Mono.empty(); + } + } + } diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java new file mode 100644 index 00000000..155a79cb --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java @@ -0,0 +1,45 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; + +/** + * Simple interface to be implemented by handles for a R2DBC Connection. + * + * @author Mark Paluch + * @see SimpleConnectionHandle + * @see ConnectionHolder + */ +@FunctionalInterface +public interface ConnectionHandle { + + /** + * Fetch the R2DBC Connection that this handle refers to. + */ + Connection getConnection(); + + /** + * Release the R2DBC Connection that this handle refers to. Assumes a non-blocking implementation without + * synchronization. + *

+ * The default implementation is empty, assuming that the lifecycle of the connection is managed externally. + * + * @param con the R2DBC Connection to release + */ + default void releaseConnection(Connection con) {} + +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java new file mode 100644 index 00000000..fcc3f3b7 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java @@ -0,0 +1,164 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; + +import org.springframework.lang.Nullable; +import org.springframework.transaction.support.ResourceHolderSupport; +import org.springframework.util.Assert; + +/** + * Resource holder wrapping a R2DBC {@link Connection}. {@link ConnectionFactoryTransactionManager} binds instances of + * this class to the thread, for a specific {@link ConnectionFactory}. + *

+ * Inherits rollback-only support for nested R2DBC transactions and reference count functionality from the base class. + *

+ * Note: This is an SPI class, not intended to be used by applications. + * + * @see ConnectionFactoryTransactionManager + * @see ConnectionFactoryUtils + */ +public class ConnectionHolder extends ResourceHolderSupport { + + @Nullable private ConnectionHandle connectionHandle; + + @Nullable private Connection currentConnection; + + private boolean transactionActive = false; + + /** + * Create a new ConnectionHolder for the given R2DBC {@link Connection}, wrapping it with a + * {@link SimpleConnectionHandle}, assuming that there is no ongoing transaction. + * + * @param connection the R2DBC {@link Connection} to hold + * @see SimpleConnectionHandle + * @see #ConnectionHolder(Connection, boolean) + */ + public ConnectionHolder(Connection connection) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } + + /** + * Create a new ConnectionHolder for the given R2DBC {@link Connection}, wrapping it with a + * {@link SimpleConnectionHandle}. + * + * @param connection the R2DBC {@link Connection} to hold + * @param transactionActive whether the given {@link Connection} is involved in an ongoing transaction + * @see SimpleConnectionHandle + */ + public ConnectionHolder(Connection connection, boolean transactionActive) { + this(connection); + this.transactionActive = transactionActive; + } + + /** + * Return the ConnectionHandle held by this ConnectionHolder. + */ + @Nullable + public ConnectionHandle getConnectionHandle() { + return this.connectionHandle; + } + + /** + * Return whether this holder currently has a {@link Connection}. + */ + protected boolean hasConnection() { + return (this.connectionHandle != null); + } + + /** + * Set whether this holder represents an active, R2DBC-managed transaction. + * + * @see ConnectionFactoryTransactionManager + */ + protected void setTransactionActive(boolean transactionActive) { + this.transactionActive = transactionActive; + } + + /** + * Return whether this holder represents an active, R2DBC-managed transaction. + */ + protected boolean isTransactionActive() { + return this.transactionActive; + } + + /** + * Override the existing Connection handle with the given {@link Connection}. Reset the handle if given {@code null}. + *

+ * Used for releasing the {@link Connection} on suspend (with a {@code null} argument) and setting a fresh + * {@link Connection} on resume. + */ + protected void setConnection(@Nullable Connection connection) { + if (this.currentConnection != null) { + if (this.connectionHandle != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + } + this.currentConnection = null; + } + if (connection != null) { + this.connectionHandle = new SimpleConnectionHandle(connection); + } else { + this.connectionHandle = null; + } + } + + /** + * Return the current {@link Connection} held by this {@link ConnectionHolder}. + *

+ * This will be the same {@link Connection} until {@code released} gets called on the {@link ConnectionHolder}, which + * will reset the held {@link Connection}, fetching a new {@link Connection} on demand. + * + * @see ConnectionHandle#getConnection() + * @see #released() + */ + public Connection getConnection() { + Assert.notNull(this.connectionHandle, "Active Connection is required"); + if (this.currentConnection == null) { + this.currentConnection = this.connectionHandle.getConnection(); + } + return this.currentConnection; + } + + /** + * Releases the current {@link Connection} held by this {@link ConnectionHolder}. + *

+ * This is necessary for {@link ConnectionHandle}s that expect "Connection borrowing", where each returned + * {@link Connection} is only temporarily leased and needs to be returned once the data operation is done, to make the + * Connection available for other operations within the same transaction. + */ + @Override + public void released() { + super.released(); + if (!isOpen() && this.currentConnection != null) { + if (this.connectionHandle != null) { + this.connectionHandle.releaseConnection(this.currentConnection); + } + this.currentConnection = null; + } + } + + /* + * (non-Javadoc) + * @see org.springframework.transaction.support.ResourceHolderSupport#clear() + */ + @Override + public void clear() { + super.clear(); + this.transactionActive = false; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index eda5f424..37f600cc 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -16,8 +16,7 @@ package org.springframework.data.r2dbc.function.connectionfactory; import io.r2dbc.spi.Connection; - -import java.sql.Wrapper; +import io.r2dbc.spi.Wrapped; /** * Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target @@ -27,12 +26,12 @@ import java.sql.Wrapper; * * @author Mark Paluch */ -public interface ConnectionProxy extends Connection, Wrapper { +public interface ConnectionProxy extends Connection, Wrapped { /** - * Return the target Connection of this proxy. + * Return the target {@link Connection} of this proxy. *

- * This will typically be the native driver Connection or a wrapper from a connection pool. + * This will typically be the native driver {@link Connection} or a wrapper from a connection pool. * * @return the underlying Connection (never {@literal null}) */ diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java index eb4fac0a..74c02193 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java @@ -29,7 +29,8 @@ class DefaultTransactionResources implements TransactionResources { private Map, Object> items = new ConcurrentHashMap<>(); - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#registerResource(java.lang.Class, java.lang.Object) */ @Override @@ -40,7 +41,8 @@ class DefaultTransactionResources implements TransactionResources { items.put(key, value); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see org.springframework.data.r2dbc.function.connectionfactory.TransactionResources#getResource(java.lang.Class) */ @SuppressWarnings("unchecked") diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java new file mode 100644 index 00000000..f28b2c89 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.ConnectionFactoryMetadata; +import io.r2dbc.spi.Wrapped; + +import org.reactivestreams.Publisher; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * R2DBC {@link ConnectionFactory} implementation that delegates all calls to a given target {@link ConnectionFactory}. + *

+ * This class is meant to be subclassed, with subclasses overriding only those methods (such as {@link #create()}) that + * should not simply delegate to the target {@link ConnectionFactory}. + * + * @author Mark Paluch + * @see #create + */ +public class DelegatingConnectionFactory implements ConnectionFactory, Wrapped { + + private final ConnectionFactory targetConnectionFactory; + + public DelegatingConnectionFactory(ConnectionFactory targetConnectionFactory) { + + Assert.notNull(targetConnectionFactory, "ConnectionFactory must not be null"); + this.targetConnectionFactory = targetConnectionFactory; + } + + /* + * (non-Javadoc) + * @see io.r2dbc.spi.ConnectionFactory#create() + */ + @Override + public Publisher create() { + return targetConnectionFactory.create(); + } + + /** + * Obtain the target {@link ConnectionFactory} for actual use (never {@code null}). + */ + protected ConnectionFactory obtainTargetConnectionFactory() { + return getTargetConnectionFactory(); + } + + /** + * Return the target {@link ConnectionFactory} that this {@link ConnectionFactory} should delegate to. + */ + @Nullable + public ConnectionFactory getTargetConnectionFactory() { + return this.targetConnectionFactory; + } + + /* + * (non-Javadoc) + * @see io.r2dbc.spi.ConnectionFactory#getMetadata() + */ + @Override + public ConnectionFactoryMetadata getMetadata() { + return obtainTargetConnectionFactory().getMetadata(); + } + + /* + * (non-Javadoc) + * @see io.r2dbc.spi.Wrapped#unwrap() + */ + @Override + public ConnectionFactory unwrap() { + return obtainTargetConnectionFactory(); + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java new file mode 100644 index 00000000..130dc495 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java @@ -0,0 +1,67 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.IsolationLevel; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Convenient base class for R2DBC-aware transaction objects. Can contain a {@link ConnectionHolder} with a R2DBC + * {@link Connection}. + * + * @author Mark Paluch + * @see ConnectionFactoryTransactionManager + */ +public abstract class R2dbcTransactionObjectSupport { + + @Nullable private ConnectionHolder connectionHolder; + + @Nullable private IsolationLevel previousIsolationLevel; + + private boolean savepointAllowed = false; + + public void setConnectionHolder(@Nullable ConnectionHolder connectionHolder) { + this.connectionHolder = connectionHolder; + } + + public ConnectionHolder getConnectionHolder() { + Assert.state(this.connectionHolder != null, "No ConnectionHolder available"); + return this.connectionHolder; + } + + public boolean hasConnectionHolder() { + return (this.connectionHolder != null); + } + + public void setPreviousIsolationLevel(@Nullable IsolationLevel previousIsolationLevel) { + this.previousIsolationLevel = previousIsolationLevel; + } + + @Nullable + public IsolationLevel getPreviousIsolationLevel() { + return this.previousIsolationLevel; + } + + public void setSavepointAllowed(boolean savepointAllowed) { + this.savepointAllowed = savepointAllowed; + } + + public boolean isSavepointAllowed() { + return this.savepointAllowed; + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java new file mode 100644 index 00000000..e396ae53 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java @@ -0,0 +1,54 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; + +import org.springframework.util.Assert; + +/** + * Simple implementation of the {@link ConnectionHandle} interface, containing a given R2DBC Connection. + * + * @author Mark Paluch + */ +public class SimpleConnectionHandle implements ConnectionHandle { + + private final Connection connection; + + /** + * Create a new SimpleConnectionHandle for the given Connection. + * + * @param connection the R2DBC Connection + */ + public SimpleConnectionHandle(Connection connection) { + Assert.notNull(connection, "Connection must not be null"); + this.connection = connection; + } + + /** + * Return the specified Connection as-is. + */ + @Override + public Connection getConnection() { + return this.connection; + } + + @Override + public String toString() { + return "SimpleConnectionHandle: " + this.connection; + } + +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java index 6beb79d4..355688c6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java @@ -44,7 +44,8 @@ class SingletonConnectionFactory implements SmartConnectionFactory { this.connectionMono = Mono.just(connection); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see io.r2dbc.spi.ConnectionFactory#create() */ @Override @@ -57,7 +58,8 @@ class SingletonConnectionFactory implements SmartConnectionFactory { return connectionMono.doOnSubscribe(s -> refCount.incrementAndGet()); } - /* (non-Javadoc) + /* + * (non-Javadoc) * @see io.r2dbc.spi.ConnectionFactory#getMetadata() */ @Override @@ -69,6 +71,10 @@ class SingletonConnectionFactory implements SmartConnectionFactory { return this.connection == connection; } + /* + * (non-Javadoc) + * @see org.springframework.data.r2dbc.function.connectionfactory.SmartConnectionFactory#shouldClose(io.r2dbc.spi.Connection) + */ @Override public boolean shouldClose(Connection connection) { return refCount.get() == 1; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java new file mode 100644 index 00000000..a87cbfab --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java @@ -0,0 +1,175 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Wrapped; +import reactor.core.publisher.Mono; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.lang.Nullable; + +/** + * Proxy for a target R2DBC {@link ConnectionFactory}, adding awareness of Spring-managed transactions. + *

+ * Data access code that should remain unaware of Spring's data access support can work with this proxy to seamlessly + * participate in Spring-managed transactions. Note that the transaction manager, for example + * {@link ConnectionFactoryTransactionManager}, still needs to work with the underlying {@link ConnectionFactory}, + * not with this proxy. + *

+ * Make sure that {@link TransactionAwareConnectionFactoryProxy} is the outermost {@link ConnectionFactory} of a + * chain of {@link ConnectionFactory} proxies/adapters. {@link TransactionAwareConnectionFactoryProxy} can delegate + * either directly to the target connection pool or to some intermediary proxy/adapter. + *

+ * Delegates to {@link ConnectionFactoryUtils} for automatically participating in thread-bound transactions, for example + * managed by {@link ConnectionFactoryTransactionManager}. {@link #create()} calls and {@code close} calls on returned + * {@link Connection} will behave properly within a transaction, i.e. always operate on the transactional Connection. If + * not within a transaction, normal {@link ConnectionFactory} behavior applies. + *

+ * This proxy allows data access code to work with the plain R2DBC API. However, if possible, use Spring's + * {@link ConnectionFactoryUtils} or {@link DatabaseClient} to get transaction participation even without a proxy for + * the target {@link ConnectionFactory}, avoiding the need to define such a proxy in the first place. + *

+ * NOTE: This {@link ConnectionFactory} proxy needs to return wrapped {@link Connection}s (which implement the + * {@link ConnectionProxy} interface) in order to handle close calls properly. Use {@link Wrapped#unwrap()} to retrieve + * the native R2DBC Connection. + * + * @author Mark Paluch + * @see ConnectionFactory#create + * @see Connection#close + * @see ConnectionFactoryUtils#doGetConnection + * @see ConnectionFactoryUtils#doReleaseConnection + */ +public class TransactionAwareConnectionFactoryProxy extends DelegatingConnectionFactory { + + /** + * Create a new {@link TransactionAwareConnectionFactoryProxy}. + * + * @param targetConnectionFactory the target {@link ConnectionFactory}. + */ + public TransactionAwareConnectionFactoryProxy(ConnectionFactory targetConnectionFactory) { + super(targetConnectionFactory); + } + + /** + * Delegates to {@link ConnectionFactoryUtils} for automatically participating in Spring-managed transactions. + *

+ * The returned {@link ConnectionFactory} handle implements the {@link ConnectionProxy} interface, allowing to + * retrieve the underlying target {@link Connection}. + * + * @return a transactional {@link Connection} if any, a new one else. + * @see ConnectionFactoryUtils#doGetConnection + * @see ConnectionProxy#getTargetConnection + */ + @Override + public Mono create() { + return getTransactionAwareConnectionProxy(obtainTargetConnectionFactory()); + } + + /** + * Wraps the given {@link Connection} with a proxy that delegates every method call to it but delegates + * {@code close()} calls to {@link ConnectionFactoryUtils}. + * + * @param targetConnectionFactory the {@link ConnectionFactory} that the {@link Connection} came from. + * @return the wrapped {@link Connection}. + * @see Connection#close() + * @see ConnectionFactoryUtils#doReleaseConnection + */ + protected Mono getTransactionAwareConnectionProxy(ConnectionFactory targetConnectionFactory) { + + return ConnectionFactoryUtils.getConnection(targetConnectionFactory).map(tuple -> { + return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), + new Class[] { ConnectionProxy.class }, + new TransactionAwareInvocationHandler(tuple.getT1(), targetConnectionFactory)); + }); + } + + /** + * Invocation handler that delegates close calls on R2DBC Connections to {@link ConnectionFactoryUtils} for being + * aware of context-bound transactions. + */ + private class TransactionAwareInvocationHandler implements InvocationHandler { + + private final Connection connection; + + private final ConnectionFactory targetConnectionFactory; + + private boolean closed = false; + + TransactionAwareInvocationHandler(Connection connection, ConnectionFactory targetConnectionFactory) { + this.connection = connection; + this.targetConnectionFactory = targetConnectionFactory; + } + + /* + * (non-Javadoc) + * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) + */ + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + + // Invocation on ConnectionProxy interface coming in... + switch (method.getName()) { + case "equals": + // Only considered as equal when proxies are identical. + return (proxy == args[0]); + case "hashCode": + // Use hashCode of Connection proxy. + return System.identityHashCode(proxy); + case "toString": + // Allow for differentiating between the proxy and the raw Connection. + StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); + if (this.connection != null) { + sb.append("[").append(this.connection.toString()).append("]"); + } else { + sb.append(" from ConnectionFactory [").append(this.targetConnectionFactory).append("]"); + } + return sb.toString(); + case "unwrap": + return this.connection; + case "close": + // Handle close method: only close if not within a transaction. + return ConnectionFactoryUtils.doReleaseConnection(this.connection, this.targetConnectionFactory) + .doOnSubscribe(n -> this.closed = true); + case "isClosed": + return this.closed; + } + + if (this.closed) { + throw new IllegalStateException("Connection handle already closed"); + } + + if (method.getName().equals("getTargetConnection")) { + // Handle getTargetConnection method: return underlying Connection. + return this.connection; + } + + // Invoke method on target Connection. + try { + return method.invoke(this.connection, args); + } catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } +} diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java index c6721a10..1ae45cc6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java @@ -17,6 +17,8 @@ package org.springframework.data.r2dbc.function.connectionfactory; import reactor.core.publisher.Mono; +import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; + /** * Transaction context for an ongoing transaction synchronization allowing to register transactional resources. *

@@ -27,6 +29,7 @@ import reactor.core.publisher.Mono; * should be bound to a transaction. * * @author Mark Paluch + * @see TransactionalDatabaseClient */ public interface TransactionResources { diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java index b04e1110..a39e633e 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java @@ -21,7 +21,6 @@ import static org.springframework.data.r2dbc.function.query.Criteria.*; import io.r2dbc.spi.ConnectionFactory; import lombok.Data; -import reactor.core.publisher.Hooks; import reactor.test.StepVerifier; import javax.sql.DataSource; @@ -54,8 +53,6 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr @Before public void before() { - Hooks.onOperatorDebug(); - connectionFactory = createConnectionFactory(); jdbc = createJdbcTemplate(createDataSource()); diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index deecc2e8..24a84ccb 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*; import io.r2dbc.spi.ConnectionFactory; import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -34,9 +33,12 @@ import org.junit.Before; import org.junit.Test; import org.springframework.dao.DataAccessException; +import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryTransactionManager; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; /** * Abstract base class for integration tests for {@link TransactionalDatabaseClient}. @@ -52,8 +54,6 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend @Before public void before() { - Hooks.onOperatorDebug(); - connectionFactory = createConnectionFactory(); jdbc = createJdbcTemplate(createDataSource()); @@ -205,25 +205,27 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend @Test // gh-2, gh-75 public void emitTransactionIds() { - TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory); + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); - Flux transactionIds = databaseClient.inTransaction(db -> { + TransactionalOperator transactionalOperator = TransactionalOperator + .create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition()); - // We have to execute a sql statement first. - // Otherwise some databases (MySql) don't have a transaction id. - Mono insert = db.execute().sql(getInsertIntoLegosetStatement()) // - .bind(0, 42055) // - .bind(1, "SCHAUFELRADBAGGER") // - .bindNull(2, Integer.class) // - .fetch().rowsUpdated(); + // We have to execute a sql statement first. + // Otherwise some databases (MySql) don't have a transaction id. + Mono insert = databaseClient.execute().sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated(); - Flux txId = db.execute() // - .sql(getCurrentTransactionIdStatement()) // - .map((row, md) -> row.get(0)) // - .all(); + Flux txId = databaseClient.execute() // + .sql(getCurrentTransactionIdStatement()) // + .map((row, md) -> row.get(0)) // + .all(); - return insert.thenMany(txId.concatWith(txId)); - }); + // insert.thenMany fails because of a cancel signal. Probably a consequence of dematerialize + // in TransactionalOperator.execute. + Flux transactionIds = txId.concatWith(txId).as(transactionalOperator::transactional); transactionIds.collectList().as(StepVerifier::create) // .consumeNextWith(actual -> { diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java index 3fc544c8..a91699f9 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java @@ -54,4 +54,5 @@ public class MySqlDatabaseClientIntegrationTests extends AbstractDatabaseClientI @Ignore("Jasync currently uses its own exceptions, see jasync-sql/jasync-sql#106") @Test public void shouldTranslateDuplicateKeyException() {} + } diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java index 615e29cf..3efbc571 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java @@ -60,4 +60,11 @@ public class MySqlTransactionalDatabaseClientIntegrationTests @Test @Ignore("MySQL creates transactions only on interaction with transactional tables. BEGIN does not create a txid") public void shouldManageUserTransaction() {} + + @Override + @Test + @Ignore("Third element is cancelled, looks like a bug") + public void emitTransactionIds() { + super.emitTransactionIds(); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java new file mode 100644 index 00000000..ede09c9f --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java @@ -0,0 +1,362 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.IsolationLevel; +import io.r2dbc.spi.Statement; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple2; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.reactive.TransactionSynchronization; +import org.springframework.transaction.reactive.TransactionSynchronizationManager; +import org.springframework.transaction.reactive.TransactionalOperator; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * Unit tests for {@link ConnectionFactoryTransactionManager}. + * + * @author Mark Paluch + */ +public class ConnectionFactoryTransactionManagerUnitTests { + + ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class); + Connection connectionMock = mock(Connection.class); + + private ConnectionFactoryTransactionManager tm; + + @Before + public void before() { + + when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock)); + when(connectionMock.beginTransaction()).thenReturn(Mono.empty()); + when(connectionMock.close()).thenReturn(Mono.empty()); + tm = new ConnectionFactoryTransactionManager(connectionFactoryMock); + } + + @Test // gh-107 + public void testSimpleTransaction() { + + TestTransactionSynchronization sync = new TestTransactionSynchronization( + TransactionSynchronization.STATUS_COMMITTED); + AtomicInteger commits = new AtomicInteger(); + when(connectionMock.commitTransaction()).thenReturn(Mono.fromRunnable(commits::incrementAndGet)); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1).flatMap(it -> { + + return TransactionSynchronizationManager.currentTransaction() + .doOnNext(synchronizationManager -> synchronizationManager.registerSynchronization(sync)); + + }) // + .as(operator::transactional) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + assertThat(commits).hasValue(1); + verify(connectionMock).beginTransaction(); + verify(connectionMock).commitTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + + assertThat(sync.beforeCommitCalled).isTrue(); + assertThat(sync.afterCommitCalled).isTrue(); + assertThat(sync.beforeCompletionCalled).isTrue(); + assertThat(sync.afterCompletionCalled).isTrue(); + } + + @Test // gh-107 + public void appliesIsolationLevel() { + + when(connectionMock.commitTransaction()).thenReturn(Mono.empty()); + when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty()); + + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_SERIALIZABLE); + + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + verify(connectionMock).beginTransaction(); + verify(connectionMock).setTransactionIsolationLevel(IsolationLevel.SERIALIZABLE); + verify(connectionMock).commitTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + + @Test // gh-107 + public void appliesReadOnly() { + + when(connectionMock.commitTransaction()).thenReturn(Mono.empty()); + when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty()); + Statement statement = mock(Statement.class); + when(connectionMock.createStatement(anyString())).thenReturn(statement); + when(statement.execute()).thenReturn(Mono.empty()); + tm.setEnforceReadOnly(true); + + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setReadOnly(true); + + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).as(operator::transactional) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + verify(connectionMock).beginTransaction(); + verify(connectionMock).createStatement("SET TRANSACTION READ ONLY"); + verify(connectionMock).commitTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + + @Test // gh-107 + public void testCommitFails() { + + when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> { + return Mono.error(new IllegalStateException()); + })); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1) // + .doOnNext(it -> { + it.createStatement("foo"); + }).then() // + .as(operator::transactional) // + .as(StepVerifier::create) // + .verifyError(); + + verify(connectionMock).beginTransaction(); + verify(connectionMock).createStatement("foo"); + verify(connectionMock).commitTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + + @Test // gh-107 + public void testRollback() { + + AtomicInteger commits = new AtomicInteger(); + when(connectionMock.commitTransaction()).thenReturn(Mono.fromRunnable(commits::incrementAndGet)); + + AtomicInteger rollbacks = new AtomicInteger(); + when(connectionMock.rollbackTransaction()).thenReturn(Mono.fromRunnable(rollbacks::incrementAndGet)); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1).doOnNext(it -> { + + throw new IllegalStateException(); + + }).as(operator::transactional) // + .as(StepVerifier::create) // + .verifyError(IllegalStateException.class); + + assertThat(commits).hasValue(0); + assertThat(rollbacks).hasValue(1); + verify(connectionMock).beginTransaction(); + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + + @Test // gh-107 + public void testTransactionSetRollbackOnly() { + + when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); + TestTransactionSynchronization sync = new TestTransactionSynchronization( + TransactionSynchronization.STATUS_ROLLED_BACK); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + operator.execute(tx -> { + + tx.setRollbackOnly(); + assertThat(tx.isNewTransaction()).isTrue(); + + return TransactionSynchronizationManager.currentTransaction().doOnNext(it -> { + + assertThat(it.hasResource(connectionFactoryMock)).isTrue(); + it.registerSynchronization(sync); + + }).then(); + }).as(StepVerifier::create) // + .verifyComplete(); + + verify(connectionMock).beginTransaction(); + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + + assertThat(sync.beforeCommitCalled).isFalse(); + assertThat(sync.afterCommitCalled).isFalse(); + assertThat(sync.beforeCompletionCalled).isTrue(); + assertThat(sync.afterCompletionCalled).isTrue(); + } + + @Test // gh-107 + public void testPropagationNeverWithExistingTransaction() { + + when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty()); + + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + + operator.execute(tx1 -> { + + assertThat(tx1.isNewTransaction()).isTrue(); + + definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_NEVER); + return operator.execute(tx2 -> { + + fail("Should have thrown IllegalTransactionStateException"); + return Mono.empty(); + }); + }).as(StepVerifier::create) // + .verifyError(IllegalTransactionStateException.class); + + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + } + + @Test // gh-107 + public void testPropagationSupportsAndRequiresNew() { + + when(connectionMock.commitTransaction()).thenReturn(Mono.empty()); + + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS); + TransactionalOperator operator = TransactionalOperator.create(tm, definition); + + operator.execute(tx1 -> { + + assertThat(tx1.isNewTransaction()).isFalse(); + + DefaultTransactionDefinition innerDef = new DefaultTransactionDefinition(); + innerDef.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + TransactionalOperator inner = TransactionalOperator.create(tm, innerDef); + + return inner.execute(tx2 -> { + + assertThat(tx2.isNewTransaction()).isTrue(); + return Mono.empty(); + }); + }).as(StepVerifier::create) // + .verifyComplete(); + + verify(connectionMock).commitTransaction(); + verify(connectionMock).close(); + } + + private static class TestTransactionSynchronization implements TransactionSynchronization { + + private int status; + + public boolean beforeCommitCalled; + + public boolean beforeCompletionCalled; + + public boolean afterCommitCalled; + + public boolean afterCompletionCalled; + + public Throwable afterCompletionException; + + public TestTransactionSynchronization(int status) { + this.status = status; + } + + @Override + public Mono suspend() { + return Mono.empty(); + } + + @Override + public Mono resume() { + return Mono.empty(); + } + + @Override + public Mono beforeCommit(boolean readOnly) { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + return Mono.fromRunnable(() -> { + assertFalse(this.beforeCommitCalled); + this.beforeCommitCalled = true; + }); + } + + @Override + public Mono beforeCompletion() { + return Mono.fromRunnable(() -> { + assertFalse(this.beforeCompletionCalled); + this.beforeCompletionCalled = true; + }); + } + + @Override + public Mono afterCommit() { + if (this.status != TransactionSynchronization.STATUS_COMMITTED) { + fail("Should never be called"); + } + return Mono.fromRunnable(() -> { + assertFalse(this.afterCommitCalled); + this.afterCommitCalled = true; + }); + } + + @Override + public Mono afterCompletion(int status) { + try { + return Mono.fromRunnable(() -> doAfterCompletion(status)); + } catch (Throwable ex) { + this.afterCompletionException = ex; + } + + return Mono.empty(); + } + + protected void doAfterCompletion(int status) { + assertFalse(this.afterCompletionCalled); + this.afterCompletionCalled = true; + assertTrue(status == this.status); + } + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java index 5793096e..002de904 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -21,6 +21,7 @@ import io.r2dbc.spi.ConnectionFactory; import reactor.test.StepVerifier; import org.junit.Test; + import org.springframework.transaction.NoTransactionException; /** @@ -30,7 +31,7 @@ import org.springframework.transaction.NoTransactionException; */ public class ConnectionFactoryUtilsUnitTests { - @Test + @Test // gh-107 public void currentReactiveTransactionSynchronizationShouldReportSynchronization() { ConnectionFactoryUtils.currentReactiveTransactionSynchronization() // @@ -41,7 +42,7 @@ public class ConnectionFactoryUtilsUnitTests { .verifyComplete(); } - @Test + @Test // gh-107 public void currentReactiveTransactionSynchronizationShouldFailWithoutTxMgmt() { ConnectionFactoryUtils.currentReactiveTransactionSynchronization() // @@ -50,7 +51,7 @@ public class ConnectionFactoryUtilsUnitTests { .verify(); } - @Test + @Test // gh-107 public void currentActiveReactiveTransactionSynchronizationShouldReportSynchronization() { ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() // @@ -63,7 +64,7 @@ public class ConnectionFactoryUtilsUnitTests { .verifyComplete(); } - @Test + @Test // gh-107 public void currentActiveReactiveTransactionSynchronization() { ConnectionFactoryUtils.currentActiveReactiveTransactionSynchronization() // @@ -74,12 +75,12 @@ public class ConnectionFactoryUtilsUnitTests { .verify(); } - @Test + @Test // gh-107 public void currentConnectionFactoryShouldReportConnectionFactory() { ConnectionFactory factoryMock = mock(ConnectionFactory.class); - ConnectionFactoryUtils.currentConnectionFactory() // + ConnectionFactoryUtils.currentConnectionFactory(factoryMock) // .subscriberContext(it -> { ReactiveTransactionSynchronization sync = new ReactiveTransactionSynchronization(); TransactionResources resources = TransactionResources.create(); diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java new file mode 100644 index 00000000..1ea43901 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java @@ -0,0 +1,60 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Mono; + +import org.junit.Test; + +/** + * Unit tests for {@link DelegatingConnectionFactory}. + * + * @author Mark Paluch + */ +public class DelegatingConnectionFactoryUnitTests { + + ConnectionFactory delegate = mock(ConnectionFactory.class); + Connection connectionMock = mock(Connection.class); + + DelegatingConnectionFactory connectionFactory = new ExampleConnectionFactory(delegate); + + @Test // gh-107 + public void shouldDelegateGetConnection() { + + Mono connectionMono = Mono.just(connectionMock); + when(delegate.create()).thenReturn((Mono) connectionMono); + + assertThat(connectionFactory.create()).isSameAs(connectionMono); + } + + @Test // gh-107 + public void shouldDelegateUnwrapWithoutImplementing() { + assertThat(connectionFactory.unwrap()).isSameAs(delegate); + } + + static class ExampleConnectionFactory extends DelegatingConnectionFactory { + + ExampleConnectionFactory(ConnectionFactory targetConnectionFactory) { + super(targetConnectionFactory); + } + } + +} diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java new file mode 100644 index 00000000..ecbdac02 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.r2dbc.function.connectionfactory; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import io.r2dbc.spi.Connection; +import io.r2dbc.spi.ConnectionFactory; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple2; + +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.transaction.reactive.TransactionalOperator; + +/** + * Unit tests for {@link TransactionAwareConnectionFactoryProxy}. + * + * @author Mark Paluch + */ +public class TransactionAwareConnectionFactoryProxyUnitTests { + + ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class); + Connection connectionMock1 = mock(Connection.class); + Connection connectionMock2 = mock(Connection.class); + Connection connectionMock3 = mock(Connection.class); + + private ConnectionFactoryTransactionManager tm; + + @Before + public void before() { + + when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock1), + (Mono) Mono.just(connectionMock2), (Mono) Mono.just(connectionMock3)); + tm = new ConnectionFactoryTransactionManager(connectionFactoryMock); + } + + @Test // gh-107 + public void shouldEmitBoundConnection() { + + when(connectionMock1.beginTransaction()).thenReturn(Mono.empty()); + when(connectionMock1.commitTransaction()).thenReturn(Mono.error(new IllegalStateException())); + when(connectionMock1.close()).thenReturn(Mono.empty()); + + TransactionalOperator rxtx = TransactionalOperator.create(tm); + AtomicReference transactionalConnection = new AtomicReference<>(); + + TransactionAwareConnectionFactoryProxy proxyCf = new TransactionAwareConnectionFactoryProxy(connectionFactoryMock); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1) // + .doOnNext(transactionalConnection::set).flatMap(it -> { + + return proxyCf.create().doOnNext(connectionFromProxy -> { + + ConnectionProxy connectionProxy = (ConnectionProxy) connectionFromProxy; + assertThat(connectionProxy.getTargetConnection()).isSameAs(it); + assertThat(connectionProxy.unwrap()).isSameAs(it); + }); + + }).as(rxtx::transactional) // + .flatMapMany(Connection::close) // + .as(StepVerifier::create) // + .verifyComplete(); + + verifyZeroInteractions(connectionMock2); + verifyZeroInteractions(connectionMock3); + verify(connectionFactoryMock, times(1)).create(); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 677be99d..01ba1016 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -22,7 +22,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -65,8 +64,6 @@ public abstract class AbstractR2dbcRepositoryIntegrationTests extends R2dbcInteg @Before public void before() { - Hooks.onOperatorDebug(); - this.jdbc = createJdbcTemplate(createDataSource()); try { diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index cbafd44b..e516dfee 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -21,7 +21,6 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -69,8 +68,6 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db @Before public void before() { - Hooks.onOperatorDebug(); - RelationalEntityInformation entityInformation = new MappingRelationalEntityInformation<>( (RelationalPersistentEntity) mappingContext.getRequiredPersistentEntity(LegoSet.class)); From 988b31b33a8ec2616cf5db31215ad0e1037a1d31 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 8 May 2019 20:01:06 +0200 Subject: [PATCH 116/665] #107 - Polishing. Extract method references for better readability. Add missing tests and update documentation. Add delay for transactional MySql tests to avoid failures due to potentially delayed transaction id storage within the database. Original Pull Request: #107 --- .../reference/r2dbc-transactions.adoc | 76 +++++--- .../ConnectionFactoryTransactionManager.java | 2 +- .../ConnectionFactoryUtils.java | 114 ++++++----- .../connectionfactory/ConnectionHandle.java | 8 +- .../connectionfactory/ConnectionHolder.java | 15 +- .../connectionfactory/ConnectionProxy.java | 6 +- .../DelegatingConnectionFactory.java | 23 ++- .../R2dbcTransactionObjectSupport.java | 5 +- .../SimpleConnectionHandle.java | 8 +- ...ransactionAwareConnectionFactoryProxy.java | 75 +++++--- ...ctionalDatabaseClientIntegrationTests.java | 181 +++++++++++++++--- ...ctionalDatabaseClientIntegrationTests.java | 33 +++- ...ionFactoryTransactionManagerUnitTests.java | 2 +- .../ConnectionFactoryUtilsUnitTests.java | 26 ++- .../DelegatingConnectionFactoryUnitTests.java | 2 +- ...nAwareConnectionFactoryProxyUnitTests.java | 118 ++++++++++-- src/test/resources/logback.xml | 2 +- 17 files changed, 514 insertions(+), 182 deletions(-) diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc index 8917eb89..0f0fdbab 100644 --- a/src/main/asciidoc/reference/r2dbc-transactions.adoc +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -4,7 +4,8 @@ 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. Using different connections hence results in utilizing different transactions. -Spring Data R2DBC includes transaction-awareness in `DatabaseClient` that allows you to group multiple statements within the same transaction using https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction[Spring's Transaction Management]. +Spring Data R2DBC includes transaction-awareness in `DatabaseClient` that allows you to group multiple statements within +the same transaction using https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction[Spring's Transaction Management]. Spring Data R2DBC provides a implementation for `ReactiveTransactionManager` with `ConnectionFactoryTransactionManager`. See <> for further details. @@ -12,54 +13,75 @@ See <> for further detail ==== [source,java] ---- -ConnectionFactoryTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory); -TransactionalOperator operator = TransactionalOperator.create(tm); -DatabaseClient db = DatabaseClient.create(connectionFactory); +ReactiveTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory); +TransactionalOperator operator = TransactionalOperator.create(tm); <1> -Mono atomicOperation = db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") - .bind("id", "joe") - .bind("name", "Joe") - .bind("age", 34) - .fetch().rowsUpdated() - .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") - .bind("id", "joe") - .bind("name", "Joe") - .fetch().rowsUpdated()) - .then() - .as(operator::transactional); +DatabaseClient client = DatabaseClient.create(connectionFactory); + +Mono atomicOperation = client.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + .bind("id", "joe") + .bind("name", "Joe") + .bind("age", 34) + .fetch().rowsUpdated() + .then(client.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .bind("id", "joe") + .bind("name", "Joe") + .fetch().rowsUpdated()) + .then() + .as(operator::transactional); <2> }); ---- +<1> Associate the `TransactionalOperator` with the `ReactiveTransactionManager`. +<2> Bind the operation to the `TransactionalOperator`. ==== -https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[Spring's declarative Transaction Management] is a less invasive, annotation-based approach to transaction demarcation. +https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction-declarative[Spring's declarative Transaction Management] +is a less invasive, annotation-based approach to transaction demarcation. .Declarative Transaction Management ==== [source,java] ---- -class MyService { +@Configuration +@EnableTransactionManagement <1> +class Config extends AbstractR2dbcConfiguration { - private final DatabaseClient db; - - MyService(DatabaseClient db) { - this.db = db; + @Override + public ConnectionFactory connectionFactory() { + return // ... } + @Bean + ReactiveTransactionManager txMgr(ConnectionFactory connectionFactory) { <2> + return new ConnectionFactoryTransactionManager(connectionFactory); + } +} + +@Service +class MyService { + + private final DatabaseClient client; + + MyService(DatabaseClient client) { + this.client = client; + } @Transactional public Mono insertPerson() { - return db.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + return client.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34) .fetch().rowsUpdated() - .then(db.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") - .bind("id", "joe") - .bind("name", "Joe") - .fetch().rowsUpdated()) - .then(); + .then(client.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .bind("id", "joe") + .bind("name", "Joe") + .fetch().rowsUpdated()) + .then(); } } ---- +<1> Enable declarative transaction management. +<2> Provide a `ReactiveTransactionManager` implementation to back reactive tansaction features. ==== diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java index a5a0ea14..4bb0caf6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java index c0172221..f54db094 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java @@ -17,14 +17,9 @@ package org.springframework.data.r2dbc.function.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; -import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; - import org.springframework.core.Ordered; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.lang.Nullable; @@ -32,6 +27,9 @@ import org.springframework.transaction.NoTransactionException; import org.springframework.transaction.reactive.TransactionSynchronization; import org.springframework.transaction.reactive.TransactionSynchronizationManager; import org.springframework.util.Assert; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; /** * Helper class that provides static methods for obtaining R2DBC Connections from a @@ -41,6 +39,7 @@ import org.springframework.util.Assert; * objects. Can also be used directly in application code. * * @author Mark Paluch + * @author Christoph Strobl */ public abstract class ConnectionFactoryUtils { @@ -52,7 +51,6 @@ public abstract class ConnectionFactoryUtils { private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class); private ConnectionFactoryUtils() { - } /** @@ -93,14 +91,20 @@ public abstract class ConnectionFactoryUtils { if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) { conHolder.requested(); if (!conHolder.hasConnection()) { - logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); + + if (logger.isDebugEnabled()) { + logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); + } return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection); } return Mono.just(conHolder.getConnection()); } // Else we either got no holder or an empty thread-bound holder here. - logger.debug("Fetching R2DBC Connection from ConnectionFactory"); + if (logger.isDebugEnabled()) { + logger.debug("Fetching R2DBC Connection from ConnectionFactory"); + } + Mono con = fetchConnection(connectionFactory); if (synchronizationManager.isSynchronizationActive()) { @@ -124,11 +128,9 @@ public abstract class ConnectionFactoryUtils { if (holderToUse != conHolder) { synchronizationManager.bindResource(connectionFactory, holderToUse); } - }).onErrorResume(e -> { // Unexpected exception from external delegation call -> close Connection and rethrow. return releaseConnection(it, connectionFactory).then(Mono.error(e)); - }); }); } @@ -158,7 +160,9 @@ public abstract class ConnectionFactoryUtils { if (synchronization.isSynchronizationActive()) { - logger.debug("Registering transaction synchronization for R2DBC Connection"); + if (logger.isDebugEnabled()) { + logger.debug("Registering transaction synchronization for R2DBC Connection"); + } TransactionResources txContext = synchronization.getCurrentTransaction(); ConnectionFactory resource = txContext.getResource(ConnectionFactory.class); @@ -166,7 +170,9 @@ public abstract class ConnectionFactoryUtils { Mono> attachNewConnection = Mono .defer(() -> Mono.from(connectionFactory.create()).map(it -> { - logger.debug("Fetching new R2DBC Connection from ConnectionFactory"); + if (logger.isDebugEnabled()) { + logger.debug("Fetching new R2DBC Connection from ConnectionFactory"); + } SingletonConnectionFactory s = new SingletonConnectionFactory(connectionFactory.getMetadata(), it); txContext.registerResource(ConnectionFactory.class, s); @@ -174,14 +180,9 @@ public abstract class ConnectionFactoryUtils { return Tuples.of(it, connectionFactory); })); - return Mono.justOrEmpty(resource).flatMap(factory -> { - - logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); - - return Mono.from(factory.create()) - .map(connection -> Tuples. of(connection, factory)); - - }).switchIfEmpty(attachNewConnection); + return Mono.justOrEmpty(resource) + .flatMap(ConnectionFactoryUtils::createConnection) + .switchIfEmpty(attachNewConnection); } return Mono.empty(); @@ -199,7 +200,7 @@ public abstract class ConnectionFactoryUtils { private static Mono fetchConnection(ConnectionFactory connectionFactory) { Publisher con = connectionFactory.create(); - if (con == null) { + if (con == null) { // TODO: seriously why would it do that? throw new IllegalStateException("ConnectionFactory returned null from getConnection(): " + connectionFactory); } return Mono.from(con); @@ -211,7 +212,7 @@ public abstract class ConnectionFactoryUtils { * * @param con the {@link io.r2dbc.spi.Connection} to close if necessary. * @param connectionFactory the {@link ConnectionFactory} that the Connection was obtained from (may be - * {@literal null}). + * {@literal null}). * @see #getConnection */ public static Mono releaseConnection(@Nullable io.r2dbc.spi.Connection con, @@ -227,7 +228,7 @@ public abstract class ConnectionFactoryUtils { * * @param con the {@link io.r2dbc.spi.Connection} to close if necessary. * @param connectionFactory the {@link ConnectionFactory} that the Connection was obtained from (may be - * {@literal null}). + * {@literal null}). * @see #doGetConnection */ public static Mono doReleaseConnection(@Nullable io.r2dbc.spi.Connection con, @@ -247,12 +248,16 @@ public abstract class ConnectionFactoryUtils { SingletonConnectionFactory factory = (SingletonConnectionFactory) connectionFactory; - logger.debug("Releasing R2DBC Connection"); + if (logger.isDebugEnabled()) { + logger.debug("Releasing R2DBC Connection"); + } return factory.close(con); } - logger.debug("Closing R2DBC Connection"); + if (logger.isDebugEnabled()) { + logger.debug("Closing R2DBC Connection"); + } return Mono.from(con.close()); }); @@ -267,6 +272,7 @@ public abstract class ConnectionFactoryUtils { * @throws DataAccessResourceFailureException if the attempt to get a {@link io.r2dbc.spi.Connection} failed */ public static Mono closeConnection(Connection connection, ConnectionFactory connectionFactory) { + return doCloseConnection(connection, connectionFactory) .onErrorMap(e -> new DataAccessResourceFailureException("Failed to obtain R2DBC Connection", e)); } @@ -295,10 +301,10 @@ public abstract class ConnectionFactoryUtils { * Obtain the currently {@link ReactiveTransactionSynchronization} from the current subscriber * {@link reactor.util.context.Context}. * + * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the + * current subscription. * @see Mono#subscriberContext() * @see ReactiveTransactionSynchronization - * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the - * current subscription. */ public static Mono currentReactiveTransactionSynchronization() { @@ -312,10 +318,10 @@ public abstract class ConnectionFactoryUtils { * Obtain the currently active {@link ReactiveTransactionSynchronization} from the current subscriber * {@link reactor.util.context.Context}. * + * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the + * current subscription. * @see Mono#subscriberContext() * @see ReactiveTransactionSynchronization - * @throws NoTransactionException if no active {@link ReactiveTransactionSynchronization} is associated with the - * current subscription. */ public static Mono currentActiveReactiveTransactionSynchronization() { @@ -341,16 +347,8 @@ public abstract class ConnectionFactoryUtils { return true; } return false; - }).map(it -> connectionFactory).onErrorResume(NoTransactionException.class, e -> { - - return currentActiveReactiveTransactionSynchronization().map(synchronization -> { - - TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); - return currentSynchronization.getResource(ConnectionFactory.class); - }).switchIfEmpty(Mono.error(new DataAccessResourceFailureException( - "Cannot extract ConnectionFactory from current TransactionContext!"))); - - }); + }).map(it -> connectionFactory) // + .onErrorResume(NoTransactionException.class, ConnectionFactoryUtils::obtainDefaultConnectionFactory); } /** @@ -359,7 +357,7 @@ public abstract class ConnectionFactoryUtils { * * @param conHolder the {@link ConnectionHolder} for the held Connection (potentially a proxy) * @param passedInCon the {@link Connection} passed-in by the user (potentially a target {@link Connection} without - * proxy) + * proxy) * @return whether the given Connections are equal * @see #getTargetConnection */ @@ -402,6 +400,7 @@ public abstract class ConnectionFactoryUtils { * @see #CONNECTION_SYNCHRONIZATION_ORDER */ private static int getConnectionSynchronizationOrder(ConnectionFactory connectionFactory) { + int order = CONNECTION_SYNCHRONIZATION_ORDER; ConnectionFactory current = connectionFactory; while (current instanceof DelegatingConnectionFactory) { @@ -411,6 +410,37 @@ public abstract class ConnectionFactoryUtils { return order; } + /** + * @param e + * @return an {@link Mono#error(Throwable) error} if not transaction present. + */ + private static Mono obtainDefaultConnectionFactory(NoTransactionException e) { + + return currentActiveReactiveTransactionSynchronization().map(synchronization -> { + + TransactionResources currentSynchronization = synchronization.getCurrentTransaction(); + return currentSynchronization.getResource(ConnectionFactory.class); + }).switchIfEmpty(Mono.error(new DataAccessResourceFailureException( + "Cannot extract ConnectionFactory from current TransactionContext!"))); + } + + /** + * Create a {@link Connection} via the given {@link ConnectionFactory#create() factory} and return a {@link Tuple2} associating the + * {@link Connection} with its creating {@link ConnectionFactory}. + * + * @param factory must not be {@literal null}. + * @return never {@literal null} + */ + private static Mono> createConnection(ConnectionFactory factory) { + + if (logger.isDebugEnabled()) { + logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory"); + } + + return Mono.from(factory.create()) + .map(connection -> Tuples.of(connection, factory)); + } + /** * Callback for resource cleanup at the end of a non-native R2DBC transaction. */ @@ -465,7 +495,6 @@ public abstract class ConnectionFactoryUtils { }).then(); } return Mono.empty(); - } @Override @@ -510,17 +539,14 @@ public abstract class ConnectionFactoryUtils { // Reset the ConnectionHolder: It might remain bound to the context. this.connectionHolder.setConnection(null); }); - } return Mono.empty(); }); - } this.connectionHolder.reset(); return Mono.empty(); } } - } diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java index 155a79cb..ed5a4e6f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -38,8 +38,8 @@ public interface ConnectionHandle { *

* The default implementation is empty, assuming that the lifecycle of the connection is managed externally. * - * @param con the R2DBC Connection to release + * @param connection the R2DBC Connection to release */ - default void releaseConnection(Connection con) {} - + default void releaseConnection(Connection connection) { + } } diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java index fcc3f3b7..f84f6c7e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -17,7 +17,6 @@ package org.springframework.data.r2dbc.function.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; - import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -30,6 +29,8 @@ import org.springframework.util.Assert; *

* Note: This is an SPI class, not intended to be used by applications. * + * @author Mark Paluch + * @author Christoph Strobl * @see ConnectionFactoryTransactionManager * @see ConnectionFactoryUtils */ @@ -39,7 +40,7 @@ public class ConnectionHolder extends ResourceHolderSupport { @Nullable private Connection currentConnection; - private boolean transactionActive = false; + private boolean transactionActive; /** * Create a new ConnectionHolder for the given R2DBC {@link Connection}, wrapping it with a @@ -50,7 +51,7 @@ public class ConnectionHolder extends ResourceHolderSupport { * @see #ConnectionHolder(Connection, boolean) */ public ConnectionHolder(Connection connection) { - this.connectionHandle = new SimpleConnectionHandle(connection); + this(connection, false); } /** @@ -62,7 +63,8 @@ public class ConnectionHolder extends ResourceHolderSupport { * @see SimpleConnectionHandle */ public ConnectionHolder(Connection connection, boolean transactionActive) { - this(connection); + + this.connectionHandle = new SimpleConnectionHandle(connection); this.transactionActive = transactionActive; } @@ -127,6 +129,7 @@ public class ConnectionHolder extends ResourceHolderSupport { * @see #released() */ public Connection getConnection() { + Assert.notNull(this.connectionHandle, "Active Connection is required"); if (this.currentConnection == null) { this.currentConnection = this.connectionHandle.getConnection(); @@ -152,7 +155,7 @@ public class ConnectionHolder extends ResourceHolderSupport { } } - /* + /* * (non-Javadoc) * @see org.springframework.transaction.support.ResourceHolderSupport#clear() */ diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java index 37f600cc..38bd4611 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2019 the original author or authors. + * 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. @@ -19,12 +19,13 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.Wrapped; /** - * Subinterface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target + * Sub interface of {@link Connection} to be implemented by Connection proxies. Allows access to the underlying target * Connection. *

* This interface can be checked when there is a need to cast to a native R2DBC {@link Connection}. * * @author Mark Paluch + * @author Christoph Strobl */ public interface ConnectionProxy extends Connection, Wrapped { @@ -34,6 +35,7 @@ public interface ConnectionProxy extends Connection, Wrapped { * This will typically be the native driver {@link Connection} or a wrapper from a connection pool. * * @return the underlying Connection (never {@literal null}) + * @throws IllegalStateException in case the connection has already been closed. */ Connection getTargetConnection(); } diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java index f28b2c89..3b577691 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -19,7 +19,6 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.Wrapped; - import org.reactivestreams.Publisher; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -43,7 +42,7 @@ public class DelegatingConnectionFactory implements ConnectionFactory, Wrapped getTransactionAwareConnectionProxy(ConnectionFactory targetConnectionFactory) { + return ConnectionFactoryUtils.getConnection(targetConnectionFactory).map(TransactionAwareConnectionFactoryProxy::proxyConnection); + } - return ConnectionFactoryUtils.getConnection(targetConnectionFactory).map(tuple -> { - return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), - new Class[] { ConnectionProxy.class }, - new TransactionAwareInvocationHandler(tuple.getT1(), targetConnectionFactory)); - }); + private static Connection proxyConnection(Tuple2 connectionConnectionFactoryTuple) { + + return (Connection) Proxy.newProxyInstance(ConnectionProxy.class.getClassLoader(), + new Class[]{ConnectionProxy.class}, + new TransactionAwareInvocationHandler(connectionConnectionFactoryTuple.getT1(), connectionConnectionFactoryTuple.getT2())); } /** * Invocation handler that delegates close calls on R2DBC Connections to {@link ConnectionFactoryUtils} for being * aware of context-bound transactions. */ - private class TransactionAwareInvocationHandler implements InvocationHandler { + private static class TransactionAwareInvocationHandler implements InvocationHandler { private final Connection connection; @@ -116,11 +123,12 @@ public class TransactionAwareConnectionFactoryProxy extends DelegatingConnection private boolean closed = false; TransactionAwareInvocationHandler(Connection connection, ConnectionFactory targetConnectionFactory) { + this.connection = connection; this.targetConnectionFactory = targetConnectionFactory; } - /* + /* * (non-Javadoc) * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object[]) */ @@ -128,23 +136,24 @@ public class TransactionAwareConnectionFactoryProxy extends DelegatingConnection @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (ReflectionUtils.isObjectMethod(method)) { + + if (ReflectionUtils.isToStringMethod(method)) { + return proxyToString(proxy); + } + + if (ReflectionUtils.isEqualsMethod(method)) { + return (proxy == args[0]); + } + + if (ReflectionUtils.isHashCodeMethod(method)) { + return System.identityHashCode(proxy); + } + } + // Invocation on ConnectionProxy interface coming in... switch (method.getName()) { - case "equals": - // Only considered as equal when proxies are identical. - return (proxy == args[0]); - case "hashCode": - // Use hashCode of Connection proxy. - return System.identityHashCode(proxy); - case "toString": - // Allow for differentiating between the proxy and the raw Connection. - StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); - if (this.connection != null) { - sb.append("[").append(this.connection.toString()).append("]"); - } else { - sb.append(" from ConnectionFactory [").append(this.targetConnectionFactory).append("]"); - } - return sb.toString(); + case "unwrap": return this.connection; case "close": @@ -171,5 +180,17 @@ public class TransactionAwareConnectionFactoryProxy extends DelegatingConnection throw ex.getTargetException(); } } + + private String proxyToString(@Nullable Object proxy) { + + // Allow for differentiating between the proxy and the raw Connection. + StringBuilder sb = new StringBuilder("Transaction-aware proxy for target Connection "); + if (this.connection != null) { + sb.append("[").append(this.connection.toString()).append("]"); + } else { + sb.append(" from ConnectionFactory [").append(this.targetConnectionFactory).append("]"); + } + return sb.toString(); + } } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java index 24a84ccb..d47d4388 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -17,33 +17,41 @@ package org.springframework.data.r2dbc.function; import static org.assertj.core.api.Assertions.*; -import io.r2dbc.spi.ConnectionFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - +import javax.sql.DataSource; import java.util.ArrayList; import java.util.List; import java.util.Queue; import java.util.concurrent.ArrayBlockingQueue; -import javax.sql.DataSource; - +import io.r2dbc.spi.ConnectionFactory; +import org.junit.After; import org.junit.Before; import org.junit.Test; - +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataAccessException; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryTransactionManager; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.NoTransactionException; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.transaction.support.DefaultTransactionDefinition; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; /** * Abstract base class for integration tests for {@link TransactionalDatabaseClient}. * * @author Mark Paluch + * @author Christoph Strobl */ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extends R2dbcIntegrationTestSupport { @@ -51,19 +59,35 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend private JdbcTemplate jdbc; + AnnotationConfigApplicationContext context; + TransactionalService service; + @Before public void before() { connectionFactory = createConnectionFactory(); + context = new AnnotationConfigApplicationContext(); + context.registerBean("theConnectionFactory", ConnectionFactory.class, () -> connectionFactory); + context.register(Config.class, TransactionalService.class); + context.refresh(); + + service = context.getBean(TransactionalService.class); + jdbc = createJdbcTemplate(createDataSource()); try { jdbc.execute("DROP TABLE legoset"); - } catch (DataAccessException e) {} + } catch (DataAccessException e) { + } jdbc.execute(getCreateTableStatement()); jdbc.execute("DELETE FROM legoset"); } + @After + public void tearDown() { + context.close(); + } + /** * Creates a {@link DataSource} to be used in this test. * @@ -97,6 +121,17 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend return "INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)"; } + /** + * Some Databases require special treatment to convince them to start a transaction. Some even start a transaction but + * store its id async so that it might show up a little late. + * + * @param client the client to use + * @return an empty {@link Mono} by default. + */ + protected Mono prepareForTransaction(DatabaseClient client) { + return Mono.empty(); + } + /** * Get a statement that returns the current transactionId. */ @@ -191,7 +226,8 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend .bind(0, 42055) // .bind(1, "SCHAUFELRADBAGGER") // .bindNull(2, Integer.class) // - .fetch().rowsUpdated().then(Mono.error(new IllegalStateException("failed"))); + .fetch().rowsUpdated() // + .then(Mono.error(new IllegalStateException("failed"))); }); integerFlux.as(StepVerifier::create) // @@ -202,7 +238,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend assertThat(count).isEqualTo(0); } - @Test // gh-2, gh-75 + @Test // gh-2, gh-75, gh-107 public void emitTransactionIds() { DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); @@ -210,22 +246,13 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend TransactionalOperator transactionalOperator = TransactionalOperator .create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition()); - // We have to execute a sql statement first. - // Otherwise some databases (MySql) don't have a transaction id. - Mono insert = databaseClient.execute().sql(getInsertIntoLegosetStatement()) // - .bind(0, 42055) // - .bind(1, "SCHAUFELRADBAGGER") // - .bindNull(2, Integer.class) // - .fetch().rowsUpdated(); - Flux txId = databaseClient.execute() // .sql(getCurrentTransactionIdStatement()) // .map((row, md) -> row.get(0)) // .all(); - // insert.thenMany fails because of a cancel signal. Probably a consequence of dematerialize - // in TransactionalOperator.execute. - Flux transactionIds = txId.concatWith(txId).as(transactionalOperator::transactional); + Flux transactionIds = prepareForTransaction(databaseClient).thenMany(txId.concatWith(txId)) // + .as(transactionalOperator::transactional); transactionIds.collectList().as(StepVerifier::create) // .consumeNextWith(actual -> { @@ -235,4 +262,114 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend }) // .verifyComplete(); } + + @Test // gh-107 + public void shouldRollbackTransactionUsingTransactionalOperator() { + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + TransactionalOperator transactionalOperator = TransactionalOperator + .create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition()); + + Flux integerFlux = databaseClient.execute() // + .sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated() // + .thenMany(Mono.fromSupplier(() -> { + throw new IllegalStateException("failed"); + })); + + integerFlux.as(transactionalOperator::transactional) // + .as(StepVerifier::create) // + .expectError(IllegalStateException.class) // + .verify(); + + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); + } + + @Test //gh-107 + public void emitTransactionIdsUsingManagedTransactions() { + + service.emitTransactionIds(prepareForTransaction(service.getDatabaseClient()), getCurrentTransactionIdStatement()).collectList().as(StepVerifier::create) // + .consumeNextWith(actual -> { + + assertThat(actual).hasSize(2); + assertThat(actual.get(0)).isEqualTo(actual.get(1)); + }) // + .verifyComplete(); + } + + @Test // gh-107 + public void shouldRollbackTransactionUsingManagedTransactions() { + + service.shouldRollbackTransactionUsingTransactionalOperator(getInsertIntoLegosetStatement()) + .as(StepVerifier::create) // + .expectError(IllegalStateException.class) // + .verify(); + + Integer count = jdbc.queryForObject("SELECT COUNT(*) FROM legoset", Integer.class); + assertThat(count).isEqualTo(0); + } + + @Configuration + @EnableTransactionManagement + static class Config extends AbstractR2dbcConfiguration { + + @Autowired GenericApplicationContext context; + + @Override + public ConnectionFactory connectionFactory() { + return lookup(); + } + + ConnectionFactory lookup() { + return context.getBean("theConnectionFactory", ConnectionFactory.class); + } + + @Bean + ReactiveTransactionManager txMgr(ConnectionFactory connectionFactory) { + return new ConnectionFactoryTransactionManager(connectionFactory); + } + } + + static class TransactionalService { + + private DatabaseClient databaseClient; + + public TransactionalService(DatabaseClient databaseClient) { + this.databaseClient = databaseClient; + } + + @Transactional + public Flux emitTransactionIds(Mono prepareTransaction, String idStatement) { + + Flux txId = databaseClient.execute() // + .sql(idStatement) // + .map((row, md) -> row.get(0)) // + .all(); + + return prepareTransaction.thenMany(txId.concatWith(txId)); + } + + + @Transactional + public Flux shouldRollbackTransactionUsingTransactionalOperator(String insertStatement) { + + return databaseClient.execute().sql(insertStatement) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated() // + .thenMany(Mono.fromSupplier(() -> { + throw new IllegalStateException("failed"); + })); + } + + public DatabaseClient getDatabaseClient() { + return databaseClient; + } + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java index 3efbc571..75194234 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java @@ -15,16 +15,16 @@ */ package org.springframework.data.r2dbc.function; -import io.r2dbc.spi.ConnectionFactory; - import javax.sql.DataSource; +import java.time.Duration; +import io.r2dbc.spi.ConnectionFactory; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; - import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.MySqlTestSupport; +import reactor.core.publisher.Mono; /** * Integration tests for {@link TransactionalDatabaseClient} against MySQL. @@ -51,6 +51,25 @@ public class MySqlTransactionalDatabaseClientIntegrationTests return MySqlTestSupport.CREATE_TABLE_LEGOSET; } + @Override + protected Mono prepareForTransaction(DatabaseClient client) { + + /* + * We have to execute a sql statement first. + * Otherwise MySql don't have a transaction id. + * And we need to delay emitting the result so that MySql has time to write the transaction id, which is done in + * batches every now and then. + * @see: https://dev.mysql.com/doc/refman/5.7/en/innodb-information-schema-internal-data.html + */ + return client.execute().sql(getInsertIntoLegosetStatement()) // + .bind(0, 42055) // + .bind(1, "SCHAUFELRADBAGGER") // + .bindNull(2, Integer.class) // + .fetch().rowsUpdated() // + .delayElement(Duration.ofMillis(50)) // + .then(); + } + @Override protected String getCurrentTransactionIdStatement() { return "SELECT tx.trx_id FROM information_schema.innodb_trx tx WHERE tx.trx_mysql_thread_id = connection_id()"; @@ -59,12 +78,6 @@ public class MySqlTransactionalDatabaseClientIntegrationTests @Override @Test @Ignore("MySQL creates transactions only on interaction with transactional tables. BEGIN does not create a txid") - public void shouldManageUserTransaction() {} - - @Override - @Test - @Ignore("Third element is cancelled, looks like a bug") - public void emitTransactionIds() { - super.emitTransactionIds(); + public void shouldManageUserTransaction() { } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java index ede09c9f..b13fb55e 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java index 002de904..b77a1a23 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -17,17 +17,20 @@ package org.springframework.data.r2dbc.function.connectionfactory; import static org.mockito.Mockito.*; +import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; -import reactor.test.StepVerifier; - +import org.assertj.core.api.Assertions; import org.junit.Test; - +import org.reactivestreams.Publisher; import org.springframework.transaction.NoTransactionException; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; /** * Unit tests for {@link ConnectionFactoryUtils}. * * @author Mark Paluch + * @author Christoph Strobl */ public class ConnectionFactoryUtilsUnitTests { @@ -91,4 +94,21 @@ public class ConnectionFactoryUtilsUnitTests { .expectNext(factoryMock) // .verifyComplete(); } + + @Test // gh-107 + public void connectionFactoryRetunsConnectionWhenNoSyncronisationActive() { + + ConnectionFactory factoryMock = mock(ConnectionFactory.class); + Connection connection = mock(Connection.class); + Publisher p = Mono.just(connection); + doReturn(p).when(factoryMock).create(); + + ConnectionFactoryUtils.getConnection(factoryMock) // + .as(StepVerifier::create) // + .consumeNextWith(it -> { + Assertions.assertThat(it.getT1()).isEqualTo(connection); + Assertions.assertThat(it.getT2()).isEqualTo(factoryMock); + }) + .verifyComplete(); + } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java index 1ea43901..d1ad0757 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java index ecbdac02..6a578eed 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, @@ -18,23 +18,22 @@ package org.springframework.data.r2dbc.function.connectionfactory; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; +import java.util.concurrent.atomic.AtomicReference; + import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import org.junit.Before; +import org.junit.Test; +import org.springframework.transaction.reactive.TransactionalOperator; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import reactor.util.function.Tuple2; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.Before; -import org.junit.Test; - -import org.springframework.transaction.reactive.TransactionalOperator; - /** * Unit tests for {@link TransactionAwareConnectionFactoryProxy}. * * @author Mark Paluch + * @author Christoph Strobl */ public class TransactionAwareConnectionFactoryProxyUnitTests { @@ -53,6 +52,96 @@ public class TransactionAwareConnectionFactoryProxyUnitTests { tm = new ConnectionFactoryTransactionManager(connectionFactoryMock); } + @Test // gh-107 + public void createShouldProxyConnection() { + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .as(StepVerifier::create) // + .consumeNextWith(connection -> { + assertThat(connection).isInstanceOf(ConnectionProxy.class); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void unwrapShouldReturnTargetConnection() { + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThat(proxy.unwrap()).isEqualTo(connectionMock1); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void unwrapShouldReturnTargetConnectionEvenWhenClosed() { + + when(connectionMock1.close()).thenReturn(Mono.empty()); + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .flatMap(it -> Mono.from(it.close()).then(Mono.just(it))) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThat(proxy.unwrap()).isEqualTo(connectionMock1); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void getTargetConnectionShouldReturnTargetConnection() { + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThat(proxy.getTargetConnection()).isEqualTo(connectionMock1); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void getTargetConnectionShouldThrowsErrorEvenWhenClosed() { + + when(connectionMock1.close()).thenReturn(Mono.empty()); + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .flatMap(it -> Mono.from(it.close()).then(Mono.just(it))) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> proxy.getTargetConnection()); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void hashCodeShouldReturnProxyHash() { + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThat(proxy.hashCode()).isEqualTo(System.identityHashCode(proxy)); + }) + .verifyComplete(); + } + + @Test // gh-107 + public void equalsShouldCompareCorrectly() { + + new TransactionAwareConnectionFactoryProxy(connectionFactoryMock).create() // + .map(ConnectionProxy.class::cast) + .as(StepVerifier::create) // + .consumeNextWith(proxy -> { + assertThat(proxy.equals(proxy)).isTrue(); + assertThat(proxy.equals(connectionMock1)).isFalse(); + }) + .verifyComplete(); + } + @Test // gh-107 public void shouldEmitBoundConnection() { @@ -68,14 +157,13 @@ public class TransactionAwareConnectionFactoryProxyUnitTests { ConnectionFactoryUtils.getConnection(connectionFactoryMock).map(Tuple2::getT1) // .doOnNext(transactionalConnection::set).flatMap(it -> { - return proxyCf.create().doOnNext(connectionFromProxy -> { + return proxyCf.create().doOnNext(connectionFromProxy -> { - ConnectionProxy connectionProxy = (ConnectionProxy) connectionFromProxy; - assertThat(connectionProxy.getTargetConnection()).isSameAs(it); - assertThat(connectionProxy.unwrap()).isSameAs(it); - }); - - }).as(rxtx::transactional) // + ConnectionProxy connectionProxy = (ConnectionProxy) connectionFromProxy; + assertThat(connectionProxy.getTargetConnection()).isSameAs(it); + assertThat(connectionProxy.unwrap()).isSameAs(it); + }); + }).as(rxtx::transactional) // .flatMapMany(Connection::close) // .as(StepVerifier::create) // .verifyComplete(); diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml index c9be4b42..32090ee6 100644 --- a/src/test/resources/logback.xml +++ b/src/test/resources/logback.xml @@ -10,7 +10,7 @@ - + From c456af0d2a6c3e8fb5dc22c471dc01868d7ca556 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 9 May 2019 18:11:55 +0200 Subject: [PATCH 117/665] #111 - Package and type renames. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package renames: - `….domain` -> `….mapping` - `….function.connectionfactory` -> `connectionfactory` - `….function.convert` -> `convert` - `….function.query` -> `query` - `….function` -> `….core` Type moves: - `….dialect.R2dbcSimpleTypeHolder` -> `….mapping.R2dbcSimpleTypeHolder` - `….mapping.BindTarget` -> `….dialect.BindTarget` - `….mapping.PreparedOperation` -> `….core.PreparedOperation` - `….mapping.QueryOperation` -> `….core.QueryOperation` - `….mapping.BindOperation` -> `….core.BindOperation` - `….connectionfactory.ConnectionFactoryTransactionManager` -> `….connectionfactory.R2dbcTransactionManager` That makes us end up with: - `connectionfactory` - `core` -> `connectionfactory`, `convert`, `dialect`, `mapping`, `query` - `query` -> `convert`, `dialect`, `mapping` - `convert` -> `dialect`, `mapping` - `dialect` -> `mapping` - `mapping` -> Spring Data Commons mapping Reflect type renames in reference documentation. --- src/main/asciidoc/reference/r2dbc-core.adoc | 42 +++++++++---------- .../reference/r2dbc-databaseclient.adoc | 25 ++++++----- src/main/asciidoc/reference/r2dbc-fluent.adoc | 42 +++++++++---------- .../reference/r2dbc-repositories.adoc | 24 +++++------ .../reference/r2dbc-transactions.adoc | 14 +++---- .../r2dbc/InvalidResultAccessException.java | 2 +- .../config/AbstractR2dbcConfiguration.java | 10 ++--- .../ConnectionFactoryUtils.java | 4 +- .../connectionfactory/ConnectionHandle.java | 2 +- .../connectionfactory/ConnectionHolder.java | 8 ++-- .../connectionfactory/ConnectionProxy.java | 2 +- .../DefaultTransactionResources.java | 2 +- .../DelegatingConnectionFactory.java | 2 +- .../R2dbcTransactionManager.java} | 12 +++--- .../R2dbcTransactionObjectSupport.java | 4 +- .../ReactiveTransactionSynchronization.java | 2 +- .../SimpleConnectionHandle.java | 2 +- .../SingletonConnectionFactory.java | 4 +- .../SmartConnectionFactory.java | 2 +- ...ransactionAwareConnectionFactoryProxy.java | 9 ++-- .../TransactionResources.java | 4 +- .../connectionfactory/package-info.java | 2 +- .../convert/ColumnMapRowMapper.java | 2 +- .../convert/EntityRowMapper.java | 2 +- .../{function => }/convert/IterableUtils.java | 2 +- .../convert/MappingR2dbcConverter.java | 6 +-- .../convert/R2dbcConverter.java | 4 +- .../convert/R2dbcConverters.java | 16 +++---- .../convert/R2dbcCustomConversions.java | 4 +- .../{function => }/convert/package-info.java | 2 +- .../BindParameterSource.java | 2 +- .../{domain => core}/BindableOperation.java | 8 ++-- .../ConnectionAccessor.java | 2 +- .../{function => core}/DatabaseClient.java | 9 ++-- .../DefaultDatabaseClient.java | 20 ++++----- .../DefaultDatabaseClientBuilder.java | 4 +- .../{function => core}/DefaultFetchSpec.java | 2 +- .../DefaultReactiveDataAccessStrategy.java | 16 +++---- .../{function => core}/DefaultSqlResult.java | 2 +- .../DefaultStatementMapper.java | 11 +++-- .../DefaultTransactionalDatabaseClient.java | 9 ++-- ...ultTransactionalDatabaseClientBuilder.java | 4 +- .../r2dbc/{function => core}/FetchSpec.java | 2 +- .../MapBindParameterSource.java | 4 +- .../NamedParameterExpander.java | 5 +-- .../NamedParameterUtils.java | 5 +-- .../r2dbc/{function => core}/ParsedSql.java | 2 +- .../{domain => core}/PreparedOperation.java | 10 +++-- .../{domain => core}/QueryOperation.java | 2 +- .../ReactiveDataAccessStrategy.java | 9 ++-- .../{function => core}/RowsFetchSpec.java | 2 +- .../r2dbc/{function => core}/SqlProvider.java | 2 +- .../r2dbc/{function => core}/SqlResult.java | 2 +- .../{function => core}/StatementMapper.java | 9 ++-- .../TransactionalDatabaseClient.java | 9 ++-- .../UpdatedRowsFetchSpec.java | 2 +- .../{function => core}/package-info.java | 2 +- .../data/r2dbc/dialect/BindMarker.java | 2 - .../r2dbc/{domain => dialect}/BindTarget.java | 4 +- .../data/r2dbc/dialect/Bindings.java | 1 - .../data/r2dbc/dialect/Dialect.java | 1 + .../data/r2dbc/dialect/IndexedBindMarker.java | 2 - .../data/r2dbc/dialect/NamedBindMarkers.java | 1 - .../{domain => mapping}/OutboundRow.java | 2 +- .../R2dbcSimpleTypeHolder.java | 3 +- .../{domain => mapping}/SettableValue.java | 2 +- .../{domain => mapping}/package-info.java | 2 +- .../query/BoundAssignments.java | 2 +- .../{function => }/query/BoundCondition.java | 2 +- .../r2dbc/{function => }/query/Criteria.java | 2 +- .../{function => }/query/QueryMapper.java | 10 ++--- .../r2dbc/{function => }/query/Update.java | 2 +- .../{function => }/query/UpdateMapper.java | 6 +-- .../{function => }/query/package-info.java | 2 +- .../config/EnableR2dbcRepositories.java | 2 +- .../repository/query/AbstractR2dbcQuery.java | 8 ++-- .../r2dbc/repository/query/BindableQuery.java | 2 +- .../repository/query/R2dbcQueryExecution.java | 2 +- .../query/StringBasedR2dbcQuery.java | 6 +-- .../repository/support/BindSpecAdapter.java | 2 +- .../support/R2dbcRepositoryFactory.java | 6 +-- .../support/R2dbcRepositoryFactoryBean.java | 4 +- .../support/SimpleR2dbcRepository.java | 13 +++--- .../DatabaseClientExtensions.kt | 2 +- .../RowsFetchSpecExtensions.kt | 2 +- .../R2dbcConfigurationIntegrationTests.java | 2 +- .../ConnectionFactoryUtilsUnitTests.java | 5 ++- .../DelegatingConnectionFactoryUnitTests.java | 3 +- .../R2dbcTransactionManagerUnitTests.java} | 13 +++--- ...nAwareConnectionFactoryProxyUnitTests.java | 10 +++-- .../convert/EntityRowMapperUnitTests.java | 6 +-- .../MappingR2dbcConverterUnitTests.java | 8 ++-- .../convert/R2dbcConvertersUnitTests.java | 21 +++++----- ...bstractDatabaseClientIntegrationTests.java | 9 ++-- ...ctionalDatabaseClientIntegrationTests.java | 12 +++--- .../DefaultDatabaseClientUnitTests.java | 6 ++- .../H2DatabaseClientIntegrationTests.java | 4 +- .../MySqlDatabaseClientIntegrationTests.java | 4 +- ...ctionalDatabaseClientIntegrationTests.java | 4 +- .../NamedParameterUtilsUnitTests.java | 10 +++-- ...ostgresDatabaseClientIntegrationTests.java | 3 +- .../PostgresIntegrationTests.java | 3 +- ...stgresReactiveDataAccessStrategyTests.java | 4 +- ...ctionalDatabaseClientIntegrationTests.java | 3 +- ...ReactiveDataAccessStrategyTestSupport.java | 7 ++-- ...lServerDatabaseClientIntegrationTests.java | 3 +- ...ServerReactiveDataAccessStrategyTests.java | 4 +- ...ctionalDatabaseClientIntegrationTests.java | 3 +- .../StatementMapperUnitTests.java | 17 ++++---- .../AnonymousBindMarkersUnitTests.java | 2 - .../data/r2dbc/dialect/BindingsUnitTests.java | 2 - .../dialect/IndexedBindMarkersUnitTests.java | 2 - .../dialect/NamedBindMarkersUnitTests.java | 2 - .../SettableValueUnitTests.java | 3 +- .../query/CriteriaUnitTests.java | 9 ++-- .../query/QueryMapperUnitTests.java | 13 +++--- .../query/UpdateMapperUnitTests.java | 13 +++--- ...stractR2dbcRepositoryIntegrationTests.java | 6 +-- .../R2dbcRepositoriesRegistrarTests.java | 6 +-- .../query/StringBasedR2dbcQueryUnitTests.java | 6 +-- ...SimpleR2dbcRepositoryIntegrationTests.java | 6 +-- .../R2dbcRepositoryFactoryUnitTests.java | 6 +-- .../DatabaseClientExtensionsTests.kt | 2 +- .../RowsFetchSpecExtensionsTests.kt | 2 +- 124 files changed, 388 insertions(+), 365 deletions(-) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ConnectionFactoryUtils.java (99%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ConnectionHandle.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ConnectionHolder.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ConnectionProxy.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/DefaultTransactionResources.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/DelegatingConnectionFactory.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function/connectionfactory/ConnectionFactoryTransactionManager.java => connectionfactory/R2dbcTransactionManager.java} (97%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/R2dbcTransactionObjectSupport.java (94%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ReactiveTransactionSynchronization.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/SimpleConnectionHandle.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/SingletonConnectionFactory.java (93%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/SmartConnectionFactory.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/TransactionAwareConnectionFactoryProxy.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/TransactionResources.java (93%) rename src/main/java/org/springframework/data/r2dbc/{function => }/connectionfactory/package-info.java (67%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/ColumnMapRowMapper.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/EntityRowMapper.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/IterableUtils.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/MappingR2dbcConverter.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/R2dbcConverter.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/R2dbcConverters.java (88%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/R2dbcCustomConversions.java (94%) rename src/main/java/org/springframework/data/r2dbc/{function => }/convert/package-info.java (71%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/BindParameterSource.java (97%) rename src/main/java/org/springframework/data/r2dbc/{domain => core}/BindableOperation.java (90%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/ConnectionAccessor.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DatabaseClient.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultDatabaseClient.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultDatabaseClientBuilder.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultFetchSpec.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultReactiveDataAccessStrategy.java (94%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultSqlResult.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultStatementMapper.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultTransactionalDatabaseClient.java (94%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/DefaultTransactionalDatabaseClientBuilder.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/FetchSpec.java (94%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/MapBindParameterSource.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/NamedParameterExpander.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/NamedParameterUtils.java (98%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/ParsedSql.java (98%) rename src/main/java/org/springframework/data/r2dbc/{domain => core}/PreparedOperation.java (79%) rename src/main/java/org/springframework/data/r2dbc/{domain => core}/QueryOperation.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/ReactiveDataAccessStrategy.java (89%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/RowsFetchSpec.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/SqlProvider.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/SqlResult.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/StatementMapper.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/TransactionalDatabaseClient.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/UpdatedRowsFetchSpec.java (95%) rename src/main/java/org/springframework/data/r2dbc/{function => core}/package-info.java (72%) rename src/main/java/org/springframework/data/r2dbc/{domain => dialect}/BindTarget.java (93%) rename src/main/java/org/springframework/data/r2dbc/{domain => mapping}/OutboundRow.java (99%) rename src/main/java/org/springframework/data/r2dbc/{dialect => mapping}/R2dbcSimpleTypeHolder.java (93%) rename src/main/java/org/springframework/data/r2dbc/{domain => mapping}/SettableValue.java (98%) rename src/main/java/org/springframework/data/r2dbc/{domain => mapping}/package-info.java (66%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/BoundAssignments.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/BoundCondition.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/Criteria.java (99%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/QueryMapper.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/Update.java (97%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/UpdateMapper.java (96%) rename src/main/java/org/springframework/data/r2dbc/{function => }/query/package-info.java (67%) rename src/main/kotlin/org/springframework/data/r2dbc/{function => core}/DatabaseClientExtensions.kt (97%) rename src/main/kotlin/org/springframework/data/r2dbc/{function => core}/RowsFetchSpecExtensions.kt (97%) rename src/test/java/org/springframework/data/r2dbc/{function => }/connectionfactory/ConnectionFactoryUtilsUnitTests.java (92%) rename src/test/java/org/springframework/data/r2dbc/{function => }/connectionfactory/DelegatingConnectionFactoryUnitTests.java (92%) rename src/test/java/org/springframework/data/r2dbc/{function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java => connectionfactory/R2dbcTransactionManagerUnitTests.java} (96%) rename src/test/java/org/springframework/data/r2dbc/{function => }/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java (92%) rename src/test/java/org/springframework/data/r2dbc/{function => }/convert/EntityRowMapperUnitTests.java (95%) rename src/test/java/org/springframework/data/r2dbc/{function => }/convert/MappingR2dbcConverterUnitTests.java (96%) rename src/test/java/org/springframework/data/r2dbc/{function => }/convert/R2dbcConvertersUnitTests.java (78%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/AbstractDatabaseClientIntegrationTests.java (98%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/AbstractTransactionalDatabaseClientIntegrationTests.java (95%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/DefaultDatabaseClientUnitTests.java (90%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/H2DatabaseClientIntegrationTests.java (93%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/MySqlDatabaseClientIntegrationTests.java (94%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/MySqlTransactionalDatabaseClientIntegrationTests.java (93%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/NamedParameterUtilsUnitTests.java (96%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/PostgresDatabaseClientIntegrationTests.java (94%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/PostgresIntegrationTests.java (97%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/PostgresReactiveDataAccessStrategyTests.java (85%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/PostgresTransactionalDatabaseClientIntegrationTests.java (89%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/ReactiveDataAccessStrategyTestSupport.java (96%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/SqlServerDatabaseClientIntegrationTests.java (93%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/SqlServerReactiveDataAccessStrategyTests.java (85%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/SqlServerTransactionalDatabaseClientIntegrationTests.java (90%) rename src/test/java/org/springframework/data/r2dbc/{function => core}/StatementMapperUnitTests.java (76%) rename src/test/java/org/springframework/data/r2dbc/{domain => mapping}/SettableValueUnitTests.java (93%) rename src/test/java/org/springframework/data/r2dbc/{function => }/query/CriteriaUnitTests.java (94%) rename src/test/java/org/springframework/data/r2dbc/{function => }/query/QueryMapperUnitTests.java (93%) rename src/test/java/org/springframework/data/r2dbc/{function => }/query/UpdateMapperUnitTests.java (87%) rename src/test/kotlin/org/springframework/data/r2dbc/{function => core}/DatabaseClientExtensionsTests.kt (98%) rename src/test/kotlin/org/springframework/data/r2dbc/{function => core}/RowsFetchSpecExtensionsTests.kt (98%) diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index 249c4cb5..aa908696 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -140,31 +140,31 @@ public class R2dbcApp { DatabaseClient client = DatabaseClient.create(connectionFactory); client.execute() - .sql("CREATE TABLE person" + - "(id VARCHAR(255) PRIMARY KEY," + - "name VARCHAR(255)," + - "age INT)") - .fetch() - .rowsUpdated() - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + .sql("CREATE TABLE person" + + "(id VARCHAR(255) PRIMARY KEY," + + "name VARCHAR(255)," + + "age INT)") + .fetch() + .rowsUpdated() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); client.insert() - .into(Person.class) - .using(new Person("joe", "Joe", 34)) - .then() - .as(StepVerifier::create) - .verifyComplete(); + .into(Person.class) + .using(new Person("joe", "Joe", 34)) + .then() + .as(StepVerifier::create) + .verifyComplete(); client.select() - .from(Person.class) - .fetch() - .first() - .doOnNext(it -> log.info(it)) - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); + .from(Person.class) + .fetch() + .first() + .doOnNext(it -> log.info(it)) + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); } } ---- diff --git a/src/main/asciidoc/reference/r2dbc-databaseclient.adoc b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc index 2809ac18..9419217c 100644 --- a/src/main/asciidoc/reference/r2dbc-databaseclient.adoc +++ b/src/main/asciidoc/reference/r2dbc-databaseclient.adoc @@ -34,10 +34,10 @@ Once built, a `DatabaseClient` instance is immutable. However, you can clone it [source,java] ---- DatabaseClient client1 = DatabaseClient.builder() - .exceptionTranslator(exceptionTranslatorA).build(); + .exceptionTranslator(exceptionTranslatorA).build(); DatabaseClient client2 = client1.mutate() - .exceptionTranslator(exceptionTranslatorB).build(); + .exceptionTranslator(exceptionTranslatorB).build(); ---- == Controlling Database Connections @@ -98,12 +98,14 @@ You can extend `SqlErrorCodeR2dbcExceptionTranslator`, as the following example ---- public class CustomSqlErrorCodeR2dbcExceptionTranslator extends SqlErrorCodeR2dbcExceptionTranslator { - protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) { - if (sqlex.getErrorCode() == -12345) { - return new DeadlockLoserDataAccessException(task, r2dbcex); - } - return null; + protected DataAccessException customTranslate(String task, String sql, R2dbcException r2dbcex) { + + if (sqlex.getErrorCode() == -12345) { + return new DeadlockLoserDataAccessException(task, r2dbcex); } + + return null; + } } ---- @@ -115,10 +117,11 @@ The following example shows how you can use this custom translator: ---- ConnectionFactory connectionFactory = …; -CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = new CustomSqlErrorCodeR2dbcExceptionTranslator(); +CustomSqlErrorCodeR2dbcExceptionTranslator exceptionTranslator = + new CustomSqlErrorCodeR2dbcExceptionTranslator(); DatabaseClient client = DatabaseClient.builder() - .connectionFactory(connectionFactory) - .exceptionTranslator(exceptionTranslator) - .build(); + .connectionFactory(connectionFactory) + .exceptionTranslator(exceptionTranslator) + .build(); ---- diff --git a/src/main/asciidoc/reference/r2dbc-fluent.adoc b/src/main/asciidoc/reference/r2dbc-fluent.adoc index 7ac0eadd..55f5a9a4 100644 --- a/src/main/asciidoc/reference/r2dbc-fluent.adoc +++ b/src/main/asciidoc/reference/r2dbc-fluent.adoc @@ -12,9 +12,9 @@ Let's take a look at a simple query: [source,java] ---- Flux people = databaseClient.select() - .from(Person.class) <1> - .fetch() - .all(); <2> + .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. @@ -26,13 +26,13 @@ The following example declares a more complex query that specifies the table nam [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> + .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. @@ -166,9 +166,9 @@ Take a look at a simple typed update operation: Person modified = … Mono update = databaseClient.update() - .table(Person.class) <1> - .using(modified) <2> - .then(); <3> + .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. @@ -181,10 +181,10 @@ 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> + .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. @@ -217,10 +217,10 @@ 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> + .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. diff --git a/src/main/asciidoc/reference/r2dbc-repositories.adoc b/src/main/asciidoc/reference/r2dbc-repositories.adoc index 3a243aee..90d5dc92 100644 --- a/src/main/asciidoc/reference/r2dbc-repositories.adoc +++ b/src/main/asciidoc/reference/r2dbc-repositories.adoc @@ -74,16 +74,16 @@ Consequently, you can retrieve all `Person` objects would resemble the following @ContextConfiguration public class PersonRepositoryTests { - @Autowired PersonRepository repository; + @Autowired PersonRepository repository; - @Test - public void readsAllEntitiesCorrectly() { + @Test + public void readsAllEntitiesCorrectly() { - repository.findAll() - .as(StepVerifier::create) - .expectNextCount(1) - .verifyComplete(); - } + repository.findAll() + .as(StepVerifier::create) + .expectNextCount(1) + .verifyComplete(); + } } ---- ==== @@ -104,11 +104,11 @@ Defining such a query is a matter of declaring a method on the repository interf ---- public interface PersonRepository extends ReactiveCrudRepository { - @Query("SELECT * FROM person WHERE lastname = :lastname") - Flux findByLastname(String lastname); <1> + @Query("SELECT * FROM person WHERE lastname = :lastname") + Flux findByLastname(String lastname); <1> - @Query("SELECT firstname, lastname FROM person WHERE lastname = $1") - Mono findFirstByLastname(String lastname) <2> + @Query("SELECT firstname, lastname FROM person WHERE lastname = $1") + Mono findFirstByLastname(String lastname) <2> } ---- diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc index 0f0fdbab..9379b51a 100644 --- a/src/main/asciidoc/reference/r2dbc-transactions.adoc +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -6,14 +6,14 @@ Relational databases typically associate a transaction with a single transport c Using different connections hence results in utilizing different transactions. Spring Data R2DBC includes transaction-awareness in `DatabaseClient` that allows you to group multiple statements within the same transaction using https://docs.spring.io/spring/docs/current/spring-framework-reference/data-access.html#transaction[Spring's Transaction Management]. -Spring Data R2DBC provides a implementation for `ReactiveTransactionManager` with `ConnectionFactoryTransactionManager`. -See <> for further details. +Spring Data R2DBC provides a implementation for `ReactiveTransactionManager` with `R2dbcTransactionManager`. +See <> for further details. .Programmatic Transaction Management ==== [source,java] ---- -ReactiveTransactionManager tm = new ConnectionFactoryTransactionManager(connectionFactory); +ReactiveTransactionManager tm = new R2dbcTransactionManager(connectionFactory); TransactionalOperator operator = TransactionalOperator.create(tm); <1> DatabaseClient client = DatabaseClient.create(connectionFactory); @@ -43,7 +43,7 @@ is a less invasive, annotation-based approach to transaction demarcation. [source,java] ---- @Configuration -@EnableTransactionManagement <1> +@EnableTransactionManagement <1> class Config extends AbstractR2dbcConfiguration { @Override @@ -52,8 +52,8 @@ class Config extends AbstractR2dbcConfiguration { } @Bean - ReactiveTransactionManager txMgr(ConnectionFactory connectionFactory) { <2> - return new ConnectionFactoryTransactionManager(connectionFactory); + ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) { <2> + return new R2dbcTransactionManager(connectionFactory); } } @@ -83,5 +83,5 @@ class MyService { } ---- <1> Enable declarative transaction management. -<2> Provide a `ReactiveTransactionManager` implementation to back reactive tansaction features. +<2> Provide a `ReactiveTransactionManager` implementation to back reactive transaction features. ==== diff --git a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java index 1900509c..e4023adf 100644 --- a/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java +++ b/src/main/java/org/springframework/data/r2dbc/InvalidResultAccessException.java @@ -24,7 +24,7 @@ import org.springframework.lang.Nullable; * Exception thrown when a {@link io.r2dbc.spi.Result} has been accessed in an invalid fashion. Such exceptions always * have a {@link io.r2dbc.spi.R2dbcException} root cause. *

- * This typically happens when an invalid {@link org.springframework.data.r2dbc.function.FetchSpec} column index or name + * This typically happens when an invalid {@link org.springframework.data.r2dbc.core.FetchSpec} column index or name * has been specified. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 85043985..f5c048ca 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -28,13 +28,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.CustomConversions.StoreConversions; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcCustomConversions; import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.data.r2dbc.support.SqlStateR2dbcExceptionTranslator; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtils.java similarity index 99% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtils.java index f54db094..a0199f94 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -35,7 +35,7 @@ import reactor.util.function.Tuples; * Helper class that provides static methods for obtaining R2DBC Connections from a * {@link io.r2dbc.spi.ConnectionFactory}. *

- * Used internally by Spring's {@link org.springframework.data.r2dbc.function.DatabaseClient}, Spring's R2DBC operation + * Used internally by Spring's {@link org.springframework.data.r2dbc.core.DatabaseClient}, Spring's R2DBC operation * objects. Can also be used directly in application code. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHandle.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHandle.java index ed5a4e6f..fb055523 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHandle.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHandle.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHolder.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHolder.java index f84f6c7e..1db09c4a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionHolder.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -22,7 +22,7 @@ import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; /** - * Resource holder wrapping a R2DBC {@link Connection}. {@link ConnectionFactoryTransactionManager} binds instances of + * Resource holder wrapping a R2DBC {@link Connection}. {@link R2dbcTransactionManager} binds instances of * this class to the thread, for a specific {@link ConnectionFactory}. *

* Inherits rollback-only support for nested R2DBC transactions and reference count functionality from the base class. @@ -31,7 +31,7 @@ import org.springframework.util.Assert; * * @author Mark Paluch * @author Christoph Strobl - * @see ConnectionFactoryTransactionManager + * @see R2dbcTransactionManager * @see ConnectionFactoryUtils */ public class ConnectionHolder extends ResourceHolderSupport { @@ -86,7 +86,7 @@ public class ConnectionHolder extends ResourceHolderSupport { /** * Set whether this holder represents an active, R2DBC-managed transaction. * - * @see ConnectionFactoryTransactionManager + * @see R2dbcTransactionManager */ protected void setTransactionActive(boolean transactionActive) { this.transactionActive = transactionActive; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionProxy.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionProxy.java index 38bd4611..ff55b56b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ConnectionProxy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.Wrapped; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/DefaultTransactionResources.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/DefaultTransactionResources.java index 74c02193..57304547 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DefaultTransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/DefaultTransactionResources.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactory.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactory.java index 3b577691..692ce10f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java index 4bb0caf6..e1b92df9 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManager.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManager.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -24,7 +24,7 @@ import reactor.core.publisher.Mono; import java.time.Duration; import org.springframework.beans.factory.InitializingBean; -import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.TransactionDefinition; @@ -70,7 +70,7 @@ import org.springframework.util.Assert; * @see TransactionAwareConnectionFactoryProxy * @see DatabaseClient */ -public class ConnectionFactoryTransactionManager extends AbstractReactiveTransactionManager +public class R2dbcTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean { private ConnectionFactory connectionFactory; @@ -83,14 +83,14 @@ public class ConnectionFactoryTransactionManager extends AbstractReactiveTransac * * @see #setConnectionFactory */ - public ConnectionFactoryTransactionManager() {} + public R2dbcTransactionManager() {} /** - * Create a new {@link ConnectionFactoryTransactionManager} instance. + * Create a new {@link R2dbcTransactionManager} instance. * * @param connectionFactory the R2DBC ConnectionFactory to manage transactions for */ - public ConnectionFactoryTransactionManager(ConnectionFactory connectionFactory) { + public R2dbcTransactionManager(ConnectionFactory connectionFactory) { this(); setConnectionFactory(connectionFactory); afterPropertiesSet(); diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionObjectSupport.java similarity index 94% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionObjectSupport.java index 4c386899..9226f183 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/R2dbcTransactionObjectSupport.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionObjectSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.IsolationLevel; import org.springframework.lang.Nullable; @@ -24,7 +24,7 @@ import org.springframework.util.Assert; * {@link io.r2dbc.spi.Connection}. * * @author Mark Paluch - * @see ConnectionFactoryTransactionManager + * @see R2dbcTransactionManager */ public abstract class R2dbcTransactionObjectSupport { diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ReactiveTransactionSynchronization.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/ReactiveTransactionSynchronization.java index a476097d..4c8487a0 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/ReactiveTransactionSynchronization.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/ReactiveTransactionSynchronization.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import java.util.Stack; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SimpleConnectionHandle.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/SimpleConnectionHandle.java index 3ef2336e..20857a09 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SimpleConnectionHandle.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SimpleConnectionHandle.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SingletonConnectionFactory.java similarity index 93% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/SingletonConnectionFactory.java index 355688c6..ed993ffd 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SingletonConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SingletonConnectionFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactoryMetadata; @@ -25,7 +25,7 @@ import org.reactivestreams.Publisher; /** * Connection holder, wrapping a R2DBC Connection. - * {@link org.springframework.data.r2dbc.function.TransactionalDatabaseClient} binds instances of this class to the + * {@link org.springframework.data.r2dbc.core.TransactionalDatabaseClient} binds instances of this class to the * {@link TransactionResources} for a specific subscription. * * @author Mark Paluch diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SmartConnectionFactory.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/SmartConnectionFactory.java index 39a3bbed..d257c8b1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/SmartConnectionFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/SmartConnectionFactory.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxy.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxy.java index 0568a78a..ad8650ee 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxy.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import static org.springframework.util.ReflectionUtils.*; @@ -25,7 +25,8 @@ import java.lang.reflect.Proxy; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Wrapped; -import org.springframework.data.r2dbc.function.DatabaseClient; + +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import reactor.core.publisher.Mono; @@ -36,7 +37,7 @@ import reactor.util.function.Tuple2; *

* Data access code that should remain unaware of Spring's data access support can work with this proxy to seamlessly * participate in Spring-managed transactions. Note that the transaction manager, for example - * {@link ConnectionFactoryTransactionManager}, still needs to work with the underlying {@link ConnectionFactory}, + * {@link R2dbcTransactionManager}, still needs to work with the underlying {@link ConnectionFactory}, * not with this proxy. *

* Make sure that {@link TransactionAwareConnectionFactoryProxy} is the outermost {@link ConnectionFactory} of a @@ -44,7 +45,7 @@ import reactor.util.function.Tuple2; * either directly to the target connection pool or to some intermediary proxy/adapter. *

* Delegates to {@link ConnectionFactoryUtils} for automatically participating in thread-bound transactions, for example - * managed by {@link ConnectionFactoryTransactionManager}. {@link #create()} calls and {@code close} calls on returned + * managed by {@link R2dbcTransactionManager}. {@link #create()} calls and {@code close} calls on returned * {@link Connection} will behave properly within a transaction, i.e. always operate on the transactional Connection. If * not within a transaction, normal {@link ConnectionFactory} behavior applies. *

diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionResources.java similarity index 93% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionResources.java index 1ae45cc6..5324ae2b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionResources.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/TransactionResources.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import reactor.core.publisher.Mono; -import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; /** * Transaction context for an ongoing transaction synchronization allowing to register transactional resources. diff --git a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/package-info.java b/src/main/java/org/springframework/data/r2dbc/connectionfactory/package-info.java similarity index 67% rename from src/main/java/org/springframework/data/r2dbc/function/connectionfactory/package-info.java rename to src/main/java/org/springframework/data/r2dbc/connectionfactory/package-info.java index bc8fcfb0..6e09329b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/connectionfactory/package-info.java +++ b/src/main/java/org/springframework/data/r2dbc/connectionfactory/package-info.java @@ -3,4 +3,4 @@ */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java b/src/main/java/org/springframework/data/r2dbc/convert/ColumnMapRowMapper.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java rename to src/main/java/org/springframework/data/r2dbc/convert/ColumnMapRowMapper.java index 4015f861..3997d023 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/ColumnMapRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/ColumnMapRowMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Row; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java rename to src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java index 5e2b9f27..a551d9c0 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/EntityRowMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java b/src/main/java/org/springframework/data/r2dbc/convert/IterableUtils.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java rename to src/main/java/org/springframework/data/r2dbc/convert/IterableUtils.java index a1de338e..a2a8976d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/IterableUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/IterableUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import java.util.ArrayList; import java.util.Collection; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java rename to src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java index 003d3273..4224c55f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Row; @@ -39,8 +39,8 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ParameterValueProvider; import org.springframework.data.r2dbc.dialect.ArrayColumns; -import org.springframework.data.r2dbc.domain.OutboundRow; -import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.data.relational.core.conversion.BasicRelationalConverter; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java rename to src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java index 6f8083ac..48522355 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverter.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -25,7 +25,7 @@ import org.springframework.data.convert.EntityReader; import org.springframework.data.convert.EntityWriter; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.ArrayColumns; -import org.springframework.data.r2dbc.domain.OutboundRow; +import org.springframework.data.r2dbc.mapping.OutboundRow; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverters.java similarity index 88% rename from src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java rename to src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverters.java index 8315fbdf..37ea4472 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcConverters.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverters.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import io.r2dbc.spi.Row; @@ -33,13 +33,13 @@ import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.Jsr310Converters; import org.springframework.data.convert.WritingConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateConverterOverride; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateTimeConverterOverride; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.LocalTimeConverterOverride; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateConverterOverride; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.LocalDateTimeConverterOverride; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.LocalTimeConverterOverride; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java similarity index 94% rename from src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java rename to src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java index 57bacf30..1013a276 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java @@ -1,4 +1,4 @@ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import java.util.ArrayList; import java.util.Collection; @@ -7,7 +7,7 @@ import java.util.List; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.JodaTimeConverters; -import org.springframework.data.r2dbc.dialect.R2dbcSimpleTypeHolder; +import org.springframework.data.r2dbc.mapping.R2dbcSimpleTypeHolder; /** * Value object to capture custom conversion. {@link R2dbcCustomConversions} also act as factory for diff --git a/src/main/java/org/springframework/data/r2dbc/function/convert/package-info.java b/src/main/java/org/springframework/data/r2dbc/convert/package-info.java similarity index 71% rename from src/main/java/org/springframework/data/r2dbc/function/convert/package-info.java rename to src/main/java/org/springframework/data/r2dbc/convert/package-info.java index 6839311b..cb313f6a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/convert/package-info.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/package-info.java @@ -3,4 +3,4 @@ */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; diff --git a/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/core/BindParameterSource.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java rename to src/main/java/org/springframework/data/r2dbc/core/BindParameterSource.java index ed709fa0..0e9c8777 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/BindParameterSource.java +++ b/src/main/java/org/springframework/data/r2dbc/core/BindParameterSource.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import org.springframework.lang.Nullable; diff --git a/src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java b/src/main/java/org/springframework/data/r2dbc/core/BindableOperation.java similarity index 90% rename from src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java rename to src/main/java/org/springframework/data/r2dbc/core/BindableOperation.java index 22f1baa3..b7bf15fd 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/BindableOperation.java +++ b/src/main/java/org/springframework/data/r2dbc/core/BindableOperation.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.core; import io.r2dbc.spi.Statement; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.domain.QueryOperation; -import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.dialect.BindTarget; +import org.springframework.data.r2dbc.mapping.SettableValue; /** * Extension to {@link QueryOperation} for operations that allow parameter substitution by binding parameter values. diff --git a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java b/src/main/java/org/springframework/data/r2dbc/core/ConnectionAccessor.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java rename to src/main/java/org/springframework/data/r2dbc/core/ConnectionAccessor.java index 5a88dbf2..ce21988d 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ConnectionAccessor.java +++ b/src/main/java/org/springframework/data/r2dbc/core/ConnectionAccessor.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Connection; import reactor.core.publisher.Flux; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java index 39f7c8bf..1b787e39 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Row; @@ -29,10 +29,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.mapping.SettableValue; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Update; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java index b0b77440..ce4923e7 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -51,16 +51,14 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.UncategorizedR2dbcException; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.BindableOperation; -import org.springframework.data.r2dbc.domain.OutboundRow; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils; -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionProxy; -import org.springframework.data.r2dbc.function.convert.ColumnMapRowMapper; -import org.springframework.data.r2dbc.function.query.Criteria; -import org.springframework.data.r2dbc.function.query.Update; +import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; +import org.springframework.data.r2dbc.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.convert.ColumnMapRowMapper; +import org.springframework.data.r2dbc.dialect.BindTarget; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Update; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java index a8d18b50..8edd56d9 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java @@ -14,15 +14,15 @@ * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import java.util.function.Consumer; +import org.springframework.data.r2dbc.core.DatabaseClient.Builder; import org.springframework.data.r2dbc.dialect.Database; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.function.DatabaseClient.Builder; import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.lang.Nullable; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultFetchSpec.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultFetchSpec.java index a1e8556c..0f64b7ed 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultFetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Connection; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java similarity index 94% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index a7b7af1b..841c6c1a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -27,16 +27,16 @@ import java.util.function.Function; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.convert.EntityRowMapper; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; import org.springframework.data.r2dbc.dialect.ArrayColumns; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.OutboundRow; -import org.springframework.data.r2dbc.domain.SettableValue; -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.UpdateMapper; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.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; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultSqlResult.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultSqlResult.java index 294e5ce1..bf318929 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultSqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultSqlResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Connection; import io.r2dbc.spi.Result; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java index bdf7c31c..ae2d8065 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultStatementMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import lombok.RequiredArgsConstructor; @@ -26,13 +26,12 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.r2dbc.dialect.BindMarkers; +import org.springframework.data.r2dbc.dialect.BindTarget; import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.function.query.BoundAssignments; -import org.springframework.data.r2dbc.function.query.BoundCondition; -import org.springframework.data.r2dbc.function.query.UpdateMapper; +import org.springframework.data.r2dbc.query.BoundAssignments; +import org.springframework.data.r2dbc.query.BoundCondition; +import org.springframework.data.r2dbc.query.UpdateMapper; import org.springframework.data.r2dbc.support.StatementRenderUtil; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClient.java similarity index 94% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClient.java index 3d78303d..741e61d6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; @@ -25,10 +25,9 @@ import reactor.util.function.Tuple2; import java.util.function.Function; import org.reactivestreams.Publisher; - -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils; -import org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization; -import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; +import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; +import org.springframework.data.r2dbc.connectionfactory.ReactiveTransactionSynchronization; +import org.springframework.data.r2dbc.connectionfactory.TransactionResources; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.transaction.NoTransactionException; diff --git a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClientBuilder.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java rename to src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClientBuilder.java index 444afd26..f7bbcfa6 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/DefaultTransactionalDatabaseClientBuilder.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultTransactionalDatabaseClientBuilder.java @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import java.util.function.Consumer; -import org.springframework.data.r2dbc.function.DatabaseClient.Builder; +import org.springframework.data.r2dbc.core.DatabaseClient.Builder; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java b/src/main/java/org/springframework/data/r2dbc/core/FetchSpec.java similarity index 94% rename from src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/core/FetchSpec.java index cf3080c6..158945ce 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/FetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/core/FetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; /** * Contract for fetching results. diff --git a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java b/src/main/java/org/springframework/data/r2dbc/core/MapBindParameterSource.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java rename to src/main/java/org/springframework/data/r2dbc/core/MapBindParameterSource.java index 2d031916..40717b75 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/MapBindParameterSource.java +++ b/src/main/java/org/springframework/data/r2dbc/core/MapBindParameterSource.java @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java b/src/main/java/org/springframework/data/r2dbc/core/NamedParameterExpander.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java rename to src/main/java/org/springframework/data/r2dbc/core/NamedParameterExpander.java index a7162061..e2f419d3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterExpander.java +++ b/src/main/java/org/springframework/data/r2dbc/core/NamedParameterExpander.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import java.util.LinkedHashMap; import java.util.Map; @@ -22,8 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.BindableOperation; +import org.springframework.data.r2dbc.dialect.BindTarget; /** * SQL translation support allowing the use of named parameters rather than native placeholders. diff --git a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java b/src/main/java/org/springframework/data/r2dbc/core/NamedParameterUtils.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java rename to src/main/java/org/springframework/data/r2dbc/core/NamedParameterUtils.java index 048d0124..88ff3134 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/NamedParameterUtils.java +++ b/src/main/java/org/springframework/data/r2dbc/core/NamedParameterUtils.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import lombok.Value; @@ -30,8 +30,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.BindableOperation; +import org.springframework.data.r2dbc.dialect.BindTarget; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java b/src/main/java/org/springframework/data/r2dbc/core/ParsedSql.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java rename to src/main/java/org/springframework/data/r2dbc/core/ParsedSql.java index 651be9c8..e25df40a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ParsedSql.java +++ b/src/main/java/org/springframework/data/r2dbc/core/ParsedSql.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/org/springframework/data/r2dbc/domain/PreparedOperation.java b/src/main/java/org/springframework/data/r2dbc/core/PreparedOperation.java similarity index 79% rename from src/main/java/org/springframework/data/r2dbc/domain/PreparedOperation.java rename to src/main/java/org/springframework/data/r2dbc/core/PreparedOperation.java index 51365804..1d21a1de 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/PreparedOperation.java +++ b/src/main/java/org/springframework/data/r2dbc/core/PreparedOperation.java @@ -13,21 +13,23 @@ * 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.core; import java.util.function.Supplier; +import org.springframework.data.r2dbc.dialect.BindTarget; + /** * Extension to {@link QueryOperation} for a prepared SQL query {@link Supplier} with bound parameters. Contains * parameter bindings that can be {@link #bindTo bound} bound to a {@link BindTarget}. *

- * Can be executed with {@link org.springframework.data.r2dbc.function.DatabaseClient}. + * Can be executed with {@link org.springframework.data.r2dbc.core.DatabaseClient}. *

* * @param underlying operation source. * @author Mark Paluch - * @see org.springframework.data.r2dbc.function.DatabaseClient - * @see org.springframework.data.r2dbc.function.DatabaseClient.SqlSpec#sql(Supplier) + * @see org.springframework.data.r2dbc.core.DatabaseClient + * @see org.springframework.data.r2dbc.core.DatabaseClient.SqlSpec#sql(Supplier) */ public interface PreparedOperation extends QueryOperation { diff --git a/src/main/java/org/springframework/data/r2dbc/domain/QueryOperation.java b/src/main/java/org/springframework/data/r2dbc/core/QueryOperation.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/domain/QueryOperation.java rename to src/main/java/org/springframework/data/r2dbc/core/QueryOperation.java index 05d70c9e..698a9c8a 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/QueryOperation.java +++ b/src/main/java/org/springframework/data/r2dbc/core/QueryOperation.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.core; import java.util.function.Supplier; diff --git a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java similarity index 89% rename from src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java rename to src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java index 7e331544..0070cd6a 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategy.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; @@ -21,12 +21,11 @@ import io.r2dbc.spi.RowMetadata; import java.util.List; import java.util.function.BiFunction; +import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.BindableOperation; -import org.springframework.data.r2dbc.domain.OutboundRow; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; /** * Data access strategy that generalizes convenience operations using mapped entities. Typically used internally by diff --git a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/core/RowsFetchSpec.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/core/RowsFetchSpec.java index 6c96962c..06f3030f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/RowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/core/RowsFetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java b/src/main/java/org/springframework/data/r2dbc/core/SqlProvider.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java rename to src/main/java/org/springframework/data/r2dbc/core/SqlProvider.java index 83843249..6a04e381 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlProvider.java +++ b/src/main/java/org/springframework/data/r2dbc/core/SqlProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import org.springframework.lang.Nullable; diff --git a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java b/src/main/java/org/springframework/data/r2dbc/core/SqlResult.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/SqlResult.java rename to src/main/java/org/springframework/data/r2dbc/core/SqlResult.java index 1c6cac09..fe4a6d4e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/SqlResult.java +++ b/src/main/java/org/springframework/data/r2dbc/core/SqlResult.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; diff --git a/src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java b/src/main/java/org/springframework/data/r2dbc/core/StatementMapper.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java rename to src/main/java/org/springframework/data/r2dbc/core/StatementMapper.java index 0f03e37c..6641eb5f 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/StatementMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/core/StatementMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import java.util.ArrayList; import java.util.Collection; @@ -26,10 +26,9 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.query.Criteria; -import org.springframework.data.r2dbc.function.query.Update; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Update; import org.springframework.lang.Nullable; /** diff --git a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java rename to src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java index f1c6a0bf..f0d6a350 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import reactor.core.publisher.Flux; @@ -23,8 +23,7 @@ import java.util.function.Consumer; import java.util.function.Function; import org.reactivestreams.Publisher; - -import org.springframework.data.r2dbc.function.connectionfactory.TransactionResources; +import org.springframework.data.r2dbc.connectionfactory.TransactionResources; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.Assert; @@ -76,9 +75,9 @@ import org.springframework.util.Assert; * @see #beginTransaction() * @see #commitTransaction() * @see #rollbackTransaction() - * @see org.springframework.data.r2dbc.function.connectionfactory.ReactiveTransactionSynchronization + * @see org.springframework.data.r2dbc.connectionfactory.ReactiveTransactionSynchronization * @see TransactionResources - * @see org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryUtils + * @see org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils * @deprecated Use {@link DatabaseClient} in combination with {@link TransactionalOperator}. */ @Deprecated diff --git a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java b/src/main/java/org/springframework/data/r2dbc/core/UpdatedRowsFetchSpec.java similarity index 95% rename from src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java rename to src/main/java/org/springframework/data/r2dbc/core/UpdatedRowsFetchSpec.java index 2410018c..c810aacc 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/UpdatedRowsFetchSpec.java +++ b/src/main/java/org/springframework/data/r2dbc/core/UpdatedRowsFetchSpec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import reactor.core.publisher.Mono; diff --git a/src/main/java/org/springframework/data/r2dbc/function/package-info.java b/src/main/java/org/springframework/data/r2dbc/core/package-info.java similarity index 72% rename from src/main/java/org/springframework/data/r2dbc/function/package-info.java rename to src/main/java/org/springframework/data/r2dbc/core/package-info.java index 67ca6025..42634076 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/package-info.java +++ b/src/main/java/org/springframework/data/r2dbc/core/package-info.java @@ -3,4 +3,4 @@ */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java index 87b47ea4..1afa700c 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/BindMarker.java @@ -2,8 +2,6 @@ package org.springframework.data.r2dbc.dialect; import io.r2dbc.spi.Statement; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * A bind marker represents a single bindable parameter within a query. Bind markers are dialect-specific and provide a * {@link #getPlaceholder() placeholder} that is used in the actual query. diff --git a/src/main/java/org/springframework/data/r2dbc/domain/BindTarget.java b/src/main/java/org/springframework/data/r2dbc/dialect/BindTarget.java similarity index 93% rename from src/main/java/org/springframework/data/r2dbc/domain/BindTarget.java rename to src/main/java/org/springframework/data/r2dbc/dialect/BindTarget.java index 0215e7ec..07ea885b 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/BindTarget.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/BindTarget.java @@ -13,7 +13,9 @@ * 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 org.springframework.data.r2dbc.core.PreparedOperation; /** * Target to apply bindings to. diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java index 94fb90e0..fce5fb76 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Bindings.java @@ -27,7 +27,6 @@ import java.util.Map; import java.util.Spliterator; import java.util.function.Consumer; -import org.springframework.data.r2dbc.domain.BindTarget; import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; import org.springframework.util.Assert; diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java index 9314527a..7edc6e82 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/Dialect.java @@ -7,6 +7,7 @@ import java.util.Set; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.r2dbc.dialect.ArrayColumns.Unsupported; +import org.springframework.data.r2dbc.mapping.R2dbcSimpleTypeHolder; /** * Represents a dialect that is implemented by a particular database. diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java index 06f8ec61..7f4e3d9b 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/IndexedBindMarker.java @@ -15,8 +15,6 @@ */ package org.springframework.data.r2dbc.dialect; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * A single indexed bind marker. */ diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java index 6f874662..7f1dd0f5 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/NamedBindMarkers.java @@ -3,7 +3,6 @@ package org.springframework.data.r2dbc.dialect; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import java.util.function.Function; -import org.springframework.data.r2dbc.domain.BindTarget; import org.springframework.util.Assert; /** diff --git a/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java b/src/main/java/org/springframework/data/r2dbc/mapping/OutboundRow.java similarity index 99% rename from src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java rename to src/main/java/org/springframework/data/r2dbc/mapping/OutboundRow.java index 159c80bc..1ae4af49 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/OutboundRow.java +++ b/src/main/java/org/springframework/data/r2dbc/mapping/OutboundRow.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.mapping; import io.r2dbc.spi.Row; diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java b/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcSimpleTypeHolder.java similarity index 93% rename from src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java rename to src/main/java/org/springframework/data/r2dbc/mapping/R2dbcSimpleTypeHolder.java index 3b6aa93f..2d6fb645 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/R2dbcSimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcSimpleTypeHolder.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.dialect; +package org.springframework.data.r2dbc.mapping; import io.r2dbc.spi.Row; @@ -25,7 +25,6 @@ import java.util.HashSet; import java.util.Set; import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.r2dbc.domain.OutboundRow; /** * Simple constant holder for a {@link SimpleTypeHolder} enriched with R2DBC specific simple types. diff --git a/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java similarity index 98% rename from src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java rename to src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java index 36926bb2..a190cfb6 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.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.mapping; import java.util.Objects; diff --git a/src/main/java/org/springframework/data/r2dbc/domain/package-info.java b/src/main/java/org/springframework/data/r2dbc/mapping/package-info.java similarity index 66% rename from src/main/java/org/springframework/data/r2dbc/domain/package-info.java rename to src/main/java/org/springframework/data/r2dbc/mapping/package-info.java index 2ca5d1d8..b3712b5e 100644 --- a/src/main/java/org/springframework/data/r2dbc/domain/package-info.java +++ b/src/main/java/org/springframework/data/r2dbc/mapping/package-info.java @@ -2,6 +2,6 @@ * Domain objects for R2DBC. */ @NonNullApi -package org.springframework.data.r2dbc.domain; +package org.springframework.data.r2dbc.mapping; import org.springframework.lang.NonNullApi; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java b/src/main/java/org/springframework/data/r2dbc/query/BoundAssignments.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java rename to src/main/java/org/springframework/data/r2dbc/query/BoundAssignments.java index 0a3bd3f1..1de3da13 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/BoundAssignments.java +++ b/src/main/java/org/springframework/data/r2dbc/query/BoundAssignments.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import java.util.List; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java b/src/main/java/org/springframework/data/r2dbc/query/BoundCondition.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java rename to src/main/java/org/springframework/data/r2dbc/query/BoundCondition.java index a9cba313..a500056b 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/BoundCondition.java +++ b/src/main/java/org/springframework/data/r2dbc/query/BoundCondition.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.relational.core.sql.Condition; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java b/src/main/java/org/springframework/data/r2dbc/query/Criteria.java similarity index 99% rename from src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java rename to src/main/java/org/springframework/data/r2dbc/query/Criteria.java index be48f741..0e0668c3 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Criteria.java +++ b/src/main/java/org/springframework/data/r2dbc/query/Criteria.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java rename to src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java index 752a1d0c..91077059 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/QueryMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/query/QueryMapper.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import java.util.ArrayList; import java.util.Collection; @@ -27,14 +27,14 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.InvalidPersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.MutableBindings; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; -import org.springframework.data.r2dbc.function.query.Criteria.Combinator; -import org.springframework.data.r2dbc.function.query.Criteria.Comparator; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.query.Criteria.Combinator; +import org.springframework.data.r2dbc.query.Criteria.Comparator; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.core.sql.Column; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/Update.java b/src/main/java/org/springframework/data/r2dbc/query/Update.java similarity index 97% rename from src/main/java/org/springframework/data/r2dbc/function/query/Update.java rename to src/main/java/org/springframework/data/r2dbc/query/Update.java index 6d590406..48e5f28e 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/Update.java +++ b/src/main/java/org/springframework/data/r2dbc/query/Update.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import java.util.Collections; import java.util.LinkedHashMap; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java b/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java similarity index 96% rename from src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java rename to src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java index b28a7abf..195348f1 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/UpdateMapper.java +++ b/src/main/java/org/springframework/data/r2dbc/query/UpdateMapper.java @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import java.util.ArrayList; import java.util.List; import java.util.Map; +import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.dialect.BindMarker; import org.springframework.data.r2dbc.dialect.BindMarkers; import org.springframework.data.r2dbc.dialect.Bindings; import org.springframework.data.r2dbc.dialect.MutableBindings; -import org.springframework.data.r2dbc.domain.SettableValue; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.sql.AssignValue; import org.springframework.data.relational.core.sql.Assignment; diff --git a/src/main/java/org/springframework/data/r2dbc/function/query/package-info.java b/src/main/java/org/springframework/data/r2dbc/query/package-info.java similarity index 67% rename from src/main/java/org/springframework/data/r2dbc/function/query/package-info.java rename to src/main/java/org/springframework/data/r2dbc/query/package-info.java index 42e57792..572bc042 100644 --- a/src/main/java/org/springframework/data/r2dbc/function/query/package-info.java +++ b/src/main/java/org/springframework/data/r2dbc/query/package-info.java @@ -3,4 +3,4 @@ */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java index b0e0e10a..eeface9f 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/config/EnableR2dbcRepositories.java @@ -117,7 +117,7 @@ public @interface EnableR2dbcRepositories { Class repositoryBaseClass() default DefaultRepositoryBaseClass.class; /** - * Configures the name of the {@link org.springframework.data.r2dbc.function.DatabaseClient} bean to be used with the + * Configures the name of the {@link org.springframework.data.r2dbc.core.DatabaseClient} bean to be used with the * repositories detected. * * @return diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java index b3e17cf3..3adca38f 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java @@ -22,10 +22,10 @@ import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.r2dbc.function.FetchSpec; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.FetchSpec; +import org.springframework.data.r2dbc.core.DatabaseClient.GenericExecuteSpec; import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingConverter; import org.springframework.data.r2dbc.repository.query.R2dbcQueryExecution.ResultProcessingExecution; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java index bff01011..7d171bbb 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/BindableQuery.java @@ -17,7 +17,7 @@ package org.springframework.data.r2dbc.repository.query; import java.util.function.Supplier; -import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; +import org.springframework.data.r2dbc.core.DatabaseClient.BindSpec; /** * Interface declaring a query that supplies SQL and can bind parameters to a {@link BindSpec}. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java index 32ce93f4..77a657ee 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/R2dbcQueryExecution.java @@ -21,7 +21,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.EntityInstantiators; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.r2dbc.function.FetchSpec; +import org.springframework.data.r2dbc.core.FetchSpec; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.repository.query.DtoInstantiatingConverter; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java index 833ee534..8840d840 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java @@ -15,9 +15,9 @@ */ package org.springframework.data.r2dbc.repository.query; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.DatabaseClient.BindSpec; import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java index 6b2cd6b0..d28609ac 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/BindSpecAdapter.java @@ -4,7 +4,7 @@ import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; import org.reactivestreams.Publisher; -import org.springframework.data.r2dbc.function.DatabaseClient.BindSpec; +import org.springframework.data.r2dbc.core.DatabaseClient.BindSpec; /** * Adapter for {@link BindSpec} to be used with {@link org.springframework.data.r2dbc.dialect.BindMarker} binding. diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java index e2bdbeec..2679df57 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactory.java @@ -23,9 +23,9 @@ import java.util.Optional; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.repository.R2dbcRepository; import org.springframework.data.r2dbc.repository.query.R2dbcQueryMethod; import org.springframework.data.r2dbc.repository.query.StringBasedR2dbcQuery; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java index 61ffd297..90b87cf0 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryBean.java @@ -18,8 +18,8 @@ package org.springframework.data.r2dbc.repository.support; import java.io.Serializable; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java index 94f43b1f..70c000b4 100644 --- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java +++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java @@ -24,13 +24,12 @@ import java.util.Collections; import java.util.List; import org.reactivestreams.Publisher; - -import org.springframework.data.r2dbc.domain.PreparedOperation; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.StatementMapper; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; -import org.springframework.data.r2dbc.function.query.Criteria; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.PreparedOperation; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.StatementMapper; +import org.springframework.data.r2dbc.query.Criteria; import org.springframework.data.relational.core.sql.Functions; import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.StatementBuilder; diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt similarity index 97% rename from src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt rename to src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt index e1348935..999139e0 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function +package org.springframework.data.r2dbc.core import kotlinx.coroutines.reactive.awaitFirstOrNull diff --git a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensions.kt similarity index 97% rename from src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt rename to src/main/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensions.kt index ed147389..69fdb224 100644 --- a/src/main/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensions.kt +++ b/src/main/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensions.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function +package org.springframework.data.r2dbc.core import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow diff --git a/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java index 2295cbd7..7e844965 100644 --- a/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/config/R2dbcConfigurationIntegrationTests.java @@ -27,7 +27,7 @@ import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.r2dbc.function.DatabaseClient; +import org.springframework.data.r2dbc.core.DatabaseClient; /** * Tests for {@link AbstractR2dbcConfiguration}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtilsUnitTests.java similarity index 92% rename from src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtilsUnitTests.java index b77a1a23..29c425fe 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/ConnectionFactoryUtilsUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import static org.mockito.Mockito.*; @@ -22,6 +22,9 @@ import io.r2dbc.spi.ConnectionFactory; import org.assertj.core.api.Assertions; import org.junit.Test; import org.reactivestreams.Publisher; +import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; +import org.springframework.data.r2dbc.connectionfactory.ReactiveTransactionSynchronization; +import org.springframework.data.r2dbc.connectionfactory.TransactionResources; import org.springframework.transaction.NoTransactionException; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactoryUnitTests.java similarity index 92% rename from src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactoryUnitTests.java index d1ad0757..3a105e4a 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/DelegatingConnectionFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/DelegatingConnectionFactoryUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -23,6 +23,7 @@ import io.r2dbc.spi.ConnectionFactory; import reactor.core.publisher.Mono; import org.junit.Test; +import org.springframework.data.r2dbc.connectionfactory.DelegatingConnectionFactory; /** * Unit tests for {@link DelegatingConnectionFactory}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManagerUnitTests.java similarity index 96% rename from src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManagerUnitTests.java index b13fb55e..a9df5dd3 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/ConnectionFactoryTransactionManagerUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/R2dbcTransactionManagerUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.*; @@ -32,7 +32,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; - +import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; +import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.reactive.TransactionSynchronization; @@ -41,16 +42,16 @@ import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.transaction.support.DefaultTransactionDefinition; /** - * Unit tests for {@link ConnectionFactoryTransactionManager}. + * Unit tests for {@link R2dbcTransactionManager}. * * @author Mark Paluch */ -public class ConnectionFactoryTransactionManagerUnitTests { +public class R2dbcTransactionManagerUnitTests { ConnectionFactory connectionFactoryMock = mock(ConnectionFactory.class); Connection connectionMock = mock(Connection.class); - private ConnectionFactoryTransactionManager tm; + private R2dbcTransactionManager tm; @Before public void before() { @@ -58,7 +59,7 @@ public class ConnectionFactoryTransactionManagerUnitTests { when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock)); when(connectionMock.beginTransaction()).thenReturn(Mono.empty()); when(connectionMock.close()).thenReturn(Mono.empty()); - tm = new ConnectionFactoryTransactionManager(connectionFactoryMock); + tm = new R2dbcTransactionManager(connectionFactoryMock); } @Test // gh-107 diff --git a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java b/src/test/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java similarity index 92% rename from src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java index 6a578eed..261fd2e7 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/connectionfactory/TransactionAwareConnectionFactoryProxyUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.connectionfactory; +package org.springframework.data.r2dbc.connectionfactory; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -24,6 +24,10 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import org.junit.Before; import org.junit.Test; +import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; +import org.springframework.data.r2dbc.connectionfactory.ConnectionFactoryUtils; +import org.springframework.data.r2dbc.connectionfactory.ConnectionProxy; +import org.springframework.data.r2dbc.connectionfactory.TransactionAwareConnectionFactoryProxy; import org.springframework.transaction.reactive.TransactionalOperator; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -42,14 +46,14 @@ public class TransactionAwareConnectionFactoryProxyUnitTests { Connection connectionMock2 = mock(Connection.class); Connection connectionMock3 = mock(Connection.class); - private ConnectionFactoryTransactionManager tm; + private R2dbcTransactionManager tm; @Before public void before() { when(connectionFactoryMock.create()).thenReturn((Mono) Mono.just(connectionMock1), (Mono) Mono.just(connectionMock2), (Mono) Mono.just(connectionMock3)); - tm = new ConnectionFactoryTransactionManager(connectionFactoryMock); + tm = new R2dbcTransactionManager(connectionFactoryMock); } @Test // gh-107 diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java similarity index 95% rename from src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java index 307d710e..215591e2 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/EntityRowMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java @@ -1,4 +1,4 @@ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -13,9 +13,9 @@ import java.util.Set; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; - +import org.springframework.data.r2dbc.convert.EntityRowMapper; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.PostgresDialect; -import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; /** * Unit tests for {@link EntityRowMapper}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java similarity index 96% rename from src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java index 69573c61..d2cfd6df 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/MappingR2dbcConverterUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverterUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -33,8 +33,10 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; -import org.springframework.data.r2dbc.domain.OutboundRow; -import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.data.relational.core.mapping.RelationalMappingContext; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/R2dbcConvertersUnitTests.java similarity index 78% rename from src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/convert/R2dbcConvertersUnitTests.java index d26aebd4..9e4eb0f6 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/convert/R2dbcConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/convert/R2dbcConvertersUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.convert; +package org.springframework.data.r2dbc.convert; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -30,15 +30,16 @@ import java.util.UUID; import org.junit.Test; import org.springframework.core.convert.converter.Converter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToBooleanConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalDateConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalDateTimeConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToLocalTimeConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; -import org.springframework.data.r2dbc.function.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToBooleanConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToLocalDateConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToLocalDateTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToLocalTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverters.RowToNumberConverterFactory.RowToZonedDateTimeConverter; /** * Unit tests for {@link R2dbcConverters}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java similarity index 98% rename from src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index a39e633e..595f7c87 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.domain.Sort.Order.*; -import static org.springframework.data.r2dbc.function.query.Criteria.*; +import static org.springframework.data.r2dbc.query.Criteria.*; import io.r2dbc.spi.ConnectionFactory; import lombok.Data; @@ -33,8 +33,9 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.r2dbc.function.query.Criteria; -import org.springframework.data.r2dbc.function.query.Update; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Update; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.Table; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java similarity index 95% rename from src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java index d47d4388..0c46b65f 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/AbstractTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; @@ -34,7 +34,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataAccessException; import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; -import org.springframework.data.r2dbc.function.connectionfactory.ConnectionFactoryTransactionManager; +import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.transaction.NoTransactionException; @@ -244,7 +246,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); TransactionalOperator transactionalOperator = TransactionalOperator - .create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition()); + .create(new R2dbcTransactionManager(connectionFactory), new DefaultTransactionDefinition()); Flux txId = databaseClient.execute() // .sql(getCurrentTransactionIdStatement()) // @@ -269,7 +271,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); TransactionalOperator transactionalOperator = TransactionalOperator - .create(new ConnectionFactoryTransactionManager(connectionFactory), new DefaultTransactionDefinition()); + .create(new R2dbcTransactionManager(connectionFactory), new DefaultTransactionDefinition()); Flux integerFlux = databaseClient.execute() // .sql(getInsertIntoLegosetStatement()) // @@ -331,7 +333,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend @Bean ReactiveTransactionManager txMgr(ConnectionFactory connectionFactory) { - return new ConnectionFactoryTransactionManager(connectionFactory); + return new R2dbcTransactionManager(connectionFactory); } } diff --git a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java b/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java similarity index 90% rename from src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java index 7fdba539..b4b7bccb 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/DefaultDatabaseClientUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.mockito.Mockito.*; @@ -30,7 +30,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; - +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.DefaultDatabaseClient; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/H2DatabaseClientIntegrationTests.java similarity index 93% rename from src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/H2DatabaseClientIntegrationTests.java index 0292ff1d..54797d0c 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/H2DatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/H2DatabaseClientIntegrationTests.java @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import javax.sql.DataSource; import org.junit.Ignore; - +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.testing.H2TestSupport; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java similarity index 94% rename from src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java index a91699f9..3aaf86ca 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/MySqlDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; @@ -22,7 +22,7 @@ import javax.sql.DataSource; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; - +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.MySqlTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/MySqlTransactionalDatabaseClientIntegrationTests.java similarity index 93% rename from src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/MySqlTransactionalDatabaseClientIntegrationTests.java index 75194234..707eadf9 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/MySqlTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/MySqlTransactionalDatabaseClientIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import javax.sql.DataSource; import java.time.Duration; @@ -22,6 +22,8 @@ import io.r2dbc.spi.ConnectionFactory; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.MySqlTestSupport; import reactor.core.publisher.Mono; diff --git a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/core/NamedParameterUtilsUnitTests.java similarity index 96% rename from src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/core/NamedParameterUtilsUnitTests.java index 784a67ba..ed4e5939 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/NamedParameterUtilsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/NamedParameterUtilsUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -22,12 +22,14 @@ import java.util.Arrays; import java.util.HashMap; import org.junit.Test; - +import org.springframework.data.r2dbc.core.BindableOperation; +import org.springframework.data.r2dbc.core.MapBindParameterSource; +import org.springframework.data.r2dbc.core.NamedParameterUtils; +import org.springframework.data.r2dbc.core.ParsedSql; import org.springframework.data.r2dbc.dialect.BindMarkersFactory; +import org.springframework.data.r2dbc.dialect.BindTarget; import org.springframework.data.r2dbc.dialect.PostgresDialect; import org.springframework.data.r2dbc.dialect.SqlServerDialect; -import org.springframework.data.r2dbc.domain.BindTarget; -import org.springframework.data.r2dbc.domain.BindableOperation; /** * Unit tests for {@link NamedParameterUtils}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresDatabaseClientIntegrationTests.java similarity index 94% rename from src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/PostgresDatabaseClientIntegrationTests.java index a0fb9a27..7f855d60 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresDatabaseClientIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; @@ -22,6 +22,7 @@ import javax.sql.DataSource; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.PostgresTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java similarity index 97% rename from src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java index ba1d5346..139c8dbf 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; @@ -31,6 +31,7 @@ import org.junit.Before; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.PostgresTestSupport; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java similarity index 85% rename from src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java rename to src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java index 0231787b..0cbf243d 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresReactiveDataAccessStrategyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresReactiveDataAccessStrategyTests.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.PostgresDialect; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresTransactionalDatabaseClientIntegrationTests.java similarity index 89% rename from src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/PostgresTransactionalDatabaseClientIntegrationTests.java index 039df349..767117c5 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/PostgresTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresTransactionalDatabaseClientIntegrationTests.java @@ -1,10 +1,11 @@ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import javax.sql.DataSource; import org.junit.ClassRule; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.PostgresTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java b/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java similarity index 96% rename from src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java rename to src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java index f0ff0903..1c43affc 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/ReactiveDataAccessStrategyTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -34,9 +34,10 @@ import java.util.function.BiConsumer; import java.util.function.Function; import org.junit.Test; - +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.Dialect; -import org.springframework.data.r2dbc.domain.SettableValue; +import org.springframework.data.r2dbc.mapping.SettableValue; /** * Abstract base class for {@link Dialect}-aware {@link DefaultReactiveDataAccessStrategy} tests. diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/SqlServerDatabaseClientIntegrationTests.java similarity index 93% rename from src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/SqlServerDatabaseClientIntegrationTests.java index c9856a81..1655b0ba 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/SqlServerDatabaseClientIntegrationTests.java @@ -13,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import javax.sql.DataSource; import org.junit.ClassRule; +import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.SqlServerTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java b/src/test/java/org/springframework/data/r2dbc/core/SqlServerReactiveDataAccessStrategyTests.java similarity index 85% rename from src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java rename to src/test/java/org/springframework/data/r2dbc/core/SqlServerReactiveDataAccessStrategyTests.java index a1766d04..90aca9e5 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerReactiveDataAccessStrategyTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/SqlServerReactiveDataAccessStrategyTests.java @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.SqlServerDialect; /** diff --git a/src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/SqlServerTransactionalDatabaseClientIntegrationTests.java similarity index 90% rename from src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java rename to src/test/java/org/springframework/data/r2dbc/core/SqlServerTransactionalDatabaseClientIntegrationTests.java index 7485a4db..0c5f924b 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/SqlServerTransactionalDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/SqlServerTransactionalDatabaseClientIntegrationTests.java @@ -1,10 +1,11 @@ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import io.r2dbc.spi.ConnectionFactory; import javax.sql.DataSource; import org.junit.ClassRule; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.SqlServerTestSupport; diff --git a/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/core/StatementMapperUnitTests.java similarity index 76% rename from src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/core/StatementMapperUnitTests.java index bd5ed805..5357284c 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/StatementMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/StatementMapperUnitTests.java @@ -13,19 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function; +package org.springframework.data.r2dbc.core; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import org.junit.Test; - +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.DefaultStatementMapper; +import org.springframework.data.r2dbc.core.PreparedOperation; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.StatementMapper; +import org.springframework.data.r2dbc.core.StatementMapper.UpdateSpec; +import org.springframework.data.r2dbc.dialect.BindTarget; 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; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Update; /** * Unit tests for {@link DefaultStatementMapper}. diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java index 55a26287..87d03bcc 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/AnonymousBindMarkersUnitTests.java @@ -20,8 +20,6 @@ import static org.mockito.Mockito.*; import org.junit.Test; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * Unit tests for {@link AnonymousBindMarkers}. * diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/BindingsUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/BindingsUnitTests.java index 29593308..fa1a218c 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/BindingsUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/BindingsUnitTests.java @@ -24,8 +24,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * Unit tests for {@link Bindings}. * diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java index eafbd640..924f3d53 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/IndexedBindMarkersUnitTests.java @@ -5,8 +5,6 @@ import static org.mockito.Mockito.*; import org.junit.Test; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * Unit tests for {@link IndexedBindMarkers}. * diff --git a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java index e92aa8bc..52729fad 100644 --- a/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/dialect/NamedBindMarkersUnitTests.java @@ -5,8 +5,6 @@ import static org.mockito.Mockito.*; import org.junit.Test; -import org.springframework.data.r2dbc.domain.BindTarget; - /** * Unit tests for {@link NamedBindMarkers}. * diff --git a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java b/src/test/java/org/springframework/data/r2dbc/mapping/SettableValueUnitTests.java similarity index 93% rename from src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/mapping/SettableValueUnitTests.java index 08d9c887..673b0c39 100644 --- a/src/test/java/org/springframework/data/r2dbc/domain/SettableValueUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/mapping/SettableValueUnitTests.java @@ -13,11 +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.mapping; import static org.assertj.core.api.Assertions.*; import org.junit.Test; +import org.springframework.data.r2dbc.mapping.SettableValue; /** * Unit tests for {@link SettableValue}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java b/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java similarity index 94% rename from src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java index d001e433..13ef11a4 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/CriteriaUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/query/CriteriaUnitTests.java @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.r2dbc.function.query.Criteria.*; +import static org.springframework.data.r2dbc.query.Criteria.*; import java.util.Arrays; import org.junit.Test; -import org.springframework.data.r2dbc.function.query.Criteria.Combinator; -import org.springframework.data.r2dbc.function.query.Criteria.Comparator; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.Criteria.Combinator; +import org.springframework.data.r2dbc.query.Criteria.Comparator; /** * Unit tests for {@link Criteria}. diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/query/QueryMapperUnitTests.java similarity index 93% rename from src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/query/QueryMapperUnitTests.java index acef4b8f..ba429cb6 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/QueryMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/query/QueryMapperUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -21,11 +21,14 @@ import static org.springframework.data.domain.Sort.Order.*; import org.junit.Test; import org.springframework.data.domain.Sort; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; 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.r2dbc.dialect.BindTarget; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.query.BoundCondition; +import org.springframework.data.r2dbc.query.Criteria; +import org.springframework.data.r2dbc.query.QueryMapper; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.sql.Table; diff --git a/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/query/UpdateMapperUnitTests.java similarity index 87% rename from src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java rename to src/test/java/org/springframework/data/r2dbc/query/UpdateMapperUnitTests.java index 83c5e4ff..3843f642 100644 --- a/src/test/java/org/springframework/data/r2dbc/function/query/UpdateMapperUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/query/UpdateMapperUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function.query; +package org.springframework.data.r2dbc.query; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -22,11 +22,14 @@ import java.util.Map; import java.util.stream.Collectors; import org.junit.Test; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; 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.r2dbc.dialect.BindTarget; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.query.BoundAssignments; +import org.springframework.data.r2dbc.query.Update; +import org.springframework.data.r2dbc.query.UpdateMapper; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.sql.AssignValue; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java index 01ba1016..eb06b0e3 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java @@ -37,10 +37,10 @@ import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.TransactionalDatabaseClient; import org.springframework.data.r2dbc.dialect.Database; -import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.TransactionalDatabaseClient; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.RelationalMappingContext; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java index b84adfa0..d9433c1c 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/config/R2dbcRepositoriesRegistrarTests.java @@ -24,10 +24,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.PostgresDialect; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DefaultReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java index de7037ee..252f53ec 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java @@ -29,9 +29,9 @@ import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.DatabaseClient.GenericExecuteSpec; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.DatabaseClient.GenericExecuteSpec; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java index e516dfee..a424a537 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/AbstractSimpleR2dbcRepositoryIntegrationTests.java @@ -38,9 +38,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Persistable; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.convert.MappingR2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; diff --git a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java index 0197149f..1c60b8e2 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/support/R2dbcRepositoryFactoryUnitTests.java @@ -25,9 +25,9 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.r2dbc.function.DatabaseClient; -import org.springframework.data.r2dbc.function.ReactiveDataAccessStrategy; -import org.springframework.data.r2dbc.function.convert.R2dbcConverter; +import org.springframework.data.r2dbc.convert.R2dbcConverter; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.repository.query.RelationalEntityInformation; import org.springframework.data.relational.repository.support.MappingRelationalEntityInformation; diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt similarity index 98% rename from src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt rename to src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt index 4f0e1f5b..fb4f7f71 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/DatabaseClientExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function +package org.springframework.data.r2dbc.core import io.mockk.every import io.mockk.mockk diff --git a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensionsTests.kt similarity index 98% rename from src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt rename to src/test/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensionsTests.kt index f6a432de..38cc6468 100644 --- a/src/test/kotlin/org/springframework/data/r2dbc/function/RowsFetchSpecExtensionsTests.kt +++ b/src/test/kotlin/org/springframework/data/r2dbc/core/RowsFetchSpecExtensionsTests.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.r2dbc.function +package org.springframework.data.r2dbc.core import io.mockk.every import io.mockk.mockk From a5c72a61b427d6ebaa5d231d36e677694cd7d4bf Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 13 May 2019 18:22:33 +0200 Subject: [PATCH 118/665] #115 - Upgrade to Spring Data Moore M4. Upgraded Spring Data parent, Commons and Relational modules to Moore M4 versions. --- pom.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index a577fa59..43b22401 100644 --- a/pom.xml +++ b/pom.xml @@ -16,15 +16,15 @@ org.springframework.data.build spring-data-parent - 2.2.0.BUILD-SNAPSHOT + 2.2.0.M4 DATAR2DBC - 2.2.0.BUILD-SNAPSHOT - 1.1.0.BUILD-SNAPSHOT + 2.2.0.M4 + 1.1.0.M4 spring.data.r2dbc reuseReports From 49fb08dc7d707327e94ccdb74e15da363b6dbb31 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Mon, 13 May 2019 19:43:58 +0200 Subject: [PATCH 119/665] #116 - Upgrade to R2DBC 0.8 M8. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 43b22401..9b533a0e 100644 --- a/pom.xml +++ b/pom.xml @@ -34,7 +34,7 @@ 5.1.47 0.9.38 7.1.2.jre8-preview - Arabba-BUILD-SNAPSHOT + Arabba-M8 1.0.1 1.10.1 From 4a46692dfe6ede1fa2ea186e25fc76b054279334 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 09:12:26 +0200 Subject: [PATCH 120/665] #117 - Upgrade to jasync-sql 0.9.51. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9b533a0e..1d2ee0f7 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ 2.4.1 42.2.5 5.1.47 - 0.9.38 + 0.9.51 7.1.2.jre8-preview Arabba-M8 1.0.1 From 430d98450364a384d1eff418369917f38ecf11b9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 09:35:46 +0200 Subject: [PATCH 121/665] #109 - Fix simple type conversion for projection queries. We now correctly consider built-in converters for simple types that are read through DatabaseClient. Simple type projections typically select a single column and expect a result stream of simple values such as selecting a count and retrieving a Mono. --- .../DefaultReactiveDataAccessStrategy.java | 3 +- .../data/r2dbc/config/H2IntegrationTests.java | 120 ++++++++++++++++++ ...bstractDatabaseClientIntegrationTests.java | 25 +++- 3 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/config/H2IntegrationTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index 841c6c1a..5e135eb6 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -76,7 +76,8 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra Assert.notNull(dialect, "Dialect must not be null"); R2dbcCustomConversions customConversions = new R2dbcCustomConversions( - StoreConversions.of(dialect.getSimpleTypeHolder()), Collections.emptyList()); + StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS), + Collections.emptyList()); RelationalMappingContext context = new RelationalMappingContext(); context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); diff --git a/src/test/java/org/springframework/data/r2dbc/config/H2IntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/config/H2IntegrationTests.java new file mode 100644 index 00000000..d0450653 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/config/H2IntegrationTests.java @@ -0,0 +1,120 @@ +/* + * 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.config; + +import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.r2dbc.core.DatabaseClient; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.repository.query.Query; +import org.springframework.data.r2dbc.testing.H2TestSupport; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration test for {@link DatabaseClient} and repositories using H2. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +@ContextConfiguration +public class H2IntegrationTests { + + private JdbcTemplate jdbc = new JdbcTemplate(H2TestSupport.createDataSource()); + + @Autowired DatabaseClient databaseClient; + @Autowired H2Repository repository; + + @Before + public void before() { + + try { + jdbc.execute("DROP TABLE legoset"); + } catch (DataAccessException e) {} + jdbc.execute(H2TestSupport.CREATE_TABLE_LEGOSET); + } + + @Test // gh-109 + public void shouldSelectCountWithDatabaseClient() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + databaseClient.execute().sql("SELECT COUNT(*) FROM legoset") // + .as(Long.class) // + .fetch() // + .all() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Test // gh-109 + public void shouldSelectCountWithRepository() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + repository.selectCount() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + } + + @Configuration + @EnableR2dbcRepositories(considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = H2Repository.class), + basePackageClasses = H2Repository.class) + static class H2Configuration extends AbstractR2dbcConfiguration { + + @Override + public ConnectionFactory connectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + } + + interface H2Repository extends ReactiveCrudRepository { + + @Query("SELECT COUNT(*) FROM legoset") + Mono selectCount(); + } + + @Data + @Table("legoset") + @AllArgsConstructor + @NoArgsConstructor + static class LegoSet { + @Id Integer id; + String name; + Integer manual; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java index 595f7c87..62b5ea2a 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java @@ -33,7 +33,6 @@ import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.annotation.Id; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; -import org.springframework.data.r2dbc.core.DatabaseClient; import org.springframework.data.r2dbc.query.Criteria; import org.springframework.data.r2dbc.query.Update; import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport; @@ -340,6 +339,30 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr .verifyComplete(); } + @Test // gh-109 + public void selectSimpleTypeProjection() { + + jdbc.execute("INSERT INTO legoset (id, name, manual) VALUES(42055, 'SCHAUFELRADBAGGER', 12)"); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.execute().sql("SELECT COUNT(*) FROM legoset") // + .as(Long.class) // + .fetch() // + .all() // + .as(StepVerifier::create) // + .expectNext(1L) // + .verifyComplete(); + + databaseClient.execute().sql("SELECT name FROM legoset") // + .as(String.class) // + .fetch() // + .one() // + .as(StepVerifier::create) // + .expectNext("SCHAUFELRADBAGGER") // + .verifyComplete(); + } + @Test // gh-8 public void selectWithCriteria() { From 55a6ae0a2a1fafe7f92c5f95f09e2c8fc779dd10 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 10:31:49 +0200 Subject: [PATCH 122/665] #37 - Updated changelog. --- src/main/resources/changelog.txt | 34 ++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/resources/changelog.txt b/src/main/resources/changelog.txt index 13f04817..20e9b0e9 100644 --- a/src/main/resources/changelog.txt +++ b/src/main/resources/changelog.txt @@ -1,6 +1,40 @@ Spring Data R2DBC Changelog =========================== +Changes in version 1.0.0.M2 (2019-05-14) +---------------------------------------- +* #117 - Upgrade to jasync-sql 0.9.51. +* #116 - Upgrade to R2DBC 0.8 M8. +* #115 - Upgrade to Spring Data Moore M4. +* #111 - Revisit package structure and naming. +* #109 - Support mapping of simple types (e.g. to Long/Integer) out of the box. +* #108 - #107 - Add ConnectionFactoryTransactionManager and reactive transaction management utilities. +* #100 - Refactor code to not require Spring JDBC as mandatory dependency. +* #95 - Use @Configuration(proxyBeanMethods=false) for AbstractR2dbcConfiguration. +* #90 - Inserting a row without key generation via R2dbcRepository.save(…) completes without emitting objects. +* #86 - Add non-nullable variant to RowsFetchSpec extensions. +* #85 - Could not read property java.math.BigDecimal. +* #75 - Add support for MySQL. +* #74 - URL Cleanup. +* #73 - Introduce PreparedOperation. +* #65 - Add converters for simple type projections. +* #64 - Add criteria API to create filter predicates. +* #63 - Add DatabaseClient Coroutines extensions. +* #61 - Move Conversion-related functionality to MappingR2dbcConverter. +* #60 - Use R2DBC's BOM for dependency management. +* #59 - Consider custom conversion in EntityRowMapper and MappingR2dbcConverter. +* #57 - Add R2DBC-specific exception translation. +* #56 - Integrate Spring Data Relational's Statement Builder. +* #54 - Upgrade to R2DBC 1.0 M7. +* #52 - Don't depend on MSSQL JDBC driver. +* #51 - #29 - Use TestContainers for integration tests. +* #47 - Add support for named parameters. +* #45 - Update copyright years to 2019. +* #41 - Add support for simple type projections. +* #39 - Add support for Custom Conversion. +* #37 - Release 1.0 M2. + + Changes in version 1.0.0.M1 (2018-12-12) ---------------------------------------- * #36 - Release 1.0 M1. From cbead6ea197fb2ac3930247f7e034643ed63a105 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 10:31:50 +0200 Subject: [PATCH 123/665] #37 - Prepare 1.0 M2. --- pom.xml | 8 +++----- src/main/resources/notice.txt | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 1d2ee0f7..ce2112d2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,5 @@ - + 4.0.0 @@ -431,8 +429,8 @@ - spring-libs-snapshot - https://repo.spring.io/libs-snapshot + spring-libs-milestone + https://repo.spring.io/libs-milestone jcenter diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 04e43adc..be2e8597 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data R2DBC 1.0 M1 +Spring Data R2DBC 1.0 M2 Copyright (c) [2018-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). From 6ea15325e382a8a3444ab5a35a98208cbd73c009 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 10:31:52 +0200 Subject: [PATCH 124/665] #37 - Release version 1.0 M2. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index ce2112d2..c5f14ae8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.M2 Spring Data R2DBC Spring Data module for R2DBC. From b7d5091635feaa4385e5223e85a8dc77b0e75c76 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 10:39:32 +0200 Subject: [PATCH 125/665] #37 - Prepare next development iteration. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c5f14ae8..ce2112d2 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-r2dbc - 1.0.0.M2 + 1.0.0.BUILD-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC. From d23dcd0d2c7b2b2827ec678671cdacbe8cfd3ce0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 May 2019 10:39:33 +0200 Subject: [PATCH 126/665] #37 - After release cleanups. --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index ce2112d2..bad4f790 100644 --- a/pom.xml +++ b/pom.xml @@ -429,8 +429,8 @@ - spring-libs-milestone - https://repo.spring.io/libs-milestone + spring-libs-snapshot + https://repo.spring.io/libs-snapshot jcenter From 14e2a3722b2bee9554d62cda026c6543057f4b15 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 15 May 2019 15:25:03 +0200 Subject: [PATCH 127/665] #118 - Upgrade to jasync-r2dbc-mysql 0.9.52. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bad4f790..dff36486 100644 --- a/pom.xml +++ b/pom.xml @@ -30,7 +30,7 @@ 2.4.1 42.2.5 5.1.47 - 0.9.51 + 0.9.52 7.1.2.jre8-preview Arabba-M8 1.0.1 From 5714709d718ef5310bcf2ad49e0d027299934f86 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 17 May 2019 23:28:03 +0200 Subject: [PATCH 128/665] #30 - Polishing. Fix custom converter documentation. Accept custom converters in DefaultReactiveDataAccessStrategy constructor. --- src/main/asciidoc/reference/mapping.adoc | 6 ++-- .../r2dbc/convert/R2dbcCustomConversions.java | 6 ++-- .../DefaultReactiveDataAccessStrategy.java | 29 +++++++++++++++---- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 7cd2b17a..3ae078e6 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -180,9 +180,9 @@ public class PersonWriteConverter implements Converter { public OutboundRow convert(Person source) { OutboundRow row = new OutboundRow(); - row.put("_d", source.getId()); - row.put("name", source.getFirstName()); - row.put("age", source.getAge()); + row.put("_d", SettableValue.from(source.getId())); + row.put("name", SettableValue.from(source.getFirstName())); + row.put("age", SettableValue.from(source.getAge())); return row; } } diff --git a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java index 1013a276..5831b258 100644 --- a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java @@ -40,7 +40,7 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(Collection converters) { - super(STORE_CONVERSIONS, appendOverriddes(converters)); + super(STORE_CONVERSIONS, appendOverrides(converters)); } /** @@ -50,10 +50,10 @@ public class R2dbcCustomConversions extends CustomConversions { * @param converters must not be {@literal null}. */ public R2dbcCustomConversions(StoreConversions storeConversions, Collection converters) { - super(storeConversions, appendOverriddes(converters)); + super(storeConversions, appendOverrides(converters)); } - private static Collection appendOverriddes(Collection converters) { + private static Collection appendOverrides(Collection converters) { List objects = new ArrayList<>(converters); objects.addAll(R2dbcConverters.getOverrideConvertersToRegister()); diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java index 5e135eb6..3385f880 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -19,11 +19,13 @@ import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.convert.CustomConversions.StoreConversions; import org.springframework.data.mapping.context.MappingContext; @@ -37,9 +39,11 @@ import org.springframework.data.r2dbc.dialect.Dialect; import org.springframework.data.r2dbc.mapping.OutboundRow; import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.data.r2dbc.query.UpdateMapper; +import org.springframework.data.relational.core.mapping.NamingStrategy; 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.mapping.Table; import org.springframework.data.relational.core.sql.Select; import org.springframework.data.relational.core.sql.render.NamingStrategies; import org.springframework.data.relational.core.sql.render.RenderContext; @@ -48,6 +52,7 @@ import org.springframework.data.relational.core.sql.render.SelectRenderContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; /** * Default {@link ReactiveDataAccessStrategy} implementation. @@ -63,21 +68,35 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private final StatementMapper statementMapper; /** - * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect}. + * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and optional + * {@link org.springframework.core.convert.converter.Converter}s. * * @param dialect the {@link Dialect} to use. */ public DefaultReactiveDataAccessStrategy(Dialect dialect) { - this(dialect, createConverter(dialect)); + this(dialect, Collections.emptyList()); } - private static R2dbcConverter createConverter(Dialect dialect) { + /** + * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and optional + * {@link org.springframework.core.convert.converter.Converter}s. + * + * @param dialect the {@link Dialect} to use. + * @param converters custom converters to register, must not be {@literal null}. + * @see R2dbcCustomConversions + * @see org.springframework.core.convert.converter.Converter + */ + public DefaultReactiveDataAccessStrategy(Dialect dialect, Collection converters) { + this(dialect, createConverter(dialect, converters)); + } + + private static R2dbcConverter createConverter(Dialect dialect, Collection converters) { Assert.notNull(dialect, "Dialect must not be null"); + Assert.notNull(converters, "Converters must not be null"); R2dbcCustomConversions customConversions = new R2dbcCustomConversions( - StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS), - Collections.emptyList()); + StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS), converters); RelationalMappingContext context = new RelationalMappingContext(); context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); From 36c4c1d06247e78e12cb6c924af9416ef6662db8 Mon Sep 17 00:00:00 2001 From: Ohad Shai Date: Sun, 19 May 2019 14:44:58 +0300 Subject: [PATCH 129/665] #120 - Fix link text for jasync-sql. Original pull request: #120. --- src/main/asciidoc/reference/r2dbc-core.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index aa908696..6fdd9cbe 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -234,7 +234,7 @@ As of writing the following drivers are available: * https://github.com/r2dbc/r2dbc-postgresql[Postgres] (`io.r2dbc:r2dbc-postgresql`) * https://github.com/r2dbc/r2dbc-h2[H2] (`io.r2dbc:r2dbc-h2`) * https://github.com/r2dbc/r2dbc-mssql[Microsoft SQL Server] (`io.r2dbc:r2dbc-mssql`) -* https://github.com/jasync-sql/jasync-sql[Microsoft SQL Server] (`com.github.jasync-sql:jasync-r2dbc-mysql`) +* https://github.com/jasync-sql/jasync-sql[jasync-sql MySQL] (`com.github.jasync-sql:jasync-r2dbc-mysql`) Spring Data R2DBC reacts to database specifics by inspecting `ConnectionFactoryMetadata` exposed by the `ConnectionFactory` and selects the appropriate database dialect accordingly. You can configure an own https://docs.spring.io/spring-data/r2dbc/docs/{version}/api/org/springframework/data/r2dbc/dialect/Dialect.html[`Dialect`] if the used driver is not yet known to Spring Data R2DBC. From 8b77aa9431af576892d527886722bb84936ffa5b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 6 May 2019 15:06:56 +0200 Subject: [PATCH 130/665] =?UTF-8?q?#89=20-=20Accept=20SQL=20directly=20in?= =?UTF-8?q?=20DatabaseClient.execute(=E2=80=A6)=20stage.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We compressed client.execute().sql(…) to client.execute(…) to not require the intermediate execute() step but rather accept the SQL to execute directly. Original pull request: #112. --- src/main/asciidoc/reference/r2dbc-core.adoc | 3 +- src/main/asciidoc/reference/r2dbc-sql.adoc | 26 +++++-------- .../reference/r2dbc-transactions.adoc | 8 ++-- .../data/r2dbc/core/DatabaseClient.java | 39 ++++++++++++++++++- .../r2dbc/core/DefaultDatabaseClient.java | 16 ++++++++ .../core/TransactionalDatabaseClient.java | 4 +- .../repository/query/AbstractR2dbcQuery.java | 2 +- .../support/SimpleR2dbcRepository.java | 8 ++-- ...bstractDatabaseClientIntegrationTests.java | 6 +-- ...ctionalDatabaseClientIntegrationTests.java | 13 +++---- 10 files changed, 83 insertions(+), 42 deletions(-) diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index 6fdd9cbe..003d4b80 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -139,8 +139,7 @@ public class R2dbcApp { DatabaseClient client = DatabaseClient.create(connectionFactory); - client.execute() - .sql("CREATE TABLE person" + + client.sql("CREATE TABLE person" + "(id VARCHAR(255) PRIMARY KEY," + "name VARCHAR(255)," + "age INT)") diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc index eb824d08..8a0632f2 100644 --- a/src/main/asciidoc/reference/r2dbc-sql.adoc +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -6,8 +6,7 @@ The following example shows what you need to include for minimal but fully funct [source,java] ---- -Mono completion = client.execute() - .sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") +Mono completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);") .then(); ---- @@ -15,7 +14,7 @@ Mono completion = client.execute() It exposes intermediate, continuation, and terminal methods at each stage of the execution specification. The example above uses `then()` to return a completion `Publisher` that completes as soon as the query (or queries, if the SQL query contains multiple statements) completes. -NOTE: `execute().sql(…)` accepts either the SQL query string or a query `Supplier` to defer the actual query creation until execution. +NOTE: `sql(…)` accepts either the SQL query string or a query `Supplier` to defer the actual query creation until execution. [[r2dbc.datbaseclient.queries]] == Running Queries @@ -27,8 +26,7 @@ The following example shows an `UPDATE` statement that returns the number of upd [source,java] ---- -Mono affectedRows = client.execute() - .sql("UPDATE person SET name = 'Joe'") +Mono affectedRows = client.sql("UPDATE person SET name = 'Joe'") .fetch().rowsUpdated(); ---- @@ -38,8 +36,7 @@ You might have noticed the use of `fetch()` in the previous example. [source,java] ---- -Mono> first = client.execute() - .sql("SELECT id, name FROM person") +Mono> first = client.sql("SELECT id, name FROM person") .fetch().first(); ---- @@ -55,8 +52,7 @@ You can consume data with the following operators: [source,java] ---- -Flux all = client.execute() - .sql("SELECT id, name FROM mytable") +Flux all = client.sql("SELECT id, name FROM mytable") .as(Person.class) .fetch().all(); ---- @@ -73,8 +69,7 @@ The following example extracts the `id` column and emits its value: [source,java] ---- -Flux names= client.execute() - .sql("SELECT name FROM person") +Flux names = client.sql("SELECT name FROM person") .map((row, rowMetadata) -> row.get("id", String.class)) .all(); ---- @@ -107,8 +102,7 @@ The following example shows parameter binding for a query: [source,java] ---- -db.execute() - .sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") +db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34); @@ -146,8 +140,7 @@ List tuples = new ArrayList<>(); tuples.add(new Object[] {"John", 35}); tuples.add(new Object[] {"Ann", 50}); -db.execute() - .sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") +db.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)") .bind("tuples", tuples); ---- @@ -157,7 +150,6 @@ A simpler variant using `IN` predicates: [source,java] ---- -db.execute() - .sql("SELECT id, name, state FROM table WHERE age IN (:ages)") +db.sql("SELECT id, name, state FROM table WHERE age IN (:ages)") .bind("ages", Arrays.asList(35, 50)); ---- diff --git a/src/main/asciidoc/reference/r2dbc-transactions.adoc b/src/main/asciidoc/reference/r2dbc-transactions.adoc index 9379b51a..e1d2cfc9 100644 --- a/src/main/asciidoc/reference/r2dbc-transactions.adoc +++ b/src/main/asciidoc/reference/r2dbc-transactions.adoc @@ -18,12 +18,12 @@ TransactionalOperator operator = TransactionalOperator.create(tm); <1> DatabaseClient client = DatabaseClient.create(connectionFactory); -Mono atomicOperation = client.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") +Mono atomicOperation = client.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34) .fetch().rowsUpdated() - .then(client.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .then(client.sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") .bind("id", "joe") .bind("name", "Joe") .fetch().rowsUpdated()) @@ -69,12 +69,12 @@ class MyService { @Transactional public Mono insertPerson() { - return client.execute().sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") + return client.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)") .bind("id", "joe") .bind("name", "Joe") .bind("age", 34) .fetch().rowsUpdated() - .then(client.execute().sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") + .then(client.sql("INSERT INTO contacts (id, name) VALUES(:id, :name)") .bind("id", "joe") .bind("name", "Joe") .fetch().rowsUpdated()) diff --git a/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java index 1b787e39..7205894a 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DatabaseClient.java @@ -46,8 +46,41 @@ import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; public interface DatabaseClient { /** - * Prepare an SQL call returning a result. + * Specify a static {@code sql} string to execute. Contract for specifying a SQL call along with options leading to + * the exchange. The SQL string can contain either native parameter bind markers or named parameters (e.g. + * {@literal :foo, :bar}) when {@link NamedParameterExpander} is enabled. + * + * @see NamedParameterExpander + * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) + * @param sql must not be {@literal null} or empty. + * @return a new {@link GenericExecuteSpec}. + * @see NamedParameterExpander + * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) */ + GenericExecuteSpec execute(String sql); + + /** + * Specify a {@link Supplier SQL supplier} that provides SQL to execute. Contract for specifying a SQL call along with + * options leading to the exchange. The SQL string can contain either native parameter bind markers or named + * parameters (e.g. {@literal :foo, :bar}) when {@link NamedParameterExpander} is enabled. + *

+ * Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}. + *

+ * + * @param sqlSupplier must not be {@literal null}. + * @return a new {@link GenericExecuteSpec}. + * @see NamedParameterExpander + * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) + * @see PreparedOperation + */ + GenericExecuteSpec execute(Supplier sqlSupplier); + + /** + * Prepare an SQL call returning a result. + * + * @deprecated will be removed with 1.0 M3. Use {@link #execute(String)} directly. + */ + @Deprecated SqlSpec execute(); /** @@ -157,7 +190,9 @@ public interface DatabaseClient { * * @see NamedParameterExpander * @see DatabaseClient.Builder#namedParameters(NamedParameterExpander) + * @deprecated use {@code DatabaseClient.execute(…)} directly. */ + @Deprecated interface SqlSpec { /** @@ -166,6 +201,7 @@ public interface DatabaseClient { * @param sql must not be {@literal null} or empty. * @return a new {@link GenericExecuteSpec}. */ + @Deprecated GenericExecuteSpec sql(String sql); /** @@ -175,6 +211,7 @@ public interface DatabaseClient { * @return a new {@link GenericExecuteSpec}. * @see PreparedOperation */ + @Deprecated GenericExecuteSpec sql(Supplier sqlSupplier); } diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java index ce4923e7..a1f14783 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClient.java @@ -124,6 +124,22 @@ class DefaultDatabaseClient implements DatabaseClient, ConnectionAccessor { return new DefaultDeleteFromSpec(); } + @Override + public GenericExecuteSpec execute(String sql) { + + Assert.hasText(sql, "SQL must not be null or empty!"); + + return execute(() -> sql); + } + + @Override + public GenericExecuteSpec execute(Supplier sqlSupplier) { + + Assert.notNull(sqlSupplier, "SQL Supplier must not be null!"); + + return createGenericExecuteSpec(sqlSupplier); + } + /** * Execute a callback {@link Function} within a {@link Connection} scope. The function is responsible for creating a * {@link Mono}. The connection is released after the {@link Mono} terminates (or the subscription is cancelled). diff --git a/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java b/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java index f0d6a350..fd3334c3 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java +++ b/src/main/java/org/springframework/data/r2dbc/core/TransactionalDatabaseClient.java @@ -41,7 +41,7 @@ import org.springframework.util.Assert; *
  * Flux transactionalFlux = databaseClient.inTransaction(db -> {
  *
- * 	return db.execute().sql("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
+ * 	return db.execute("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
  * 			.bind("id", 1) //
  * 			.bind("firstname", "Walter") //
  * 			.bind("lastname", "White") //
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
  * 
  * Mono mono = databaseClient.beginTransaction()
  * 		.then(databaseClient.execute()
- * 				.sql("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
+ * 				.execute("INSERT INTO person (id, firstname, lastname) VALUES(:id, :firstname, :lastname)") //
  * 				.bind("id", 1) //
  * 				.bind("firstname", "Walter") //
  * 				.bind("lastname", "White") //
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java
index 3adca38f..531f1b5c 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/query/AbstractR2dbcQuery.java
@@ -103,7 +103,7 @@ public abstract class AbstractR2dbcQuery implements RepositoryQuery {
 		BindableQuery query = createQuery(parameterAccessor);
 
 		ResultProcessor processor = method.getResultProcessor().withDynamicProjection(parameterAccessor);
-		GenericExecuteSpec boundQuery = query.bind(databaseClient.execute().sql(query));
+		GenericExecuteSpec boundQuery = query.bind(databaseClient.execute(query));
 		FetchSpec fetchSpec = boundQuery.as(resolveResultType(processor)).fetch();
 
 		String tableName = method.getEntityInformation().getTableName();
diff --git a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
index 70c000b4..786d495d 100644
--- a/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
+++ b/src/main/java/org/springframework/data/r2dbc/repository/support/SimpleR2dbcRepository.java
@@ -117,7 +117,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository operation = mapper.getMappedObject(selectSpec);
 
-		return this.databaseClient.execute().sql(operation) //
+		return this.databaseClient.execute(operation) //
 				.as(this.entity.getJavaType()) //
 				.fetch() //
 				.one();
@@ -148,7 +148,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository operation = mapper.getMappedObject(selectSpec);
 
-		return this.databaseClient.execute().sql(operation) //
+		return this.databaseClient.execute(operation) //
 				.map((r, md) -> r) //
 				.first() //
 				.hasElement();
@@ -205,7 +205,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository operation = mapper.getMappedObject(selectSpec);
 
-			return this.databaseClient.execute().sql(operation).as(this.entity.getJavaType()).fetch().all();
+			return this.databaseClient.execute(operation).as(this.entity.getJavaType()).fetch().all();
 		});
 	}
 
@@ -221,7 +221,7 @@ public class SimpleR2dbcRepository implements ReactiveCrudRepository r.get(0, Long.class)) //
 				.first() //
 				.defaultIfEmpty(0L);
diff --git a/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
index 62b5ea2a..55d8a534 100644
--- a/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java
@@ -101,7 +101,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
 
 		DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
 
-		databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
+		databaseClient.execute(getInsertIntoLegosetStatement()) //
 				.bind("id", 42055) //
 				.bind("name", "SCHAUFELRADBAGGER") //
 				.bindNull("manual", Integer.class) //
@@ -120,7 +120,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
 
 		executeInsert();
 
-		databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
+		databaseClient.execute(getInsertIntoLegosetStatement()) //
 				.bind(0, 42055) //
 				.bind(1, "SCHAUFELRADBAGGER") //
 				.bindNull(2, Integer.class) //
@@ -139,7 +139,7 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr
 
 		DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
 
-		databaseClient.execute().sql("SELECT id, name, manual FROM legoset") //
+		databaseClient.execute("SELECT id, name, manual FROM legoset") //
 				.as(LegoSet.class) //
 				.fetch().all() //
 				.as(StepVerifier::create) //
diff --git a/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java
index 0c46b65f..37a5f9a5 100644
--- a/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractTransactionalDatabaseClientIntegrationTests.java
@@ -145,8 +145,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 		TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
 
 		Flux integerFlux = databaseClient.inTransaction(db -> db //
-				.execute() //
-				.sql(getInsertIntoLegosetStatement()) //
+				.execute(getInsertIntoLegosetStatement()) //
 				.bind(0, 42055) //
 				.bind(1, "SCHAUFELRADBAGGER") //
 				.bindNull(2, Integer.class) //
@@ -165,7 +164,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 
 		TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
 
-		Mono integerFlux = databaseClient.execute().sql(getInsertIntoLegosetStatement()) //
+		Mono integerFlux = databaseClient.execute(getInsertIntoLegosetStatement()) //
 				.bind(0, 42055) //
 				.bind(1, "SCHAUFELRADBAGGER") //
 				.bindNull(2, Integer.class) //
@@ -185,8 +184,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 		TransactionalDatabaseClient databaseClient = TransactionalDatabaseClient.create(connectionFactory);
 
 		Flux txId = databaseClient //
-				.execute() //
-				.sql(getCurrentTransactionIdStatement()) //
+				.execute(getCurrentTransactionIdStatement()) //
 				.map((r, md) -> r.get(0, Long.class)) //
 				.all();
 
@@ -224,7 +222,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 
 		Flux integerFlux = databaseClient.inTransaction(db -> {
 
-			return db.execute().sql(getInsertIntoLegosetStatement()) //
+			return db.execute(getInsertIntoLegosetStatement()) //
 					.bind(0, 42055) //
 					.bind(1, "SCHAUFELRADBAGGER") //
 					.bindNull(2, Integer.class) //
@@ -248,8 +246,7 @@ public abstract class AbstractTransactionalDatabaseClientIntegrationTests extend
 		TransactionalOperator transactionalOperator = TransactionalOperator
 				.create(new R2dbcTransactionManager(connectionFactory), new DefaultTransactionDefinition());
 
-		Flux txId = databaseClient.execute() //
-				.sql(getCurrentTransactionIdStatement()) //
+		Flux txId = databaseClient.execute(getCurrentTransactionIdStatement()) //
 				.map((row, md) -> row.get(0)) //
 				.all();
 

From 9bcaa2470709eae0e276121d07272d7206a07211 Mon Sep 17 00:00:00 2001
From: Jonas Bark 
Date: Tue, 21 May 2019 13:24:47 +0200
Subject: [PATCH 131/665] #122 - Improve Kotlin extensions for CriteriaStep and
 DatabaseClient.

Original pull request: #123.
---
 .../data/r2dbc/core/CriteriaStepExtensions.kt | 46 +++++++++++++++++
 .../r2dbc/core/DatabaseClientExtensions.kt    | 18 +++++++
 .../r2dbc/core/CriteriaStepExtensionsTests.kt | 51 +++++++++++++++++++
 .../core/DatabaseClientExtensionsTests.kt     | 35 +++++++++++++
 4 files changed, 150 insertions(+)
 create mode 100644 src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
 create mode 100644 src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt

diff --git a/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
new file mode 100644
index 00000000..55fd9250
--- /dev/null
+++ b/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2018-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.core
+
+import org.springframework.data.r2dbc.query.Criteria
+
+/**
+ * Extension for [Criteria.CriteriaStep.is] providing a
+ * `eq(value)` variant.
+ *
+ * @author Jonas Bark
+ */
+infix fun Criteria.CriteriaStep.isEquals(value: Any): Criteria =
+		`is`(value)
+
+/**
+ * Extension for [Criteria.CriteriaStep.in] providing a
+ * `isIn(value)` variant.
+ *
+ * @author Jonas Bark
+ */
+fun Criteria.CriteriaStep.isIn(vararg value: Any): Criteria =
+		`in`(value)
+
+
+/**
+ * Extension for [Criteria.CriteriaStep.in] providing a
+ * `isIn(value)` variant.
+ *
+ * @author Jonas Bark
+ */
+fun Criteria.CriteriaStep.isIn(values: Collection): Criteria =
+		`in`(values)
diff --git a/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
index 999139e0..b4c9e9ee 100644
--- a/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
+++ b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
@@ -16,6 +16,7 @@
 package org.springframework.data.r2dbc.core
 
 import kotlinx.coroutines.reactive.awaitFirstOrNull
+import org.springframework.data.r2dbc.query.Criteria
 
 /**
  * Coroutines variant of [DatabaseClient.GenericExecuteSpec.then].
@@ -80,3 +81,20 @@ suspend fun  DatabaseClient.InsertSpec.await() {
 inline fun  DatabaseClient.InsertIntoSpec.into(): DatabaseClient.TypedInsertSpec =
 		into(T::class.java)
 
+/**
+ * Extension for [DatabaseClient.SelectFromSpec.from] providing a
+ * `from()` variant.
+ *
+ * @author Jonas Bark
+ */
+inline fun  DatabaseClient.SelectFromSpec.from(): DatabaseClient.TypedSelectSpec =
+		from(T::class.java)
+
+/**
+ * Extension for [DatabaseClient.SelectFromSpec.from] providing a
+ * `from()` variant.
+ *
+ * @author Jonas Bark
+ */
+inline fun  DatabaseClient.DeleteFromSpec.from(): DatabaseClient.TypedDeleteSpec =
+		from(T::class.java)
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt
new file mode 100644
index 00000000..82ba1466
--- /dev/null
+++ b/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018-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.core
+
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.runBlocking
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.springframework.data.r2dbc.query.Criteria
+import reactor.core.publisher.Mono
+
+/**
+ * Unit tests for [Criteria.CriteriaStep] extensions.
+ *
+ * @author Jonas Bark
+ */
+class CriteriaStepExtensionsTests {
+
+	@Test // gh-122
+	fun eqIsCriteriaStep() {
+
+		val spec = mockk()
+		val eqSpec = mockk()
+
+		every { spec.`is`("test") } returns eqSpec
+
+		runBlocking {
+			assertThat(spec isEquals "test").isEqualTo(eqSpec)
+		}
+
+		verify {
+			spec.`is`("test")
+		}
+	}
+}
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
index fb4f7f71..bae36a1e 100644
--- a/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
+++ b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
@@ -17,16 +17,19 @@ package org.springframework.data.r2dbc.core
 
 import io.mockk.every
 import io.mockk.mockk
+import io.mockk.spyk
 import io.mockk.verify
 import kotlinx.coroutines.runBlocking
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
+import org.springframework.data.r2dbc.query.Criteria
 import reactor.core.publisher.Mono
 
 /**
  * Unit tests for [DatabaseClient] extensions.
  *
  * @author Sebastien Deleuze
+ * @author Jonas Bark
  */
 class DatabaseClientExtensionsTests {
 
@@ -137,4 +140,36 @@ class DatabaseClientExtensionsTests {
 			spec.into(String::class.java)
 		}
 	}
+
+	@Test // gh-122
+	fun selectFromSpecInto() {
+
+		val spec = mockk()
+		val typedSpec: DatabaseClient.TypedSelectSpec = mockk()
+		every { spec.from(String::class.java) } returns typedSpec
+
+		runBlocking {
+			assertThat(spec.from()).isEqualTo(typedSpec)
+		}
+
+		verify {
+			spec.from(String::class.java)
+		}
+	}
+
+	@Test // gh-122
+	fun deleteFromSpecInto() {
+
+		val spec = mockk()
+		val typedSpec: DatabaseClient.TypedDeleteSpec = mockk()
+		every { spec.from(String::class.java) } returns typedSpec
+
+		runBlocking {
+			assertThat(spec.from()).isEqualTo(typedSpec)
+		}
+
+		verify {
+			spec.from(String::class.java)
+		}
+	}
 }

From 972bc0960659c7fca9662d8bd74584f4a39dbe27 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Tue, 21 May 2019 15:52:54 +0200
Subject: [PATCH 132/665] #122 - Polishing.

Add extension for UpdateTableSpec.table. Add tests. Formatting.

Original pull request: #123.
---
 .../data/r2dbc/core/CriteriaStepExtensions.kt |  3 +-
 .../r2dbc/core/DatabaseClientExtensions.kt    | 12 +++++-
 .../r2dbc/core/CriteriaStepExtensionsTests.kt | 43 +++++++++++++++----
 .../core/DatabaseClientExtensionsTests.kt     | 29 ++++++++-----
 4 files changed, 64 insertions(+), 23 deletions(-)

diff --git a/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
index 55fd9250..cb2979c0 100644
--- a/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
+++ b/src/main/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensions.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * 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.
@@ -35,7 +35,6 @@ infix fun Criteria.CriteriaStep.isEquals(value: Any): Criteria =
 fun Criteria.CriteriaStep.isIn(vararg value: Any): Criteria =
 		`in`(value)
 
-
 /**
  * Extension for [Criteria.CriteriaStep.in] providing a
  * `isIn(value)` variant.
diff --git a/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
index b4c9e9ee..63e12b27 100644
--- a/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
+++ b/src/main/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensions.kt
@@ -16,7 +16,6 @@
 package org.springframework.data.r2dbc.core
 
 import kotlinx.coroutines.reactive.awaitFirstOrNull
-import org.springframework.data.r2dbc.query.Criteria
 
 /**
  * Coroutines variant of [DatabaseClient.GenericExecuteSpec.then].
@@ -55,7 +54,7 @@ suspend fun  DatabaseClient.TypedExecuteSpec.await() {
 }
 
 /**
- * Extension for [DatabaseClient.TypedExecuteSpec.as] providing a
+ * Extension for [DatabaseClient.TypedExecuteSpec. as] providing a
  * `asType()` variant.
  *
  * @author Sebastien Deleuze
@@ -90,6 +89,15 @@ inline fun  DatabaseClient.InsertIntoSpec.into(): DatabaseClien
 inline fun  DatabaseClient.SelectFromSpec.from(): DatabaseClient.TypedSelectSpec =
 		from(T::class.java)
 
+/**
+ * Extension for [DatabaseClient.UpdateTableSpec.table] providing a
+ * `table()` variant.
+ *
+ * @author Mark Paluch
+ */
+inline fun  DatabaseClient.UpdateTableSpec.table(): DatabaseClient.TypedUpdateSpec =
+		table(T::class.java)
+
 /**
  * Extension for [DatabaseClient.SelectFromSpec.from] providing a
  * `from()` variant.
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt
index 82ba1466..6b8d473c 100644
--- a/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt
+++ b/src/test/kotlin/org/springframework/data/r2dbc/core/CriteriaStepExtensionsTests.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018-2019 the original author or authors.
+ * 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.
@@ -17,13 +17,10 @@ package org.springframework.data.r2dbc.core
 
 import io.mockk.every
 import io.mockk.mockk
-import io.mockk.spyk
 import io.mockk.verify
-import kotlinx.coroutines.runBlocking
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
 import org.springframework.data.r2dbc.query.Criteria
-import reactor.core.publisher.Mono
 
 /**
  * Unit tests for [Criteria.CriteriaStep] extensions.
@@ -36,16 +33,44 @@ class CriteriaStepExtensionsTests {
 	fun eqIsCriteriaStep() {
 
 		val spec = mockk()
-		val eqSpec = mockk()
+		val criteria = mockk()
 
-		every { spec.`is`("test") } returns eqSpec
+		every { spec.`is`("test") } returns criteria
 
-		runBlocking {
-			assertThat(spec isEquals "test").isEqualTo(eqSpec)
-		}
+		assertThat(spec isEquals "test").isEqualTo(criteria)
 
 		verify {
 			spec.`is`("test")
 		}
 	}
+
+	@Test // gh-122
+	fun inVarargCriteriaStep() {
+
+		val spec = mockk()
+		val criteria = mockk()
+
+		every { spec.`in`(any() as Array) } returns criteria
+
+		assertThat(spec.isIn("test")).isEqualTo(criteria)
+
+		verify {
+			spec.`in`(arrayOf("test"))
+		}
+	}
+
+	@Test // gh-122
+	fun inListCriteriaStep() {
+
+		val spec = mockk()
+		val criteria = mockk()
+
+		every { spec.`in`(listOf("test")) } returns criteria
+
+		assertThat(spec.isIn(listOf("test"))).isEqualTo(criteria)
+
+		verify {
+			spec.`in`(listOf("test"))
+		}
+	}
 }
diff --git a/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
index bae36a1e..6bea7906 100644
--- a/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
+++ b/src/test/kotlin/org/springframework/data/r2dbc/core/DatabaseClientExtensionsTests.kt
@@ -17,12 +17,10 @@ package org.springframework.data.r2dbc.core
 
 import io.mockk.every
 import io.mockk.mockk
-import io.mockk.spyk
 import io.mockk.verify
 import kotlinx.coroutines.runBlocking
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Test
-import org.springframework.data.r2dbc.query.Criteria
 import reactor.core.publisher.Mono
 
 /**
@@ -30,6 +28,7 @@ import reactor.core.publisher.Mono
  *
  * @author Sebastien Deleuze
  * @author Jonas Bark
+ * @author Mark Paluch
  */
 class DatabaseClientExtensionsTests {
 
@@ -142,15 +141,13 @@ class DatabaseClientExtensionsTests {
 	}
 
 	@Test // gh-122
-	fun selectFromSpecInto() {
+	fun selectFromSpecFrom() {
 
 		val spec = mockk()
 		val typedSpec: DatabaseClient.TypedSelectSpec = mockk()
 		every { spec.from(String::class.java) } returns typedSpec
 
-		runBlocking {
-			assertThat(spec.from()).isEqualTo(typedSpec)
-		}
+		assertThat(spec.from()).isEqualTo(typedSpec)
 
 		verify {
 			spec.from(String::class.java)
@@ -158,15 +155,27 @@ class DatabaseClientExtensionsTests {
 	}
 
 	@Test // gh-122
-	fun deleteFromSpecInto() {
+	fun updateTableSpecTable() {
+
+		val spec = mockk()
+		val typedSpec: DatabaseClient.TypedUpdateSpec = mockk()
+		every { spec.table(String::class.java) } returns typedSpec
+
+		assertThat(spec.table()).isEqualTo(typedSpec)
+
+		verify {
+			spec.table(String::class.java)
+		}
+	}
+
+	@Test // gh-122
+	fun deleteFromSpecFrom() {
 
 		val spec = mockk()
 		val typedSpec: DatabaseClient.TypedDeleteSpec = mockk()
 		every { spec.from(String::class.java) } returns typedSpec
 
-		runBlocking {
-			assertThat(spec.from()).isEqualTo(typedSpec)
-		}
+		assertThat(spec.from()).isEqualTo(typedSpec)
 
 		verify {
 			spec.from(String::class.java)

From 46082147d3bd3700cc05939a251153d0ff176c0a Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Wed, 22 May 2019 09:41:23 +0200
Subject: [PATCH 133/665] #69 - Allow object creation with a subset of columns.

We now allow object creation when not all columns are present by leveraging R2DBC RowMetadata. A column subset is necessary for projections.
---
 .../data/r2dbc/convert/EntityRowMapper.java   |  2 +-
 .../r2dbc/convert/MappingR2dbcConverter.java  | 48 +++++++++++++++----
 .../data/r2dbc/convert/R2dbcConverter.java    | 10 ++++
 .../convert/EntityRowMapperUnitTests.java     | 12 ++++-
 ...ReactiveDataAccessStrategyTestSupport.java |  7 ++-
 .../H2R2dbcRepositoryIntegrationTests.java    |  2 +-
 .../MySqlR2dbcRepositoryIntegrationTests.java |  2 +-
 ...stgresR2dbcRepositoryIntegrationTests.java |  2 +-
 ...ServerR2dbcRepositoryIntegrationTests.java |  2 +-
 9 files changed, 70 insertions(+), 17 deletions(-)

diff --git a/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java b/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java
index a551d9c0..c5458ca7 100644
--- a/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java
+++ b/src/main/java/org/springframework/data/r2dbc/convert/EntityRowMapper.java
@@ -43,6 +43,6 @@ public class EntityRowMapper implements BiFunction {
 	 */
 	@Override
 	public T apply(Row row, RowMetadata metadata) {
-		return converter.read(typeRoRead, row);
+		return converter.read(typeRoRead, row, metadata);
 	}
 }
diff --git a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
index 4224c55f..72e63f01 100644
--- a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
@@ -84,8 +84,21 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	// Entity reading
 	// ----------------------------------
 
+	/* 
+	 * (non-Javadoc)
+	 * @see org.springframework.data.convert.EntityReader#read(java.lang.Class, S)
+	 */
 	@Override
 	public  R read(Class type, Row row) {
+		return read(type, row, null);
+	}
+
+	/* 
+	 * (non-Javadoc)
+	 * @see org.springframework.data.r2dbc.convert.R2dbcConverter#read(java.lang.Class, io.r2dbc.spi.Row, io.r2dbc.spi.RowMetadata)
+	 */
+	@Override
+	public  R read(Class type, Row row, @Nullable RowMetadata metadata) {
 
 		TypeInformation typeInfo = ClassTypeInformation.from(type);
 		Class rawType = typeInfo.getType();
@@ -99,10 +112,10 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 			return getConversionService().convert(row, rawType);
 		}
 
-		return read(getRequiredPersistentEntity(type), row);
+		return read(getRequiredPersistentEntity(type), row, metadata);
 	}
 
-	private  R read(RelationalPersistentEntity entity, Row row) {
+	private  R read(RelationalPersistentEntity entity, Row row, @Nullable RowMetadata metadata) {
 
 		R result = createInstance(row, "", entity);
 
@@ -115,7 +128,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 				continue;
 			}
 
-			Object value = readFrom(row, property, "");
+			Object value = readFrom(row, metadata, property, "");
 
 			if (value != null) {
 				propertyAccessor.setProperty(property, value);
@@ -129,20 +142,27 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	 * Read a single value or a complete Entity from the {@link Row} passed as an argument.
 	 *
 	 * @param row the {@link Row} to extract the value from. Must not be {@literal null}.
+	 * @param metadata the {@link RowMetadata}. Can be {@literal null}.
 	 * @param property the {@link RelationalPersistentProperty} for which the value is intended. Must not be
 	 *          {@literal null}.
 	 * @param prefix to be used for all column names accessed by this method. Must not be {@literal null}.
 	 * @return the value read from the {@link Row}. May be {@literal null}.
 	 */
-	private Object readFrom(Row row, RelationalPersistentProperty property, String prefix) {
+	private Object readFrom(Row row, @Nullable RowMetadata metadata, RelationalPersistentProperty property,
+			String prefix) {
 
 		try {
 
 			if (property.isEntity()) {
-				return readEntityFrom(row, property);
+				return readEntityFrom(row, metadata, property);
 			}
 
-			Object value = row.get(prefix + property.getColumnName());
+			String identifier = prefix + property.getColumnName();
+			if (metadata != null && !metadata.getColumnNames().contains(identifier)) {
+				return null;
+			}
+
+			Object value = row.get(identifier);
 			return getPotentiallyConvertedSimpleRead(value, property.getTypeInformation().getType());
 
 		} catch (Exception o_O) {
@@ -178,13 +198,13 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	}
 
 	@SuppressWarnings("unchecked")
-	private  S readEntityFrom(Row row, PersistentProperty property) {
+	private  S readEntityFrom(Row row, RowMetadata metadata, PersistentProperty property) {
 
 		String prefix = property.getName() + "_";
 
 		RelationalPersistentEntity entity = getMappingContext().getRequiredPersistentEntity(property.getActualType());
 
-		if (readFrom(row, entity.getRequiredIdProperty(), prefix) == null) {
+		if (readFrom(row, metadata, entity.getRequiredIdProperty(), prefix) == null) {
 			return null;
 		}
 
@@ -195,7 +215,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 
 		for (RelationalPersistentProperty p : entity) {
 			if (!entity.isConstructorArgument(property)) {
-				propertyAccessor.setProperty(p, readFrom(row, p, prefix));
+				propertyAccessor.setProperty(p, readFrom(row, metadata, p, prefix));
 			}
 		}
 
@@ -213,6 +233,10 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	// Entity writing
 	// ----------------------------------
 
+	/* 
+	 * (non-Javadoc)
+	 * @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object)
+	 */
 	@Override
 	public void write(Object source, OutboundRow sink) {
 
@@ -313,6 +337,11 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 		return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value;
 	}
 
+	/* 
+	 * (non-Javadoc)
+	 * @see org.springframework.data.r2dbc.convert.R2dbcConverter#getArrayValue(org.springframework.data.r2dbc.dialect.ArrayColumns, org.springframework.data.relational.core.mapping.RelationalPersistentProperty, java.lang.Object)
+	 */
+	@Override
 	public Object getArrayValue(ArrayColumns arrayColumns, RelationalPersistentProperty property, Object value) {
 
 		Class targetType = arrayColumns.getArrayType(property.getActualType());
@@ -337,6 +366,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	 * @param object must not be {@literal null}.
 	 * @return
 	 */
+	@Override
 	@SuppressWarnings("unchecked")
 	public  BiFunction populateIdIfNecessary(T object) {
 
diff --git a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
index 48522355..ed9d345d 100644
--- a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
@@ -71,4 +71,14 @@ public interface R2dbcConverter
 	 * @return
 	 */
 	 BiFunction populateIdIfNecessary(T object);
+
+	/**
+	 * Reads the given source into the given type.
+	 *
+	 * @param type they type to convert the given source to.
+	 * @param source the source to create an object of the given type from.
+	 * @param metadata the {@link RowMetadata}.
+	 * @return
+	 */
+	 R read(Class type, Row source, RowMetadata metadata);
 }
diff --git a/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java
index 215591e2..360b528d 100644
--- a/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/convert/EntityRowMapperUnitTests.java
@@ -7,13 +7,15 @@ import io.r2dbc.spi.Row;
 import io.r2dbc.spi.RowMetadata;
 import lombok.RequiredArgsConstructor;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.Set;
 
+import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.junit.MockitoJUnitRunner;
-import org.springframework.data.r2dbc.convert.EntityRowMapper;
+
 import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
 import org.springframework.data.r2dbc.dialect.PostgresDialect;
 
@@ -30,6 +32,14 @@ public class EntityRowMapperUnitTests {
 
 	Row rowMock = mock(Row.class);
 	RowMetadata metadata = mock(RowMetadata.class);
+	Collection columns = mock(Collection.class);
+
+	@Before
+	public void before() {
+
+		when(columns.contains(anyString())).thenReturn(true);
+		when(metadata.getColumnNames()).thenReturn(columns);
+	}
 
 	@Test // gh-22
 	public void shouldMapSimpleEntity() {
diff --git a/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java b/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java
index 1c43affc..8be51db6 100644
--- a/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java
+++ b/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java
@@ -29,13 +29,13 @@ import java.time.LocalDateTime;
 import java.time.LocalTime;
 import java.time.OffsetDateTime;
 import java.time.ZonedDateTime;
+import java.util.Collection;
 import java.util.UUID;
 import java.util.function.BiConsumer;
 import java.util.function.Function;
 
 import org.junit.Test;
-import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
-import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
+
 import org.springframework.data.r2dbc.dialect.Dialect;
 import org.springframework.data.r2dbc.mapping.SettableValue;
 
@@ -177,6 +177,9 @@ public abstract class ReactiveDataAccessStrategyTestSupport {
 		ReactiveDataAccessStrategy strategy = getStrategy();
 		Row rowMock = mock(Row.class);
 		RowMetadata metadataMock = mock(RowMetadata.class);
+		Collection columnNames = mock(Collection.class);
+		when(metadataMock.getColumnNames()).thenReturn(columnNames);
+		when(columnNames.contains(fieldname)).thenReturn(true);
 
 		PrimitiveTypes toSave = new PrimitiveTypes();
 		setter.accept(toSave, testValue);
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java
index 96257352..0c7b8026 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/H2R2dbcRepositoryIntegrationTests.java
@@ -81,7 +81,7 @@ public class H2R2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositoryIn
 		Flux findByNameContains(String name);
 
 		@Override
-		@Query("SELECT * FROM legoset")
+		@Query("SELECT name FROM legoset")
 		Flux findAsProjection();
 
 		@Override
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java
index 5faf2cd4..aeb61e88 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/MySqlR2dbcRepositoryIntegrationTests.java
@@ -85,7 +85,7 @@ public class MySqlR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepositor
 		Flux findByNameContains(String name);
 
 		@Override
-		@Query("SELECT * FROM legoset")
+		@Query("SELECT name FROM legoset")
 		Flux findAsProjection();
 
 		@Override
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
index 4107fce1..7fc6977b 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java
@@ -85,7 +85,7 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi
 		Flux findByNameContains(String name);
 
 		@Override
-		@Query("SELECT * FROM legoset")
+		@Query("SELECT name FROM legoset")
 		Flux findAsProjection();
 
 		@Override
diff --git a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
index 719d188f..ca2f7900 100644
--- a/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
+++ b/src/test/java/org/springframework/data/r2dbc/repository/SqlServerR2dbcRepositoryIntegrationTests.java
@@ -90,7 +90,7 @@ public class SqlServerR2dbcRepositoryIntegrationTests extends AbstractR2dbcRepos
 		Flux findByNameContains(String name);
 
 		@Override
-		@Query("SELECT * FROM legoset")
+		@Query("SELECT name FROM legoset")
 		Flux findAsProjection();
 
 		@Override

From 32f1770529c5fd4916644e8f46afdcce953773b5 Mon Sep 17 00:00:00 2001
From: Mark Paluch 
Date: Wed, 22 May 2019 10:07:18 +0200
Subject: [PATCH 134/665] #55 - Reuse Dialect support provided by Spring Data
 Relational.

We now reuse the existing Dialect infrastructure provided by Spring Data Relational to enhance it for R2DBC specifics such as bind markers.

Original pull request: #125.
---
 .../config/AbstractR2dbcConfiguration.java    | 20 ++---
 .../r2dbc/convert/MappingR2dbcConverter.java  | 10 +--
 .../data/r2dbc/convert/R2dbcConverter.java    |  2 +-
 .../core/DefaultDatabaseClientBuilder.java    |  4 +-
 .../DefaultReactiveDataAccessStrategy.java    | 63 +++++-----------
 .../r2dbc/core/DefaultStatementMapper.java    | 38 ++--------
 .../core/ReactiveDataAccessStrategy.java      |  5 +-
 .../data/r2dbc/core/StatementMapper.java      |  4 +-
 .../data/r2dbc/dialect/ArrayColumns.java      | 53 --------------
 .../data/r2dbc/dialect/Database.java          | 20 ++---
 .../data/r2dbc/dialect/LimitClause.java       | 42 -----------
 .../data/r2dbc/dialect/MySqlDialect.java      | 51 +------------
 .../data/r2dbc/dialect/PostgresDialect.java   | 73 ++++---------------
 .../{Dialect.java => R2dbcDialect.java}       | 22 +-----
 .../data/r2dbc/dialect/SqlServerDialect.java  | 42 +----------
 .../r2dbc/support/StatementRenderUtil.java    | 66 -----------------
 ...ReactiveDataAccessStrategyTestSupport.java |  4 +-
 .../dialect/PostgresDialectUnitTests.java     |  1 +
 .../dialect/SqlServerDialectUnitTests.java    |  1 +
 19 files changed, 81 insertions(+), 440 deletions(-)
 delete mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/ArrayColumns.java
 delete mode 100644 src/main/java/org/springframework/data/r2dbc/dialect/LimitClause.java
 rename src/main/java/org/springframework/data/r2dbc/dialect/{Dialect.java => R2dbcDialect.java} (67%)
 delete mode 100644 src/main/java/org/springframework/data/r2dbc/support/StatementRenderUtil.java

diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
index f5c048ca..2f509179 100644
--- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
+++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java
@@ -34,7 +34,7 @@ import org.springframework.data.r2dbc.core.DatabaseClient;
 import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy;
 import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
 import org.springframework.data.r2dbc.dialect.Database;
-import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.R2dbcDialect;
 import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator;
 import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
 import org.springframework.data.r2dbc.support.SqlStateR2dbcExceptionTranslator;
@@ -78,15 +78,15 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw
 	public abstract ConnectionFactory connectionFactory();
 
 	/**
-	 * Return a {@link Dialect} for the given {@link ConnectionFactory}. This method attempts to resolve a {@link Dialect}
-	 * from {@link io.r2dbc.spi.ConnectionFactoryMetadata}. Override this method to specify a dialect instead of
-	 * attempting to resolve one.
+	 * Return a {@link R2dbcDialect} for the given {@link ConnectionFactory}. This method attempts to resolve a
+	 * {@link R2dbcDialect} from {@link io.r2dbc.spi.ConnectionFactoryMetadata}. Override this method to specify a dialect
+	 * instead of attempting to resolve one.
 	 *
 	 * @param connectionFactory the configured {@link ConnectionFactory}.
-	 * @return the resolved {@link Dialect}.
-	 * @throws UnsupportedOperationException if the {@link Dialect} cannot be determined.
+	 * @return the resolved {@link R2dbcDialect}.
+	 * @throws UnsupportedOperationException if the {@link R2dbcDialect} cannot be determined.
 	 */
-	public Dialect getDialect(ConnectionFactory connectionFactory) {
+	public R2dbcDialect getDialect(ConnectionFactory connectionFactory) {
 
 		return Database.findDatabase(connectionFactory)
 				.orElseThrow(() -> new UnsupportedOperationException(
@@ -172,13 +172,13 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw
 	}
 
 	/**
-	 * Returns the {@link Dialect}-specific {@link StoreConversions}.
+	 * Returns the {@link R2dbcDialect}-specific {@link StoreConversions}.
 	 *
-	 * @return the {@link Dialect}-specific {@link StoreConversions}.
+	 * @return the {@link R2dbcDialect}-specific {@link StoreConversions}.
 	 */
 	protected StoreConversions getStoreConversions() {
 
-		Dialect dialect = getDialect(lookupConnectionFactory());
+		R2dbcDialect dialect = getDialect(lookupConnectionFactory());
 		return StoreConversions.of(dialect.getSimpleTypeHolder(), R2dbcCustomConversions.STORE_CONVERTERS);
 	}
 
diff --git a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
index 72e63f01..d337ebab 100644
--- a/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/convert/MappingR2dbcConverter.java
@@ -38,11 +38,11 @@ import org.springframework.data.mapping.PreferredConstructor.Parameter;
 import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
 import org.springframework.data.mapping.model.ParameterValueProvider;
-import org.springframework.data.r2dbc.dialect.ArrayColumns;
 import org.springframework.data.r2dbc.mapping.OutboundRow;
 import org.springframework.data.r2dbc.mapping.SettableValue;
 import org.springframework.data.relational.core.conversion.BasicRelationalConverter;
 import org.springframework.data.relational.core.conversion.RelationalConverter;
+import org.springframework.data.relational.core.dialect.ArrayColumns;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
 import org.springframework.data.util.ClassTypeInformation;
@@ -84,7 +84,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	// Entity reading
 	// ----------------------------------
 
-	/* 
+	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.convert.EntityReader#read(java.lang.Class, S)
 	 */
@@ -93,7 +93,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 		return read(type, row, null);
 	}
 
-	/* 
+	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.convert.R2dbcConverter#read(java.lang.Class, io.r2dbc.spi.Row, io.r2dbc.spi.RowMetadata)
 	 */
@@ -233,7 +233,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 	// Entity writing
 	// ----------------------------------
 
-	/* 
+	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object)
 	 */
@@ -337,7 +337,7 @@ public class MappingR2dbcConverter extends BasicRelationalConverter implements R
 		return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum) value).name() : value;
 	}
 
-	/* 
+	/*
 	 * (non-Javadoc)
 	 * @see org.springframework.data.r2dbc.convert.R2dbcConverter#getArrayValue(org.springframework.data.r2dbc.dialect.ArrayColumns, org.springframework.data.relational.core.mapping.RelationalPersistentProperty, java.lang.Object)
 	 */
diff --git a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
index ed9d345d..81015a84 100644
--- a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
+++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcConverter.java
@@ -24,9 +24,9 @@ import org.springframework.core.convert.ConversionService;
 import org.springframework.data.convert.EntityReader;
 import org.springframework.data.convert.EntityWriter;
 import org.springframework.data.mapping.context.MappingContext;
-import org.springframework.data.r2dbc.dialect.ArrayColumns;
 import org.springframework.data.r2dbc.mapping.OutboundRow;
 import org.springframework.data.relational.core.conversion.RelationalConverter;
+import org.springframework.data.relational.core.dialect.ArrayColumns;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
 
diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java
index 8edd56d9..8853f5be 100644
--- a/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java
+++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultDatabaseClientBuilder.java
@@ -22,7 +22,7 @@ import java.util.function.Consumer;
 
 import org.springframework.data.r2dbc.core.DatabaseClient.Builder;
 import org.springframework.data.r2dbc.dialect.Database;
-import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.R2dbcDialect;
 import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator;
 import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator;
 import org.springframework.lang.Nullable;
@@ -121,7 +121,7 @@ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder {
 
 		if (accessStrategy == null) {
 
-			Dialect dialect = Database.findDatabase(this.connectionFactory)
+			R2dbcDialect dialect = Database.findDatabase(this.connectionFactory)
 					.orElseThrow(() -> new UnsupportedOperationException(
 							"Cannot determine a Dialect. Configure the dialect by providing DefaultReactiveDataAccessStrategy(Dialect)"))
 					.defaultDialect();
diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java
index 3385f880..e7ee9999 100644
--- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java
+++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java
@@ -23,9 +23,7 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.function.BiFunction;
-import java.util.function.Function;
 
-import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.dao.InvalidDataAccessResourceUsageException;
 import org.springframework.data.convert.CustomConversions.StoreConversions;
 import org.springframework.data.mapping.context.MappingContext;
@@ -33,26 +31,19 @@ import org.springframework.data.r2dbc.convert.EntityRowMapper;
 import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
 import org.springframework.data.r2dbc.convert.R2dbcConverter;
 import org.springframework.data.r2dbc.convert.R2dbcCustomConversions;
-import org.springframework.data.r2dbc.dialect.ArrayColumns;
 import org.springframework.data.r2dbc.dialect.BindMarkersFactory;
-import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.R2dbcDialect;
 import org.springframework.data.r2dbc.mapping.OutboundRow;
 import org.springframework.data.r2dbc.mapping.SettableValue;
 import org.springframework.data.r2dbc.query.UpdateMapper;
-import org.springframework.data.relational.core.mapping.NamingStrategy;
+import org.springframework.data.relational.core.dialect.ArrayColumns;
+import org.springframework.data.relational.core.dialect.RenderContextFactory;
 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.mapping.Table;
-import org.springframework.data.relational.core.sql.Select;
-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;
-import org.springframework.data.relational.core.sql.render.SelectRenderContext;
 import org.springframework.lang.Nullable;
 import org.springframework.util.Assert;
 import org.springframework.util.ClassUtils;
-import org.springframework.util.StringUtils;
 
 /**
  * Default {@link ReactiveDataAccessStrategy} implementation.
@@ -61,36 +52,36 @@ import org.springframework.util.StringUtils;
  */
 public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStrategy {
 
-	private final Dialect dialect;
+	private final R2dbcDialect dialect;
 	private final R2dbcConverter converter;
 	private final UpdateMapper updateMapper;
 	private final MappingContext, ? extends RelationalPersistentProperty> mappingContext;
 	private final StatementMapper statementMapper;
 
 	/**
-	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and optional
+	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link R2dbcDialect} and optional
 	 * {@link org.springframework.core.convert.converter.Converter}s.
 	 *
-	 * @param dialect the {@link Dialect} to use.
+	 * @param dialect the {@link R2dbcDialect} to use.
 	 */
-	public DefaultReactiveDataAccessStrategy(Dialect dialect) {
+	public DefaultReactiveDataAccessStrategy(R2dbcDialect dialect) {
 		this(dialect, Collections.emptyList());
 	}
 
 	/**
-	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and optional
+	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link R2dbcDialect} and optional
 	 * {@link org.springframework.core.convert.converter.Converter}s.
 	 *
-	 * @param dialect the {@link Dialect} to use.
+	 * @param dialect the {@link R2dbcDialect} to use.
 	 * @param converters custom converters to register, must not be {@literal null}.
 	 * @see R2dbcCustomConversions
 	 * @see org.springframework.core.convert.converter.Converter
 	 */
-	public DefaultReactiveDataAccessStrategy(Dialect dialect, Collection converters) {
+	public DefaultReactiveDataAccessStrategy(R2dbcDialect dialect, Collection converters) {
 		this(dialect, createConverter(dialect, converters));
 	}
 
-	private static R2dbcConverter createConverter(Dialect dialect, Collection converters) {
+	private static R2dbcConverter createConverter(R2dbcDialect dialect, Collection converters) {
 
 		Assert.notNull(dialect, "Dialect must not be null");
 		Assert.notNull(converters, "Converters must not be null");
@@ -105,13 +96,13 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 	}
 
 	/**
-	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link Dialect} and {@link R2dbcConverter}.
+	 * Creates a new {@link DefaultReactiveDataAccessStrategy} given {@link R2dbcDialect} and {@link R2dbcConverter}.
 	 *
-	 * @param dialect the {@link Dialect} to use.
+	 * @param dialect the {@link R2dbcDialect} to use.
 	 * @param converter must not be {@literal null}.
 	 */
 	@SuppressWarnings("unchecked")
-	public DefaultReactiveDataAccessStrategy(Dialect dialect, R2dbcConverter converter) {
+	public DefaultReactiveDataAccessStrategy(R2dbcDialect dialect, R2dbcConverter converter) {
 
 		Assert.notNull(dialect, "Dialect must not be null");
 		Assert.notNull(converter, "RelationalConverter must not be null");
@@ -122,29 +113,9 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
 				.getMappingContext();
 		this.dialect = dialect;
 
-		RenderContext renderContext = new RenderContext() {
-			@Override
-			public RenderNamingStrategy getNamingStrategy() {
-				return NamingStrategies.asIs();
-			}
-
-			@Override
-			public SelectRenderContext getSelect() {
-				return new SelectRenderContext() {
-					@Override
-					public Function afterSelectList() {
-						return it -> "";
-					}
-
-					@Override
-					public Function afterOrderBy(boolean hasOrderBy) {
-						return it -> "";
-					}
-				};
-			}
-		};
-
-		this.statementMapper = new DefaultStatementMapper(dialect, renderContext, this.updateMapper, this.mappingContext);
+		RenderContextFactory factory = new RenderContextFactory(dialect);
+		this.statementMapper = new DefaultStatementMapper(dialect, factory.createRenderContext(), this.updateMapper,
+				this.mappingContext);
 	}
 
 	/*
diff --git a/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java b/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java
index ae2d8065..9bff339e 100644
--- a/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java
+++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultStatementMapper.java
@@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.OptionalLong;
 
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.domain.Sort;
@@ -28,28 +27,14 @@ import org.springframework.data.mapping.context.MappingContext;
 import org.springframework.data.r2dbc.dialect.BindMarkers;
 import org.springframework.data.r2dbc.dialect.BindTarget;
 import org.springframework.data.r2dbc.dialect.Bindings;
-import org.springframework.data.r2dbc.dialect.Dialect;
+import org.springframework.data.r2dbc.dialect.R2dbcDialect;
 import org.springframework.data.r2dbc.query.BoundAssignments;
 import org.springframework.data.r2dbc.query.BoundCondition;
 import org.springframework.data.r2dbc.query.UpdateMapper;
-import org.springframework.data.r2dbc.support.StatementRenderUtil;
 import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
 import org.springframework.data.relational.core.mapping.RelationalPersistentProperty;
-import org.springframework.data.relational.core.sql.AssignValue;
-import org.springframework.data.relational.core.sql.Assignment;
-import org.springframework.data.relational.core.sql.Column;
-import org.springframework.data.relational.core.sql.Delete;
-import org.springframework.data.relational.core.sql.DeleteBuilder;
-import org.springframework.data.relational.core.sql.Insert;
-import org.springframework.data.relational.core.sql.InsertBuilder;
+import org.springframework.data.relational.core.sql.*;
 import org.springframework.data.relational.core.sql.InsertBuilder.InsertValuesWithBuild;
-import org.springframework.data.relational.core.sql.OrderByField;
-import org.springframework.data.relational.core.sql.Select;
-import org.springframework.data.relational.core.sql.SelectBuilder;
-import org.springframework.data.relational.core.sql.StatementBuilder;
-import org.springframework.data.relational.core.sql.Table;
-import org.springframework.data.relational.core.sql.Update;
-import org.springframework.data.relational.core.sql.UpdateBuilder;
 import org.springframework.data.relational.core.sql.render.RenderContext;
 import org.springframework.data.relational.core.sql.render.SqlRenderer;
 import org.springframework.lang.Nullable;
@@ -63,7 +48,7 @@ import org.springframework.util.Assert;
 @RequiredArgsConstructor
 class DefaultStatementMapper implements StatementMapper {
 
-	private final Dialect dialect;
+	private final R2dbcDialect dialect;
 	private final RenderContext renderContext;
 	private final UpdateMapper updateMapper;
 	private final MappingContext, ? extends RelationalPersistentProperty> mappingContext;
@@ -116,26 +101,15 @@ class DefaultStatementMapper implements StatementMapper {
 			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();
+
+			selectBuilder.limitOffset(page.getPageSize(), page.getOffset());
 		}
 
 		Select select = selectBuilder.build();
-		return new DefaultPreparedOperation