Querydsl ersetzen: Abfrageverträge vor dem Werkzeugwechsel prüfen
Wählen Sie ein Abfragewerkzeug danach aus, welche Aufgaben Sie ausdrücken und prüfen müssen. Eine Veröffentlichungspause kann Anlass zur Untersuchung geben, belegt aber nicht, dass ein Ersatz das Abfrageverhalten einer Anwendung bewahrt.
Die ursprüngliche Sorge aus 2023 lag vor der späteren Veröffentlichung von Querydsl 5.1.0. Ein Release-Tag belegt die Existenz dieser Veröffentlichung; bewerten Sie Repository, Artefaktkoordinaten und Kompatibilität der gewünschten Linie separat. Das folgende Beispiel fixiert eine Reproduktionsumgebung, statt eine aktuelle Produktionsversion zu empfehlen.
Ein repräsentatives Prädikat vergleichen
Legen Sie dieselben fünf Zeilen an, erstellen Sie beide Prädikate und prüfen Sie ausdrückliche erwartete Ergebnisse. Die Tests decken diese Entität auf H2 ab. Eine Migrationsentscheidung erfordert auch die Joins, Projektionen, Paginierung und das Datenbankverhalten Ihrer tatsächlichen Anwendung.
Spring Data JPA Specifications kombinieren Criteria-Prädikate und führen sie über JpaSpecificationExecutor aus. Für einfache Repository-Filter kann das ausreichen, ohne das JPA-Persistenzmodell zu ersetzen.
Drei Dateien zum Querydsl-Beispiel hinzufügen
Beginnen Sie mit allen fünf Dateien aus dem vollständigen Jakarta-Querydsl-Projekt, einschließlich Employee.getName(). Seine Reproduktion verwendet Java17, Gradle8.4, Boot3.5.7, Querydsl5.1.0 Jakarta und H2. Fügen Sie die folgenden drei vollständigen Dateien hinzu; vermischen Sie diese Konfiguration nicht mit den separaten Boot4-Beispielen im Leitfaden.
src/main/java/example/EmployeeRepository.java
package example;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface EmployeeRepository extends JpaRepository<Employee, Long>,
JpaSpecificationExecutor<Employee> {}
src/main/java/example/EmployeeSpecs.java
package example;
import org.springframework.data.jpa.domain.Specification;
public final class EmployeeSpecs {
private EmployeeSpecs() {}
public static Specification<Employee> named(String name) {
if (name == null) throw new IllegalArgumentException("Use withoutName for null");
return (root, query, builder) -> builder.equal(root.get("name"), name);
}
public static Specification<Employee> withoutName() {
return (root, query, builder) -> builder.isNull(root.get("name"));
}
}
src/test/java/example/QueryStrategyTest.java
package example;
import java.util.Arrays;
import java.util.List;
import com.querydsl.core.types.Predicate;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.junit.jupiter.api.BeforeEach;
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 org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import static org.assertj.core.api.Assertions.*;
@DataJpaTest
class QueryStrategyTest {
@Autowired TestEntityManager entityManager;
@Autowired EmployeeRepository employees;
final QEmployee employee = QEmployee.employee;
@BeforeEach
void seed() {
for (String name : Arrays.asList("Alice", "Bob", "Alice", "A_%", null)) {
entityManager.persist(new Employee(name));
}
entityManager.flush();
}
void equivalent(Predicate predicate, Specification<Employee> specification,
List<String> expected) {
var querydsl = new JPAQueryFactory(entityManager.getEntityManager())
.select(employee.name).from(employee).where(predicate)
.orderBy(employee.name.asc()).fetch();
var criteria = employees.findAll(specification, Sort.by("name"))
.stream().map(Employee::getName).toList();
assertThat(querydsl).containsExactlyElementsOf(expected);
assertThat(criteria).containsExactlyElementsOf(expected);
}
@Test
void exactEqualityPreservesDuplicates() {
equivalent(employee.name.eq("Alice"), EmployeeSpecs.named("Alice"),
List.of("Alice", "Alice"));
}
@Test
void missingValueReturnsNoRows() {
equivalent(employee.name.eq("Missing"), EmployeeSpecs.named("Missing"), List.of());
}
@Test
void wildcardCharactersAreLiteralForEquality() {
equivalent(employee.name.eq("A_%"), EmployeeSpecs.named("A_%"), List.of("A_%"));
}
@Test
void andCanEliminateAllRows() {
equivalent(employee.name.eq("Alice").and(employee.name.eq("Bob")),
EmployeeSpecs.named("Alice").and(EmployeeSpecs.named("Bob")), List.of());
}
@Test
void orKeepsMatchesAndDuplicatesInOrder() {
equivalent(employee.name.eq("Alice").or(employee.name.eq("Bob")),
EmployeeSpecs.named("Alice").or(EmployeeSpecs.named("Bob")),
List.of("Alice", "Alice", "Bob"));
}
@Test
void nullRequiresAnExplicitPredicate() {
equivalent(employee.name.isNull(), EmployeeSpecs.withoutName(),
Arrays.asList((String) null));
}
@Test
void stringAttributeTypoFailsAtRuntime() {
Specification<Employee> typo = (root, query, builder) ->
builder.equal(root.get("missingName"), "Alice");
assertThatThrownBy(() -> employees.findAll(typo))
.isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@Test
void equalityHelpersRejectNull() {
assertThatThrownBy(() -> employee.name.eq((String) null))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> EmployeeSpecs.named(null))
.isInstanceOf(IllegalArgumentException.class);
}
}
Führen Sie dies im Projektstamm mit ausgewähltem JDK17 aus:
gradle --version
gradle --no-daemon clean test
Das Projekt hat nun zehn Tests: die ursprünglichen zwei Querydsl-Prüfungen und acht Vergleichs-/Vertragsprüfungen. Bei den sechs Ergebnisvergleichen müssen beide Implementierungen einer unabhängig vorgegebenen Liste entsprechen; bloße Übereinstimmung miteinander reicht nicht. Wiederholen Sie clean test, um auch die Neuerzeugung der Q-Typen zu prüfen.
| Fall | Erwartetes Ergebnis |
|---|---|
| Name ist gleich Alice | Alice zweimal; Duplikate bleiben erhalten |
| Nicht vorhandener Name | Keine Zeilen |
Name ist gleich A_% |
Dieser wörtliche Wert, kein LIKE-Muster |
| Alice AND Bob | Keine Zeilen |
| Alice OR Bob | Alice, Alice, Bob nach Namen sortiert |
| Explizites Null-Prädikat | Ein Null-Name |
Unterschiedliche Verträge bewahren
Das generierte employee.name von Querydsl ist ein Java-Member. In diesem Specification-Beispiel verwendet root.get("name") ein String-Attribut; daher kompiliert der absichtliche Tippfehler missingName und scheitert erst bei der Ausführung durch das Repository. Specifications müssen deshalb nicht zwangsläufig Strings verwenden: Das statische JPA-Metamodell ist eine weitere Möglichkeit.
Der Hilfsbaustein named dieses Beispiels lehnt null ausdrücklich ab; Querydsls Gleichheitsaufruf ebenfalls. Verwenden Sie für Nullwerte die separaten Prädikate withoutName()/isNull(). Dies ist die Eingaberichtlinie des Beispiels; ändern Sie null bei einer realen Migration nicht stillschweigend in einen fehlenden Filter.
| Werkzeug | Was zu bewerten ist |
|---|---|
| Querydsl JPA | Generierte Q-Typen, JPA-Abfrageformen und Prozessorkompatibilität |
| JPA Specification | Wiederverwendbare Criteria-Filter und Repository-Integration |
| jOOQ | SQL-orientierte DSL und Schemamodellierung; Generierung ist optional |
| Jdbi | JDBC-Abfragen und Ergebniszuordnung |
| Doma | Verarbeitung zur Kompilierzeit, SQL-Vorlagen und Criteria-APIs |
Diese Werkzeuge liegen auf unterschiedlichen Ebenen. Der Test vergleicht weder jOOQ, Jdbi oder Doma noch misst er Leistung oder belegt Gleichwertigkeit auf der Produktionsdatenbank. Ergänzen Sie vor einem Wechsel Ihre tatsächlichen Joins, Sortierungen, Paginierung, Null-Richtlinien und Transaktionsfälle; prüfen Sie die ausgewählte Datenbank und die Lizenzbedingungen der gewählten Artefakte.
Der Spring-Boot-Lernleitfaden verbindet Projekteinrichtung, Persistenzprüfungen, HTTP-Tests und Paketierung. Jeder Artikel dokumentiert seine eigenen Reproduktionsversionen.
Diskussion