Imagine having a tool that can automatically detect JPA and Hibernate performance issues.
Hypersistence Optimizer is that tool!
Introduction
In this article, we are going to see how we can map multiple JPA entities to one table with Hibernate. Using multiple entities can speed up both read and write operations.
This question has been a recurring theme on StackOverflow, so I decided to explain the advantages of mapping multiple JPA entities to the same database table.
Let’s assume we have a book database table that looks as follows:
Notice that the properties column is of the jsonb type. Therefore, the properties column value is as large as the containing JSON object. For this reason, we don’t want to map the book table to a single Book entity because we would have to fetch the properties column every time we load a Book entity from the database.
So, we will map two entities to the book table. One entity is BookSummary, which maps only a subset of book table columns. On the other hand, the Book entity maps all columns from the book table.
Both the BookSummary and the Book entities extend the BaseBook abstract class as illustrated in the following diagram.
The BaseBook is an abstract class that contains the base attributes that will be shared by all entities mapped to the book table, and it looks as follows:
@MappedSuperclass
public abstract class BaseBook<T extends BaseBook> {
@Id
@GeneratedValue
private Long id;
@NaturalId
@Column(length = 15)
private String isbn;
@Column(length = 50)
private String title;
@Column(length = 50)
private String author;
public Long getId() {
return id;
}
public T setId(Long id) {
this.id = id;
return (T) this;
}
public String getIsbn() {
return isbn;
}
public T setIsbn(String isbn) {
this.isbn = isbn;
return (T) this;
}
public String getTitle() {
return title;
}
public T setTitle(String title) {
this.title = title;
return (T) this;
}
public String getAuthor() {
return author;
}
public T setAuthor(String author) {
this.author = author;
return (T) this;
}
}
The abstract BaseBook is using the @MappedSuperclass annotation, as otherwise, base class attributes are not inherited by entities extending the BaseBook class.
For more details about the @MappedSuperclass JPA annotation, check out this article.
Notice that the setter methods use the fluent interface pattern, and the returning type is given by the <T> type parameter which can be defined by each extending class so that the fluent API always returns the object type reference where the calling method is defined, and not the superclass object reference.
For more details about using the Fluent API pattern with JPA entities, check out this article.
The BookSummary entity simply extends the BaseBook superclass and adds no additional entity attribute.
@Entity(name = "BookSummary")
@Table(name = "book")
public class BookSummary extends BaseBook<BookSummary> {
}
On the other hand, the Book entity extends the BaseBook superclass and maps the properties attribute.
@Entity(name = "Book")
@Table(name = "book")
@TypeDef(
name = "jsonb",
typeClass = JsonBinaryType.class
)
@DynamicUpdate
public class Book extends BaseBook<Book> {
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private String properties;
public String getProperties() {
return properties;
}
public Book setProperties(String properties) {
this.properties = properties;
return this;
}
public ObjectNode getJsonProperties() {
return (ObjectNode) JacksonUtil
.toJsonNode(properties);
}
}
By default, Hibernate does not support JSON column types. However, you can use the hibernate-types open-source project which provides a great variety of extra Hibernate Types, like JSON, Array, Range, HStore, PostgreSQL Enum types.
For more details about mapping a jsonb column type to a String attribute, check out this article
And the Hibernate dirty checking mechanism will detect the change and trigger an UPDATE statement when flushing the Persistence Context:
UPDATE
book
SET
author = 'Vlad Mihalcea',
title = 'High-Performance Java Persistence, 2nd edition'
WHERE
id = 1
Notice that the UPDATE is done only for the attributes defined by the BookSummary entity.
Now, in a subsequent Persistence Context, we can also fetch the Book entity associated with the High-Performance Java Persistence book table row.
Book book = entityManager
.unwrap(Session.class)
.bySimpleNaturalId(Book.class)
.load("978-9730228236");
assertEquals(
"High-Performance Java Persistence, 2nd edition",
book.getTitle()
);
Because the Book entity maps the properties attribute as well, we can both read and write the properties attribute.
ObjectNode jsonProperties = book.getJsonProperties();
assertEquals(
"4.7 out of 5 stars",
jsonProperties.get("average_review").asText()
);
jsonProperties.put(
"average_review",
"4.8 out of 5 stars"
);
book.setProperties(
JacksonUtil.toString(jsonProperties)
);
When flushing the current Persistence Context, Hibernate will issue an UPDATE statement that will set the properties column accordingly:
UPDATE
book
SET
properties = {
"url": "https:\/\/amzn.com\/973022823X",
"price": 44.99,
"weight": "2.5 pounds",
"publisher": "Amazon",
"dimensions": "8.5 x 1.1 x 11 inches",
"average_review": "4.8 out of 5 stars",
"publication_date": "2016-20-12"
}
WHERE
id = 1
This time, Hibernate only sets the properties column in the UPDATE statement because the Book entity uses the @DynamicUpdate annotation.
For more details about how the @DynamicUpdate annotation works when using JPA and Hibernate, check out this article.
Online Workshops
If you enjoyed this article, I bet you are going to love my upcoming Online Workshops.
So mapping multiple entities to the same database table, not only that it allows us to fetch data more efficiently, but it also speeds up the dirty checking process as Hibernate has to inspect fewer entity properties.
The only drawback of using this approach is that you have to make sure you don’t fetch more than one entity type for the same database table record, as otherwise, this can cause inconsistencies when flushing the Persistence Context.