← Notes

Remplacer Querydsl : vérifier les contrats de requête avant de changer d’outil

Choisissez un outil de requête selon le travail à exprimer et à vérifier. Une interruption des releases peut justifier une enquête, mais ne démontre pas qu’un remplacement préserve le comportement des requêtes d’une application.

L’inquiétude initiale de 2023 précède la release Querydsl 5.1.0 publiée ensuite. Un tag de release prouve son existence ; évaluez séparément le dépôt, les coordonnées des artefacts et la compatibilité de la branche à adopter. L’exemple ci-dessous fige un environnement de reproduction plutôt que de recommander une version actuelle pour la production.

Comparer un prédicat représentatif

Comparer les résultats des requêtes avant de changer d’outil

Insérez les cinq mêmes lignes, construisez les deux prédicats et vérifiez des résultats attendus explicites. Les tests couvrent cette entité sur H2. Une décision de migration nécessite également les jointures, projections, paginations et comportements de base de données réellement utilisés par votre application.

Les Specifications Spring Data JPA combinent des prédicats Criteria et les exécutent via JpaSpecificationExecutor. Pour de simples filtres de repository, cela peut suffire sans remplacer le modèle de persistance JPA.

Ajouter trois fichiers à l’exemple Querydsl

Partez des cinq fichiers du projet Jakarta Querydsl complet, notamment Employee.getName(). Sa reproduction utilise Java17, Gradle8.4, Boot3.5.7, Querydsl5.1.0 Jakarta et H2. Ajoutez les trois fichiers complets suivants ; ne mélangez pas cette configuration avec les exemples Boot4 distincts du guide.

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);
    }
}

Exécutez depuis la racine du projet avec JDK17 sélectionné :

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

Le projet compte maintenant dix tests : les deux contrôles Querydsl d’origine et huit contrôles de comparaison/contrat. Pour les six comparaisons de résultats, chaque implémentation doit correspondre à une liste définie indépendamment ; leur seul accord mutuel ne suffit pas. Répétez clean test pour vérifier aussi la régénération des types Q.

Cas Résultat attendu
Nom égal à Alice Alice deux fois ; les doublons sont conservés
Nom absent Aucune ligne
Nom égal à A_% Cette valeur littérale, et non un motif LIKE
Alice AND Bob Aucune ligne
Alice OR Bob Alice, Alice, Bob dans l’ordre des noms
Prédicat null explicite Un nom null

Préserver les contrats qui diffèrent

Le employee.name généré par Querydsl est un membre Java. Dans cet exemple Specification, root.get("name") utilise un attribut sous forme de chaîne : la faute volontaire missingName compile donc et échoue lorsque le repository l’exécute. Cela ne signifie pas que les Specifications doivent utiliser des chaînes : le métamodèle statique JPA est une autre possibilité.

L’utilitaire named de l’exemple refuse explicitement null, tout comme l’appel d’égalité de Querydsl. Utilisez les prédicats distincts withoutName()/isNull() pour les valeurs nulles. C’est la politique d’entrée de l’exemple ; lors d’une véritable migration, ne transformez pas silencieusement null en filtre absent.

Outil Éléments à évaluer
Querydsl JPA Types Q générés, formes de requêtes JPA et compatibilité du processeur
JPA Specification Filtres Criteria réutilisables et intégration au repository
jOOQ DSL centrée sur SQL et modélisation du schéma ; la génération est facultative
Jdbi Requêtes JDBC et correspondance des résultats
Doma Traitement à la compilation, modèles SQL et API Criteria

Ces outils interviennent à des couches différentes. Le test ne compare pas jOOQ, Jdbi ou Doma, ne mesure pas les performances et n’établit pas l’équivalence sur la base de production. Avant de changer d’outil, ajoutez vos véritables jointures, tris, paginations, politiques null et cas transactionnels ; vérifiez la base choisie et les licences des artefacts retenus.

Le guide d’apprentissage Spring Boot relie configuration du projet, contrôles de persistance, tests HTTP et empaquetage. Chaque article consigne ses propres versions de reproduction.

Discussion

Voir sur GitHub ↗