From bac5c343b72da03118ab7ceddfcef004fc200839 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 3 Sep 2019 14:25:02 +0200 Subject: [PATCH] #169 - Allow usage of Entity-level converters. We now support Converters on Entity-level if object materialization/dematerialization is handled by application code. We're using a custom R2DBC MappingContext to create mapping metadata for types that have custom converters registered. --- .../config/AbstractR2dbcConfiguration.java | 17 +- .../DefaultReactiveDataAccessStrategy.java | 3 +- .../r2dbc/mapping/R2dbcMappingContext.java | 51 +++++ ...ertingR2dbcRepositoryIntegrationTests.java | 176 ++++++++++++++++++ .../mapping/R2dbcMappingContextUnitTests.java | 76 ++++++++ 5 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/springframework/data/r2dbc/mapping/R2dbcMappingContext.java create mode 100644 src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java create mode 100644 src/test/kotlin/org/springframework/data/r2dbc/mapping/R2dbcMappingContextUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java index 55ccbe1..cebf039 100644 --- a/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java +++ b/src/main/java/org/springframework/data/r2dbc/config/AbstractR2dbcConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.data.r2dbc.core.DefaultReactiveDataAccessStrategy; import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy; import org.springframework.data.r2dbc.dialect.DialectResolver; import org.springframework.data.r2dbc.dialect.R2dbcDialect; +import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; import org.springframework.data.r2dbc.support.R2dbcExceptionSubclassTranslator; import org.springframework.data.r2dbc.support.R2dbcExceptionTranslator; import org.springframework.data.r2dbc.support.SqlStateR2dbcExceptionTranslator; @@ -126,7 +127,7 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw Assert.notNull(namingStrategy, "NamingStrategy must not be null!"); - RelationalMappingContext relationalMappingContext = new RelationalMappingContext( + R2dbcMappingContext relationalMappingContext = new R2dbcMappingContext( namingStrategy.orElse(NamingStrategy.INSTANCE)); relationalMappingContext.setSimpleTypeHolder(r2dbcCustomConversions.getSimpleTypeHolder()); @@ -159,13 +160,23 @@ public abstract class AbstractR2dbcConfiguration implements ApplicationContextAw * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These * {@link CustomConversions} will be registered with the {@link BasicRelationalConverter} and * {@link #r2dbcMappingContext(Optional, R2dbcCustomConversions)}. Returns an empty {@link R2dbcCustomConversions} - * instance by default. + * instance by default. Override {@link #getCustomConverters()} to supply custom converters. * * @return must not be {@literal null}. + * @see #getCustomConverters() */ @Bean public R2dbcCustomConversions r2dbcCustomConversions() { - return new R2dbcCustomConversions(getStoreConversions(), Collections.emptyList()); + return new R2dbcCustomConversions(getStoreConversions(), getCustomConverters()); + } + + /** + * Customization hook to return custom converters. + * + * @return return custom converters. + */ + protected List getCustomConverters() { + return Collections.emptyList(); } /** 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 ecf3303..1599b26 100644 --- a/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java +++ b/src/main/java/org/springframework/data/r2dbc/core/DefaultReactiveDataAccessStrategy.java @@ -34,6 +34,7 @@ import org.springframework.data.r2dbc.convert.R2dbcConverter; import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; import org.springframework.data.r2dbc.dialect.R2dbcDialect; import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.R2dbcMappingContext; import org.springframework.data.r2dbc.mapping.SettableValue; import org.springframework.data.r2dbc.query.UpdateMapper; import org.springframework.data.relational.core.dialect.ArrayColumns; @@ -100,7 +101,7 @@ public class DefaultReactiveDataAccessStrategy implements ReactiveDataAccessStra R2dbcCustomConversions customConversions = new R2dbcCustomConversions( StoreConversions.of(dialect.getSimpleTypeHolder(), storeConverters), storeConverters); - RelationalMappingContext context = new RelationalMappingContext(); + R2dbcMappingContext context = new R2dbcMappingContext(); context.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); return new MappingR2dbcConverter(context, customConversions); diff --git a/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcMappingContext.java b/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcMappingContext.java new file mode 100644 index 0000000..644f256 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/mapping/R2dbcMappingContext.java @@ -0,0 +1,51 @@ +/* + * Copyright 2019 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.mapping; + +import org.springframework.data.relational.core.mapping.NamingStrategy; +import org.springframework.data.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.util.TypeInformation; + +/** + * R2DBC-specific extension to {@link RelationalMappingContext}. + * + * @author Mark Paluch + */ +public class R2dbcMappingContext extends RelationalMappingContext { + + /** + * Create a new {@link R2dbcMappingContext}. + */ + public R2dbcMappingContext() {} + + /** + * Create a new {@link R2dbcMappingContext} using the given {@link NamingStrategy}. + * + * @param namingStrategy must not be {@literal null}. + */ + public R2dbcMappingContext(NamingStrategy namingStrategy) { + super(namingStrategy); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.context.AbstractMappingContext#shouldCreatePersistentEntityFor(org.springframework.data.util.TypeInformation) + */ + @Override + protected boolean shouldCreatePersistentEntityFor(TypeInformation type) { + return !R2dbcSimpleTypeHolder.HOLDER.isSimpleType(type.getType()); + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java new file mode 100644 index 0000000..61f94a8 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/ConvertingR2dbcRepositoryIntegrationTests.java @@ -0,0 +1,176 @@ +/* + * Copyright 2019 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.repository; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.ConnectionFactory; +import io.r2dbc.spi.Row; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.List; + +import javax.sql.DataSource; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; +import org.springframework.data.r2dbc.mapping.OutboundRow; +import org.springframework.data.r2dbc.mapping.SettableValue; +import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; +import org.springframework.data.r2dbc.testing.H2TestSupport; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * Integration tests for {@link ConvertedRepository} that uses {@link Converter}s on entity-level. + * + * @author Mark Paluch + */ +@RunWith(SpringRunner.class) +public class ConvertingR2dbcRepositoryIntegrationTests { + + @Autowired private ConvertedRepository repository; + private JdbcTemplate jdbc; + + @Configuration + @EnableR2dbcRepositories( + includeFilters = @ComponentScan.Filter(value = ConvertedRepository.class, type = FilterType.ASSIGNABLE_TYPE), + considerNestedRepositories = true) + static class TestConfiguration extends AbstractR2dbcConfiguration { + @Override + public ConnectionFactory connectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + @Override + protected List getCustomConverters() { + return Arrays.asList(ConvertedEntityToRow.INSTANCE, RowToConvertedEntity.INSTANCE); + } + } + + @Before + public void before() { + + this.jdbc = new JdbcTemplate(createDataSource()); + + try { + this.jdbc.execute("DROP TABLE CONVERTED_ENTITY"); + } catch (DataAccessException e) {} + + this.jdbc.execute("CREATE TABLE CONVERTED_ENTITY (id serial PRIMARY KEY, name varchar(255))"); + } + + /** + * Creates a {@link DataSource} to be used in this test. + * + * @return the {@link DataSource} to be used in this test. + */ + protected DataSource createDataSource() { + return H2TestSupport.createDataSource(); + } + + /** + * Creates a {@link ConnectionFactory} to be used in this test. + * + * @return the {@link ConnectionFactory} to be used in this test. + */ + protected ConnectionFactory createConnectionFactory() { + return H2TestSupport.createConnectionFactory(); + } + + @Test + public void shouldInsertAndReadItems() { + + ConvertedEntity entity = new ConvertedEntity(); + entity.setName("name"); + + repository.save(entity) // + .as(StepVerifier::create) // + .expectNextCount(1) // + .verifyComplete(); + + repository.findAll() // + .as(StepVerifier::create) // + .consumeNextWith(actual -> { + assertThat(actual.getName()).isEqualTo("read: prefixed: name"); + }).verifyComplete(); + } + + interface ConvertedRepository extends ReactiveCrudRepository { + + } + + @AllArgsConstructor + @NoArgsConstructor + @Data + static class ConvertedEntity { + @Id Integer id; + String name; + } + + @WritingConverter + enum ConvertedEntityToRow implements Converter { + + INSTANCE; + + @Override + public OutboundRow convert(ConvertedEntity convertedEntity) { + + OutboundRow outboundRow = new OutboundRow(); + + if (convertedEntity.getId() != null) { + outboundRow.put("id", SettableValue.from(convertedEntity.getId())); + } + + outboundRow.put("name", SettableValue.from("prefixed: " + convertedEntity.getName())); + + return outboundRow; + } + } + + @ReadingConverter + enum RowToConvertedEntity implements Converter { + + INSTANCE; + + @Override + public ConvertedEntity convert(Row source) { + + ConvertedEntity entity = new ConvertedEntity(); + entity.setId(source.get("id", Integer.class)); + entity.setName("read: " + source.get("name", String.class)); + + return entity; + } + } +} diff --git a/src/test/kotlin/org/springframework/data/r2dbc/mapping/R2dbcMappingContextUnitTests.java b/src/test/kotlin/org/springframework/data/r2dbc/mapping/R2dbcMappingContextUnitTests.java new file mode 100644 index 0000000..7740ce7 --- /dev/null +++ b/src/test/kotlin/org/springframework/data/r2dbc/mapping/R2dbcMappingContextUnitTests.java @@ -0,0 +1,76 @@ +/* + * Copyright 2019 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.mapping; + +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.spi.Row; + +import java.util.Arrays; + +import org.junit.Test; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; + +/** + * Unit tests for {@link R2dbcMappingContext}. + * + * @author Mark Paluch + */ +public class R2dbcMappingContextUnitTests { + + @Test + public void shouldCreateMetadataForConvertedTypes() { + + R2dbcCustomConversions conversions = new R2dbcCustomConversions( + Arrays.asList(ConvertedEntityToRow.INSTANCE, RowToConvertedEntity.INSTANCE)); + R2dbcMappingContext context = new R2dbcMappingContext(); + context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); + context.afterPropertiesSet(); + + assertThat(context.getPersistentEntity(ConvertedEntity.class)).isNotNull(); + } + + static class ConvertedEntity { + + } + + @WritingConverter + enum ConvertedEntityToRow implements Converter { + + INSTANCE; + + @Override + public OutboundRow convert(ConvertedEntity convertedEntity) { + + return new OutboundRow(); + } + } + + @ReadingConverter + enum RowToConvertedEntity implements Converter { + + INSTANCE; + + @Override + public ConvertedEntity convert(Row source) { + return new ConvertedEntity(); + } + } +}