From 7bf9ea7156dc07b6190e724bce23c8c42488ab65 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 6 Aug 2020 10:59:42 +0200 Subject: [PATCH] #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. --- src/main/asciidoc/reference/mapping.adoc | 41 ++++++++++- .../data/r2dbc/convert/EnumWriteSupport.java | 59 ++++++++++++++++ .../r2dbc/convert/R2dbcCustomConversions.java | 34 ++++++++- .../DefaultReactiveDataAccessStrategy.java | 7 +- .../r2dbc/core/PostgresIntegrationTests.java | 70 +++++++++++++++++++ .../r2dbc/testing/PostgresTestSupport.java | 4 +- 6 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/convert/EnumWriteSupport.java diff --git a/src/main/asciidoc/reference/mapping.adoc b/src/main/asciidoc/reference/mapping.adoc index faaa552..81a2c9e 100644 --- a/src/main/asciidoc/reference/mapping.adoc +++ b/src/main/asciidoc/reference/mapping.adoc @@ -122,6 +122,10 @@ The following table explains how property types of an entity affect mapping: |Passthru |Can be customized using <>. +|`Enum` +|String +|Can be customized by registering a <>. + |`Blob` and `Clob` |Passthru |Can be customized using <>. @@ -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 <>, 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 <>, 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 <> 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 { } ---- ==== + +[[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 { + +} + + +class Product { + @Id long id; + Color color; + + // … +} +---- +==== diff --git a/src/main/java/org/springframework/data/r2dbc/convert/EnumWriteSupport.java b/src/main/java/org/springframework/data/r2dbc/convert/EnumWriteSupport.java new file mode 100644 index 0000000..2b7cd1a --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/convert/EnumWriteSupport.java @@ -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. + *

+ * 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. + *

+ * 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. + *

+ * For example: + * + *

+ * enum Color {
+ * 	Grey, Blue
+ * }
+ *
+ * class ColorConverter extends EnumWriteSupport<Color> {
+ *
+ * }
+ * 
+ * + * @author Mark Paluch + * @param the enum type that should be written using the actual value. + * @since 1.2 + */ +@WritingConverter +public abstract class EnumWriteSupport> implements Converter { + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public E convert(E enumInstance) { + return enumInstance; + } + +} 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 6d8cb90..c7e32e3 100644 --- a/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java +++ b/src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java @@ -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 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 objects = new ArrayList<>(converters); 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 b91978e..d628bc7 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -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 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()); diff --git a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java index be70ca5..78f7c9e 100644 --- a/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java @@ -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 { + + } + 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 { 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 815957a..5f68e9e 100644 --- a/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java +++ b/src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java @@ -80,7 +80,9 @@ public class PostgresTestSupport { .port(5432) // .database("postgres") // .username("postgres") // - .password("").build(); + .password("") // + .jdbcUrl("jdbc:postgresql://localhost/postgres") // + .build(); } /**