How to store schema-less EAV (Entity-Attribute-Value) data using JSON and 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

One of my Twitter followers has recently asked me about a way of storing EAV (Entity-Attribute-Value) data using JPA and Hibernate, and, because this is a very good question, I decided to turn into a blog post.

In this article, you are going to see how you can use JSON types to store schema-less EAV data.

Domain Model

Let’s assume we have an online bookstore, and books are modeled via the Book entity.

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 = "jsonb")
    private JsonNode properties;

    //Getters and setters omitted for brevity
}

And for Hibernate 5, like this:

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

    @Id
    @GeneratedValue
    private Long id;

    @NaturalId
    private String isbn;

    @Column(columnDefinition = "jsonb")
    private JsonNode properties;

    //Getters and setters omitted for brevity
}

The Book has a unique isbn attribute, hence it’s mapped as a @NaturalId.

For more details about the @NaturalId annotation, check out this article.

The properties attribute is of type JsonNode, which contains the EAV data structure.

Because Hibernate does not support JSON types natively, we need a JsonType to handle the Jackson-specific JsonNode object type.

Hypersistence Utils

The first thing you need to do is to set up the following Maven dependency in your project pom.xml configuration file:

<dependency>
    <groupId>io.hypersistence</groupId>
    <artifactId>hypersistence-utils-hibernate-55</artifactId>
    <version>${hypersistence-utils.version}</version>
</dependency>

That’s it!

Testing time

When persisting the following Book entity:

Book book = new Book();
book.setIsbn( "978-9730228236" );
book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99" +
        "}"
    )
);

entityManager.persist( book );

Hibernate generates the proper SQL INSERT statement:

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

We can also fetch the Book entity and even modify it, and Hibernate will take care of all the persistence logic:

Session session = entityManager.unwrap( Session.class );

Book book = session
    .bySimpleNaturalId( Book.class )
    .load( "978-9730228236" );

LOGGER.info( "Book details: {}", book.getProperties() );

book.setProperties(
    JacksonUtil.toJsonNode(
        "{" +
        "   \"title\": \"High-Performance Java Persistence\"," +
        "   \"author\": \"Vlad Mihalcea\"," +
        "   \"publisher\": \"Amazon\"," +
        "   \"price\": 44.99," +
        "   \"url\": \"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/dp/973022823X/\"" +
        "}"
    )
);

When running the test case above, Hibernate generates the following SQL statements:

SELECT  b.id AS id1_0_
FROM    book b
WHERE   b.isbn = '978-9730228236'

SELECT  b.id AS id1_0_0_ ,
        b.isbn AS isbn2_0_0_ ,
        b.properties AS properti3_0_0_
FROM    book b
WHERE   b.id = 1

-- Book details: {"price":44.99,"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon"}

UPDATE book 
SET properties = '{"title":"High-Performance Java Persistence","author":"Vlad Mihalcea","publisher":"Amazon","price":44.99,"url":"https://www.amazon.com/High-Performance-Java-Persistence-Vlad-Mihalcea/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

Hibernate types are great! Even if Hibernate comes with a great variety of types, even Java 1.8 Date/Time, you can easily map custom types like ARRAY or JSON.

For JSON, not only that you can map JSON structures as Java objects, as we saw in this article, but you benefit from schema-less structures as well.

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.