Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE

1. Overview

This tutorial will provide us with a comprehensive understanding of storing JSON data in a PostgreSQL JSONB column.

We’ll quickly review how we cope with a JSON value stored in a variable character (VARCHAR) database column using JPA. After that, we’ll compare the differences between the VARCHAR type and the JSONB type, understanding the additional features of JSONB. Finally, we’ll address the mapping JSONB type in JPA.

2. VARCHAR Mapping

In this section, we’ll explore how to convert a JSON value in VARCHAR type to a custom Java POJO using AttributeConverter.

The purpose of it is to facilitate the conversion between entity attribute value in Java data type and its corresponding value in the database column.

2.1. Maven Dependency

To create an AttributeConverter, we have to include the Spring Data JPA dependency in the pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.7.18</version>
</dependency>

2.2. Table Definition

Let’s illustrate this concept with a simple example using the following database table definition:

CREATE TABLE student (
    student_id VARCHAR(8) PRIMARY KEY,
    admit_year VARCHAR(4),
    address VARCHAR(500)
);

The student table has three fields, and we’re expecting the address column to store JSON values with the following structure:

{
  "postCode": "TW9 2SF",
  "city": "London"
}

2.3. Entity Class

To handle this, we’ll create a corresponding POJO class to represent the address data in Java:

public class Address {
    private String postCode;

    private String city;

    // constructor, getters and setters
}

Next, we’ll create an entity class, StudentEntity, and map it to the student table we created earlier:

@Entity
@Table(name = "student")
public class StudentEntity {
    @Id
    @Column(name = "student_id", length = 8)
    private String id;

    @Column(name = "admit_year", length = 4)
    private String admitYear;

    @Convert(converter = AddressAttributeConverter.class)
    @Column(name = "address", length = 500)
    private Address address;

    // constructor, getters and setters
}

We’ll annotate the address field with @Convert and apply AddressAttributeConverter to convert the Address instance into its JSON representation.

2.4. AttributeConverter

We map the address field in the entity class to a VARCHAR type in the database. However, JPA cannot perform the conversion between the custom Java type and the VARCHAR type automatically. AttributeConverter comes in to bridge this gap by providing a mechanism to handle the conversion process.

We use AttributeConverter to persist a custom Java data type to a database column. It’s mandatory to define two conversion methods for every AttributeConverter implementation. One converts the Java data type to its corresponding database data type, while the other converts the database data type to the Java data type:

@Converter
public class AddressAttributeConverter implements AttributeConverter<Address, String> {
    private static final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public String convertToDatabaseColumn(Address address) {
        try {
            return objectMapper.writeValueAsString(address);
        } catch (JsonProcessingException jpe) {
            log.warn("Cannot convert Address into JSON");
            return null;
        }
    }

    @Override
    public Address convertToEntityAttribute(String value) {
        try {
            return objectMapper.readValue(value, Address.class);
        } catch (JsonProcessingException e) {
            log.warn("Cannot convert JSON into Address");
            return null;
        }
    }
}

convertToDatabaseColumn() is responsible for converting an entity field value to the corresponding database column value, whereas convertToEntityAttribute() is responsible for converting a database column value to the corresponding entity field value.

2.5. Test Case

Now, let’s create a test case to persist a Student instance in the database:

@Test
void whenSaveAnStudentEntityAndFindById_thenTheRecordPresentsInDb() {
    String studentId = "23876213";
    String postCode = "KT5 8LJ";

    Address address = new Address(postCode, "London");
    StudentEntity studentEntity = StudentEntity.builder()
      .id(studentId)
      .admitYear("2023")
      .address(address)
      .build();

    StudentEntity savedStudentEntity = studentRepository.save(studentEntity);

    Optional<StudentEntity> studentEntityOptional = studentRepository.findById(studentId);
    assertThat(studentEntityOptional.isPresent()).isTrue();

    studentEntity = studentEntityOptional.get();
    assertThat(studentEntity.getId()).isEqualTo(studentId);
    assertThat(studentEntity.getAddress().getPostCode()).isEqualTo(postCode);
}

When we run the test, JPA triggers the following insert SQL:

Hibernate: 
    insert 
    into
        "public"
        ."student_str" ("address", "admit_year", "student_id") 
    values
        (?, ?, ?)
binding parameter [1] as [VARCHAR] - [{"postCode":"KT6 7BB","city":"London"}]
binding parameter [2] as [VARCHAR] - [2023]
binding parameter [3] as [VARCHAR] - [23876371]

We’ll see the 1st parameter has been converted successfully from our Address instance by the AddressAttributeConverter and binds as a VARCHAR type.

3. JSONB Over VARCHAR

We have explored the conversion where we have JSON data stored in the VARCHAR column. Now, let’s change the column definition of address from VARCHAR to JSONB:

CREATE TABLE student (
    student_id VARCHAR(8) PRIMARY KEY,
    admit_year VARCHAR(4),
    address jsonb
);

A commonly asked question often arises when we explore the JSONB data type: What’s the significance of using JSONB to store JSON in PostgreSQL over VARCHAR since it’s essentially a string?

JSONB is a designated data type for processing JSON data in PostgreSQL. This type stores data in a decomposed binary format, which has a bit of overhead when storing JSON due to the additional conversion.

Indeed, it provides additional features compared to VARCHAR that make JSONB a more favorable choice for storing JSON data in PostgreSQL.

3.1. Validation

JSONB type enforces data validation on the stored value that makes sure the column value is a valid JSON. PostgreSQL rejects any attempts to insert or update data with invalid JSON values.

To demonstrate this, we can consider an insert SQL query with an invalid JSON value for the address column where a double quote is missing at the end of the city attribute:

INSERT INTO student(student_id, admit_year, address) 
VALUES ('23134572', '2022', '{"postCode": "E4 8ST, "city":"London}');

The execution of this query in PostgreSQL results in a validation error indicating the JSON isn’t valid:

SQL Error: ERROR: invalid input syntax for type json
  Detail: Token "city" is invalid.
  Position: 83
  Where: JSON data, line 1: {"postCode": "E4 8ST, "city...

3.2. Querying

PostgreSQL supports querying using JSON columns in SQL queries. JPA supports using native queries to search for records in the database. In Spring Data, we can define a custom query method that finds a list of Student:

@Repository
public interface StudentRepository extends CrudRepository<StudentEntity, String> {
    @Query(value = "SELECT * FROM student WHERE address->>'postCode' = :postCode", nativeQuery = true)
    List<StudentEntity> findByAddressPostCode(@Param("postCode") String postCode);
}

This query is a native SQL query that selects all Student instances in the database where the address JSON attribute postCode equals the provided parameter.

3.3. Indexing

JSONB supports JSON data indexing. This gives JSONB a significant advantage when we have to query the data by keys or attributes in the JSON column.

Various types of indexes can be applied to a JSON column, including GIN, HASH, and BTREE. GIN is suitable for indexing complex data structures, including arrays and JSON. HASH is important when we only need to consider the equality operator =. BTREE allows efficient queries when we deal with range operators such as < and >=.

For example, we could create the following index if we always need to retrieve data according to the postCode attribute in the address column:

CREATE INDEX idx_postcode ON student USING HASH((address->'postCode'));

4. JSONB Mapping

We cannot apply the same AttributeConverter when the databases column is defined as JSONB. Our application ]throws the following error upon start-up if we attempt to:

org.postgresql.util.PSQLException: ERROR: column "address" is of type jsonb but expression is of type character varying

This is the case even if we change the AttributeConverter class definition to use Object as the converted column value instead of String:

@Converter 
public class AddressAttributeConverter implements AttributeConverter<Address, Object> {
    // 2 conversion methods implementation
}

Our application complains about the unsupported type:

org.postgresql.util.PSQLException: Unsupported Types value: 1,943,105,171

This indicates that JPA doesn’t support JSONB type natively. However, our underlying JPA implementation, Hibernate, does support JSON custom types that allow us to map a complex type to a Java class.

4.1. Maven Dependency

Practically, we have to define a custom type for JSONB conversion. However, we don’t have to reinvent the wheel because of an existing library Hypersistence Utilities.

Hypersistence Utilities is a general-purpose utility library for Hibernate. One of its features is having the definitions of  JSON column type mapping for different databases such as PostgreSQL and Oracle. Thus, we can simply include this additional dependency in the pom.xml:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>3.7.0</version>
</dependency>

4.2. Updated Entity Class

Hypersistence Utilities defines different custom types that are database-dependent. In PostgreSQL, we’ll use the JsonBinaryType class for the JSONB column type. In our entity class, we define the custom type using Hibernate’s @TypeDef annotation and then apply the defined type to the address field via @Type:

@Entity
@Table(name = "student")
@TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
public class StudentEntity {
    @Id
    @Column(name = "student_id", length = 8)
    private String id;

    @Column(name = "admit_year", length = 4)
    private String admitYear;

    @Type(type = "jsonb")
    @Column(name = "address", columnDefinition = "jsonb")
    private Address address;

    // getters and setters
}

For this case of using @Type, we don’t need to apply the AttributeConverter to the address field anymore. The custom type from Hypersistence Utilities handles the conversion task for us, making our code more neat. But note that @TypeDef and @Type annotations are deprecated in Hibernate 6.

4.3. Test Case

After all these changes, let’s run the Student persistence test case again:

Hibernate: 
    insert 
    into
        "public"
        ."student" ("address", "admit_year", "student_id") 
    values
        (?, ?, ?)
binding parameter [1] as [OTHER] - [Address(postCode=KT6 7BB, city=London)]
binding parameter [2] as [VARCHAR] - [2023]
binding parameter [3] as [VARCHAR] - [23876371]

We’ll see that JPA triggers the same insert SQL as before, except the first parameter is binding as OTHER instead of VARCHAR. This indicates that Hibernate binds the parameter as a JSONB type this time.

5. Conclusion

This comprehensive guide equipped us with the knowledge to proficiently store and manage JSON data in PostgreSQL using Spring Boot and JPA.

It addressed the mapping of JSON value to VARCHAR type and JSONB type. It also highlighted the significance of JSONB in enforcing JSON validation and facilitating querying and indexing.

As always, the sample code is available over on GitHub.

Course – LSD (cat=Persistence)

Get started with Spring Data JPA through the reference Learn Spring Data JPA course:

>> CHECK OUT THE COURSE
Course – LS – All

Get started with Spring and Spring Boot, through the Learn Spring course:

>> CHECK OUT THE COURSE
res – Persistence (eBook) (cat=Persistence)
3 Comments
Oldest
Newest
Inline Feedbacks
View all comments
Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.