Skip to content

Commit d1e8e72

Browse files
nkonevmp911de
authored andcommitted
Consider PGobject as simple type.
Closes #920 Original pull request: #1008.
1 parent 29d4f1e commit d1e8e72

File tree

4 files changed

+274
-1
lines changed

4 files changed

+274
-1
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ target/
1010
*.graphml
1111

1212
#prevent license accepting file to get accidentially commited to git
13-
container-license-acceptance.txt
13+
container-license-acceptance.txt
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package org.springframework.data.jdbc.core.dialect;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import lombok.AllArgsConstructor;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.BeforeAll;
10+
import org.junit.jupiter.api.Test;
11+
import org.junit.jupiter.api.condition.EnabledIfSystemProperty;
12+
import org.junit.jupiter.api.extension.ExtendWith;
13+
import org.postgresql.util.PGobject;
14+
import org.springframework.beans.factory.annotation.Autowired;
15+
import org.springframework.context.annotation.*;
16+
import org.springframework.core.convert.converter.Converter;
17+
import org.springframework.data.annotation.Id;
18+
import org.springframework.data.convert.CustomConversions;
19+
import org.springframework.data.convert.ReadingConverter;
20+
import org.springframework.data.convert.WritingConverter;
21+
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
22+
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
23+
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
24+
import org.springframework.data.jdbc.testing.TestConfiguration;
25+
import org.springframework.data.mapping.model.SimpleTypeHolder;
26+
import org.springframework.data.relational.core.dialect.Dialect;
27+
import org.springframework.data.relational.core.mapping.Table;
28+
import org.springframework.data.repository.CrudRepository;
29+
import org.springframework.test.context.ContextConfiguration;
30+
import org.springframework.test.context.junit.jupiter.SpringExtension;
31+
import org.springframework.transaction.annotation.Transactional;
32+
33+
import java.io.ByteArrayOutputStream;
34+
import java.io.PrintStream;
35+
import java.sql.SQLException;
36+
import java.util.ArrayList;
37+
import java.util.List;
38+
import java.util.Optional;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
42+
/**
43+
* Tests for PostgreSQL Dialect.
44+
* Start this test with -Dspring.profiles.active=postgres
45+
*
46+
* @author Nikita Konev
47+
*/
48+
@EnabledIfSystemProperty(named = "spring.profiles.active", matches = "postgres")
49+
@ContextConfiguration
50+
@Transactional
51+
@ExtendWith(SpringExtension.class)
52+
public class PostgresDialectIntegrationTests {
53+
54+
private static final ByteArrayOutputStream capturedOutContent = new ByteArrayOutputStream();
55+
private static PrintStream previousOutput;
56+
57+
@Profile("postgres")
58+
@Configuration
59+
@Import(TestConfiguration.class)
60+
@EnableJdbcRepositories(considerNestedRepositories = true,
61+
includeFilters = @ComponentScan.Filter(value = CustomerRepository.class, type = FilterType.ASSIGNABLE_TYPE))
62+
static class Config {
63+
64+
private final ObjectMapper objectMapper = new ObjectMapper();
65+
66+
@Bean
67+
Class<?> testClass() {
68+
return PostgresDialectIntegrationTests.class;
69+
}
70+
71+
@WritingConverter
72+
static class PersonDataWritingConverter extends AbstractPostgresJsonWritingConverter<PersonData> {
73+
74+
public PersonDataWritingConverter(ObjectMapper objectMapper) {
75+
super(objectMapper, true);
76+
}
77+
}
78+
79+
@ReadingConverter
80+
static class PersonDataReadingConverter extends AbstractPostgresJsonReadingConverter<PersonData> {
81+
public PersonDataReadingConverter(ObjectMapper objectMapper) {
82+
super(objectMapper, PersonData.class);
83+
}
84+
}
85+
86+
@WritingConverter
87+
static class SessionDataWritingConverter extends AbstractPostgresJsonWritingConverter<SessionData> {
88+
public SessionDataWritingConverter(ObjectMapper objectMapper) {
89+
super(objectMapper, true);
90+
}
91+
}
92+
93+
@ReadingConverter
94+
static class SessionDataReadingConverter extends AbstractPostgresJsonReadingConverter<SessionData> {
95+
public SessionDataReadingConverter(ObjectMapper objectMapper) {
96+
super(objectMapper, SessionData.class);
97+
}
98+
}
99+
100+
private List<Object> storeConverters(Dialect dialect) {
101+
102+
List<Object> converters = new ArrayList<>();
103+
converters.addAll(dialect.getConverters());
104+
converters.addAll(JdbcCustomConversions.storeConverters());
105+
return converters;
106+
}
107+
108+
protected List<?> userConverters() {
109+
final List<Converter> list = new ArrayList<>();
110+
list.add(new PersonDataWritingConverter(objectMapper));
111+
list.add(new PersonDataReadingConverter(objectMapper));
112+
list.add(new SessionDataWritingConverter(objectMapper));
113+
list.add(new SessionDataReadingConverter(objectMapper));
114+
return list;
115+
}
116+
117+
@Primary
118+
@Bean
119+
CustomConversions jdbcCustomConversions(Dialect dialect) {
120+
SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER);
121+
122+
return new JdbcCustomConversions(CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)),
123+
userConverters());
124+
}
125+
126+
}
127+
128+
@BeforeAll
129+
public static void ba() {
130+
previousOutput = System.out;
131+
System.setOut(new PrintStream(capturedOutContent));
132+
}
133+
134+
@AfterAll
135+
public static void aa() {
136+
System.setOut(previousOutput);
137+
previousOutput = null;
138+
}
139+
140+
/**
141+
* An abstract class for building your own converter for PostgerSQL's JSON[b].
142+
*/
143+
static class AbstractPostgresJsonReadingConverter<T> implements Converter<PGobject, T> {
144+
private final ObjectMapper objectMapper;
145+
private final Class<T> valueType;
146+
147+
public AbstractPostgresJsonReadingConverter(ObjectMapper objectMapper, Class<T> valueType) {
148+
this.objectMapper = objectMapper;
149+
this.valueType = valueType;
150+
}
151+
152+
@Override
153+
public T convert(PGobject pgObject) {
154+
try {
155+
final String source = pgObject.getValue();
156+
return objectMapper.readValue(source, valueType);
157+
} catch (JsonProcessingException e) {
158+
throw new RuntimeException("Unable to deserialize to json " + pgObject, e);
159+
}
160+
}
161+
}
162+
163+
/**
164+
* An abstract class for building your own converter for PostgerSQL's JSON[b].
165+
*/
166+
static class AbstractPostgresJsonWritingConverter<T> implements Converter<T, PGobject> {
167+
private final ObjectMapper objectMapper;
168+
private final boolean jsonb;
169+
170+
public AbstractPostgresJsonWritingConverter(ObjectMapper objectMapper, boolean jsonb) {
171+
this.objectMapper = objectMapper;
172+
this.jsonb = jsonb;
173+
}
174+
175+
@Override
176+
public PGobject convert(T source) {
177+
try {
178+
final PGobject pGobject = new PGobject();
179+
pGobject.setType(jsonb ? "jsonb" : "json");
180+
pGobject.setValue(objectMapper.writeValueAsString(source));
181+
return pGobject;
182+
} catch (JsonProcessingException | SQLException e) {
183+
throw new RuntimeException("Unable to serialize to json " + source, e);
184+
}
185+
}
186+
}
187+
188+
@Data
189+
@AllArgsConstructor
190+
@Table("customers")
191+
public static class Customer {
192+
193+
@Id
194+
private Long id;
195+
private String name;
196+
private PersonData personData;
197+
private SessionData sessionData;
198+
}
199+
200+
@Data
201+
@NoArgsConstructor
202+
@AllArgsConstructor
203+
public static class PersonData {
204+
private int age;
205+
private String petName;
206+
}
207+
208+
@Data
209+
@NoArgsConstructor
210+
@AllArgsConstructor
211+
public static class SessionData {
212+
private String token;
213+
private Long ttl;
214+
}
215+
216+
interface CustomerRepository extends CrudRepository<Customer, Long> {
217+
218+
}
219+
220+
@Autowired
221+
CustomerRepository customerRepository;
222+
223+
@Test
224+
void testWarningShouldNotBeShown() {
225+
final Customer saved = customerRepository.save(new Customer(null, "Adam Smith", new PersonData(30, "Casper"), null));
226+
assertThat(saved.getId()).isNotZero();
227+
final Optional<Customer> byId = customerRepository.findById(saved.getId());
228+
assertThat(byId.isPresent()).isTrue();
229+
final Customer foundCustomer = byId.get();
230+
assertThat(foundCustomer.getName()).isEqualTo("Adam Smith");
231+
assertThat(foundCustomer.getPersonData()).isNotNull();
232+
assertThat(foundCustomer.getPersonData().getAge()).isEqualTo(30);
233+
assertThat(foundCustomer.getPersonData().getPetName()).isEqualTo("Casper");
234+
assertThat(foundCustomer.getSessionData()).isNull();
235+
236+
assertThat(capturedOutContent.toString()).doesNotContain("although it doesn't convert from a store-supported type");
237+
}
238+
239+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
DROP TABLE customers;
2+
3+
CREATE TABLE customers (
4+
id BIGSERIAL PRIMARY KEY,
5+
name TEXT NOT NULL,
6+
person_data JSONB,
7+
session_data JSONB
8+
);

spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/PostgresDialect.java

+26
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717

1818
import java.util.Collection;
1919
import java.util.Collections;
20+
import java.util.HashSet;
2021
import java.util.List;
22+
import java.util.Set;
23+
import java.util.function.Consumer;
2124

2225
import org.springframework.data.relational.core.sql.IdentifierProcessing;
2326
import org.springframework.data.relational.core.sql.LockOptions;
@@ -34,6 +37,7 @@
3437
* @author Mark Paluch
3538
* @author Myeonghyeon Lee
3639
* @author Jens Schauder
40+
* @author Nikita Konev
3741
* @since 1.1
3842
*/
3943
public class PostgresDialect extends AbstractDialect {
@@ -203,4 +207,26 @@ public IdentifierProcessing getIdentifierProcessing() {
203207
return IdentifierProcessing.create(Quoting.ANSI, LetterCasing.LOWER_CASE);
204208
}
205209

210+
/*
211+
* (non-Javadoc)
212+
* @see org.springframework.data.relational.core.dialect.Dialect#simpleTypes()
213+
*/
214+
@Override
215+
public Set<Class<?>> simpleTypes() {
216+
Set<Class<?>> simpleTypes = new HashSet<>();
217+
ifClassPresent("org.postgresql.util.PGobject", simpleTypes::add);
218+
return Collections.unmodifiableSet(simpleTypes);
219+
}
220+
221+
/**
222+
* If the class is present on the class path, invoke the specified consumer {@code action} with the class object,
223+
* otherwise do nothing.
224+
*
225+
* @param action block to be executed if a value is present.
226+
*/
227+
private static void ifClassPresent(String className, Consumer<Class<?>> action) {
228+
if (ClassUtils.isPresent(className, PostgresDialect.class.getClassLoader())) {
229+
action.accept(ClassUtils.resolveClassName(className, PostgresDialect.class.getClassLoader()));
230+
}
231+
}
206232
}

0 commit comments

Comments
 (0)