├── .gitignore
├── README.md
├── pom.xml
└── src
├── main
├── java
│ └── eu
│ │ └── java
│ │ └── pg
│ │ └── jsonb
│ │ ├── PgJsonApplication.java
│ │ ├── dialect
│ │ └── JSONBPostgreSQLDialect.java
│ │ ├── domain
│ │ ├── CommonPerson.java
│ │ ├── Person.java
│ │ ├── Professor.java
│ │ ├── Student.java
│ │ └── info
│ │ │ ├── Info.java
│ │ │ ├── ProfessorInfo.java
│ │ │ └── StudentInfo.java
│ │ ├── repository
│ │ ├── CommonPersonRepository.java
│ │ ├── ProfessorRepository.java
│ │ └── StudentRepository.java
│ │ └── types
│ │ └── JSONBUserType.java
└── resources
│ └── application.properties
└── test
└── java
└── eu
└── java
└── pg
└── jsonb
└── PgJsonDemoTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | # Intellij
2 | .idea/
3 | *.iml
4 | *.ipr
5 | *.iws
6 |
7 | # Maven
8 | target/
9 |
10 | # Tests
11 | FORK_DIRECTORY_*
12 |
13 | # JRebel
14 | rebel.xml
15 |
16 | # Log Files
17 | *.log
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | spring-data-jpa usage of Postgres JSONB fields
2 | ===============================================================
3 |
4 | This simple project can be used as PoC in storing generically data in JSONB fields.
5 | In a scenario where profession specific information would be stored in joined tables, with the help of JSONB fields
6 | in Postgres we can store all the information in one table and avoid the table joins needed to retrieve all the
7 | required information about a person.
8 |
9 | What this PoC brings new is that specific entities :
10 |
11 | - Student
12 | - Professor
13 |
14 | can be handled in a type-safe manner :
15 |
16 | - Student class has a StudentInfo field
17 | - Professor class has a ProfessorInfo field
18 |
19 | On the other hand, if some batch processes need to deal with all the persons, this can be done with the help of
20 | CommonPerson class (and associated CommonPersonRepository).
21 |
22 |
23 | The table containing the persons (generated here by Hibernate) looks like this :
24 |
25 | ```sql
26 |
27 | CREATE TABLE person
28 | (
29 | dtype character varying(31) NOT NULL,
30 | id bigint NOT NULL,
31 | email character varying(255),
32 | info jsonb,
33 | CONSTRAINT person_pkey PRIMARY KEY (id)
34 | )
35 | WITH (
36 | OIDS=FALSE
37 | );
38 | ```
39 |
40 |
41 | ## Incovenients
42 |
43 | The main inconvenient in using JSON fields seen in this project is that the json fields can not be queried
44 | (at least when using hibernate as JPA provider) via JPQL queries.
45 | The support of Postgres for JSON fields being considered specific (most of the other database engines don't deal
46 | with JSON/JSONB fields) lead to not having introduced direct support for it in JPA.
47 |
48 | This PoC there should give an idea on how to store generic data in a single table by using JSON fields,
49 | but it seems clear that, in order to query the data, native (Postgres specific) SQL should be used.
50 |
51 |
52 |
53 | ## Similar projects
54 |
55 | - https://github.com/brant-hwang/springboot-postgresql94-hibernate5-example.git
56 | - https://github.com/sasa7812/psql-cache-evict-POC.git
57 |
58 | psql-cache-evict-POC project (via eclipselink JPA provider) offers the possibility to execute queries
59 | related to JSON fields :
60 |
61 | ```
62 | String jpql = "SELECT c FROM Course c where SQL('course_mapped ->> ''?'' = ''Second one''',c.name) ";
63 | ```
64 |
65 | On the other hand, hibernate JPA provider doesn't support such constructs.
66 |
67 |
68 | ## Environment requirements
69 | - Java 8
70 | - Spring Boot 1.3.2.RELEASE
71 | - Hibernate 5.0.2
72 | - PostgreSQL 9.4
73 | - Maven 3
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | eu.java.pg.json
8 | postgres-jsonb-jpa
9 | 1.0-SNAPSHOT
10 |
11 |
12 | 9.4.1207
13 | 1.3.2.RELEASE
14 | 4.2.4.RELEASE
15 | 5.0.7.Final
16 | 1.8
17 |
18 |
19 |
20 |
21 | org.springframework.boot
22 | spring-boot-starter-data-jpa
23 | ${spring.boot.version}
24 |
25 |
26 | org.hibernate
27 | hibernate-entitymanager
28 |
29 |
30 |
31 |
32 | org.springframework
33 | spring-context
34 | ${spring.version}
35 |
36 |
37 |
38 | org.hibernate
39 | hibernate-entitymanager
40 | ${hibernate.version}
41 |
42 |
43 |
44 | org.springframework.boot
45 | spring-boot-starter-test
46 | test
47 | ${spring.boot.version}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | org.postgresql
58 | postgresql
59 | ${postgresql.version}
60 | compile
61 |
62 |
63 |
64 |
65 | com.fasterxml.jackson.core
66 | jackson-databind
67 | 2.6.3
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | org.springframework.boot
77 | spring-boot-maven-plugin
78 |
79 |
80 | org.apache.maven.plugins
81 | maven-compiler-plugin
82 |
83 | 1.8
84 | 1.8
85 |
86 |
87 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/PgJsonApplication.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.transaction.annotation.EnableTransactionManagement;
6 |
7 | @SpringBootApplication
8 | @EnableTransactionManagement(proxyTargetClass = true)
9 | public class PgJsonApplication {
10 | public static void main(String[] args) {
11 | SpringApplication.run(PgJsonApplication.class, args);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/dialect/JSONBPostgreSQLDialect.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
3 | * use this file except in compliance with the License. You may obtain a copy of
4 | * the License at
5 | * http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 | * License for the specific language governing permissions and limitations under
11 | * the License.
12 | */
13 | package eu.java.pg.jsonb.dialect;
14 |
15 | import eu.java.pg.jsonb.types.JSONBUserType;
16 | import org.hibernate.dialect.PostgreSQL94Dialect;
17 |
18 | import java.sql.Types;
19 |
20 | public class JSONBPostgreSQLDialect extends PostgreSQL94Dialect {
21 |
22 | public JSONBPostgreSQLDialect() {
23 | super();
24 | registerColumnType(Types.JAVA_OBJECT, JSONBUserType.JSONB_TYPE);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/CommonPerson.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain;
2 |
3 | import eu.java.pg.jsonb.domain.info.Info;
4 |
5 | import javax.persistence.Entity;
6 | import javax.persistence.Inheritance;
7 | import javax.persistence.InheritanceType;
8 | import javax.persistence.Table;
9 |
10 | @Entity
11 | @Inheritance(strategy = InheritanceType.SINGLE_TABLE)
12 | @Table(name = "person")
13 | public abstract class CommonPerson extends Person {
14 |
15 | public abstract void setInfo(T info);
16 |
17 | public abstract T getInfo();
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/Person.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain;
2 |
3 | import javax.persistence.GeneratedValue;
4 | import javax.persistence.Id;
5 | import javax.persistence.MappedSuperclass;
6 |
7 |
8 | @MappedSuperclass
9 | public class Person {
10 | @Id
11 | @GeneratedValue
12 | protected Long id;
13 |
14 | protected String email;
15 |
16 |
17 | public Long getId() {
18 | return id;
19 | }
20 |
21 | public void setId(Long id) {
22 | this.id = id;
23 | }
24 |
25 | public String getEmail() {
26 | return email;
27 | }
28 |
29 | public void setEmail(String email) {
30 | this.email = email;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/Professor.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain;
2 |
3 | import eu.java.pg.jsonb.domain.info.ProfessorInfo;
4 | import eu.java.pg.jsonb.types.JSONBUserType;
5 | import org.hibernate.annotations.Parameter;
6 | import org.hibernate.annotations.Type;
7 | import org.hibernate.annotations.TypeDef;
8 |
9 | import javax.persistence.Column;
10 | import javax.persistence.Entity;
11 |
12 | @Entity
13 | @TypeDef(name = "professorJsonb", typeClass = JSONBUserType.class, parameters = {
14 | @Parameter(name = JSONBUserType.CLASS, value = "eu.java.pg.jsonb.domain.info.ProfessorInfo")})
15 | public class Professor extends CommonPerson {
16 |
17 | @Type(type = "professorJsonb")
18 | @Column(name = "info")
19 | private ProfessorInfo info;
20 |
21 | public ProfessorInfo getInfo() {
22 | return info;
23 | }
24 |
25 | public void setInfo(ProfessorInfo info) {
26 | this.info = info;
27 | }
28 |
29 | @Override
30 | public String toString() {
31 | return "Professor{" +
32 | "id=" + id +
33 | ", email='" + email + '\'' +
34 | ", info=" + info +
35 | '}';
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/Student.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain;
2 |
3 | import eu.java.pg.jsonb.domain.info.StudentInfo;
4 | import eu.java.pg.jsonb.types.JSONBUserType;
5 | import org.hibernate.annotations.Parameter;
6 | import org.hibernate.annotations.Type;
7 | import org.hibernate.annotations.TypeDef;
8 |
9 | import javax.persistence.Column;
10 | import javax.persistence.Entity;
11 |
12 | @Entity
13 | @TypeDef(name = "studentJsonb", typeClass = JSONBUserType.class, parameters = {
14 | @Parameter(name = JSONBUserType.CLASS, value = "eu.java.pg.jsonb.domain.info.StudentInfo")})
15 | public class Student extends CommonPerson {
16 |
17 | @Type(type = "studentJsonb")
18 | @Column(name = "info")
19 | private StudentInfo info;
20 |
21 | public StudentInfo getInfo() {
22 | return info;
23 | }
24 |
25 | public void setInfo(StudentInfo info) {
26 | this.info = info;
27 | }
28 |
29 | @Override
30 | public String toString() {
31 | return "Student{" +
32 | "id=" + id +
33 | ", email='" + email + '\'' +
34 | ", info=" + info +
35 | '}';
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/info/Info.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain.info;
2 |
3 | public interface Info {
4 | }
5 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/info/ProfessorInfo.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain.info;
2 |
3 | import java.util.List;
4 | import java.util.Objects;
5 |
6 | public class ProfessorInfo implements Info {
7 | private String firstName;
8 | private String lastName;
9 | private List courses;
10 |
11 | public String getFirstName() {
12 | return firstName;
13 | }
14 |
15 | public void setFirstName(String firstName) {
16 | this.firstName = firstName;
17 | }
18 |
19 | public String getLastName() {
20 | return lastName;
21 | }
22 |
23 | public void setLastName(String lastName) {
24 | this.lastName = lastName;
25 | }
26 |
27 | public List getCourses() {
28 | return courses;
29 | }
30 |
31 | public void setCourses(List courses) {
32 | this.courses = courses;
33 | }
34 |
35 | @Override
36 | public boolean equals(Object o) {
37 | if (this == o) return true;
38 | if (o == null || getClass() != o.getClass()) return false;
39 | ProfessorInfo that = (ProfessorInfo) o;
40 | return Objects.equals(firstName, that.firstName) &&
41 | Objects.equals(lastName, that.lastName) &&
42 | Objects.equals(courses, that.courses);
43 | }
44 |
45 | @Override
46 | public int hashCode() {
47 | return Objects.hash(firstName, lastName, courses);
48 | }
49 |
50 | @Override
51 | public String toString() {
52 | return "ProfessorInfo{" +
53 | "firstName='" + firstName + '\'' +
54 | ", lastName='" + lastName + '\'' +
55 | ", courses=" + courses +
56 | '}';
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/domain/info/StudentInfo.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.domain.info;
2 |
3 | import java.util.Objects;
4 |
5 | public class StudentInfo implements Info {
6 | private String firstName;
7 | private String lastName;
8 | private int age;
9 |
10 | public String getFirstName() {
11 | return firstName;
12 | }
13 |
14 | public void setFirstName(String firstName) {
15 | this.firstName = firstName;
16 | }
17 |
18 | public String getLastName() {
19 | return lastName;
20 | }
21 |
22 | public void setLastName(String lastName) {
23 | this.lastName = lastName;
24 | }
25 |
26 | public int getAge() {
27 | return age;
28 | }
29 |
30 | public void setAge(int age) {
31 | this.age = age;
32 | }
33 |
34 | @Override
35 | public boolean equals(Object o) {
36 | if (this == o) return true;
37 | if (o == null || getClass() != o.getClass()) return false;
38 | StudentInfo that = (StudentInfo) o;
39 | return age == that.age &&
40 | Objects.equals(firstName, that.firstName) &&
41 | Objects.equals(lastName, that.lastName);
42 | }
43 |
44 | @Override
45 | public int hashCode() {
46 | return Objects.hash(firstName, lastName, age);
47 | }
48 |
49 | @Override
50 | public String toString() {
51 | return "StudentInfo{" +
52 | "firstName='" + firstName + '\'' +
53 | ", lastName='" + lastName + '\'' +
54 | ", age=" + age +
55 | '}';
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/repository/CommonPersonRepository.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.repository;
2 |
3 | import eu.java.pg.jsonb.domain.CommonPerson;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface CommonPersonRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/repository/ProfessorRepository.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.repository;
2 |
3 | import eu.java.pg.jsonb.domain.Professor;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 | import org.springframework.stereotype.Repository;
6 |
7 | @Repository
8 | public interface ProfessorRepository extends JpaRepository {
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/repository/StudentRepository.java:
--------------------------------------------------------------------------------
1 | package eu.java.pg.jsonb.repository;
2 |
3 | import eu.java.pg.jsonb.domain.Student;
4 | import org.springframework.data.jpa.repository.JpaRepository;
5 |
6 | public interface StudentRepository extends JpaRepository {
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/eu/java/pg/jsonb/types/JSONBUserType.java:
--------------------------------------------------------------------------------
1 | /**
2 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not
3 | * use this file except in compliance with the License. You may obtain a copy of
4 | * the License at
5 | * http://www.apache.org/licenses/LICENSE-2.0
6 | *
7 | * Unless required by applicable law or agreed to in writing, software
8 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10 | * License for the specific language governing permissions and limitations under
11 | * the License.
12 | */
13 | package eu.java.pg.jsonb.types;
14 |
15 | import com.fasterxml.jackson.core.JsonProcessingException;
16 | import com.fasterxml.jackson.databind.ObjectMapper;
17 | import org.hibernate.HibernateException;
18 | import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
19 | import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
20 | import org.hibernate.engine.spi.SessionImplementor;
21 | import org.hibernate.type.SerializationException;
22 | import org.hibernate.usertype.ParameterizedType;
23 | import org.hibernate.usertype.UserType;
24 | import org.postgresql.util.PGobject;
25 |
26 | import java.io.IOException;
27 | import java.io.Serializable;
28 | import java.sql.PreparedStatement;
29 | import java.sql.ResultSet;
30 | import java.sql.SQLException;
31 | import java.sql.Types;
32 | import java.util.ArrayList;
33 | import java.util.Collection;
34 | import java.util.HashSet;
35 | import java.util.List;
36 | import java.util.Properties;
37 | import java.util.Set;
38 | import java.util.stream.Collectors;
39 |
40 | public class JSONBUserType implements ParameterizedType, UserType {
41 |
42 | private static final ObjectMapper objectMapper = new ObjectMapper();
43 | private static final ClassLoaderService classLoaderService = new ClassLoaderServiceImpl();
44 |
45 | public static final String JSONB_TYPE = "jsonb";
46 | public static final String CLASS = "CLASS";
47 |
48 | private Class jsonClassType;
49 |
50 | @Override
51 | public Class