From df7919a98d99ff3e1189eecbc1ad065845d3cd7f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 31 Mar 2020 12:37:56 +0200 Subject: [PATCH] #318 - Add support for Postgres JSON type. We now pass-thru Postgres's JSON type that can be used in entities and simple queries. --- .../data/r2dbc/dialect/PostgresDialect.java | 16 +++- ...ostgresMappingR2dbcConverterUnitTests.java | 82 +++++++++++++++++++ ...stgresR2dbcRepositoryIntegrationTests.java | 48 ++++++++++- 3 files changed, 143 insertions(+), 3 deletions(-) create mode 100644 src/test/java/org/springframework/data/r2dbc/convert/PostgresMappingR2dbcConverterUnitTests.java diff --git a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java index 31a6cd15..b26d6313 100644 --- a/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java +++ b/src/main/java/org/springframework/data/r2dbc/dialect/PostgresDialect.java @@ -22,8 +22,20 @@ import org.springframework.util.ClassUtils; public class PostgresDialect extends org.springframework.data.relational.core.dialect.PostgresDialect implements R2dbcDialect { - private static final Set> SIMPLE_TYPES = new HashSet<>( - Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); + private static final Set> SIMPLE_TYPES; + + static { + + Set> simpleTypes = new HashSet<>(Arrays.asList(UUID.class, URL.class, URI.class, InetAddress.class)); + + if (ClassUtils.isPresent("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader())) { + + simpleTypes + .add(ClassUtils.resolveClassName("io.r2dbc.postgresql.codec.Json", PostgresDialect.class.getClassLoader())); + } + + SIMPLE_TYPES = simpleTypes; + } /** * Singleton instance. diff --git a/src/test/java/org/springframework/data/r2dbc/convert/PostgresMappingR2dbcConverterUnitTests.java b/src/test/java/org/springframework/data/r2dbc/convert/PostgresMappingR2dbcConverterUnitTests.java new file mode 100644 index 00000000..085c8e45 --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/convert/PostgresMappingR2dbcConverterUnitTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019-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 static org.assertj.core.api.Assertions.*; + +import io.r2dbc.postgresql.codec.Json; +import lombok.AllArgsConstructor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.r2dbc.dialect.PostgresDialect; +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.relational.core.mapping.RelationalMappingContext; +import org.springframework.data.relational.core.sql.SqlIdentifier; + +/** + * Postgres-specific unit tests for {@link MappingR2dbcConverter}. + * + * @author Mark Paluch + */ +public class PostgresMappingR2dbcConverterUnitTests { + + RelationalMappingContext mappingContext = new R2dbcMappingContext(); + MappingR2dbcConverter converter = new MappingR2dbcConverter(mappingContext); + + @Before + public void before() { + + List converters = new ArrayList<>(PostgresDialect.INSTANCE.getConverters()); + converters.addAll(R2dbcCustomConversions.STORE_CONVERTERS); + CustomConversions.StoreConversions storeConversions = CustomConversions.StoreConversions + .of(PostgresDialect.INSTANCE.getSimpleTypeHolder(), converters); + + R2dbcCustomConversions customConversions = new R2dbcCustomConversions(storeConversions, Collections.emptyList()); + + mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + + converter = new MappingR2dbcConverter(mappingContext, customConversions); + } + + @Test // gh-318 + public void shouldPassThruJson() { + + JsonPerson person = new JsonPerson(null, Json.of("{\"hello\":\"world\"}")); + + OutboundRow row = new OutboundRow(); + converter.write(person, row); + + assertThat(row).containsEntry(SqlIdentifier.unquoted("json_value"), SettableValue.from(person.jsonValue)); + } + + @AllArgsConstructor + static class JsonPerson { + + @Id Long id; + + Json jsonValue; + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java index 56df078d..cc4b500b 100644 --- a/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java +++ b/src/test/java/org/springframework/data/r2dbc/repository/PostgresR2dbcRepositoryIntegrationTests.java @@ -15,24 +15,34 @@ */ package org.springframework.data.r2dbc.repository; +import static org.assertj.core.api.Assertions.*; + +import io.r2dbc.postgresql.codec.Json; import io.r2dbc.spi.ConnectionFactory; +import lombok.AllArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; import javax.sql.DataSource; import org.junit.ClassRule; +import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan.Filter; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; +import org.springframework.data.annotation.Id; import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory; import org.springframework.data.r2dbc.testing.ExternalDatabase; import org.springframework.data.r2dbc.testing.PostgresTestSupport; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringRunner; @@ -47,9 +57,12 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi @ClassRule public static final ExternalDatabase database = PostgresTestSupport.database(); + @Autowired JsonPersonRepository jsonPersonRepository; + @Configuration @EnableR2dbcRepositories(considerNestedRepositories = true, - includeFilters = @Filter(classes = PostgresLegoSetRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + includeFilters = @Filter(classes = { PostgresLegoSetRepository.class, JsonPersonRepository.class }, + type = FilterType.ASSIGNABLE_TYPE)) static class IntegrationTestConfiguration extends AbstractR2dbcConfiguration { @Bean @@ -93,4 +106,37 @@ public class PostgresR2dbcRepositoryIntegrationTests extends AbstractR2dbcReposi @Query("SELECT id FROM legoset") Flux findAllIds(); } + + @Test + public void shouldSaveAndLoadJson() { + + JdbcTemplate template = new JdbcTemplate(createDataSource()); + + template.execute("DROP TABLE IF EXISTS json_person"); + template.execute("CREATE TABLE json_person (\n" // + + " id SERIAL PRIMARY KEY,\n" // + + " json_value JSONB NOT NULL" // + + ");"); + + JsonPerson person = new JsonPerson(null, Json.of("{\"hello\": \"world\"}")); + jsonPersonRepository.save(person).as(StepVerifier::create).expectNextCount(1).verifyComplete(); + + jsonPersonRepository.findAll().as(StepVerifier::create).consumeNextWith(actual -> { + + assertThat(actual.jsonValue).isNotNull(); + assertThat(actual.jsonValue.asString()).isEqualTo("{\"hello\": \"world\"}"); + }).verifyComplete(); + } + + @AllArgsConstructor + static class JsonPerson { + + @Id Long id; + + Json jsonValue; + } + + interface JsonPersonRepository extends ReactiveCrudRepository { + + } }