中文 English

How to Map JSON Objects Using Generic Hibernate Types

Published: 2021-01-29
java jpa hibernate json jsonb maven MySQL PostgreSQL

Introduction

In this article, we’ll see how to map JSON columns to JPA entity attributes using the Hibernate Types open-source project.

While you can create your own custom Hibernate type to map JSON column types on Oracle, SQL Server, PostgreSQL, or MySQL, you don’t need to implement your own Hibernate Type since the Hibernate Types project already provides this functionality.

Domain Model

Assuming we have the following domain model: JsonType域模型

Location and Ticket are JSON Object(s), while Event and Participant are JPA entities. Our goal is to provide a Type that works with any kind of JSON JavaObject and any relational database that supports JSON columns.

Maven Dependencies

The first thing you need to do is configure the following Maven dependency in your project’s pom.xml:

<dependency>
    <groupId>com.vladmihalcea</groupId>
    <artifactId>hibernate-types-52</artifactId>
    <version>${hibernate-types.version}</version>
</dependency>

If you’re using an older version of Hibernate, check hibernate-typesGitHub存储库 for more information about matching dependencies for the current Hibernate version.

Declaring Hibernate Types

To use the JSON Hibernate Types, we must declare them using the @TypeDef annotation:

@TypeDefs({
    @TypeDef(name = "json", typeClass = JsonStringType.class),
    @TypeDef(name = "jsonb", typeClass = JsonBinaryType.class)
})
@MappedSuperclass
public class BaseEntity {
    //Code omitted for brevity
}

The TypeDef annotation can be applied to the base entity class or to the package-info.java file associated with the current entity package.

MySQL

MySQL 5.7 added support for the JSON type. At the JDBC level, it needs to be implemented as a String. Therefore, we will use JsonStringType.

The entity mapping is as follows:

@Entity(name = "Event")
@Table(name = "event")
public class Event extends BaseEntity {
 
    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Location location;
 
    public Location getLocation() {
        return location;
    }
 
    public void setLocation(Location location) {
        this.location = location;
    }
}
 
@Entity(name = "Participant")
@Table(name = "participant")
public class Participant extends BaseEntity {
 
    @Type(type = "json")
    @Column(columnDefinition = "json")
    private Ticket ticket;
 
    @ManyToOne
    private Event event;
 
    public Ticket getTicket() {
        return ticket;
    }
 
    public void setTicket(Ticket ticket) {
        this.ticket = ticket;
    }
 
    public Event getEvent() {
        return event;
    }
 
    public void setEvent(Event event) {
        this.event = event;
    }
}

When inserting the following entity:

final AtomicReference<Event> eventHolder = new AtomicReference<>();
final AtomicReference<Participant> participantHolder = new AtomicReference<>();
 
doInJPA(entityManager -> {
    Event nullEvent = new Event();
    nullEvent.setId(0L);
    entityManager.persist(nullEvent);
 
    Location location = new Location();
    location.setCountry("Romania");
    location.setCity("Cluj-Napoca");
 
    Event event = new Event();
    event.setId(1L);
    event.setLocation(location);
    entityManager.persist(event);
 
    Ticket ticket = new Ticket();
    ticket.setPrice(12.34d);
    ticket.setRegistrationCode("ABC123");
 
    Participant participant = new Participant();
    participant.setId(1L);
    participant.setTicket(ticket);
    participant.setEvent(event);
 
    entityManager.persist(participant);
 
    eventHolder.set(event);
    participantHolder.set(participant);
});

Hibernate generates the following statement:

INSERT INTO event (location, id)
VALUES (NULL(OTHER), 0)
 
INSERT INTO event (location, id)
VALUES ('{"country":"Romania","city":"Cluj-Napoca"}', 1)
 
INSERT INTO participant (event_id, ticket, id)
VALUES (1, {"registrationCode":"ABC123","price":12.34}, 1)

The JSONObject(s) are correctly implemented in their associated database columns.

Not only are the JSONObject(s) correctly converted from their database representation:

Event event = entityManager.find(Event.class, eventHolder.get().getId());
 
assertEquals("Cluj-Napoca", event.getLocation().getCity());
 
Participant participant = entityManager.find(
    Participant.class, participantHolder.get().getId());
assertEquals("ABC123", participant.getTicket().getRegistrationCode());

But we can even issue JSON-based native SQL queries:

List<String> participants = entityManager.createNativeQuery(
    "SELECT p.ticket -> \"$.registrationCode\" " +
    "FROM participant p " +
    "WHERE JSON_EXTRACT(p.ticket, \"$.price\") > 1 ")
.getResultList();

The JSON Object(s) can be modified:

event.getLocation().setCity("Constanța");
entityManager.flush();

Hibernate generates the correct UPDATE statement:

UPDATE event
SET location = '{"country":"Romania","city":"Constanța"}'
WHERE id = 1

PostgreSQL

PostgreSQL has supported JSON types since version 9.2. There are two types available:

PostgreSQL JSON Column Type

For the JSON column type, the Object(s) need to change both JSON mappings as follows:

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

Insertion and entity update work the same way, and we can even query the JSON column as follows:

List<String> participants = entityManager.createNativeQuery(
    "SELECT p.ticket ->>'registrationCode' " +
    "FROM participant p " +
    "WHERE p.ticket ->> 'price' > '10'")
.getResultList();

PostgreSQL JSONB Column Type

For the JSONB column type, we only need to change the columnDefinition attribute, since both json and jsonb PostgreSQL column types are handled by JsonBinaryType:

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

Insertion and JSONObject update work the same way, and the JSONB column type provides more advanced querying capabilities:

List<String> participants = entityManager.createNativeQuery(
    "SELECT jsonb_pretty(p.ticket) " +
    "FROM participant p " +
    "WHERE p.ticket ->> 'price' > '10'")
.getResultList();

References How to map JSON objects using generic Hibernate Types