Mapping Java Records to JSON columns using Hibernate

Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?

Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.

So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!

Introduction

In this article, I’m going to explain how we can map Java Records to JSON columns when using Hibernate.

Because Hibernate ORM does not offer built-in support for JSON, we are going to use the Hypersistence Utils library, which allows you to map entity attributes to JSON column types, no matter if you are using Oracle, SQL Server, PostgreSQL, or MySQL.

Java Records

As I explained in this article, Java Records are a great way to build structured data types.

In our case, we want to encapsulate the book info in the following Book data structure:

Book Java Record

To create the above Book structure, we can use the following Java Record definition:

public record BookRecord (
    String title,
    String author,
    String publisher,
    Long priceInCents,
    URL url
) {}

How to serialize Java Records to JSON using Jackson

As I explained in this article, Java Records cannot be used as JPA or Hibernate entities, but you can use them as basic attributes:

Book Record JPA entity attribute

Because Hibernate ORM does not have built-in support for Java Records, we need to provide a custom Hibernate Type to map the Java Records to the proper database column type, and one option is to persist the Java Record in a JSON column.

To make sure that the Java Records is properly marshaled to a JSON object and unmarshaled back to a Java Record, we need to change the BookRecord, like this:

@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
public record BookRecord (
    String title,
    String author,
    String publisher,
    Long priceInCents,
    URL url
) implements Serializable {
    @JsonCreator
    public BookRecord(
        @JsonProperty("title") String title,
        @JsonProperty("author") String author,
        @JsonProperty("publisher") String publisher,
        @JsonProperty("priceInCents") String priceInCents,
        @JsonProperty("url") String url) {
        this(
            title,
            author,
            publisher,
            longValue(priceInCents),
            urlValue(url)
        );
    }
}

The following changes are required to be done in order to properly transform the Java Record to a JSON object using Jackson:

  • the @JsonAutoDetect annotation allows Jackson to access the private fields of the Java Record when serializing or deserializing it from its String representation that’s sent to the database
  • the Serializable interface is needed because, according to the JPA specification, every entity attribute must be serializable using the Java serialization mechanism.
  • the @JsonCreator annotation was added to mark that the extra constructor can be called by Jackson to pass the Strong-based JSON attribute in order to initialize the Java Record fields.

How to map Java Records to JSON columns using Hibernate

The Hypersistence Utils framework has support for Oracle, SQL Server, PostgreSQL, and MySQL, so depending on the database engine and the associated JSON column type, you can use one of the following options.

For Oracle, SQL Server, MySQL, PostgreSQL, and H2, you can use the JsonType from the Hypersistence Utils project to map Java Records to JSON columns.

In case you’re using MySQL, then you can use the dedicated JSON column type to store the properties attributes, while for PostgreSQL, you need to use the jsonb column type.

So, the Book entity would be mapped as follows.

For Hibernate 6, the mapping will look as follows:

@Entity(name = "Book")
@Table(name = "book")
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String ISBN;

    @Type(JsonType.class)
    @Column(columnDefinition = "JSON")
    private BookRecord properties;
    
    //Getters and setters omitted for brevity
}

And for Hibernate 5, like this:

@Entity(name = "Book")
@Table(name = "book")
@TypeDef(typeClass = JsonType.class, defaultForType = BookRecord.class)
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    @Column(columnDefinition = "JSON")
    private BookRecord properties;
    
    //Getters and setters omitted for brevity
}

Persisting Java Records to JSON columns using Hibernate

When persisting the following Book entity:

entityManager.persist(
    new Book()
        .setIsbn("978-9730228236")
        .setProperties(
            new BookRecord(
                "High-Performance Java Persistence",
                "Vlad Mihalcea",
                "Amazon",
                4499L,
                null
            )
        )
);

Hibernate generates the following INSERT statement:

INSERT INTO book (
    isbn, 
    properties, 
    id
) 
VALUES (
    '978-9730228236', 
    {
       "title":"High-Performance Java Persistence",
       "author":"Vlad Mihalcea",
       "publisher":"Amazon",
       "priceInCents":4499,
       "url":null
    }, 
    1
)

Fetching Java Records to JSON columns using Hibernate

When fetching the Book entity, we can see that we get the expected BookRecord:

Book book = entityManager
    .unwrap(Session.class)
    .bySimpleNaturalId(Book.class)
    .load("978-9730228236");

BookRecord bookRecord = book.getProperties();

assertEquals(
    "High-Performance Java Persistence",
    bookRecord.title()
);

assertEquals(
    "Vlad Mihalcea",
    bookRecord.author()
);

Updating Java Records

Java Records are immutable, so when we want to change the properties attribute, we have to create a new BookRecord instance, like this:

book.setProperties(
    new BookRecord(
        bookRecord.title(),
        bookRecord.author(),
        bookRecord.publisher(),
        bookRecord.priceInCents(),
        urlValue("https://www.amazon.com/dp/973022823X/")
    )
);

When the Persistence Context is flushed, Hibernate generates the following UPDATE statement:

UPDATE 
    book 
SET 
    properties = {
       "title":"High-Performance Java Persistence",
       "author":"Vlad Mihalcea",
       "publisher":"Amazon",
       "priceInCents":4499,
       "url":"https://www.amazon.com/dp/973022823X/"
    } 
WHERE 
    id = 1

Cool, right?

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.

Conclusion

As you can see, Java Records are very convenient to use when you want to structure your data using immutable objects that are compact to write and flexible to customize. By adding the Jackson annotations and the extra constructor, we were able to serialize the Java Record to a JSON object when persisting the Book entity to the database and deserialize the JSON column type back to its Java Record representation.

If you want to map an entity attribute as a Java Record and persist it to a JSON column, then the Hypersistence Utils project will surely allow you to achieve your goal.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.