#411 - Introduce EnumWriteSupport for simpler pass-thru of enum values.

We now provide EnumWriteSupport as base class for enum write converters that should be written as-is to the driver.

R2dbcCustomConversions can now also be created from a dialect for easier R2dbcCustomConversions creation.
This commit is contained in:
Mark Paluch
2020-08-06 10:59:42 +02:00
parent 67c3d492a2
commit 7bf9ea7156
6 changed files with 205 additions and 10 deletions

View File

@@ -122,6 +122,10 @@ The following table explains how property types of an entity affect mapping:
|Passthru
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
|`Enum`
|String
|Can be customized by registering a <<mapping.explicit.converters, Explicit Converters>>.
|`Blob` and `Clob`
|Passthru
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
@@ -138,6 +142,10 @@ The following table explains how property types of an entity affect mapping:
|Array of wrapper type (e.g. `int[]` -> `Integer[]`)
|Conversion to Array type if supported by the configured <<r2dbc.drivers, driver>>, not supported otherwise.
|Driver-specific types
|Passthru
|Contributed as simple type be the used `R2dbcDialect`.
|Complex objects
|Target type depends on registered `Converter`.
|Requires a <<mapping.explicit.converters, Explicit Converters>>, not supported otherwise.
@@ -204,7 +212,7 @@ To selectively handle the conversion yourself, register one or more one or more
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.
NOTE: Custom top-level entity conversion requires asymmetric types for conversion. Inbound data is extracted from R2DBC's `Row`.
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:
@@ -248,3 +256,34 @@ public class PersonWriteConverter implements Converter<Person, OutboundRow> {
}
----
====
[[mapping.explicit.enum.converters]]
==== Overriding Enum Mapping with Explicit Converters
Some databases, such as https://github.com/pgjdbc/r2dbc-postgresql#postgres-enum-types[Postgres], can natively write enum values using their database-specific enumerated column type.
Spring Data converts `Enum` values by default to `String` values for maximum portability.
To retain the actual enum value, register a `@Writing` converter whose source and target types use the actual enum type to avoid using `Enum.name()` conversion.
Additionally, you need to configure the enum type on the driver level so that the driver is aware how to represent the enum type.
The following example shows the involved components to read and write `Color` enum values natively:
====
[source,java]
----
enum Color {
Grey, Blue
}
class ColorConverter extends EnumWriteSupport<Color> {
}
class Product {
@Id long id;
Color color;
// …
}
----
====

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2020 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.convert;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;
/**
* Support class to natively write {@link Enum} values to the database.
* <p>
* By default, Spring Data converts enum values by to {@link Enum#name() String} for maximum portability. Registering a
* {@link WritingConverter} allows retaining the enum type so that actual enum values get passed thru to the driver.
* <p>
* Enum types that should be written using their actual enum value to the database should require a converter for type
* pinning. Extend this class as the {@link org.springframework.data.convert.CustomConversions} support inspects
* {@link Converter} generics to identify conversion rules.
* <p>
* For example:
*
* <pre class="code">
* enum Color {
* Grey, Blue
* }
*
* class ColorConverter extends EnumWriteSupport&lt;Color&gt; {
*
* }
* </pre>
*
* @author Mark Paluch
* @param <E> the enum type that should be written using the actual value.
* @since 1.2
*/
@WritingConverter
public abstract class EnumWriteSupport<E extends Enum<E>> implements Converter<E, E> {
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
*/
@Override
public E convert(E enumInstance) {
return enumInstance;
}
}

View File

@@ -1,6 +1,7 @@
package org.springframework.data.r2dbc.convert;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -8,6 +9,7 @@ import java.util.List;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.JodaTimeConverters;
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
import org.springframework.data.r2dbc.mapping.R2dbcSimpleTypeHolder;
/**
@@ -36,7 +38,7 @@ public class R2dbcCustomConversions extends CustomConversions {
}
/**
* Creates a new {@link R2dbcCustomConversions} instance registering the given converters.
* Create a new {@link R2dbcCustomConversions} instance registering the given converters.
*
* @param converters must not be {@literal null}.
*/
@@ -45,7 +47,7 @@ public class R2dbcCustomConversions extends CustomConversions {
}
/**
* Creates a new {@link R2dbcCustomConversions} instance registering the given converters.
* Create a new {@link R2dbcCustomConversions} instance registering the given converters.
*
* @param storeConversions must not be {@literal null}.
* @param converters must not be {@literal null}.
@@ -54,6 +56,34 @@ public class R2dbcCustomConversions extends CustomConversions {
super(new R2dbcCustomConversionsConfiguration(storeConversions, appendOverrides(converters)));
}
/**
* Create a new {@link R2dbcCustomConversions} from the given {@link R2dbcDialect} and {@code converters}.
*
* @param dialect must not be {@literal null}.
* @param converters must not be {@literal null}.
* @return the custom conversions object.
* @since 1.2
*/
public static R2dbcCustomConversions of(R2dbcDialect dialect, Object... converters) {
return of(dialect, Arrays.asList(converters));
}
/**
* Create a new {@link R2dbcCustomConversions} from the given {@link R2dbcDialect} and {@code converters}.
*
* @param dialect must not be {@literal null}.
* @param converters must not be {@literal null}.
* @return the custom conversions object.
* @since 1.2
*/
public static R2dbcCustomConversions of(R2dbcDialect dialect, Collection<?> converters) {
List<Object> storeConverters = new ArrayList<>(dialect.getConverters());
storeConverters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
return new R2dbcCustomConversions(StoreConversions.of(dialect.getSimpleTypeHolder(), storeConverters), converters);
}
private static List<?> appendOverrides(Collection<?> converters) {
List<Object> objects = new ArrayList<>(converters);

View File

@@ -28,7 +28,6 @@ import java.util.function.BiFunction;
import org.springframework.dao.InvalidDataAccessApiUsageException;
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;
@@ -101,11 +100,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra
Assert.notNull(dialect, "Dialect must not be null");
Assert.notNull(converters, "Converters must not be null");
List<Object> storeConverters = new ArrayList<>(dialect.getConverters());
storeConverters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
R2dbcCustomConversions customConversions = new R2dbcCustomConversions(
StoreConversions.of(dialect.getSimpleTypeHolder(), storeConverters), converters);
R2dbcCustomConversions customConversions = R2dbcCustomConversions.of(dialect, converters);
R2dbcMappingContext context = new R2dbcMappingContext();
context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());

View File

@@ -16,12 +16,19 @@
package org.springframework.data.r2dbc.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.relational.core.query.Criteria.*;
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import io.r2dbc.postgresql.codec.EnumCodec;
import io.r2dbc.postgresql.extension.CodecRegistrar;
import io.r2dbc.spi.ConnectionFactory;
import lombok.AllArgsConstructor;
import lombok.Data;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -29,13 +36,18 @@ import javax.sql.DataSource;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.r2dbc.convert.EnumWriteSupport;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
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.data.relational.core.query.Query;
import org.springframework.jdbc.core.JdbcTemplate;
/**
@@ -120,6 +132,57 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport {
.as(StepVerifier::create).verifyComplete();
}
@Test // gh-411
@Ignore("Depends on https://github.com/pgjdbc/r2dbc-postgresql/issues/301")
public void shouldWriteAndReadEnumValuesUsingDriverInternals() {
CodecRegistrar codecRegistrar = EnumCodec.builder().withEnum("state_enum", State.class).build();
PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() //
.host(database.getHostname()) //
.port(database.getPort()) //
.database(database.getDatabase()) //
.username(database.getUsername()) //
.password(database.getPassword()) //
.codecRegistrar(codecRegistrar).build();
PostgresqlConnectionFactory connectionFactory = new PostgresqlConnectionFactory(configuration);
try {
template.execute("CREATE TYPE state_enum as enum ('Good', 'Bad')");
} catch (DataAccessException e) {
// ignore
}
template.execute("CREATE TABLE IF NOT EXISTS entity_with_enum (" //
+ "id serial PRIMARY KEY," //
+ "my_state state_enum)");
template.execute("DELETE FROM entity_with_enum");
ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE,
Collections.singletonList(new StateConverter()));
R2dbcEntityTemplate entityTemplate = new R2dbcEntityTemplate(
org.springframework.r2dbc.core.DatabaseClient.create(connectionFactory), strategy);
entityTemplate.insert(new EntityWithEnum(0, State.Good)) //
.as(StepVerifier::create) //
.expectNextCount(1) //
.verifyComplete();
entityTemplate.select(Query.query(where("my_state").is(State.Good)), EntityWithEnum.class) //
.as(StepVerifier::create) //
.consumeNextWith(actual -> {
assertThat(actual.myState).isEqualTo(State.Good);
}).verifyComplete();
}
enum State {
Good, Bad
}
static class StateConverter extends EnumWriteSupport<State> {
}
private void insert(EntityWithArrays object) {
client.insert() //
@@ -139,6 +202,13 @@ public class PostgresIntegrationTests extends R2dbcIntegrationTestSupport {
.consumeNextWith(assertion).verifyComplete();
}
@Data
@AllArgsConstructor
static class EntityWithEnum {
@Id long id;
State myState;
}
@Table("with_arrays")
@AllArgsConstructor
static class EntityWithArrays {

View File

@@ -80,7 +80,9 @@ public class PostgresTestSupport {
.port(5432) //
.database("postgres") //
.username("postgres") //
.password("").build();
.password("") //
.jdbcUrl("jdbc:postgresql://localhost/postgres") //
.build();
}
/**