← Writing

Spring Boot 3 and Querydsl: Jakarta Setup and Q-Type Generation

For Querydsl JPA on Spring Boot 3, align the versions and jakarta classifiers of querydsl-jpa and querydsl-apt, then check both Q-type generation and a real query. Keeping Gradle’s default generated-source paths avoids configuring main and test compilation to write to the same directory.

This guide develops the original 2023 Korean example into a runnable configuration. Querydsl’s 5.0 release notes document Jakarta classifier support. The versions below pin the reproduction environment; they are not a recommendation of current production dependencies.

Reproduction environment and complete Gradle setup

Component Reproduction version
JDK / Gradle 17.0.8.1 / 8.4
Spring Boot BOM 3.5.7
Querydsl JPA / APT 5.1.0, both with the jakarta classifier
Hibernate / H2 6.6.33.Final / 2.3.232, managed by the BOM

Create these five files in a new directory. This test project uses the Java plugin and Boot BOM; it does not need the Boot Gradle plugin. In an existing Boot application, add only the dependencies needed alongside its selected dependency management.

settings.gradle

rootProject.name = 'querydsl-jakarta-example'

build.gradle

plugins { id 'java' }
repositories { mavenCentral() }
dependencies {
    implementation platform('org.springframework.boot:spring-boot-dependencies:3.5.7')
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'

    annotationProcessor platform('org.springframework.boot:spring-boot-dependencies:3.5.7')
    annotationProcessor 'com.querydsl:querydsl-apt:5.1.0:jakarta'
    annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
    annotationProcessor 'jakarta.annotation:jakarta.annotation-api'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'com.h2database:h2'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
test { useJUnitPlatform() }

Gradle resolves annotationProcessor separately from the normal compile and runtime classpaths. The example therefore applies the BOM to the processor configuration too and explicitly supplies the Jakarta Persistence and Annotation APIs. A dependency on implementation is not automatically available to the processor. See the Gradle Java plugin documentation.

Create the application and entity

The application class lets the JPA test discover its Boot configuration. There is no need to start a separate server process for the test.

src/main/java/example/Application.java

package example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

src/main/java/example/Employee.java

package example;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    protected Employee() {}

    public Employee(String name) {
        this.name = name;
    }
}

Employee uses jakarta.persistence. Do not write QEmployee yourself: the annotation processor generates the type referenced by the test below.

Query through the generated Q type

src/test/java/example/EmployeeQueryTest.java

package example;

import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class EmployeeQueryTest {
    @Autowired
    private TestEntityManager entityManager;

    @Test
    void findsEmployeeByName() {
        entityManager.persistAndFlush(new Employee("Alice"));
        entityManager.persistAndFlush(new Employee("Bob"));
        QEmployee employee = QEmployee.employee;
        var names = new JPAQueryFactory(entityManager.getEntityManager())
            .select(employee.name).from(employee)
            .where(employee.name.eq("Alice")).fetch();
        assertThat(names).containsExactly("Alice");
    }

    @Test
    void returnsNoRowsForMissingName() {
        entityManager.persistAndFlush(new Employee("Alice"));
        QEmployee employee = QEmployee.employee;
        var names = new JPAQueryFactory(entityManager.getEntityManager())
            .select(employee.name).from(employee)
            .where(employee.name.eq("Missing")).fetch();
        assertThat(names).isEmpty();
    }
}

@DataJpaTest uses H2 from this example’s classpath. Each test runs in a transaction and rolls back afterward. After inserting data with persistAndFlush, the generated QEmployee supports two checks: an exact-name match and a query returning no rows. See Spring Boot’s JPA testing guide.

These tests check this entity and query on H2. They do not verify your production database’s SQL dialect, collation, locking or migration behavior.

Run and inspect the generated files

Select JDK 17 and use an installed Gradle 8.4. Run from the project root:

java -version
gradle --version
gradle --no-daemon clean test

Check these generated artifacts and the test report:

build/generated/sources/annotationProcessor/java/main/example/QEmployee.java
build/classes/java/main/example/QEmployee.class
build/reports/tests/test/index.html

Both tests passed with this configuration. Repeating clean test regenerated the Q type and passed both tests again. For an existing project with a Gradle wrapper, use its selected version through ./gradlew clean test.

The generated Q type in this example imports javax.annotation.processing.Generated, a Java SE java.compiler API. That import does not need a Jakarta replacement. JDK 17 Generated API.

Troubleshoot missing or duplicate Q types

Symptom First check
QEmployee cannot be found Check the CLI clean compileJava result, processor dependencies, and the entity’s jakarta.persistence imports.
The processor cannot find Persistence APIs Check that the required Jakarta APIs are on the processor path, separately from the normal classpath.
An incompatible javax.persistence.EntityManager type appears Check matching versions and Jakarta classifiers for both Querydsl artifacts.
Duplicate Q types, or a build that works only after clean Remove hand-written Q sources and shared generated paths; inspect main/test output configuration.
CLI passes but the IDE cannot see Q types Check Gradle synchronization, delegated build settings and IDE annotation processing.

The example does not add custom sourceSets, JavaCompile or clean configuration for generated files. If you need a custom path, keep main and test outputs separate. configureEach controls lazy task configuration; it does not fix Jakarta dependency compatibility. Gradle configuration avoidance.

For the surrounding upgrade sequence, see the Spring Boot 3 migration guide. For outbound HTTP boundaries, use the WebClient and MockWebServer tests.

Discussion