Maison >Java >javaDidacticiel >JOOQ ne remplace pas Hibernate. Ils résolvent différents problèmes

JOOQ ne remplace pas Hibernate. Ils résolvent différents problèmes

DDD
DDDoriginal
2025-01-11 20:10:41472parcourir

J'ai initialement écrit cet article en russe. Donc, si vous êtes natif, vous pouvez le lire via ce lien.

Au cours de la dernière année, je suis tombé sur des articles et des discussions suggérant que JOOQ est une alternative moderne et supérieure à Hibernate. Les arguments incluent généralement :

  1. JOOQ vous permet de tout vérifier au moment de la compilation, contrairement à Hibernate !
  2. Hibernate génère des requêtes étranges et pas toujours optimales, alors qu'avec JOOQ, tout est transparent !
  3. Les entités Hibernate sont mutables, ce qui est mauvais. JOOQ permet à toutes les entités d'être immuables (bonjour la programmation fonctionnelle) !
  4. JOOQ n'implique aucune « magie » avec les annotations !

Permettez-moi de préciser d'emblée que je considère JOOQ comme une excellente bibliothèque (en particulier une bibliothèque, pas un framework comme Hibernate). Il excelle dans sa tâche : travailler avec SQL de manière statique pour détecter la plupart des erreurs au moment de la compilation.

Cependant, quand j'entends l'argument selon lequel l'époque d'Hibernate est révolue et que nous devrions maintenant tout écrire en utilisant JOOQ, cela me semble dire que l'ère des bases de données relationnelles est révolue et que nous ne devrions utiliser que NoSQL maintenant. Ça a l'air drôle ? Et pourtant, il n’y a pas si longtemps, de telles discussions étaient plutôt sérieuses.

Je pense que le problème réside dans une mauvaise compréhension des problèmes fondamentaux résolus par ces deux outils. Dans cet article, je vise à clarifier ces questions. Nous explorerons :

  1. Qu'est-ce que le script de transaction ?
  2. Qu'est-ce que le modèle de modèle de domaine ?
  3. Quels problèmes spécifiques Hibernate et JOOQ résolvent-ils ?
  4. Pourquoi l’un ne remplace-t-il pas l’autre, et comment peuvent-ils coexister ?

JOOQ Is Not a Replacement for Hibernate. They Solve Different Problems

Script de transaction

La manière la plus simple et la plus intuitive de travailler avec une base de données est le modèle Transaction Script. En bref, vous organisez toute votre logique métier sous la forme d'un ensemble de commandes SQL combinées en une seule transaction. En règle générale, chaque méthode d'une classe représente une opération commerciale et se limite à une seule transaction.

Supposons que nous développions une application qui permet aux intervenants de soumettre leurs exposés à une conférence (pour plus de simplicité, nous n'enregistrerons que le titre de l'exposé). En suivant le modèle Transaction Script, la méthode de soumission d'une présentation pourrait ressembler à ceci (en utilisant JDBI pour SQL) :

@Service
@RequiredArgsConstructor
public class TalkService {
    private final Jdbi jdbi;

    public TalkSubmittedResult submitTalk(Long speakerId, String title) {
        var talkId = jdbi.inTransaction(handle -> {
            // Count the number of accepted talks by the speaker
            var acceptedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // Check if the speaker is experienced
            var experienced = acceptedTalksCount >= 10;
            // Determine the maximum allowable number of submitted talks
            var maxSubmittedTalksCount = experienced ? 5 : 3;
            var submittedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // If the maximum number of submitted talks is exceeded, throw an exception
            if (submittedTalksCount >= maxSubmittedTalksCount) {
                throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount);
            }
            return handle.createUpdate(
                    "INSERT INTO talk (speaker_id, status, title) " +
                    "VALUES (:id, 'SUBMITTED', :title)"
                ).bind("id", speakerId)
                   .bind("title", title)
                   .executeAndReturnGeneratedKeys("id")
                   .mapTo(Long.class)
                   .one();
        });
        return new TalkSubmittedResult(talkId);
    }
}

Dans ce code :

  1. Nous comptons le nombre d'exposés que l'orateur a déjà soumis.
  2. Nous vérifions si le nombre maximum autorisé d'exposés soumis est dépassé.
  3. Si tout va bien, nous créons une nouvelle discussion avec le statut SOUMIS.

Il existe ici une condition de concurrence potentielle, mais par souci de simplicité, nous ne nous concentrerons pas sur cela.

Avantages de cette approche :

  1. Le SQL en cours d'exécution est simple et prévisible. Il est facile de le modifier pour améliorer les performances si nécessaire.
  2. Nous récupérons uniquement les données nécessaires de la base de données.
  3. Avec JOOQ, ce code peut être écrit de manière plus simple, plus concise et avec un typage statique !

Inconvénients :

  1. Il est impossible de tester la logique métier avec les seuls tests unitaires. Vous aurez besoin de tests d’intégration (et de nombreux d’entre eux).
  2. Si le domaine est complexe, cette approche peut rapidement conduire à du code spaghetti.
  3. Il existe un risque de duplication de code, ce qui pourrait entraîner des bugs inattendus au fur et à mesure de l'évolution du système.

Cette approche est valable et logique si votre service a une logique très simple qui ne devrait pas devenir plus complexe avec le temps. Cependant, les domaines sont souvent plus grands. Par conséquent, nous avons besoin d’une alternative.

Modèle de domaine

L'idée du modèle de modèle de domaine est que nous ne lions plus notre logique métier directement aux commandes SQL. Au lieu de cela, nous créons des objets de domaine (dans le contexte de Java, des classes) qui décrivent le comportement et stockent des données sur les entités de domaine.

Dans cet article, nous ne discuterons pas de la différence entre les modèles anémiques et riches. Si vous êtes intéressé, j'ai écrit un article détaillé sur ce sujet.

Les scénarios métier (services) doivent utiliser uniquement ces objets et éviter d'être liés à des requêtes de base de données spécifiques.

Bien sûr, en réalité, nous pouvons avoir un mélange d'interactions avec des objets de domaine et de requêtes directes de base de données pour répondre aux exigences de performances. Ici, nous discutons de l'approche classique de mise en œuvre du modèle de domaine, où l'encapsulation et l'isolation ne sont pas violées.

Par exemple, si nous parlons des entités Speaker et Talk, comme mentionné précédemment, les objets de domaine pourraient ressembler à ceci :

@Service
@RequiredArgsConstructor
public class TalkService {
    private final Jdbi jdbi;

    public TalkSubmittedResult submitTalk(Long speakerId, String title) {
        var talkId = jdbi.inTransaction(handle -> {
            // Count the number of accepted talks by the speaker
            var acceptedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // Check if the speaker is experienced
            var experienced = acceptedTalksCount >= 10;
            // Determine the maximum allowable number of submitted talks
            var maxSubmittedTalksCount = experienced ? 5 : 3;
            var submittedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // If the maximum number of submitted talks is exceeded, throw an exception
            if (submittedTalksCount >= maxSubmittedTalksCount) {
                throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount);
            }
            return handle.createUpdate(
                    "INSERT INTO talk (speaker_id, status, title) " +
                    "VALUES (:id, 'SUBMITTED', :title)"
                ).bind("id", speakerId)
                   .bind("title", title)
                   .executeAndReturnGeneratedKeys("id")
                   .mapTo(Long.class)
                   .one();
        });
        return new TalkSubmittedResult(talkId);
    }
}

Ici, la classe Speaker contient la logique métier pour soumettre un exposé. L'interaction avec la base de données est abstraite, permettant au modèle de domaine de se concentrer sur les règles métier.

En supposant cette interface de référentiel :

@AllArgsConstructor
public class Speaker {
    private Long id;
    private String firstName;
    private String lastName;
    private List<Talk> talks;

    public Talk submitTalk(String title) {
        boolean experienced = countTalksByStatus(Status.ACCEPTED) >= 10;
        int maxSubmittedTalksCount = experienced ? 3 : 5;
        if (countTalksByStatus(Status.SUBMITTED) >= maxSubmittedTalksCount) {
            throw new CannotSubmitTalkException(
              "Submitted talks count is maximum: " + maxSubmittedTalksCount);
        }
        Talk talk = Talk.newTalk(this, Status.SUBMITTED, title);
        talks.add(talk);
        return talk;
    }

    private long countTalksByStatus(Talk.Status status) {
        return talks.stream().filter(t -> t.getStatus().equals(status)).count();
    }
}

@AllArgsConstructor
public class Talk {
    private Long id;
    private Speaker speaker;
    private Status status;
    private String title;
    private int talkNumber;

    void setStatus(Function<Status, Status> fnStatus) {
        this.status = fnStatus.apply(this.status);
    }

    public enum Status {
        SUBMITTED, ACCEPTED, REJECTED
    }
}

Ensuite, SpeakerService peut être implémenté de cette façon :

public interface SpeakerRepository {
    Speaker findById(Long id);
    void save(Speaker speaker);
}

Avantages du modèle de domaine :

  1. Les objets de domaine sont complètement découplés des détails d'implémentation (c'est-à-dire la base de données). Cela les rend faciles à tester avec des tests unitaires réguliers.
  2. La logique métier est centralisée au sein des objets du domaine. Cela réduit considérablement le risque de propagation de la logique dans l'application, contrairement à l'approche Transaction Script.
  3. Si vous le souhaitez, les objets de domaine peuvent être rendus entièrement immuables, ce qui augmente la sécurité lorsque vous travaillez avec eux (vous pouvez les transmettre à n'importe quelle méthode sans vous soucier des modifications accidentelles).
  4. Les champs des objets de domaine peuvent être remplacés par des objets de valeur, ce qui non seulement améliore la lisibilité mais garantit également la validité des champs au moment de l'affectation (vous ne pouvez pas créer un objet de valeur avec un contenu non valide).

Bref, les avantages ne manquent pas. Il existe cependant un défi important. Il est intéressant de noter que dans les livres sur la conception basée sur le domaine, qui font souvent la promotion du modèle de modèle de domaine, ce problème n'est pas mentionné du tout ou n'est que brièvement évoqué.

Le problème est comment enregistrer les objets de domaine dans la base de données, puis les relire ? En d’autres termes, comment implémenter un référentiel ?

Aujourd’hui, la réponse est évidente. Utilisez simplement Hibernate (ou encore mieux, Spring Data JPA) et évitez les ennuis. Mais imaginons que nous soyons dans un monde où les frameworks ORM n’ont pas été inventés. Comment pourrions-nous résoudre ce problème ?

Cartographie manuelle

Pour implémenter SpeakerRepository, j'utilise également JDBI :

@Service
@RequiredArgsConstructor
public class TalkService {
    private final Jdbi jdbi;

    public TalkSubmittedResult submitTalk(Long speakerId, String title) {
        var talkId = jdbi.inTransaction(handle -> {
            // Count the number of accepted talks by the speaker
            var acceptedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'ACCEPTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // Check if the speaker is experienced
            var experienced = acceptedTalksCount >= 10;
            // Determine the maximum allowable number of submitted talks
            var maxSubmittedTalksCount = experienced ? 5 : 3;
            var submittedTalksCount =
                handle.select("SELECT count(*) FROM talk WHERE speaker_id = :id AND status = 'SUBMITTED'")
                    .bind("id", speakerId)
                    .mapTo(Long.class)
                    .one();
            // If the maximum number of submitted talks is exceeded, throw an exception
            if (submittedTalksCount >= maxSubmittedTalksCount) {
                throw new CannotSubmitTalkException("Submitted talks count is maximum: " + maxSubmittedTalksCount);
            }
            return handle.createUpdate(
                    "INSERT INTO talk (speaker_id, status, title) " +
                    "VALUES (:id, 'SUBMITTED', :title)"
                ).bind("id", speakerId)
                   .bind("title", title)
                   .executeAndReturnGeneratedKeys("id")
                   .mapTo(Long.class)
                   .one();
        });
        return new TalkSubmittedResult(talkId);
    }
}

L'approche est simple. Pour chaque référentiel, nous écrivons une implémentation distincte qui fonctionne avec la base de données en utilisant n'importe quelle bibliothèque SQL (comme JOOQ ou JDBI).

À première vue (et peut-être même au second), cette solution peut paraître plutôt bonne. Considérez ceci :

  1. Le code reste très transparent, tout comme dans l'approche Transaction Script.
  2. Plus de problèmes liés au test de la logique métier uniquement via des tests d'intégration. Ceux-ci ne sont nécessaires que pour les implémentations de référentiels (et peut-être quelques scénarios E2E).
  3. Le code de cartographie est juste devant nous. Aucune magie Hibernate n’est impliquée. Vous avez trouvé un bug ? Localisez la bonne ligne et corrigez-la.

Le besoin d’hiberner

Les choses deviennent beaucoup plus intéressantes dans le monde réel, où vous pourriez rencontrer des scénarios comme ceux-ci :

  1. Les objets de domaine peuvent devoir prendre en charge l'héritage.
  2. Un groupe de champs peut être combiné dans un objet de valeur distinct (intégré dans JPA/Hibernate).
  3. Certains champs ne doivent pas être chargés à chaque fois que vous récupérez un objet de domaine, mais uniquement lors de l'accès, pour améliorer les performances (chargement paresseux).
  4. Il peut y avoir des relations complexes entre les objets (un-à-plusieurs, plusieurs-à-plusieurs, etc.).
  5. Vous devez inclure uniquement les champs qui ont changé dans l'instruction UPDATE car les autres champs changent rarement et cela ne sert à rien de les envoyer sur le réseau (annotation DynamicUpdate).

En plus de cela, vous devrez conserver le code de mappage à mesure que votre logique métier et vos objets de domaine évoluent.

Si vous essayez de gérer chacun de ces points par vous-même, vous finirez par vous retrouver (surprise !) à écrire votre framework de type Hibernate - ou plus probablement, une version beaucoup plus simple de celui-ci.

Objectifs de JOOQ et Hibernate

JOOQ résout le manque de typage statique lors de l'écriture de requêtes SQL. Cela permet de réduire le nombre d'erreurs au stade de la compilation. Avec la génération de code directement à partir du schéma de base de données, toute mise à jour du schéma indiquera immédiatement où le code doit être corrigé (il ne sera tout simplement pas compilé).

Hibernate résout le problème du mappage des objets de domaine à une base de données relationnelle et vice versa (lire les données de la base de données et les mapper aux objets de domaine).

Par conséquent, cela n’a aucun sens de prétendre qu’Hibernate est pire ou que JOOQ est meilleur. Ces outils sont conçus à des fins différentes. Si votre application est construite autour du paradigme Transaction Script, JOOQ est sans aucun doute le choix idéal. Mais si vous souhaitez utiliser le modèle de modèle de domaine et éviter la mise en veille prolongée, vous devrez composer avec les joies du mappage manuel dans les implémentations de référentiels personnalisés. Bien sûr, si votre employeur vous paie pour créer encore un autre tueur Hibernate, pas de questions. Mais très probablement, ils s'attendent à ce que vous vous concentriez sur la logique métier, et non sur le code d'infrastructure pour le mappage objet-base de données.

Au fait, je pense que la combinaison d'Hibernate et de JOOQ fonctionne bien pour CQRS. Vous disposez d'une application (ou d'une partie logique de celle-ci) qui exécute des commandes, comme les opérations CREATE/UPDATE/DELETE — c'est là qu'Hibernate s'intègre parfaitement. D'un autre côté, vous disposez d'un service de requête qui lit les données. Ici, JOOQ est génial. Cela rend la création de requêtes complexes et leur optimisation beaucoup plus faciles qu'avec Hibernate.

Qu’en est-il des DAO dans JOOQ ?

C'est vrai. JOOQ vous permet de générer des DAO contenant des requêtes standard pour récupérer des entités de la base de données. Vous pouvez même étendre ces DAO avec vos méthodes. De plus, JOOQ générera des entités qui pourront être renseignées à l'aide de setters, similaires à Hibernate, et transmises aux méthodes d'insertion ou de mise à jour du DAO. N'est-ce pas comme Spring Data ?

Pour les cas simples, cela peut effectivement fonctionner. Cependant, ce n’est pas très différent de l’implémentation manuelle d’un référentiel. Les problèmes sont similaires :

  1. Les entités n’auront aucune relation : pas de ManyToOne, pas de OneToMany. Juste les colonnes de la base de données, ce qui rend l'écriture de la logique métier beaucoup plus difficile.
  2. Les entités sont générées individuellement. Vous ne pouvez pas les organiser selon une hiérarchie d’héritage.
  3. Le fait que les entités soient générées avec les DAO signifie que vous ne pouvez pas les modifier à votre guise. Par exemple, remplacer un champ par un objet de valeur, ajouter une relation à une autre entité ou regrouper des champs dans un élément intégrable ne sera pas possible car la régénération des entités écrasera vos modifications. Oui, vous pouvez configurer le générateur pour créer des entités légèrement différemment, mais les options de personnalisation sont limitées (et pas aussi pratiques que d'écrire le code vous-même).

Donc, si vous souhaitez créer un modèle de domaine complexe, vous devrez le faire manuellement. Sans Hibernate, la responsabilité de la cartographie vous incombera entièrement. Bien sûr, utiliser JOOQ est plus agréable que JDBI, mais le processus demandera toujours beaucoup de travail.

Même Lukas Eder, le créateur de JOOQ, mentionne dans son blog que les DAO ont été ajoutés à la bibliothèque parce que c'est un modèle populaire, pas parce qu'il recommande nécessairement de les utiliser.

Conclusion

Merci d’avoir lu l’article. Je suis un grand fan d’Hibernate et je le considère comme un excellent framework. Cependant, je comprends que certains puissent trouver JOOQ plus pratique. Le point principal de mon article est qu’Hibernate et JOOQ ne sont pas rivaux. Ces outils peuvent coexister même au sein d'un même produit s'ils apportent de la valeur.

Si vous avez des commentaires ou des retours sur le contenu, je serai ravi d'en discuter. Passez une journée productive !

Ressources

  1. JDBI
  2. Script de transaction
  3. Modèle de domaine
  4. Mon article – Modèle de domaine enrichi avec Spring Boot et Hibernate
  5. Modèle de référentiel
  6. Objet valeur
  7. JPA intégré
  8. Mise à jour dynamique JPA
  9. CQRS
  10. Lukas Eder : Vers DAO ou pas vers DAO

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn