Featured Image with Sidebar

Entities or DTOs – When should you use which projection?

By Thorben Janssen

Criteria API, Jpql, Query

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.

About the author

Thorben is an independent consultant, international speaker, and trainer specialized in solving Java persistence problems with JPA and Hibernate.
He is also the author of Amazon’s bestselling book Hibernate Tips - More than 70 solutions to common Hibernate problems.

Books and Courses

Coaching and Consulting

Leave a Repl​​​​​y

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.

  1. Hello,

    One small suggestion from me that, if you could add a small section at the end of each topic, something like, ‘Supported Implementation’, it would be really really helpful. I know that topics are mainly based on Hibernate but I dont find any place to see if the feature discussion here will be supported by any other JPA implementation or not.

    Thanks
    Rajeesh

    Reply

    1. Hi Rajeesh,

      Thats’s an interesting idea. I will look into it.

      In general:
      If it’s a JPA feature, I try to always reference it as a JPA feature. Otherwise, I talk about a Hibernate feature.
      JPA features have to be supported by all JPA implementations.

      Regards,
      Thorben

      Reply

    1. Hi,

      no, they are not the same.
      The Book class is an entity which gets managed by the persistence context. E.g., when you load a Book entity from the database and change any of its attributes, Hibernate will generate an SQL UPDATE statement to persist that change in the database.
      The BookValue class is a simple DTO which just gets loaded from the database. It’s a simple class which doesn’t get managed by the persistence context.

      Regards,
      Thorben

      Reply

    2. No they aren’t the same, in fact the Book is an entity (a mapped class with jpa annotations) and BookValue is the DTO (a simple POJO with attributes and getters+setters). If I understood well this article, sometimes when we want to read a specific data from a table it’s better to use the DTO’s proejctions (Because ,I’m not sure, by selecting entites, hibernate consume must take time to manage them and maybe do some dirty checks ‘What others think about that?’).

      Reply

  2. Nice article, I have a question. It is fair to think that eager fetching is a bad thing but we do DTO join fetch on multiple tables?

    What would be the performance for that scenario?

    Reply

    1. Hi Manuel,

      The DTO is your best option as long as you load only the data that you need for the specific use case.

      Regards,
      Thorben

      Reply

  3. Hi Thorben,

    Nice article, as always.
    I would like to ask you how to handle, using DTO approach, with the following scenarios:

    Fetch data from database with a single query using a BookDTO class that has an AuthorDTO class as its property.

    Even better, fetch data from database with a single query using a Author DTO class that has a List of BookDTO class as its property.

    Should I use more than just one query to do that? Filling the property member using another query inside a loop of the main’s query result?

    Thanks in advance!!

    Reply

    1. Hi Leandro,

      that depends on your use case and the number of records you’re selecting.

      In the 1st case (BookDTO + 1 AuthorDTO), you can implement a constructor on the BookDTO which instantiates the BookDTO and the AuthorDTO. Unfortunately, you can’t use more than 1 constructor expression in your projection. So, this is the only option to instantiate both DTOs in 1 query.

      The AuthorDTO with a List of BookDTO scenario is dangerous. Most often the fastest option is to use entities and to initialize the associations within your query.
      If you use DTO’s, you most often need to perform too many queries to fetch the associated BookDTOs. The additional queries then outweigh the performance benefit of the DTO projection.

      Regards,
      Thorben

      Reply

        1. A ResultTransformer is also an option. But if each Book was written by 1 Author, you can also implement a constructor on the BookDTO which takes the attributes of the BookDTO and the AuthorDTO. You can then instantiate the AuthorDTO within the constructor of the BookDTO.
          That’s not the most beautiful solution, but you can’t use more than 1 constructor expression per query. So, it’s a necessary workaround.

          Reply

  4. Many thakns for this really interesting article.
    I wonder, what about ResultMappings for DTOs?
    If you map the result straight to a DTO does the performance stand or does the fact that ResultMappings is still one more annotation on an Entity (and hence falls back on EntityManager work) just take you back to square one.

    Will have to test this sometime

    Reply

    1. Hi Michel,

      I haven’t tested it but I expect that the ResultMapping should provide a similar performance as the constructor expression.
      I’ll put it on my list and extend the test and this post in the future.

      Regards,
      Thorben

      Reply

  5. Very illustrative article! I’m an beginner and these things are so much helpful for newbies like me. One silly question, do we need to provide parameterized constructor for using these constructor queries?

    Reply

    1. Hi Mohammed,

      Yes, your class needs a constructor that matches the constructor expression.
      The constructor expression that you can use in JPQL or CriteriaQuery just describe a constructor call which Hibernate will execute for each record in the result set.

      Regards,
      Thorben

      Reply

  6. Excellent article my good man, I usually do do this em.createQuery(“select b.name from Book b, String.class”).getResultList(); Optimal or not?

    Reply

  7. 1 ) what about:
    query.setHint(QueryHints.HINT_READONLY, true);
    when doing an entity projection? I use it but I have yet to benchmark it.

    2) try this:
    List books = em.createQuery(“SELECT new org.thoughts.on.java.model.BookValue(b.id, b.title, a) FROM Book b JOIN FETCH b.author a”).getResultList();
    and enjoy this issue: https://hibernate.atlassian.net/browse/HHH-3345.

    Reply

    1. Thanks for the suggestion Andrew. I added a test using the read-only query hint here: https://thorben-janssen.com/entities-dtos-use-projection/#readonly
      Similar to the @Immutable test, you could expect better performance. But at least in this test scenario, it doesn’t provide any noticeable performance improvement.

      And yes, the constructor expression has several limitations. With Hibernate’s current implementation, you can’t reference entities (= you need to reference entity attributes) or attributes that map associations. You also can’t use multiple constructor expressions.
      I hope that they will improve that in Hibernate 6.

      Reply

      1. Erratum: Thanks for comparing HINT_READONLY to the other tests.

        I just remembered that I learned about that hint in Vlad’s book: “This optimization addresses both memory and CPU resources. Since the persistent data snapshot is not stored anymore, the Persistence Context consumes half the memory required by a default read-write Session.”.

        He also states that it helps with GC pressure. Your benchmark won’t show any of these advantages.

        Reply

        1. Yes, it helps when your persistence context gets so huge that it creates GC issues (or at least a lot of work…). That’s obviously not the case in this test.

          Reply

  8. Excellent Article Thorben.
    My biggest concern with DTOs is when we have large entities with a lot of fields. The constructor would start to get big, and hard to create queries.

    What about @Immutable entities?
    It would be a good comparison in your article to add @Immutable entities to the tests.
    What do you think?

    Thanks,

    Reply

    1. Thanks for the kind words and the interesting question, Joao!

      I added an immutable annotation to the test: https://thorben-janssen.com/entities-dtos-use-projection/#immutable
      And the result is interesting. I expected that it would perform better than a “normal” entity but not as good as the DTO. But it doesn’t seem to make any difference. I ran the tests multiple times and every time the results were almost identical.

      Regarding the constructor: Yes, that’s definitely a downside of the constructor expression. If you’re selecting a lot of columns, the constructor gets huge and the query gets hard to read and maintain.

      Reply

      1. WOW! That was fast! 🙂
        Thanks, Thorben.
        And too bad the immutable entity does not perform better than a mutable one! This would be very interesting to solve complex and large entities queries.

        Thanks again!

        Reply

{"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}