#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:
@@ -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;
|
||||
|
||||
// …
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
@@ -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<Color> {
|
||||
*
|
||||
* }
|
||||
* </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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -80,7 +80,9 @@ public class PostgresTestSupport {
|
||||
.port(5432) //
|
||||
.database("postgres") //
|
||||
.username("postgres") //
|
||||
.password("").build();
|
||||
.password("") //
|
||||
.jdbcUrl("jdbc:postgresql://localhost/postgres") //
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user