Skip to content

Commit 7bf9ea7

Browse files
committed
#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.
1 parent 67c3d49 commit 7bf9ea7

File tree

6 files changed

+205
-10
lines changed

6 files changed

+205
-10
lines changed

Diff for: src/main/asciidoc/reference/mapping.adoc

+40-1
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ The following table explains how property types of an entity affect mapping:
122122
|Passthru
123123
|Can be customized using <<mapping.explicit.converters, Explicit Converters>>.
124124

125+
|`Enum`
126+
|String
127+
|Can be customized by registering a <<mapping.explicit.converters, Explicit Converters>>.
128+
125129
|`Blob` and `Clob`
126130
|Passthru
127131
|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:
138142
|Array of wrapper type (e.g. `int[]` -> `Integer[]`)
139143
|Conversion to Array type if supported by the configured <<r2dbc.drivers, driver>>, not supported otherwise.
140144

145+
|Driver-specific types
146+
|Passthru
147+
|Contributed as simple type be the used `R2dbcDialect`.
148+
141149
|Complex objects
142150
|Target type depends on registered `Converter`.
143151
|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
204212
You can use the `r2dbcCustomConversions` method in `AbstractR2dbcConfiguration` to configure converters.
205213
The examples <<mapping.configuration, at the beginning of this chapter>> show how to perform the configuration with Java.
206214

207-
NOTE: Custom top-level entity conversion requires asymmetric types for conversion. Inbound data is extracted from R2DBC's `Row`.
215+
NOTE: Custom top-level entity conversion requires asymmetric types for conversion.Inbound data is extracted from R2DBC's `Row`.
208216
Outbound data (to be used with `INSERT`/`UPDATE` statements) is represented as `OutboundRow` and later assembled to a statement.
209217

210218
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> {
248256
}
249257
----
250258
====
259+
260+
[[mapping.explicit.enum.converters]]
261+
==== Overriding Enum Mapping with Explicit Converters
262+
263+
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.
264+
Spring Data converts `Enum` values by default to `String` values for maximum portability.
265+
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.
266+
Additionally, you need to configure the enum type on the driver level so that the driver is aware how to represent the enum type.
267+
268+
The following example shows the involved components to read and write `Color` enum values natively:
269+
270+
====
271+
[source,java]
272+
----
273+
enum Color {
274+
Grey, Blue
275+
}
276+
277+
class ColorConverter extends EnumWriteSupport<Color> {
278+
279+
}
280+
281+
282+
class Product {
283+
@Id long id;
284+
Color color;
285+
286+
// …
287+
}
288+
----
289+
====
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.r2dbc.convert;
17+
18+
import org.springframework.core.convert.converter.Converter;
19+
import org.springframework.data.convert.WritingConverter;
20+
21+
/**
22+
* Support class to natively write {@link Enum} values to the database.
23+
* <p>
24+
* By default, Spring Data converts enum values by to {@link Enum#name() String} for maximum portability. Registering a
25+
* {@link WritingConverter} allows retaining the enum type so that actual enum values get passed thru to the driver.
26+
* <p>
27+
* Enum types that should be written using their actual enum value to the database should require a converter for type
28+
* pinning. Extend this class as the {@link org.springframework.data.convert.CustomConversions} support inspects
29+
* {@link Converter} generics to identify conversion rules.
30+
* <p>
31+
* For example:
32+
*
33+
* <pre class="code">
34+
* enum Color {
35+
* Grey, Blue
36+
* }
37+
*
38+
* class ColorConverter extends EnumWriteSupport&lt;Color&gt; {
39+
*
40+
* }
41+
* </pre>
42+
*
43+
* @author Mark Paluch
44+
* @param <E> the enum type that should be written using the actual value.
45+
* @since 1.2
46+
*/
47+
@WritingConverter
48+
public abstract class EnumWriteSupport<E extends Enum<E>> implements Converter<E, E> {
49+
50+
/*
51+
* (non-Javadoc)
52+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
53+
*/
54+
@Override
55+
public E convert(E enumInstance) {
56+
return enumInstance;
57+
}
58+
59+
}

Diff for: src/main/java/org/springframework/data/r2dbc/convert/R2dbcCustomConversions.java

+32-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package org.springframework.data.r2dbc.convert;
22

33
import java.util.ArrayList;
4+
import java.util.Arrays;
45
import java.util.Collection;
56
import java.util.Collections;
67
import java.util.Date;
78
import java.util.List;
89

910
import org.springframework.data.convert.CustomConversions;
1011
import org.springframework.data.convert.JodaTimeConverters;
12+
import org.springframework.data.r2dbc.dialect.R2dbcDialect;
1113
import org.springframework.data.r2dbc.mapping.R2dbcSimpleTypeHolder;
1214

1315
/**
@@ -36,7 +38,7 @@ public class R2dbcCustomConversions extends CustomConversions {
3638
}
3739

3840
/**
39-
* Creates a new {@link R2dbcCustomConversions} instance registering the given converters.
41+
* Create a new {@link R2dbcCustomConversions} instance registering the given converters.
4042
*
4143
* @param converters must not be {@literal null}.
4244
*/
@@ -45,7 +47,7 @@ public R2dbcCustomConversions(Collection<?> converters) {
4547
}
4648

4749
/**
48-
* Creates a new {@link R2dbcCustomConversions} instance registering the given converters.
50+
* Create a new {@link R2dbcCustomConversions} instance registering the given converters.
4951
*
5052
* @param storeConversions must not be {@literal null}.
5153
* @param converters must not be {@literal null}.
@@ -54,6 +56,34 @@ public R2dbcCustomConversions(StoreConversions storeConversions, Collection<?> c
5456
super(new R2dbcCustomConversionsConfiguration(storeConversions, appendOverrides(converters)));
5557
}
5658

59+
/**
60+
* Create a new {@link R2dbcCustomConversions} from the given {@link R2dbcDialect} and {@code converters}.
61+
*
62+
* @param dialect must not be {@literal null}.
63+
* @param converters must not be {@literal null}.
64+
* @return the custom conversions object.
65+
* @since 1.2
66+
*/
67+
public static R2dbcCustomConversions of(R2dbcDialect dialect, Object... converters) {
68+
return of(dialect, Arrays.asList(converters));
69+
}
70+
71+
/**
72+
* Create a new {@link R2dbcCustomConversions} from the given {@link R2dbcDialect} and {@code converters}.
73+
*
74+
* @param dialect must not be {@literal null}.
75+
* @param converters must not be {@literal null}.
76+
* @return the custom conversions object.
77+
* @since 1.2
78+
*/
79+
public static R2dbcCustomConversions of(R2dbcDialect dialect, Collection<?> converters) {
80+
81+
List<Object> storeConverters = new ArrayList<>(dialect.getConverters());
82+
storeConverters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
83+
84+
return new R2dbcCustomConversions(StoreConversions.of(dialect.getSimpleTypeHolder(), storeConverters), converters);
85+
}
86+
5787
private static List<?> appendOverrides(Collection<?> converters) {
5888

5989
List<Object> objects = new ArrayList<>(converters);

Diff for: src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java

+1-6
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
import org.springframework.dao.InvalidDataAccessApiUsageException;
3030
import org.springframework.dao.InvalidDataAccessResourceUsageException;
31-
import org.springframework.data.convert.CustomConversions.StoreConversions;
3231
import org.springframework.data.mapping.context.MappingContext;
3332
import org.springframework.data.r2dbc.convert.EntityRowMapper;
3433
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
@@ -101,11 +100,7 @@ public static R2dbcConverter createConverter(R2dbcDialect dialect, Collection<?>
101100
Assert.notNull(dialect, "Dialect must not be null");
102101
Assert.notNull(converters, "Converters must not be null");
103102

104-
List<Object> storeConverters = new ArrayList<>(dialect.getConverters());
105-
storeConverters.addAll(R2dbcCustomConversions.STORE_CONVERTERS);
106-
107-
R2dbcCustomConversions customConversions = new R2dbcCustomConversions(
108-
StoreConversions.of(dialect.getSimpleTypeHolder(), storeConverters), converters);
103+
R2dbcCustomConversions customConversions = R2dbcCustomConversions.of(dialect, converters);
109104

110105
R2dbcMappingContext context = new R2dbcMappingContext();
111106
context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());

Diff for: src/test/java/org/springframework/data/r2dbc/core/PostgresIntegrationTests.java

+70
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,38 @@
1616
package org.springframework.data.r2dbc.core;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.springframework.data.relational.core.query.Criteria.*;
1920

21+
import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
22+
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
23+
import io.r2dbc.postgresql.codec.EnumCodec;
24+
import io.r2dbc.postgresql.extension.CodecRegistrar;
2025
import io.r2dbc.spi.ConnectionFactory;
2126
import lombok.AllArgsConstructor;
27+
import lombok.Data;
2228
import reactor.test.StepVerifier;
2329

2430
import java.util.Arrays;
31+
import java.util.Collections;
2532
import java.util.List;
2633
import java.util.function.Consumer;
2734

2835
import javax.sql.DataSource;
2936

3037
import org.junit.Before;
3138
import org.junit.ClassRule;
39+
import org.junit.Ignore;
3240
import org.junit.Test;
3341

42+
import org.springframework.dao.DataAccessException;
3443
import org.springframework.data.annotation.Id;
44+
import org.springframework.data.r2dbc.convert.EnumWriteSupport;
45+
import org.springframework.data.r2dbc.dialect.PostgresDialect;
3546
import org.springframework.data.r2dbc.testing.ExternalDatabase;
3647
import org.springframework.data.r2dbc.testing.PostgresTestSupport;
3748
import org.springframework.data.r2dbc.testing.R2dbcIntegrationTestSupport;
3849
import org.springframework.data.relational.core.mapping.Table;
50+
import org.springframework.data.relational.core.query.Query;
3951
import org.springframework.jdbc.core.JdbcTemplate;
4052

4153
/**
@@ -120,6 +132,57 @@ public void shouldReadAndWriteMultiDimensionArrays() {
120132
.as(StepVerifier::create).verifyComplete();
121133
}
122134

135+
@Test // gh-411
136+
@Ignore("Depends on https://github.com/pgjdbc/r2dbc-postgresql/issues/301")
137+
public void shouldWriteAndReadEnumValuesUsingDriverInternals() {
138+
139+
CodecRegistrar codecRegistrar = EnumCodec.builder().withEnum("state_enum", State.class).build();
140+
141+
PostgresqlConnectionConfiguration configuration = PostgresqlConnectionConfiguration.builder() //
142+
.host(database.getHostname()) //
143+
.port(database.getPort()) //
144+
.database(database.getDatabase()) //
145+
.username(database.getUsername()) //
146+
.password(database.getPassword()) //
147+
.codecRegistrar(codecRegistrar).build();
148+
149+
PostgresqlConnectionFactory connectionFactory = new PostgresqlConnectionFactory(configuration);
150+
151+
try {
152+
template.execute("CREATE TYPE state_enum as enum ('Good', 'Bad')");
153+
} catch (DataAccessException e) {
154+
// ignore
155+
}
156+
template.execute("CREATE TABLE IF NOT EXISTS entity_with_enum (" //
157+
+ "id serial PRIMARY KEY," //
158+
+ "my_state state_enum)");
159+
template.execute("DELETE FROM entity_with_enum");
160+
161+
ReactiveDataAccessStrategy strategy = new DefaultReactiveDataAccessStrategy(PostgresDialect.INSTANCE,
162+
Collections.singletonList(new StateConverter()));
163+
R2dbcEntityTemplate entityTemplate = new R2dbcEntityTemplate(
164+
org.springframework.r2dbc.core.DatabaseClient.create(connectionFactory), strategy);
165+
166+
entityTemplate.insert(new EntityWithEnum(0, State.Good)) //
167+
.as(StepVerifier::create) //
168+
.expectNextCount(1) //
169+
.verifyComplete();
170+
171+
entityTemplate.select(Query.query(where("my_state").is(State.Good)), EntityWithEnum.class) //
172+
.as(StepVerifier::create) //
173+
.consumeNextWith(actual -> {
174+
assertThat(actual.myState).isEqualTo(State.Good);
175+
}).verifyComplete();
176+
}
177+
178+
enum State {
179+
Good, Bad
180+
}
181+
182+
static class StateConverter extends EnumWriteSupport<State> {
183+
184+
}
185+
123186
private void insert(EntityWithArrays object) {
124187

125188
client.insert() //
@@ -139,6 +202,13 @@ private void selectAndAssert(Consumer<? super EntityWithArrays> assertion) {
139202
.consumeNextWith(assertion).verifyComplete();
140203
}
141204

205+
@Data
206+
@AllArgsConstructor
207+
static class EntityWithEnum {
208+
@Id long id;
209+
State myState;
210+
}
211+
142212
@Table("with_arrays")
143213
@AllArgsConstructor
144214
static class EntityWithArrays {

Diff for: src/test/java/org/springframework/data/r2dbc/testing/PostgresTestSupport.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ private static ExternalDatabase local() {
8080
.port(5432) //
8181
.database("postgres") //
8282
.username("postgres") //
83-
.password("").build();
83+
.password("") //
84+
.jdbcUrl("jdbc:postgresql://localhost/postgres") //
85+
.build();
8486
}
8587

8688
/**

0 commit comments

Comments
 (0)