Why JPQL Projection Beats Manual Mapping and MapStruct in Spring Boot 3

A Spring Boot 3.4.2 tutorial compares four entity‑to‑DTO conversion strategies—manual mapping, MapStruct, JPQL DTO projection, and JPQL record projection—using JMH benchmarks on an H2 in‑memory database, revealing that JPQL projection is roughly three times faster than the other approaches.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Why JPQL Projection Beats Manual Mapping and MapStruct in Spring Boot 3

1. Introduction

Converting JPA entities to Data Transfer Objects (DTOs) is a common practice to decouple the persistence layer from the API layer, control exposed fields, and improve security. Different conversion techniques, however, have markedly different performance characteristics.

2. Benchmark Setup

Environment : Spring Boot 3.4.2, Spring Data JPA, H2 in‑memory database, JMH for micro‑benchmarking.

2.1 Dependencies

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
</dependency>
<dependency>
  <groupId>io.github.linpeilie</groupId>
  <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
  <version>1.5.0</version>
</dependency>

2.2 Application Configuration

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password: ""
  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    show-sql: true
  h2:
    console:
      enabled: true
      path: /h2-console

spring:
  sql:
    init:
      mode: embedded
      schema-locations:
        - optional:classpath*:scripts/schema.sql
      data-locations:
        - optional:classpath*:scripts/data.sql

2.3 Entity, DTO, and Record

@AutoMapper(target = BookDTO.class)
@Entity
@Table(name = "t_book")
public class Book {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String name;
  private String author;
  private LocalDate releaseDate;
  private Long pages;
  private String lang;
}

public class BookDTO {
  private Long id;
  private String name;
  private String author;
  private LocalDate releaseDate;
  private Long pages;
  private String lang;
}

public record BookRecord(Long id, String name, String author, LocalDate releaseDate, Long pages, String lang) {}

2.4 Repository with JPQL Projections

public interface BookRepository extends JpaRepository<Book, Long> {
  @Query("""
    SELECT new com.pack.entity_dto_performance.BookDTO(
      e.id, e.name, e.author, e.releaseDate, e.pages, e.lang)
    FROM Book e
  """)
  List<BookDTO> queryBookToDTO();

  @Query("""
    SELECT new com.pack.entity_dto_performance.BookRecord(
      e.id, e.name, e.author, e.releaseDate, e.pages, e.lang)
    FROM Book e
  """)
  List<BookRecord> queryBookToRecord();
}

2.5 Service Layer Offering Four Mapping Strategies

@Service
public class BookService {
  private final BookRepository bookRepository;
  private final Converter converter;

  public BookService(BookRepository bookRepository, Converter converter) {
    this.bookRepository = bookRepository;
    this.converter = converter;
  }

  public List<BookDTO> getBooksManual() {
    return bookRepository.findAll().stream()
        .map(b -> new BookDTO(b.getId(), b.getName(), b.getAuthor(),
                               b.getReleaseDate(), b.getPages(), b.getLang()))
        .toList();
  }

  public List<BookDTO> getBooksByMapStruct() {
    return converter.convert(bookRepository.findAll(), BookDTO.class);
  }

  public List<BookDTO> getBooksByJpql() {
    return bookRepository.queryBookToDTO();
  }

  public List<BookRecord> getBooksByJpqlRecord() {
    return bookRepository.queryBookToRecord();
  }
}

2.6 JMH Benchmark

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(value = 1, jvmArgs = {"-Xms2g", "-Xmx2g"})
@Warmup(iterations = 2, time = 3)
@Measurement(iterations = 3, time = 3)
public class BookServiceBenchmark {
  private ConfigurableApplicationContext context;
  private BookService bookService;

  @Setup
  public void setup() {
    this.context = SpringApplication.run(SpringBootJpaApplication.class,
        "--spring.main.web-application-type=none");
    this.bookService = context.getBean(BookService.class);
  }

  @TearDown
  public void tearDown() {
    if (context != null) {
      context.close();
    }
  }

  @Benchmark
  public void testManualMapping(Blackhole bh) {
    bh.consume(bookService.getBooksManual());
  }

  @Benchmark
  public void testMapStruct(Blackhole bh) {
    bh.consume(bookService.getBooksByMapStruct());
  }

  @Benchmark
  public void testJpqlDto(Blackhole bh) {
    bh.consume(bookService.getBooksByJpql());
  }

  @Benchmark
  public void testJpqlRecord(Blackhole bh) {
    bh.consume(bookService.getBooksByJpqlRecord());
  }

  public static void main(String[] args) throws Exception {
    Options opts = new OptionsBuilder()
        .include(BookServiceBenchmark.class.getSimpleName())
        .forks(1)
        .build();
    new Runner(opts).run();
  }
}

3. Performance Test Results

The benchmark measured average execution time for each method. The result image below shows the measured latencies.

Benchmark results
Benchmark results

JPQL constructor expressions that directly create BookDTO or BookRecord achieve the best performance, around 6.2 µs per call, which is roughly three times faster than manual mapping or MapStruct (≈ 17 µs). The speedup comes from projecting only the required columns at the database level, avoiding full entity materialization and a second mapping step, thus reducing memory usage and CPU overhead—especially beneficial for read‑only queries.

4. Conclusion

When the goal is to retrieve read‑only data, using JPQL projection (either DTO or Java record) is the most efficient approach in Spring Boot applications. It simplifies code, eliminates unnecessary entity loading, and delivers significant performance gains over manual mapping or MapStruct.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

PerformancedtoSpring BootMapStructJMHjpaJPQL
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.