From 1096838dec2aa8b5e257dae17c73778d30270371 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 13 Sep 2019 14:34:00 +0200 Subject: [PATCH] #186 - Consider byte[] binary data when mapping entities. We now exclude byte[] properties from being mapped to array types. To map data to a 1-dimensional BYTE[] Postgres type, properties can be declared as Collection or Byte[]. --- src/main/asciidoc/reference/mapping.adoc | 61 ++++++++++++++++--- src/main/asciidoc/reference/r2dbc-core.adoc | 2 +- src/main/asciidoc/reference/r2dbc-sql.adoc | 2 +- .../DefaultReactiveDataAccessStrategy.java | 4 ++ .../data/r2dbc/mapping/SettableValue.java | 3 +- ...bstractDatabaseClientIntegrationTests.java | 31 ++++++++++ .../MySqlDatabaseClientIntegrationTests.java | 6 ++ ...ReactiveDataAccessStrategyTestSupport.java | 7 +++ .../data/r2dbc/testing/H2TestSupport.java | 3 +- .../data/r2dbc/testing/MySqlTestSupport.java | 3 +- .../r2dbc/testing/PostgresTestSupport.java | 3 +- .../r2dbc/testing/SqlServerTestSupport.java | 3 +- 12 files changed, 113 insertions(+), 15 deletions(-) diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index 83d5e30..434ccfd 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -1,4 +1,4 @@ -[[mapping-chapter]] +[[mapping]] = Mapping Rich mapping support is provided by the `MappingR2dbcConverter`. `MappingR2dbcConverter` has a rich metadata model that allows mapping domain objects to a data row. @@ -8,7 +8,7 @@ The `MappingR2dbcConverter` also lets you map objects to rows without providing This section describes the features of the `MappingR2dbcConverter`, including how to use conventions for mapping objects to rows and how to override those conventions with annotation-based mapping metadata. -[[mapping-conventions]] +[[mapping.conventions]] == Convention-based Mapping `MappingR2dbcConverter` has a few conventions for mapping objects to rows when no additional mapping metadata is provided. @@ -28,7 +28,7 @@ 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 is thrown. -[[mapping-configuration]] +[[mapping.configuration]] == Mapping Configuration By default (unless explicitly configured) an instance of `MappingR2dbcConverter` is created when you create a `DatabaseClient`. @@ -69,7 +69,7 @@ You can add additional converters to the converter by overriding the `r2dbcCusto NOTE: `AbstractR2dbcConfiguration` creates a `DatabaseClient` instance and registers it with the container under the name of `databaseClient`. -[[mapping-usage]] +[[mapping.usage]] == Metadata-based Mapping To take full advantage of the object mapping functionality inside the Spring Data R2DBC support, you should annotate your mapped objects with the `@Table` annotation. @@ -100,8 +100,53 @@ public class Person { IMPORTANT: The `@Id` annotation tells the mapper which property you want to use as the primary key. +[[mapping.types]] +=== Default Type Mapping -[[mapping-usage-annotations]] +The following table explains how property types of an entity affect mapping: + +|=== +|Source Type | Target Type | Remarks + +|Primitive types and wrapper types +|Passthru +|Can be customized using <>. + +|JSR-310 Date/Time types +|Passthru +|Can be customized using <>. + + +|`String`, `BigInteger`, `BigDecimal`, and `UUID` +|Passthru +|Can be customized using <>. + +|`Blob` and `Clob` +|Passthru +|Can be customized using <>. + +|`byte[]`, `ByteBuffer` +|Passthru +|Considered a binary payload. + +|`Collection` +|Array of `T` +|Conversion to Array type if supported by the configured <>, not supported otherwise. + +|Arrays of primitive types, wrapper types and `String` +|Array of wrapper type (e.g. `int[]` -> `Integer[]`) +|Conversion to Array type if supported by the configured <>, not supported otherwise. + +|Complex objects +|Target type depends on registered `Converter`. +|Requires a <>, not supported otherwise. + +|=== + +NOTE: The native data type for a column depends on the R2DBC driver type mapping. +Drivers can contribute additional simple types such as Geometry types. + +[[mapping.usage.annotations]] === Mapping Annotation Overview The `MappingR2dbcConverter` can use metadata to drive the mapping of objects to rows. The following annotations are available: @@ -117,7 +162,7 @@ The mapping metadata infrastructure is defined in the separate `spring-data-comm Specific subclasses are used in the R2DBC support to support annotation based metadata. Other strategies can also be put in place (if there is demand). -[[mapping-custom-object-construction]] +[[mapping.custom.object.construction]] === Customized Object Construction 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: @@ -147,7 +192,7 @@ class OrderItem { ---- ==== -[[mapping-explicit-converters]] +[[mapping.explicit.converters]] === Overriding Mapping with Explicit Converters When storing and querying your objects, it is often convenient to have a `R2dbcConverter` instance to handle the mapping of all Java types to `OutboundRow` instances. @@ -156,7 +201,7 @@ However, you may sometimes want the `R2dbcConverter` instances to do most of the 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 with Java. +The examples <> show how to perform the configuration with 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. diff --git a/src/main/asciidoc/reference/r2dbc-core.adoc b/src/main/asciidoc/reference/r2dbc-core.adoc index 1e2388b..e5bdddc 100644 --- a/src/main/asciidoc/reference/r2dbc-core.adoc +++ b/src/main/asciidoc/reference/r2dbc-core.adoc @@ -203,7 +203,7 @@ When you run the main program, the preceding examples produce output similar to 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 <>.). +* 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. diff --git a/src/main/asciidoc/reference/r2dbc-sql.adoc b/src/main/asciidoc/reference/r2dbc-sql.adoc index f2d5fb6..58c0ce0 100644 --- a/src/main/asciidoc/reference/r2dbc-sql.adoc +++ b/src/main/asciidoc/reference/r2dbc-sql.adoc @@ -67,7 +67,7 @@ Flux all = client.execute("SELECT id, name FROM mytable") ---- ==== -`as(…)` applies <> and maps the resulting columns to your POJO. +`as(…)` applies <> and maps the resulting columns to your POJO. [[r2dbc.datbaseclient.mapping]] == Mapping Results 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 0c5cee7..ef2e59c 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -218,6 +218,10 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra private SettableValue getArrayValue(SettableValue value, RelationalPersistentProperty property) { + if (value.getType().equals(byte[].class)) { + return value; + } + ArrayColumns arrayColumns = this.dialect.getArraySupport(); if (!arrayColumns.isSupported()) { diff --git a/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java b/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java index a190cfb..501a08f 100644 --- a/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java +++ b/src/main/java/org/springframework/data/r2dbc/mapping/SettableValue.java @@ -20,6 +20,7 @@ import java.util.Objects; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; /** * A database value that can be set in a statement. @@ -121,7 +122,7 @@ public class SettableValue { if (!(o instanceof SettableValue)) return false; SettableValue value1 = (SettableValue) o; - return Objects.equals(this.value, value1.value) && Objects.equals(this.type, value1.type); + return ObjectUtils.nullSafeEquals(this.value, value1.value) && ObjectUtils.nullSafeEquals(this.type, value1.type); } @Override 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 656aeff..2710149 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/AbstractDatabaseClientIntegrationTests.java @@ -219,6 +219,36 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr assertThat(jdbc.queryForMap("SELECT id, name, manual FROM legoset")).hasEntrySatisfying("id", numberOf(42055)); } + @Test // gh-2 + public void insertTypedObjectWithBinary() { + + LegoSet legoSet = new LegoSet(); + legoSet.setId(42055); + legoSet.setName("SCHAUFELRADBAGGER"); + legoSet.setManual(12); + legoSet.setCert(new byte[] { 1, 2, 3, 4, 5 }); + + DatabaseClient databaseClient = DatabaseClient.create(connectionFactory); + + databaseClient.insert().into(LegoSet.class)// + .using(legoSet) // + .fetch() // + .rowsUpdated() // + .as(StepVerifier::create) // + .expectNext(1) // + .verifyComplete(); + + databaseClient.select().from(LegoSet.class) // + .matching(where("name").is("SCHAUFELRADBAGGER")) // + .fetch() // + .first() // + .as(StepVerifier::create) // + .assertNext(actual -> { + + assertThat(actual.getCert()).isEqualTo(new byte[] { 1, 2, 3, 4, 5 }); + }).verifyComplete(); + } + @Test // gh-64 public void update() { @@ -491,5 +521,6 @@ public abstract class AbstractDatabaseClientIntegrationTests extends R2dbcIntegr @Id int id; String name; Integer manual; + byte[] cert; } } diff --git a/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java index a6eb8ec..3ca5635 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/MySqlDatabaseClientIntegrationTests.java @@ -24,6 +24,7 @@ import reactor.test.StepVerifier; import javax.sql.DataSource; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Test; import org.springframework.dao.DataAccessException; @@ -83,6 +84,11 @@ public class MySqlDatabaseClientIntegrationTests extends AbstractDatabaseClientI .verifyComplete(); } + @Ignore("https://github.com/mirromutth/r2dbc-mysql/issues/62") + @Test + @Override + public void insertTypedObjectWithBinary() {} + @Table("boolean_mapping") @Data static class BooleanMapping { 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 d9db558..ccdd156 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTestSupport.java @@ -171,6 +171,11 @@ public abstract class ReactiveDataAccessStrategyTestSupport { testType(PrimitiveTypes::setUuid, PrimitiveTypes::getUuid, UUID.randomUUID(), "uuid"); } + @Test // gh-186 + public void shouldReadAndWriteBinary() { + testType(PrimitiveTypes::setBinary, PrimitiveTypes::getBinary, "hello".getBytes(), "binary"); + } + private void testType(BiConsumer setter, Function getter, T testValue, String fieldname) { @@ -224,6 +229,8 @@ public abstract class ReactiveDataAccessStrategyTestSupport { OffsetDateTime offsetDateTime; ZonedDateTime zonedDateTime; + byte[] binary; + UUID uuid; } } diff --git a/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java index 7e986e3..167af01 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/H2TestSupport.java @@ -33,7 +33,8 @@ 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" // + + " manual integer NULL\n," // + + " cert bytea NULL\n" // + ");"; public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // 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 4b1e5e6..92c8ac6 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/MySqlTestSupport.java @@ -43,7 +43,8 @@ public class MySqlTestSupport { 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" // + + " manual integer NULL\n," // + + " cert varbinary(255) NULL\n" // + ") ENGINE=InnoDB;"; public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // 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 31475a4..2152e47 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -26,7 +26,8 @@ public class PostgresTestSupport { 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" // + + " manual integer NULL\n," // + + " cert bytea NULL\n" // + ");"; public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" // 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 fd12621..6ab0569 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/SqlServerTestSupport.java @@ -18,7 +18,8 @@ public class SqlServerTestSupport { 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" // + + " manual integer NULL\n," // + + " cert varbinary(255) NULL\n" // + ");"; public static String CREATE_TABLE_LEGOSET_WITH_ID_GENERATION = "CREATE TABLE legoset (\n" //