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 :
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 :
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 :
Il existe ici une condition de concurrence potentielle, mais par souci de simplicité, nous ne nous concentrerons pas sur cela.
Avantages de cette approche :
Inconvénients :
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.
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 :
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 ?
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 :
Les choses deviennent beaucoup plus intéressantes dans le monde réel, où vous pourriez rencontrer des scénarios comme ceux-ci :
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.
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.
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 :
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.
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 !
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!