Site icon JVM Advent

How to map JSON collections using JPA and Hibernate

map-json-collections-using-jpa-hibernate

map-json-collections-using-jpa-hibernate

Introduction

This article uses the old 1.2.0 version of the hibernate-types project.

The API has greatly changed ever since, so you should read this article instead.

The open-source hibernate-types project allows you to map Java objects or Jackson JsonNode as JPA entity properties.

Recently, thanks to our awesome contributors, we added support for type-safe collections to be persisted as JSON as well. In this article, you are going to see how to achieve this goal.

Maven dependency

First of all, you need to set up the following Maven dependency in your project pom.xml configuration file:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>1.2.0</version>
</dependency>

If you’re using older versions of Hibernate, check out the hibernate-types GitHub repository for more info about the matching dependency for your current Hibernate version.

Domain Model

Let’s assume we have the following Location Java object type.

public class Location implements Serializable {

    private String country;

    private String city;

    //Getters and setters omitted for brevity

    @Override
    public String toString() {
        return "Location{" +
                "country='" + country + '\'' +
                ", city='" + city + '\'' +
                '}';
    }
}

And, one Event entity:

@Entity(name = "Event")
@Table(name = "event")
public class Event extends BaseEntity {

    @Type(type = "jsonb")
    @Column(columnDefinition = "jsonb")
    private Location location;

    @Type(
        type = "jsonb",
        parameters = {
            @org.hibernate.annotations.Parameter(
                name = TypeReferenceFactory.FACTORY_CLASS,
                value = "com.vladmihalcea.hibernate.type.json.PostgreSQLGenericJsonBinaryTypeTest$AlternativeLocationsTypeReference"
            )
        }
    )
    @Column(columnDefinition = "jsonb")
    private List<Location> alternativeLocations = new ArrayList<Location>();

    //Getters and setters omitted for brevity
}

The BaseEntity defines some basic properties (e.g. @Id, @Version) and several customs Hibernate types, among which, we are interested in the JsonBinaryType one.

@TypeDefs({
    @TypeDef(name = "string-array", typeClass = StringArrayType.class),
    @TypeDef(name = "int-array", typeClass = IntArrayType.class),
    @TypeDef(name = "json", typeClass = JsonStringType.class),
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class),
    @TypeDef(name = "jsonb-node", typeClass = JsonNodeBinaryType.class),
    @TypeDef(name = "json-node", typeClass = JsonNodeStringType.class),
})
@MappedSuperclass
public class BaseEntity {

    @Id
    private Long id;

    @Version
    private Integer version;

    //Getters and setters omitted for brevity
}

For more details about using @MappedSuperclass, check out this article.

TypeReferenceFactory

To store the Location object in a jsonb PostgreSQL column, we just need to annotate the location property with @Type(type = "jsonb").

However, for the alternativeLocations collection, we need to provide the associated Jackson TypeReference so that we can reconstruct the very same type-safe Java collection when reading the JSON object from the relational database.

For this purpose, we provide the fully-qualified class of the TypeReferenceFactory implementation which looks as follows:

public static class AlternativeLocationsTypeReference 
    implements TypeReferenceFactory {
    
    @Override
    public TypeReference<?> newTypeReference() {
        return new TypeReference<List<Location>>() {};
    }
}

That’s it!

Testing time

When saving the following Event entity:

Location cluj = new Location();
cluj.setCountry("Romania");
cluj.setCity("Cluj-Napoca");

Location newYork = new Location();
newYork.setCountry("US");
newYork.setCity("New-York");

Location london = new Location();
london.setCountry("UK");
london.setCity("London");

Event event = new Event();
event.setId(1L);
event.setLocation(cluj);
event.setAlternativeLocations(
    Arrays.asList(newYork, london)
);

entityManager.persist(event);

Hibernate will generate the following SQL INSERT statement:

INSERT INTO event (
    version, 
    alternativeLocations, 
    location, 
    id
) 
VALUES (
    0, 
    [
        {"country":"US","city":"New-York"},
        {"country":"UK","city":"London"}
    ], 
    {"country":"Romania","city":"Cluj-Napoca"}, 
    1
)

Also, when retrieving back the Event entity, both the location and thealternativeLocations` properties are properly fetched:

Event event = entityManager.find(Event.class, eventId);

assertEquals(
    "Cluj-Napoca", 
    event.getLocation().getCity()
);

assertEquals(2, event.getAlternativeLocations().size());

assertEquals(
    "New-York", 
    event.getAlternativeLocations().get(0).getCity()
);
assertEquals(
    "London", 
    event.getAlternativeLocations().get(1).getCity()
);

Cool, right?

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

Conclusion

The hibernate-types project supports more than JSON types. You can map PostgreSQL ARRAY types or PostgreSQL-specific Enums, nullable Character, or even provide your own immutable Hibernate custom Types.

Author: Vlad Mihalcea

I work as a Hibernate Developer Advocate, I wrote the High-Performance Java Persistence book and video course series. I speak at conferences and I created several open-source projects, like FlexyPool, hibernate-types, and db-util.

Exit mobile version