Entity to DTO: A Performance Comparison of Three Mapping Approaches



This content originally appeared on Level Up Coding – Medium and was authored by Flavius Zichil

Using DTOs when exposing data from an application is highly recommended since it has a lot of advantages. From managing the data that is exposed to addressing performance and security concerns, DTOs should be present in any application.

However, DTOs come with a significant drawback: they introduce additional complexity because you always need to convert between the entity and the DTO, and vice versa.

In this article, we’ll go through a couple of ways of converting from Entities to DTOs, and we’ll try to find which of them is the most performant.

Mapping techniques

The mapping options that will be compared during this article are:

  • Manual mapping — this is the most basic technique, where the developer manually maps the fields of the Entity into a DTO.
  • MapStruct — similar to the manual approach, but this is done automatically, with no need for the developer to manually map the fields.
  • JPQL — using JPQL, the developer can convert the Entity to a DTO directly from the query

All these approaches have their advantages and drawbacks, and using one of them may depend on the complexity of the development or the architectural requirements.

Comparison

We’ll now try to compare the performance of the 3 approaches using 2 scenarios:

  • a simple scenario
  • a more complex scenario

The simple scenario

The experiment starts with a simple entity that has no relationships with other entities. Inside the database table, 1000 books were inserted.

@Entity
public class Book {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@Column(name = "author")
private String author;

@Column(name = "releaseDate")
private LocalDate releaseDate;

@Column(name = "numberOfPages")
private Long numberOfPages;

@Column(name = "language")
private String language;
}

The DTO has the same structure as the entity.

public class BookDTO {

private Long id;
private String name;
private String author;
private LocalDate releaseDate;
private Long numberOfPages;
private String language;
}

The 3 types of mapping from Entity to DTO were implemented as presented below:

// manual mapping   
public List<BookDTO> getBooksManual() {
return repository.findAll()
.stream()
.map(book -> new BookDTO(
book.getId(),
book.getName(),
book.getAuthor(),
book.getReleaseDate(),
book.getNumberOfPages(),
book.getLanguage())
).collect(Collectors.toList());
}

// MapStruct mapping
public List<BookDTO> getBooksMapStruct() {
return mapper.booksToBookDTOs(repository.findAll());
}

// JPQL mapping
public List<BookDTO> getBooksJpql() {
return repository.findAllAsDTO();
}

@Query("SELECT new com.medium.domain.BookDTO(" +
"b.id, " +
"b.name, " +
"b.author, " +
"b.releaseDate, " +
"b.numberOfPages, " +
"b.language) " +
"FROM Book b")
List<BookDTO> findAllAsDTO();

In all 3 cases, the return type and values are the same: a list of BookDTO.

Now that we have all the necessary data, let’s compare the times it takes for them to execute. For each scenario, 100 requests were executed, and below you can find the average duration in ms for each of them.

Performance comparison (ms)

The manual approach took 11ms, the MapStruct one took 10ms, and the JPQL one took 8ms.

So, as you can see, the differences are negligible between them. The JPQL version is a little bit faster, but there is no significant difference.

The more complex scenario

Now that we know how they perform on a simple entity, let’s make the entity a little bit more complex by adding a @OneToMany relationship to it. Each Book will now have a Publisher.

@Entity
public class Book {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "name")
private String name;

@Column(name = "author")
private String author;

@Column(name = "releaseDate")
private LocalDate releaseDate;

@Column(name = "numberOfPages")
private Long numberOfPages;

@Column(name = "language")
private String language;

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
@JoinColumn(name = "publisher_id", nullable = false)
private Publisher publisher;
}
@Entity
public class Publisher {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "publisher_id")
private Long id;

@Column(name = "name")
private String name;

@Column(name = "since")
private LocalDate since;

@Column(name = "address")
private String address;

@Column(name = "country")
private String country;

@OneToMany(mappedBy = "publisher",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Book> books;
}

The BookDTO now becomes:

public class BookDTO {

private Long id;
private String name;
private String author;
private LocalDate releaseDate;
private Long numberOfPages;
private String language;
private PublisherDTO publisher;
}

public class PublisherDTO {

private Long id;
private String name;
private LocalDate since;
private String address;
private String country;
}

Also, the methods were modified to include the latest changes. The MapStruct method does not change; only the implementation of the mapper.

// manual mapping
public List<BookDTO> getBooksManual() {
return repository.findAllWithPublisher()
.stream()
.map(book -> new BookDTO(
book.getId(),
book.getName(),
book.getAuthor(),
book.getReleaseDate(),
book.getNumberOfPages(),
book.getLanguage(),
new PublisherDTO(
book.getPublisher().getId(),
book.getPublisher().getName(),
book.getPublisher().getSince(),
book.getPublisher().getAddress(),
book.getPublisher().getCountry())
)
)
.collect(Collectors.toList());
}

// JPQL mapping
@Query("SELECT new com.medium.domain.BookDTO(" +
"b.id, b.name, b.author, b.releaseDate, b.numberOfPages, b.language," +
"new com.medium.domain.PublisherDTO(" +
"b.publisher.id, " +
"b.publisher.name, " +
"b.publisher.since, " +
"b.publisher.address, " +
"b.publisher.country)) " +
"FROM Book b")
List<BookDTO> findAllAsDTO();

After executing again the 100 requests for each scenario, the following results were obtained.

Performance comparison (ms)

The manual approach took 472ms, the MapStruct one took 494ms, and the JPQL one took 19ms.

This time, the difference is notable. The JPQL option performed way better than the other 2 options, while manual and MapStruct had similar performances. But isn’t the difference too big? Let’s analyse it further to find out why this is happening.

Both manual and MapStruct versions are using the findAll() method from JpaRepository. Also, we can see that the fetching type for the publisher is FetchType.LAZY, that means we have the N+1 problem here. That means that there will be 1001 selects done in the database for each fetch:

  • 1 select to get all the books
  • 1000 selects to get the publisher for each book (there were 1000 books)

To fix this N+1 problem, the findAll() method that was used by the manual and MapStruct versions was replaced with this method:

@Query("SELECT b FROM Book b JOIN FETCH b.publisher")
List<Book> findAllWithPublisher();

Let’s check the results after this change.

Performance comparison (ms)

This time, the manual approach took 24ms, the MapStruct one took 23ms, and the JPQL one took 19ms.

There was a major improvement in the execution time for the manual and the MapStruct versions. The results are quite similar to those obtained in the simple scenario. The JPQL one is again a little bit faster than the other ones, but there is no significant difference.

Conclusions

After running the experiment on 2 scenarios with different complexities, it turned out that there is no big difference between the 3 types of mapping from Entity to DTO. However, in both scenarios, the JPQL solution was the best, followed by MapStruct and finally the manual solution.

Their performance is similar, but neither solution is universally ideal, as some projects may align better with one approach over the other. Choosing one of the options may be a matter of preference or specific requirements.

Thank you for reading!


Entity to DTO: A Performance Comparison of Three Mapping Approaches was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.


This content originally appeared on Level Up Coding – Medium and was authored by Flavius Zichil