Ich habe diesen Artikel ursprünglich auf Russisch geschrieben. Wenn Sie also Muttersprachler sind, können Sie es über diesen Link lesen.
Im letzten Jahr oder so bin ich auf Artikel und Vorträge gestoßen, die darauf hinwiesen, dass JOOQ eine moderne und überlegene Alternative zu Hibernate ist. Zu den Argumenten gehören typischerweise:
Lassen Sie mich vorweg sagen, dass ich JOOQ für eine ausgezeichnete Bibliothek halte (insbesondere eine Bibliothek, kein Framework wie Hibernate). Es übertrifft seine Aufgabe – statisch typisiertes Arbeiten mit SQL, um die meisten Fehler zur Kompilierungszeit zu erkennen.
Wenn ich jedoch das Argument höre, dass die Zeit von Hibernate vorbei sei und wir jetzt alles mit JOOQ schreiben sollten, klingt das für mich so, als würde ich sagen, die Ära der relationalen Datenbanken sei vorbei und wir sollten jetzt nur noch NoSQL verwenden. Klingt lustig? Noch vor nicht allzu langer Zeit waren solche Diskussionen noch recht ernst.
Ich glaube, das Problem liegt in einem Missverständnis der Kernprobleme, die diese beiden Tools ansprechen. In diesem Artikel möchte ich diese Fragen klären. Wir werden Folgendes erkunden:
Die einfachste und intuitivste Möglichkeit, mit einer Datenbank zu arbeiten, ist das Transaktionsskriptmuster. Kurz gesagt: Sie organisieren Ihre gesamte Geschäftslogik als eine Reihe von SQL-Befehlen, die in einer einzigen Transaktion zusammengefasst sind. Normalerweise stellt jede Methode in einer Klasse einen Geschäftsvorgang dar und ist auf eine Transaktion beschränkt.
Angenommen, wir entwickeln eine Anwendung, die es Rednern ermöglicht, ihre Vorträge bei einer Konferenz einzureichen (der Einfachheit halber zeichnen wir nur den Titel des Vortrags auf). Dem Transaction Script-Muster folgend könnte die Methode zum Einreichen eines Vortrags wie folgt aussehen (mit JDBI für 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); } }
In diesem Code:
Hier besteht eine potenzielle Rennbedingung, aber der Einfachheit halber werden wir uns nicht darauf konzentrieren.
Vorteile dieses Ansatzes:
Nachteile:
Dieser Ansatz ist gültig und sinnvoll, wenn Ihr Dienst über eine sehr einfache Logik verfügt, von der nicht zu erwarten ist, dass sie mit der Zeit komplexer wird. Allerdings sind Domains oft größer. Deshalb brauchen wir eine Alternative.
Die Idee des Domänenmodellmusters besteht darin, dass wir unsere Geschäftslogik nicht mehr direkt an SQL-Befehle binden. Stattdessen erstellen wir Domänenobjekte (im Kontext von Java Klassen), die das Verhalten beschreiben und Daten über Domänenentitäten speichern.
In diesem Artikel werden wir nicht auf den Unterschied zwischen anämischen und reichen Modellen eingehen. Wenn Sie interessiert sind, habe ich einen ausführlichen Artikel zu diesem Thema geschrieben.
Geschäftsszenarien (Dienste) sollten nur diese Objekte verwenden und eine Bindung an bestimmte Datenbankabfragen vermeiden.
Natürlich können wir in der Realität eine Mischung aus Interaktionen mit Domänenobjekten und direkten Datenbankabfragen haben, um die Leistungsanforderungen zu erfüllen. Hier diskutieren wir den klassischen Ansatz zur Implementierung des Domänenmodells, bei dem Kapselung und Isolation nicht verletzt werden.
Wenn wir beispielsweise über die Entitäten „Speaker“ und „Talk“ sprechen, wie bereits erwähnt, könnten die Domänenobjekte wie folgt aussehen:
@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); } }
Hier enthält die Speaker-Klasse die Geschäftslogik zum Einreichen eines Vortrags. Die Datenbankinteraktion wird abstrahiert, sodass sich das Domänenmodell auf Geschäftsregeln konzentrieren kann.
Angenommen, diese Repository-Schnittstelle:
@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 } }
Dann kann der SpeakerService folgendermaßen implementiert werden:
public interface SpeakerRepository { Speaker findById(Long id); void save(Speaker speaker); }
Vorteile des Domain-Modells:
Kurz gesagt, es gibt viele Vorteile. Es gibt jedoch eine wichtige Herausforderung. Interessanterweise wird dieses Problem in Büchern über Domain-Driven Design, die häufig das Domain-Model-Muster fördern, entweder überhaupt nicht erwähnt oder nur kurz angesprochen.
Das Problem ist wie speichert man Domänenobjekte in der Datenbank und liest sie dann zurück? Mit anderen Worten: Wie implementiert man ein Repository?
Heutzutage liegt die Antwort auf der Hand. Verwenden Sie einfach Hibernate (oder noch besser Spring Data JPA) und ersparen Sie sich die Mühe. Aber stellen wir uns vor, wir leben in einer Welt, in der ORM-Frameworks noch nicht erfunden wurden. Wie würden wir dieses Problem lösen?
Zur Implementierung von SpeakerRepository verwende ich auch 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); } }
Der Ansatz ist einfach. Für jedes Repository schreiben wir eine separate Implementierung, die mit der Datenbank unter Verwendung einer beliebigen SQL-Bibliothek (wie JOOQ oder JDBI) funktioniert.
Auf den ersten Blick (und vielleicht sogar auf den zweiten) scheint diese Lösung recht gut zu sein. Bedenken Sie Folgendes:
Viel interessanter wird es in der realen Welt, wo Sie möglicherweise auf Szenarien wie diese stoßen:
Darüber hinaus müssen Sie den Zuordnungscode pflegen, während sich Ihre Geschäftslogik und Domänenobjekte weiterentwickeln.
Wenn Sie versuchen, jeden dieser Punkte alleine zu bewältigen, werden Sie irgendwann (Überraschung!) Ihr Hibernate-ähnliches Framework schreiben – oder wahrscheinlicher, eine viel einfachere Version davon.
JOOQ behebt den Mangel an statischer Typisierung beim Schreiben von SQL-Abfragen. Dies trägt dazu bei, die Anzahl der Fehler in der Kompilierungsphase zu reduzieren. Bei der Codegenerierung direkt aus dem Datenbankschema zeigen alle Aktualisierungen des Schemas sofort an, wo der Code korrigiert werden muss (er lässt sich einfach nicht kompilieren).
Hibernate löst das Problem der Zuordnung von Domänenobjekten zu einer relationalen Datenbank und umgekehrt (Lesen von Daten aus der Datenbank und Zuordnen zu Domänenobjekten).
Daher macht es keinen Sinn zu argumentieren, dass Hibernate schlechter oder JOOQ besser ist. Diese Werkzeuge sind für unterschiedliche Zwecke konzipiert. Wenn Ihre Anwendung auf dem Transaktionsskript-Paradigma basiert, ist JOOQ zweifellos die ideale Wahl. Wenn Sie jedoch das Domänenmodellmuster verwenden und Hibernate vermeiden möchten, müssen Sie sich mit den Freuden der manuellen Zuordnung in benutzerdefinierten Repository-Implementierungen auseinandersetzen. Wenn Ihr Arbeitgeber Sie dafür bezahlt, einen weiteren Hibernate-Killer zu bauen, gibt es natürlich keine Fragen. Aber höchstwahrscheinlich erwarten sie, dass Sie sich auf die Geschäftslogik konzentrieren und nicht auf den Infrastrukturcode für die Objekt-zu-Datenbank-Zuordnung.
Übrigens glaube ich, dass die Kombination von Hibernate und JOOQ für CQRS gut funktioniert. Sie haben eine Anwendung (oder einen logischen Teil davon), die Befehle wie CREATE-/UPDATE-/DELETE-Vorgänge ausführt – hier passt Hibernate perfekt. Andererseits verfügen Sie über einen Abfragedienst, der Daten liest. Hier ist JOOQ brillant. Es macht das Erstellen und Optimieren komplexer Abfragen viel einfacher als mit Hibernate.
Es ist wahr. Mit JOOQ können Sie DAOs generieren, die Standardabfragen zum Abrufen von Entitäten aus der Datenbank enthalten. Sie können diese DAOs sogar mit Ihren Methoden erweitern. Darüber hinaus generiert JOOQ Entitäten, die ähnlich wie Hibernate mithilfe von Settern gefüllt und an die Einfügungs- oder Aktualisierungsmethoden im DAO übergeben werden können. Ist das nicht wie Spring Data?
In einfachen Fällen kann dies tatsächlich funktionieren. Es unterscheidet sich jedoch nicht wesentlich von der manuellen Implementierung eines Repositorys. Die Probleme sind ähnlich:
Wenn Sie also ein komplexes Domänenmodell erstellen möchten, müssen Sie dies manuell tun. Ohne Hibernate liegt die Verantwortung für die Zuordnung vollständig bei Ihnen. Sicherlich ist die Verwendung von JOOQ angenehmer als JDBI, aber der Prozess wird immer noch arbeitsintensiv sein.
Sogar Lukas Eder, der Schöpfer von JOOQ, erwähnt in seinem Blog, dass DAOs zur Bibliothek hinzugefügt wurden, weil es ein beliebtes Muster ist, und nicht, weil er unbedingt deren Verwendung empfiehlt.
Vielen Dank, dass Sie den Artikel gelesen haben. Ich bin ein großer Fan von Hibernate und halte es für ein hervorragendes Framework. Ich verstehe jedoch, dass einige JOOQ möglicherweise bequemer finden. Der Hauptpunkt meines Artikels ist, dass Hibernate und JOOQ keine Rivalen sind. Diese Tools können sogar innerhalb desselben Produkts nebeneinander existieren, wenn sie einen Mehrwert bieten.
Wenn Sie Kommentare oder Feedback zum Inhalt haben, bespreche ich diese gerne mit Ihnen. Ich wünsche Ihnen einen produktiven Tag!
Das obige ist der detaillierte Inhalt vonJOOQ ist kein Ersatz für Hibernate. Sie lösen verschiedene Probleme. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!