diff --git a/.gitignore b/.gitignore index 5b7f3dabcf..d1bc7d6b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,7 @@ target/ *.graphml #prevent license accepting file to get accidentially commited to git -container-license-acceptance.txt \ No newline at end of file +container-license-acceptance.txt + +# macOS Finder files +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml index eeaa0b9e93..734b62f6b9 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ 4.0.3 0.1.4 + 2.12.3 2017 diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index af9ad0904e..b325609949 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -264,6 +264,13 @@ test + + com.fasterxml.jackson.core + jackson-databind + ${jackson.databind.version} + test + + diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/PostgresDialectIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/PostgresDialectIntegrationTests.java new file mode 100644 index 0000000000..9d0f4cd735 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/dialect/PostgresDialectIntegrationTests.java @@ -0,0 +1,239 @@ +package org.springframework.data.jdbc.core.dialect; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; +import org.junit.jupiter.api.extension.ExtendWith; +import org.postgresql.util.PGobject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.*; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.annotation.Id; +import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; +import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; +import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories; +import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.mapping.Table; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for PostgreSQL Dialect. + * Start this test with -Dspring.profiles.active=postgres + * + * @author Nikita Konev + */ +@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "postgres") +@ContextConfiguration +@Transactional +@ExtendWith(SpringExtension.class) +public class PostgresDialectIntegrationTests { + + private static final ByteArrayOutputStream capturedOutContent = new ByteArrayOutputStream(); + private static PrintStream previousOutput; + + @Profile("postgres") + @Configuration + @Import(TestConfiguration.class) + @EnableJdbcRepositories(considerNestedRepositories = true, + includeFilters = @ComponentScan.Filter(value = CustomerRepository.class, type = FilterType.ASSIGNABLE_TYPE)) + static class Config { + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Bean + Class testClass() { + return PostgresDialectIntegrationTests.class; + } + + @WritingConverter + static class PersonDataWritingConverter extends AbstractPostgresJsonWritingConverter { + + public PersonDataWritingConverter(ObjectMapper objectMapper) { + super(objectMapper, true); + } + } + + @ReadingConverter + static class PersonDataReadingConverter extends AbstractPostgresJsonReadingConverter { + public PersonDataReadingConverter(ObjectMapper objectMapper) { + super(objectMapper, PersonData.class); + } + } + + @WritingConverter + static class SessionDataWritingConverter extends AbstractPostgresJsonWritingConverter { + public SessionDataWritingConverter(ObjectMapper objectMapper) { + super(objectMapper, true); + } + } + + @ReadingConverter + static class SessionDataReadingConverter extends AbstractPostgresJsonReadingConverter { + public SessionDataReadingConverter(ObjectMapper objectMapper) { + super(objectMapper, SessionData.class); + } + } + + private List storeConverters(Dialect dialect) { + + List converters = new ArrayList<>(); + converters.addAll(dialect.getConverters()); + converters.addAll(JdbcCustomConversions.storeConverters()); + return converters; + } + + protected List userConverters() { + final List list = new ArrayList<>(); + list.add(new PersonDataWritingConverter(objectMapper)); + list.add(new PersonDataReadingConverter(objectMapper)); + list.add(new SessionDataWritingConverter(objectMapper)); + list.add(new SessionDataReadingConverter(objectMapper)); + return list; + } + + @Primary + @Bean + CustomConversions jdbcCustomConversions(Dialect dialect) { + SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER); + + return new JdbcCustomConversions(CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), + userConverters()); + } + + } + + @BeforeAll + public static void ba() { + previousOutput = System.out; + System.setOut(new PrintStream(capturedOutContent)); + } + + @AfterAll + public static void aa() { + System.setOut(previousOutput); + previousOutput = null; + } + + /** + * An abstract class for building your own converter for PostgerSQL's JSON[b]. + */ + static class AbstractPostgresJsonReadingConverter implements Converter { + private final ObjectMapper objectMapper; + private final Class valueType; + + public AbstractPostgresJsonReadingConverter(ObjectMapper objectMapper, Class valueType) { + this.objectMapper = objectMapper; + this.valueType = valueType; + } + + @Override + public T convert(PGobject pgObject) { + try { + final String source = pgObject.getValue(); + return objectMapper.readValue(source, valueType); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to deserialize to json " + pgObject, e); + } + } + } + + /** + * An abstract class for building your own converter for PostgerSQL's JSON[b]. + */ + static class AbstractPostgresJsonWritingConverter implements Converter { + private final ObjectMapper objectMapper; + private final boolean jsonb; + + public AbstractPostgresJsonWritingConverter(ObjectMapper objectMapper, boolean jsonb) { + this.objectMapper = objectMapper; + this.jsonb = jsonb; + } + + @Override + public PGobject convert(T source) { + try { + final PGobject pGobject = new PGobject(); + pGobject.setType(jsonb ? "jsonb" : "json"); + pGobject.setValue(objectMapper.writeValueAsString(source)); + return pGobject; + } catch (JsonProcessingException | SQLException e) { + throw new RuntimeException("Unable to serialize to json " + source, e); + } + } + } + + @Data + @AllArgsConstructor + @Table("customers") + public static class Customer { + + @Id + private Long id; + private String name; + private PersonData personData; + private SessionData sessionData; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class PersonData { + private int age; + private String petName; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class SessionData { + private String token; + private Long ttl; + } + + interface CustomerRepository extends CrudRepository { + + } + + @Autowired + CustomerRepository customerRepository; + + @Test + void testWarningShouldNotBeShown() { + final Customer saved = customerRepository.save(new Customer(null, "Adam Smith", new PersonData(30, "Casper"), null)); + assertThat(saved.getId()).isNotZero(); + final Optional byId = customerRepository.findById(saved.getId()); + assertThat(byId.isPresent()).isTrue(); + final Customer foundCustomer = byId.get(); + assertThat(foundCustomer.getName()).isEqualTo("Adam Smith"); + assertThat(foundCustomer.getPersonData()).isNotNull(); + assertThat(foundCustomer.getPersonData().getAge()).isEqualTo(30); + assertThat(foundCustomer.getPersonData().getPetName()).isEqualTo("Casper"); + assertThat(foundCustomer.getSessionData()).isNull(); + + assertThat(capturedOutContent.toString()).doesNotContain("although it doesn't convert from a store-supported type"); + } + +} diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core.dialect/PostgresDialectIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core.dialect/PostgresDialectIntegrationTests-postgres.sql new file mode 100644 index 0000000000..0d5df184f1 --- /dev/null +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.core.dialect/PostgresDialectIntegrationTests-postgres.sql @@ -0,0 +1,8 @@ +DROP TABLE customers; + +CREATE TABLE customers ( + id BIGSERIAL PRIMARY KEY, + name TEXT NOT NULL, + person_data JSONB, + session_data JSONB +); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java index 6c93a52d18..a8d95c517c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java @@ -17,7 +17,10 @@ import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.function.Consumer; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.LockOptions; @@ -34,6 +37,7 @@ * @author Mark Paluch * @author Myeonghyeon Lee * @author Jens Schauder + * @author Nikita Konev * @since 1.1 */ public class PostgresDialect extends AbstractDialect { @@ -203,4 +207,26 @@ public IdentifierProcessing getIdentifierProcessing() { return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE); } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#simpleTypes() + */ + @Override + public Set> simpleTypes() { + Set> simpleTypes = new HashSet<>(); + ifClassPresent("org.postgresql.util.PGobject", simpleTypes::add); + return Collections.unmodifiableSet(simpleTypes); + } + + /** + * If the class is present on the class path, invoke the specified consumer {@code action} with the class object, + * otherwise do nothing. + * + * @param action block to be executed if a value is present. + */ + private static void ifClassPresent(String className, Consumer> action) { + if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) { + action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader())); + } + } }