Heim > Java > javaLernprogramm > JOOQ ist kein Ersatz für Hibernate. Sie lösen verschiedene Probleme

JOOQ ist kein Ersatz für Hibernate. Sie lösen verschiedene Probleme

DDD
Freigeben: 2025-01-11 20:10:41
Original
470 Leute haben es durchsucht

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:

  1. Mit JOOQ können Sie alles zur Kompilierungszeit überprüfen, im Gegensatz zu Hibernate!
  2. Hibernate generiert seltsame und nicht immer optimale Abfragen, während bei JOOQ alles transparent ist!
  3. Hibernate-Entitäten sind veränderlich, was schlecht ist. JOOQ ermöglicht, dass alle Entitäten unveränderlich sind (Hallo, funktionale Programmierung)!
  4. JOOQ beinhaltet keine „Magie“ mit Anmerkungen!

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:

  1. Was ist ein Transaktionsskript?
  2. Was ist das Domänenmodellmuster?
  3. Welche spezifischen Probleme lösen Hibernate und JOOQ?
  4. Warum ist das eine kein Ersatz für das andere und wie können sie nebeneinander existieren?

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

Transaktionsskript

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);
    }
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

In diesem Code:

  1. Wir zählen, wie viele Vorträge der Referent bereits eingereicht hat.
  2. Wir prüfen, ob die maximal zulässige Anzahl eingereichter Vorträge überschritten wird.
  3. Wenn alles in Ordnung ist, erstellen wir einen neuen Talk mit dem Status GESENDET.

Hier besteht eine potenzielle Rennbedingung, aber der Einfachheit halber werden wir uns nicht darauf konzentrieren.

Vorteile dieses Ansatzes:

  1. Das ausgeführte SQL ist unkompliziert und vorhersehbar. Es ist einfach, es bei Bedarf für Leistungsverbesserungen zu optimieren.
  2. Wir holen nur die notwendigen Daten aus der Datenbank.
  3. Mit JOOQ kann dieser Code einfacher, präziser und mit statischer Typisierung geschrieben werden!

Nachteile:

  1. Es ist unmöglich, die Geschäftslogik allein mit Unit-Tests zu testen. Sie benötigen Integrationstests (und zwar einige davon).
  2. Wenn die Domäne komplex ist, kann dieser Ansatz schnell zu Spaghetti-Code führen.
  3. Es besteht das Risiko einer Codeduplizierung, die bei der Weiterentwicklung des Systems zu unerwarteten Fehlern führen kann.

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.

Domänenmodell

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);
    }
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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
    }
}
Nach dem Login kopieren

Dann kann der SpeakerService folgendermaßen implementiert werden:

public interface SpeakerRepository {
    Speaker findById(Long id);
    void save(Speaker speaker);
}
Nach dem Login kopieren

Vorteile des Domain-Modells:

  1. Domänenobjekte sind vollständig von Implementierungsdetails (d. h. der Datenbank) entkoppelt. Dadurch können sie einfach mit regelmäßigen Unit-Tests getestet werden.
  2. Die Geschäftslogik ist innerhalb der Domänenobjekte zentralisiert. Dadurch wird im Gegensatz zum Transaktionsskript-Ansatz das Risiko einer Logikausbreitung in der Anwendung erheblich reduziert.
  3. Auf Wunsch können Domänenobjekte vollständig unveränderlich gemacht werden, was die Sicherheit bei der Arbeit mit ihnen erhöht (Sie können sie an jede Methode übergeben, ohne sich Gedanken über versehentliche Änderungen machen zu müssen).
  4. Felder in Domänenobjekten können durch Wertobjekte ersetzt werden, was nicht nur die Lesbarkeit verbessert, sondern auch die Gültigkeit von Feldern zum Zeitpunkt der Zuweisung gewährleistet (Sie können kein Wertobjekt mit ungültigem Inhalt erstellen).

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?

Manuelle Zuordnung

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);
    }
}
Nach dem Login kopieren
Nach dem Login kopieren
Nach dem Login kopieren

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:

  1. Der Code bleibt hochtransparent, genau wie beim Transaction Script-Ansatz.
  2. Keine Probleme mehr beim Testen der Geschäftslogik nur durch Integrationstests. Diese werden nur für Repository-Implementierungen (und möglicherweise einige E2E-Szenarien) benötigt.
  3. Der Zuordnungscode liegt direkt vor uns. Es ist keine Hibernate-Magie beteiligt. Einen Fehler gefunden? Suchen Sie die richtige Zeile und korrigieren Sie sie.

Die Notwendigkeit des Winterschlafs

Viel interessanter wird es in der realen Welt, wo Sie möglicherweise auf Szenarien wie diese stoßen:

  1. Domänenobjekte müssen möglicherweise die Vererbung unterstützen.
  2. Eine Gruppe von Feldern kann zu einem separaten Wertobjekt zusammengefasst werden (eingebettet in JPA/Hibernate).
  3. Einige Felder sollten nicht jedes Mal geladen werden, wenn Sie ein Domänenobjekt abrufen, sondern nur, wenn darauf zugegriffen wird, um die Leistung zu verbessern (Lazy Loading).
  4. Es kann komplexe Beziehungen zwischen Objekten geben (eins-zu-viele, viele-zu-viele usw.).
  5. Sie müssen nur die Felder in die UPDATE-Anweisung aufnehmen, die sich geändert haben, da sich andere Felder selten ändern und es keinen Sinn macht, sie über das Netzwerk zu senden (Annotation „DynamicUpdate“).

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.

Ziele von JOOQ und Hibernate

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.

Was ist mit DAOs in JOOQ?

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:

  1. Die Entitäten haben keine Beziehungen: kein ManyToOne, kein OneToMany. Nur die Datenbankspalten, was das Schreiben von Geschäftslogik erheblich erschwert.
  2. Entitäten werden einzeln generiert. Sie können sie nicht in einer Vererbungshierarchie organisieren.
  3. Die Tatsache, dass Entitäten zusammen mit den DAOs generiert werden, bedeutet, dass Sie sie nicht nach Ihren Wünschen ändern können. Beispielsweise ist das Ersetzen eines Felds durch ein Wertobjekt, das Hinzufügen einer Beziehung zu einer anderen Entität oder das Gruppieren von Feldern in einem Embeddable nicht möglich, da die Neugenerierung der Entitäten Ihre Änderungen überschreibt. Ja, Sie können den Generator so konfigurieren, dass Entitäten etwas anders erstellt werden, aber die Anpassungsoptionen sind begrenzt (und nicht so praktisch wie das Schreiben des Codes selbst).

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.

Abschluss

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!

Ressourcen

  1. JDBI
  2. Transaktionsskript
  3. Domänenmodell
  4. Mein Artikel – Rich Domain Model mit Spring Boot und Hibernate
  5. Repository-Muster
  6. Wertobjekt
  7. JPA Embedded
  8. JPA DynamicUpdate
  9. CQRS
  10. Lukas Eder: Zu DAO oder nicht zu DAO

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!

Quelle:dev.to
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage