#59 - Consider custom conversion in MappingR2dbcConverter.

MappingR2dbcConverter now considers custom conversions for inbound and outbound conversion of top-level types (Row to Entity, Entity to OutboundRow) and on property level (e.g. convert an object to String and vice versa).

Original pull request: #70.
This commit is contained in:
Mark Paluch
2019-03-12 11:26:58 +01:00
committed by Jens Schauder
parent 39936c67fa
commit 6654db34c4
16 changed files with 544 additions and 50 deletions

View File

@@ -0,0 +1,56 @@
/*
* 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
*
* http://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.domain;
import static org.assertj.core.api.Assertions.*;
import org.junit.Test;
/**
* Unit tests for {@link SettableValue}.
*
* @author Mark Paluch
*/
public class SettableValueUnitTests {
@Test // gh-59
public void shouldCreateSettableValue() {
SettableValue value = SettableValue.from("foo");
assertThat(value.isEmpty()).isFalse();
assertThat(value.hasValue()).isTrue();
assertThat(value).isEqualTo(SettableValue.from("foo"));
}
@Test // gh-59
public void shouldCreateEmpty() {
SettableValue value = SettableValue.empty(Object.class);
assertThat(value.isEmpty()).isTrue();
assertThat(value.hasValue()).isFalse();
assertThat(value).isEqualTo(SettableValue.empty(Object.class));
assertThat(value).isNotEqualTo(SettableValue.empty(String.class));
}
@Test // gh-59
public void shouldCreatePotentiallyEmpty() {
assertThat(SettableValue.fromOrEmpty("foo", Object.class).isEmpty()).isFalse();
assertThat(SettableValue.fromOrEmpty(null, Object.class).isEmpty()).isTrue();
}
}

View File

@@ -14,7 +14,7 @@ import java.util.Map;
import org.junit.Test;
import org.springframework.data.r2dbc.dialect.PostgresDialect;
import org.springframework.data.r2dbc.function.convert.SettableValue;
import org.springframework.data.r2dbc.domain.SettableValue;
/**
* Unit tests for {@link DefaultReactiveDataAccessStrategy}.

View File

@@ -21,9 +21,20 @@ import static org.mockito.Mockito.*;
import io.r2dbc.spi.Row;
import lombok.AllArgsConstructor;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.r2dbc.domain.OutboundRow;
import org.springframework.data.r2dbc.domain.SettableValue;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
/**
@@ -33,7 +44,21 @@ import org.springframework.data.relational.core.mapping.RelationalMappingContext
*/
public class MappingR2dbcConverterUnitTests {
MappingR2dbcConverter converter = new MappingR2dbcConverter(new RelationalMappingContext());
RelationalMappingContext mappingContext = new RelationalMappingContext();
MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext);
@Before
public void before() {
R2dbcCustomConversions conversions = new R2dbcCustomConversions(
Arrays.asList(StringToMapConverter.INSTANCE, MapToStringConverter.INSTANCE,
CustomConversionPersonToOutboundRowConverter.INSTANCE, RowToCustomConversionPerson.INSTANCE));
mappingContext = new RelationalMappingContext();
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
converter = new MappingR2dbcConverter(mappingContext, conversions);
}
@Test // gh-61
public void shouldIncludeAllPropertiesInOutboundRow() {
@@ -42,9 +67,9 @@ public class MappingR2dbcConverterUnitTests {
converter.write(new Person("id", "Walter", "White"), row);
assertThat(row).containsEntry("id", new SettableValue("id", String.class));
assertThat(row).containsEntry("firstname", new SettableValue("Walter", String.class));
assertThat(row).containsEntry("lastname", new SettableValue("White", String.class));
assertThat(row).containsEntry("id", SettableValue.fromOrEmpty("id", String.class));
assertThat(row).containsEntry("firstname", SettableValue.fromOrEmpty("Walter", String.class));
assertThat(row).containsEntry("lastname", SettableValue.fromOrEmpty("White", String.class));
}
@Test // gh-41
@@ -68,9 +93,188 @@ public class MappingR2dbcConverterUnitTests {
assertThat(result).isEqualTo(42);
}
@Test // gh-59
public void shouldFailOnUnsupportedEntity() {
PersonWithConversions withMap = new PersonWithConversions(null, null, new NonMappableEntity());
OutboundRow row = new OutboundRow();
assertThatThrownBy(() -> converter.write(withMap, row)).isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@Test // gh-59
public void shouldConvertMapToString() {
PersonWithConversions withMap = new PersonWithConversions("foo", Collections.singletonMap("map", "value"), null);
OutboundRow row = new OutboundRow();
converter.write(withMap, row);
assertThat(row).containsEntry("nested", SettableValue.from("map"));
}
@Test // gh-59
public void shouldReadMapFromString() {
Row rowMock = mock(Row.class);
when(rowMock.get("nested")).thenReturn("map");
PersonWithConversions result = converter.read(PersonWithConversions.class, rowMock);
assertThat(result.nested).isEqualTo(Collections.singletonMap("map", "map"));
}
@Test // gh-59
public void shouldConvertEnum() {
WithEnum withMap = new WithEnum("foo", Condition.Mint);
OutboundRow row = new OutboundRow();
converter.write(withMap, row);
assertThat(row).containsEntry("condition", SettableValue.from("Mint"));
}
@Test // gh-59
public void shouldConvertNullEnum() {
WithEnum withMap = new WithEnum("foo", null);
OutboundRow row = new OutboundRow();
converter.write(withMap, row);
assertThat(row).containsEntry("condition", SettableValue.fromOrEmpty(null, String.class));
}
@Test // gh-59
public void shouldReadEnum() {
Row rowMock = mock(Row.class);
when(rowMock.get("condition")).thenReturn("Mint");
WithEnum result = converter.read(WithEnum.class, rowMock);
assertThat(result.condition).isEqualTo(Condition.Mint);
}
@Test // gh-59
public void shouldWriteTopLevelEntity() {
CustomConversionPerson person = new CustomConversionPerson();
person.entity = new NonMappableEntity();
person.foo = "bar";
OutboundRow row = new OutboundRow();
converter.write(person, row);
assertThat(row).containsEntry("foo_column", SettableValue.from("bar")).containsEntry("entity",
SettableValue.from("nested_entity"));
}
@Test // gh-59
public void shouldReadTopLevelEntity() {
Row rowMock = mock(Row.class);
when(rowMock.get("foo_column", String.class)).thenReturn("bar");
when(rowMock.get("nested_entity")).thenReturn("map");
CustomConversionPerson result = converter.read(CustomConversionPerson.class, rowMock);
assertThat(result.foo).isEqualTo("bar");
assertThat(result.entity).isNotNull();
}
@AllArgsConstructor
static class Person {
@Id String id;
String firstname, lastname;
}
@AllArgsConstructor
static class WithEnum {
@Id String id;
Condition condition;
}
enum Condition {
Mint, Used
}
@AllArgsConstructor
static class PersonWithConversions {
@Id String id;
Map<String, String> nested;
NonMappableEntity unsupported;
}
static class CustomConversionPerson {
String foo;
NonMappableEntity entity;
}
static class NonMappableEntity {}
@ReadingConverter
enum StringToMapConverter implements Converter<String, Map<String, String>> {
INSTANCE;
@Override
public Map<String, String> convert(String source) {
if (source != null) {
return Collections.singletonMap(source, source);
}
return null;
}
}
@WritingConverter
enum MapToStringConverter implements Converter<Map<String, String>, String> {
INSTANCE;
@Override
public String convert(Map<String, String> source) {
if (!source.isEmpty()) {
return source.keySet().iterator().next();
}
return null;
}
}
@WritingConverter
enum CustomConversionPersonToOutboundRowConverter implements Converter<CustomConversionPerson, OutboundRow> {
INSTANCE;
@Override
public OutboundRow convert(CustomConversionPerson source) {
OutboundRow row = new OutboundRow();
row.put("foo_column", SettableValue.from(source.foo));
row.put("entity", SettableValue.from("nested_entity"));
return row;
}
}
@ReadingConverter
enum RowToCustomConversionPerson implements Converter<Row, CustomConversionPerson> {
INSTANCE;
@Override
public CustomConversionPerson convert(Row source) {
CustomConversionPerson person = new CustomConversionPerson();
person.foo = source.get("foo_column", String.class);
Object nested_entity = source.get("nested_entity");
person.entity = nested_entity != null ? new NonMappableEntity() : null;
return person;
}
}
}