JPA and Hibernate allow you to use DTOs and entities as projections in your JPQL and Criteria queries. When I talk about Hibernate performance in my online training or at a workshop, I get often asked, if it matters which projection you use.

The answer is: YES! Choosing the right projection for your use case can have a huge performance impact.

And I’m not talking about selecting only the data that you need. It should be obvious that selecting unnecessary information will not provide you any performance benefits.



The Main Difference Between DTOs And Entities

There is another, often ignored difference between entities and DTOs. Your persistence context manages the entities.

That is a great thing when you want to update an entity. You just need to call a setter method with the new value. Hibernate will take care of the required SQL statements and write the changes to the database.

That’s comfortable to use, but you don’t get it for free. Hibernate has to perform dirty checks on all managed entities to find out if it needs to store any changes in the database. That takes time and is completely unnecessary when you just want to send a few information to the client.

You also need to keep in mind that Hibernate and any other JPA implementation, stores all managed entities in the 1st level cache. That seems to be a great thing. It prevents the execution of duplicate queries and is required for Hibernate’s write behind optimization. But managing the 1st level cache takes time and can even become a problem if you select hundreds or thousands of entities.

So, using entities creates an overhead, which you can avoid when you use DTOs. But does that mean that you shouldn’t use entities?

No, it doesn’t.

Projections For Write Operations

Entity projections are great for all write operations. Hibernate and any other JPA implementation manages the state of your entities and creates the required SQL statements to persist your changes in the database. That makes the implementation of most create, update and remove operations very easy and efficient.

EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); Author a = em.find(Author.class, 1L); a.setFirstName("Thorben"); em.getTransaction().commit(); em.close();

Projections For Read Operations

But read-only operations should be handled differently. Hibernate doesn’t need to manage any states or perform dirty checks if you just want to read some data from the database.

So, from a theoretical point of view, DTOs should be the better projection for reading your data. But does it make a real difference?

I did a small performance test to answer this question.

Test Setup

I used the following domain model for the test. It consists of an Author and a Book entity which are associated by a many-to-one association. So, each Book was written by 1 Author.

@Entity public class Author { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", updatable = false, nullable = false) private Long id; @Version private int version; private String firstName; private String lastName; @OneToMany(mappedBy = "author") private List bookList = new ArrayList(); ... }

To make sure that Hibernate doesn’t fetch any extra data, I set the FetchType for the @ManyToOne association on the Book entity to LAZY. You can read more about the different FetchTypes and their effect in my Introduction to JPA FetchTypes.

@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", updatable = false, nullable = false) private Long id; @Version private int version; private String title; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fk_author") private Author author; ... }

And I created a test database with 10 Authors. Each of them wrote 10 Books. So the database contains 100 Books in total.

In each test, I will use a different projection to select all 100 Books and measure the time required to execute the query and the transaction. To reduce the impact of any side-effects, I do this 1000 times and measure the average time.

OK, so let’s get started.

Selecting An Entity

Entity projections are the most popular ones in most applications. You already have the entity and JPA makes it easy to use them as a projection.

So, let’s run this small test case and measure how long it takes to retrieve 100 Book entities.

long timeTx = 0; long timeQuery = 0; long iterations = 1000; // Perform 1000 iterations for (int i = 0; i < iterations; i++) { EntityManager em = emf.createEntityManager(); long startTx = System.currentTimeMillis(); em.getTransaction().begin(); // Execute Query long startQuery = System.currentTimeMillis(); List<Book> books = em.createQuery("SELECT b FROM Book b").getResultList(); long endQuery = System.currentTimeMillis(); timeQuery += endQuery - startQuery; em.getTransaction().commit(); long endTx = System.currentTimeMillis(); em.close(); timeTx += endTx - startTx; } System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations); System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

On average, it takes 2ms to execute the query, retrieve the result and map it to 100 Book entities. And 2.89ms if you include the transaction handling. Not bad for a small and not so new laptop.

Transaction: total 2890 per iteration 2.89 Query: total 2000 per iteration 2.0

The Effect Of The Default FetchType For To-One Associations

When I showed you the Book entity, I pointed out that I set the FetchType to LAZY to avoid additional queries. By default, the FetchtType of a to-one association is EAGER which tells Hibernate to initialize the association immediately.

That requires additional queries and has a huge performance impact if your query selects multiple entities. Let’s change the Book entity to use the default FetchType and perform the same test.

@Entity public class Book { @ManyToOne @JoinColumn(name = "fk_author") private Author author; ... }

That little change more than tripled the execution time of the test case. Instead of 2ms it now took 7.797ms to execute the query and map the result. And the time per transaction went up to 8.681ms instead of 2.89ms.

Transaction: total 8681 per iteration 8.681 Query: total 7797 per iteration 7.797

So, better make sure to set the FetchType to LAZY for your to-one associations.

Selecting An @Immutable Entity

Joao Charnet asked me in the comments to add an immutable entity to the test. The interesting question is: Does a query that returns entities annotated with @Immutable perform better?

Hibernate knows that it doesn’t have to perform any dirty checks on these entities because they’re immutable. That could result in a better performance. So, let’s give it a try.

I added the following ImmutableBook entity to the test.

@Entity @Table(name = "book") @Immutable public class ImmutableBook { @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "id", updatable = false, nullable = false) private Long id; @Version private int version; private String title; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fk_author") private Author author; ... }

It’s a copy of the Book entity with 2 additional annotations. The @Immutable annotation tells Hibernate that this entity can’t be changed. And the @Table(name = “book”) maps the entity to the book table. So, it maps the same table as the Book entity and we can run the same test with the same data as before.

long timeTx = 0; long timeQuery = 0; long iterations = 1000; // Perform 1000 iterations for (int i = 0; i < iterations; i++) { EntityManager em = emf.createEntityManager(); long startTx = System.currentTimeMillis(); em.getTransaction().begin(); // Execute Query long startQuery = System.currentTimeMillis(); List<Book> books = em.createQuery("SELECT b FROM ImmutableBook b") .getResultList(); long endQuery = System.currentTimeMillis(); timeQuery += endQuery - startQuery; em.getTransaction().commit(); long endTx = System.currentTimeMillis(); em.close(); timeTx += endTx - startTx; } System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations); System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

Interestingly enough, it doesn’t make any difference, if the entity is immutable or not. The measured average execution time for the transaction and the query are almost identical to the previous test.

Transaction: total 2879 per iteration 2.879 Query: total 2047 per iteration 2.047

Selecting An Entity With QueryHints.HINT_READONLY

Andrew Bourgeois suggested to include a test with a read-only query. So, here it is.

This test uses the Book entity that I showed you at the beginning of the post. But it requires a change to test case.

JPA and Hibernate support a set of query hints which allow you to provide additional information about the query and how it should be executed. The query hint QueryHints.HINT_READONLY tells Hibernate to select the entities in read-only mode. So, Hibernate doesn’t need to perform any dirty checks on them, and it can apply other optimizations.

You can set this hint by calling the setHint method on the Query interface.

long timeTx = 0; long timeQuery = 0; long iterations = 1000; // Perform 1000 iterations for (int i = 0; i < iterations; i++) { EntityManager em = emf.createEntityManager(); long startTx = System.currentTimeMillis(); em.getTransaction().begin(); // Execute Query long startQuery = System.currentTimeMillis(); Query query = em.createQuery("SELECT b FROM Book b"); query.setHint(QueryHints.HINT_READONLY, true); query.getResultList(); long endQuery = System.currentTimeMillis(); timeQuery += endQuery - startQuery; em.getTransaction().commit(); long endTx = System.currentTimeMillis(); em.close(); timeTx += endTx - startTx; } System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations); System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

You might expect that setting the query to read-only provides a noticeable performance benefit. Hibernate has to perform less work so it should be faster.

But as you can see below, the execution times are almost identical to the previous tests. At least in this test scenario, setting QueryHints.HINT_READONLY to true doesn’t improve the performance.

Transaction: total 2842 per iteration 2.842 Query: total 2006 per iteration 2.006

Selecting A DTO

Loading 100 Book entities took about 2ms. Let’s see if fetching the same data with a constructor expression in a JPQL query performs better.

And you can, of course, also use constructor expressions in your Criteria queries.

long timeTx = 0; long timeQuery = 0; long iterations = 1000; // Perform 1000 iterations for (int i = 0; i < iterations; i++) { EntityManager em = emf.createEntityManager(); long startTx = System.currentTimeMillis(); em.getTransaction().begin(); // Execute the query long startQuery = System.currentTimeMillis(); List<BookValue> books = em.createQuery("SELECT new org.thoughts.on.java.model.BookValue(b.id, b.title) FROM Book b").getResultList(); long endQuery = System.currentTimeMillis(); timeQuery += endQuery - startQuery; em.getTransaction().commit(); long endTx = System.currentTimeMillis(); em.close(); timeTx += endTx - startTx; } System.out.println("Transaction: total " + timeTx + " per iteration " + timeTx / (double)iterations); System.out.println("Query: total " + timeQuery + " per iteration " + timeQuery / (double)iterations);

As expected, the DTO projection performs much better than the entity projection.

Transaction: total 1678 per iteration 1.678 Query: total 1143 per iteration 1.143

On average, it took 1.143ms to execute the query and 1.678ms to perform the transaction. That’s is a performance improvement of ~43% for the query and ~42% for the transaction.

Not bad for a small change that just takes a minute to implement.

And in most projects, the performance improvement of the DTO projection will be even higher. It allows you to select the data that you need for your use case and not just all the attributes mapped by the entity. And selecting less data almost always results in a better performance.

Summary

Choosing the right projection for your use case is easier and more important than you might have thought.

When you want to implement a write operation, you should use an entity as your projection. Hibernate will manage its state, and you just have to update its attributes within your business logic. Hibernate will then take care of the rest.

You’ve seen the results of my small performance test. My laptop might not be the best environment to run these tests and it is definitely slower than your production environment. But the performance improvement is so big that it’s obvious which projection you should use.

The query that used a DTO projection was ~40% faster than the one that selected entities. So, better spend the additional effort to create a DTO for your read-only operations and use it as the projection.

And you should also make sure to use FetchType.LAZY for all associations. As you’ve seen in the test, even one eagerly fetched to-one association might triple the execution time of your query. So, better use FetchType.LAZY and initialize the relationships that you need for your use case.