6 Hibernate Mappings You Should Avoid for High-Performance Applications


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Hibernate provides lots of mapping features that allow you to map complex domain and table models. But the availability of these features doesn’t mean that you should use them in all of your applications. Some of them might be a great fit for smaller applications that are only used by a few users in parallel. But you should definitely not use them if you need to create a high-performance persistence layer.

In this article, I will show you 6 mapping features that will slow down your persistence layer. And if you want to dive deeper into these topics, I recommend you sign up for the Hibernate Performance Tuning Online Training.

Let’s start with some of Hibernate’s and JPA’s standard features.

1. Avoid FetchType.EAGER (and be cautious about to-one associations)

You probably already read a few articles and recommendations about the FetchTypes supported by JPA and Hibernate. So, I keep this recommendation short.

FetchType.EAGER tells your persistence provider to fetch a managed association as soon as you load the entity. So, it gets loaded from the database, whether or not you use the association in your business code. For most of your use cases, that means that you execute a few unnecessary database queries, which obviously slows down your application.

You can easily avoid that by using FetchType.LAZY. Hibernate will then only fetch the associated entities if you use the managed relationship in your business code. This is the default behavior for all to-many associations. For to-one associations, you need to set the FetchType in your association mapping explicitly.

@Entity
public class Review {

	@Id
	@GeneratedValue
	private Long id;

	private String comment;

	@ManyToOne(fetch = FetchType.LAZY)
	private Book book;
	
	...
}

When you do that, you need to pay special attention to one-to-one associations. As I explained in a recent Hibernate Tip, lazy loading of one-to-one associations only works reliably for the entity that maps the foreign key column. I will get into more details on that in section 3.

2. Don’t map Many-to-Many associations to a List

Hibernate can map a many-to-many association to a java.util.List or a java.util.Set. Most developers expect that the mapping to a java.util.List is the easier and more efficient one. But that’s not the case!

Removing an entry from a many-to-many association that you mapped to a List, is very inefficient.

b = em.find(Book.class, 1L);
		
b.getAuthors().remove(a);

Hibernate will remove all records from the association table before it adds the remaining ones.

06:12:51,636 DEBUG [org.hibernate.SQL] - 
    select
        book0_.id as id1_1_0_,
        book0_.title as title2_1_0_,
        book0_.version as version3_1_0_ 
    from
        Book book0_ 
    where
        book0_.id=?
06:12:51,639 DEBUG [org.hibernate.SQL] - 
    select
        authors0_.books_id as books_id1_2_0_,
        authors0_.authors_id as authors_2_2_0_,
        author1_.id as id1_0_1_,
        author1_.firstName as firstNam2_0_1_,
        author1_.lastName as lastName3_0_1_,
        author1_.version as version4_0_1_ 
    from
        Book_Author authors0_ 
    inner join
        Author author1_ 
            on authors0_.authors_id=author1_.id 
    where
        authors0_.books_id=?
06:12:51,642 DEBUG [org.hibernate.SQL] - 
    update
        Book 
    set
        title=?,
        version=? 
    where
        id=? 
        and version=?
06:12:51,644 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        Book_Author 
    where
        books_id=?
06:12:51,645 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        Book_Author
        (books_id, authors_id) 
    values
        (?, ?)
06:12:51,646 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        Book_Author
        (books_id, authors_id) 
    values
        (?, ?)

That’s obviously not the most efficient approach. If you remove only one association from the List, you would expect that Hibernate only deletes the corresponding record from the association table and keeps all other records untouched. You can achieve that by mapping the association as a java.util.Set.

@Entity
public class Book {

	@Id
	@GeneratedValue
	private Long id;

	@Version
	private int version;

	private String title;

	@ManyToMany
	private Set<Author> authors = new HashSet<Author>();
	
	...
	
}

If you now remove an associated entity from the Set, Hibernate only executes the expected SQL DELETE statement.

06:09:32,412 DEBUG [org.hibernate.SQL] - 
    select
        book0_.id as id1_1_0_,
        book0_.title as title2_1_0_,
        book0_.version as version3_1_0_ 
    from
        Book book0_ 
    where
        book0_.id=?
06:09:32,414 DEBUG [org.hibernate.SQL] - 
    select
        authors0_.books_id as books_id1_2_0_,
        authors0_.authors_id as authors_2_2_0_,
        author1_.id as id1_0_1_,
        author1_.firstName as firstNam2_0_1_,
        author1_.lastName as lastName3_0_1_,
        author1_.version as version4_0_1_ 
    from
        Book_Author authors0_ 
    inner join
        Author author1_ 
            on authors0_.authors_id=author1_.id 
    where
        authors0_.books_id=?
06:09:32,417 DEBUG [org.hibernate.SQL] - 
    update
        Book 
    set
        title=?,
        version=? 
    where
        id=? 
        and version=?
06:09:32,420 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        Book_Author 
    where
        books_id=? 
        and authors_id=?

3. Don’t use bidirectional One-to-One mappings

I briefly mentioned lazy loading of one-to-one associations in the first section. But it’s important and tricky enough to get into more details on it.

For all managed associations, you can use the fetch attribute of the defining annotation to set the FetchType. But even though that includes the @OneToOne annotation, that mapping is a little bit special. That’s because it’s the only relationship for which you can define a to-one association on the entity that doesn’t map the foreign key column.

If you do that, Hibernate needs to perform a query to check if it has to initialize the attribute with null or a proxy object. And the Hibernate team decided, that if they have to execute a query anyways, it’s better to fetch the associated entity instead of just checking if it exists and fetching it later. Due to that, lazy loading doesn’t work for this kind of one-to-one association mapping. But it works perfectly fine on the entity that maps the foreign key column.

So, what should you do instead?

You should only model unidirectional one-to-one associations that share the same primary key value on the entity that maps the foreign key column. Bidirectional and unidirectional associations on the entity that doesn’t model the foreign key column don’t support any lazy fetching.

Modeling a unidirectional one-to-one association with a shared primary key value is pretty simple. You just need to annotate the association with an additional @MapsId annotation. That tells your persistence provider to use the primary key value of the associated entity as the primary key value of this entity.

@Entity
public class Manuscript {
 
    @Id
    private Long id;
 
    @OneToOne
    @MapsId
    @JoinColumn(name = "id")
    private Book book;
 
    ...
}

Due to the shared primary key value, you don’t need a bidirectional association mapping. When you know the primary key value of a Book entity, you also know the primary key value of the associated Manuscript entity. So, you can simply call the find method on your EntityManager to fetch the Manuscript entity.

Book b = em.find(Book.class, 100L);
Manuscript m = em.find(Manuscript.class, b.getId());

4. Avoid the @Formula annotation

The @Formula annotation enables you to map the return value of an SQL snippet to a read-only entity attribute. It’s an interesting feature that you can use in smaller applications that don’t need to handle lots of parallel requests. But it’s not a great fit for a high-performance persistence layer.

Here you can see an example of the @Formula annotation. I use it to calculate the age of an Author based on her/his date of birth.

@Entity
public class Author {

	@Id
	@GeneratedValue
	private Long id;
	
	@Version
	private int version;

	private String firstName;

	private String lastName;
	
	private LocalDate dateOfBirth;
	
	@Formula(value = "date_part('year', age(dateOfBirth))")
	private int age;
	
	...
}

The main issue with the @Formula annotation is that the provided SQL snippet gets executed every time you fetch the entity. But I have never seen an application that used the read-only attributes every time the entity got fetched.

06:16:30,054 DEBUG [org.hibernate.SQL] - 
    select
        author0_.id as id1_0_0_,
        author0_.dateOfBirth as dateOfBi2_0_0_,
        author0_.firstName as firstNam3_0_0_,
        author0_.lastName as lastName4_0_0_,
        author0_.version as version5_0_0_,
        date_part('year',
        age(author0_.dateOfBirth)) as formula0_0_ 
    from
        Author author0_ 
    where
        author0_.id=?

In a smaller application, that isn’t an issue. Your database can easily execute the more complex SQL statement. But in a high-performance persistence layer that needs to handle lots of parallel requests, you should avoid any unnecessary complexity. In these cases, you can better call a database function and use a DTO projection.

5. Don’t use the @OrderBy annotation

My recommendation for the @OrderBy annotation is basically the same as for the @Formula annotation: It’s a great feature for smaller applications but not a great fit for a high-performance persistence layer.

@Entity
public class Book {
	
	@Id
	@GeneratedValue
	private Long id;

	@Version
	private int version;

	private String title;

	@ManyToMany
	@OrderBy(value = "lastName ASC, firstName ASC")
	private Set<Author> authors = new HashSet<Author>();
	
	...
}

Using the @OrderBy annotation, you can define an ORDER BY clause that gets used when Hibernate fetches the associated entities. But not all of your use cases will need to retrieve the association in a specific order. If you don’t need it, the ordering creates an overhead that you should avoid, if you need to optimize your persistence layer for performance.

If performance is more important than ease of use of your persistence layer, you should prefer a use case specific JPQL query. By doing that, you can add the ORDER BY clause whenever you need it. In all other use cases, you can fetch the associated entities in an undefined order.

6. Avoid CascadeType.REMOVE for large associations

Cascading tells Hibernate to perform an operation not only on the entity on which you triggered it but also on the associated entities. That makes persist, merge, and remove operations much easier.

But using CascadeType.REMOVE on a large association is very inefficient. It requires Hibernate to fetch all associated entities, to change the life cycle state of each entity to removed and execute an SQL DELETE statement for each of them. Doing that for a few dozen or more entities can take a considerable amount of time.

06:32:42,988 DEBUG [org.hibernate.SQL] - 
    select
        author0_.id as id1_0_0_,
        author0_.firstName as firstNam2_0_0_,
        author0_.lastName as lastName3_0_0_,
        author0_.version as version4_0_0_ 
    from
        Author author0_ 
    where
        author0_.id=?
06:32:43,014 DEBUG [org.hibernate.SQL] - 
    select
        books0_.authorId as authorId2_2_0_,
        books0_.bookId as bookId1_2_0_,
        book1_.id as id1_1_1_,
        book1_.publisherid as publishe5_1_1_,
        book1_.publishingDate as publishi2_1_1_,
        book1_.title as title3_1_1_,
        book1_.version as version4_1_1_,
        publisher2_.id as id1_3_2_,
        publisher2_.name as name2_3_2_,
        publisher2_.version as version3_3_2_ 
    from
        BookAuthor books0_ 
    inner join
        Book book1_ 
            on books0_.bookId=book1_.id 
    left outer join
        Publisher publisher2_ 
            on book1_.publisherid=publisher2_.id 
    where
        books0_.authorId=?
06:32:43,032 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        BookAuthor 
    where
        bookId=?
06:32:43,034 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        BookAuthor 
    where
        bookId=?
06:32:43,036 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        Book 
    where
        id=? 
        and version=?
06:32:43,039 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        Book 
    where
        id=? 
        and version=?
06:32:43,042 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        Author 
    where
        id=? 
        and version=?

Using a CriteriaDelete or a JPQL DELETE statement enables you to remove all associated entities with one statement. That’s avoids the life cycle state transitions and drastically reduces the number of executed queries. So, it shouldn’t be a surprise that it’s also much faster.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<Book> delete = cb.createCriteriaDelete(Book.class);
Root<Book> book = delete.from(Book.class);
ParameterExpression<Author> p = cb.parameter(Author.class, "author");
delete.where(cb.isMember(p, book.get(Book_.authors)));

Query query = em.createQuery(delete);
query.setParameter(p, em.find(Author.class, 8L));
query.executeUpdate();

But please keep in mind, that Hibernate doesn’t trigger any life cycle events for these entities and that it doesn’t remove entities in your 1st level cache.

Conclusion

Hibernate provides lots of mapping features that can make implementing and using your persistence layer much easier. But not all of them are an excellent fit for a high-performance persistence layer.

In general, you should avoid all mappings that are not required for every use case or that make your mapping more complex. 2 typical examples for that are the @Formula and the @OrderBy annotations.

In addition to that, you should always monitor the executed SQL statements. It should be evident that the fewer queries your use cases require, the faster they are. So, make sure that Hibernate uses your mappings efficiently.

3 Comments

  1. Avatar photo Cassey John says:

    Well Explained. Thanks

  2. Avatar photo Binh Thanh Nguyen says:

    Thanks, nice tips.

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.