#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<Byte> or Byte[].
This commit is contained in:
Mark Paluch
2019-09-13 14:34:00 +02:00
parent 5ef8285288
commit 1096838dec
12 changed files with 113 additions and 15 deletions

View File

@@ -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 <<mapping.explicit.converters, Explicit Converters>>.
|JSR-310 Date/Time types
|Passthru
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
|`String`, `BigInteger`, `BigDecimal`, and `UUID`
|Passthru
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
|`Blob` and `Clob`
|Passthru
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
|`byte[]`, `ByteBuffer`
|Passthru
|Considered a binary payload.
|`Collection<T>`
|Array of `T`
|Conversion to Array type if supported by the configured <<r2dbc.drivers, driver>>, 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 <<r2dbc.drivers, driver>>, not supported otherwise.
|Complex objects
|Target type depends on registered `Converter`.
|Requires a <<mapping.explicit.converters, Explicit Converters>>, 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 <<mapping-configuration, at the beginning of this chapter>> show how to perform the configuration with Java.
The examples <<mapping.configuration, at the beginning of this chapter>> 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.

View File

@@ -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 (<<r2dbc.datbaseclient,`DatabaseClient`>>) 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-chapter,here>>.).
* The mapper works against standard POJO objects without the need for any additional metadata (though you can, optionally, provide that information -- see <<mapping,here>>.).
* 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.

View File

@@ -67,7 +67,7 @@ Flux<Person> all = client.execute("SELECT id, name FROM mytable")
----
====
`as(…)` applies <<mapping-conventions,Convention-based Object Mapping>> and maps the resulting columns to your POJO.
`as(…)` applies <<mapping.conventions,Convention-based Object Mapping>> and maps the resulting columns to your POJO.
[[r2dbc.datbaseclient.mapping]]
== Mapping Results

View File

@@ -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()) {

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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 <T> void testType(BiConsumer<PrimitiveTypes, T> setter, Function<PrimitiveTypes, T> getter, T testValue,
String fieldname) {
@@ -224,6 +229,8 @@ public abstract class ReactiveDataAccessStrategyTestSupport {
OffsetDateTime offsetDateTime;
ZonedDateTime zonedDateTime;
byte[] binary;
UUID uuid;
}
}

View File

@@ -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" //

View File

@@ -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" //

View File

@@ -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" //

View File

@@ -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" //