Hallo, Habra!
Wir setzen unsere Erkundung des Themas fort und , einschließlich auf Datenbankebene. Heute empfehlen wir, darüber zu lesen, warum bei der Planung großer Anwendungen die Struktur der Datenbank und nicht der Java-Code entscheidend sein sollte, wie das gemacht wird und welche Ausnahmen es von dieser Regel gibt.
In diesem etwas verspäteten Artikel erkläre ich, warum ich denke, dass in praktisch allen Fällen das Datenmodell in einer Anwendung "von der Datenbank aus" und nicht "von den Möglichkeiten Java aus" (oder einer anderen Client-Sprache, mit der Sie arbeiten) entworfen werden sollte. Wenn Sie sich für den zweiten Ansatz entscheiden, betreten Sie einen langen Weg voller Schmerzen und Leiden, sobald Ihr Projekt zu wachsen beginnt.
Der Artikel ist inspiriert von , die auf Stack Overflow gestellt wurde.
Interessante Diskussionen auf reddit in den Bereichen und .
Codegenerierung
Wie überrascht war ich, dass es eine so kleine Schicht von Benutzern gibt, die, nachdem sie mit jOOQ vertraut wurden, sich über die Tatsache empören, dass bei der Arbeit mit jOOQ ernsthaft auf die Codegenerierung vertraut wird. Niemand hindert Sie daran, jOOQ so zu verwenden, wie Sie es für richtig halten, und niemand zwingt Sie zur Codegenerierung. Doch standardmäßig (so wie im Handbuch beschrieben) erfolgt die Arbeit mit jOOQ wie folgt: Sie beginnen mit dem (geerbten) Datenbankschema, führen dessen Rückprojektierung mit dem jOOQ-Codegenerator durch, um so eine Reihe von Klassen zu erhalten, die Ihre Tabellen repräsentieren, und schreiben dann typsichere Anfragen an diese Tabellen:
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Typinformationen stammen aus
// dem generierten Code, auf den die folgende
// SELECT-Bedingung verweist
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ generierte Namen
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}Der Code wird entweder manuell außerhalb der Build-Umgebung oder manuell bei jedem Build generiert. Beispielsweise kann eine solche Regenerierung sofort nach .
Codegenerierung
Bei solchen Ansätzen zur Code-Generierung – sowohl manuell als auch automatisiert – gibt es verschiedene Philosophien, Vorteile und Nachteile, die ich in diesem Artikel nicht im Detail behandeln möchte. Aber insgesamt besteht der Sinn des generierten Codes darin, dass er es ermöglicht, in Java die „Wahrheit“, die wir als gegeben betrachten, entweder innerhalb oder außerhalb unseres Systems zu reproduzieren. In gewisser Hinsicht tun das auch Compiler, die Bytecode, Maschinencode oder eine andere Art von Code auf Basis der Quelltexte generieren – wir erhalten eine Darstellung unserer „Wahrheit“ in einer anderen Sprache, unabhängig von den spezifischen Gründen.
Es gibt viele solcher Code-Generatoren. Zum Beispiel, Das Prinzip ist immer gleich:
- Es gibt eine bestimmte Wahrheit (innere oder äußere) – zum Beispiel, eine Spezifikation, ein Datenmodell, usw.
- Wir benötigen eine lokale Darstellung dieser Wahrheit in unserer Programmiersprache.
Es ist oft sinnvoll, eine solche Darstellung zu generieren – um Redundanz zu vermeiden.
Typanbieter und Annotation-Processing
Zur Kenntnis: Ein weiterer, modernerer und spezifischerer Ansatz zur Code-Generierung für jOOQ ist die Verwendung von Typanbietern, In diesem Fall wird der Code vom Compiler generiert, und zwar tatsächlich während des Kompilierungsprozesses. In Form von Quelltext existiert dieser Code prinzipiell nicht. In Java gibt es ähnliche, wenn auch nicht so elegante Werkzeuge – das sind die Annotation-Processor, zum Beispiel, .
In gewisser Hinsicht geschieht hier dasselbe wie im ersten Fall, mit Ausnahme von:
- Sie sehen den generierten Code nicht (vielleicht ist eine solche Situation für einige nicht so abstoßend?).
- Sie müssen garantieren, dass die Typen bereitgestellt werden können, d.h. die „Wahrheit“ muss immer verfügbar sein. Das ist einfach bei Lombok, das die „Wahrheit“ annotiert. Schwieriger wird es mit Datenbankmodellen, deren Funktion von einer ständig verfügbaren aktiven Verbindung abhängt.
Was ist das Problem mit der Code-Generierung?
Neben der kniffligen Frage, wie man die Code-Generierung besser starten kann - manuell oder automatisch - muss auch erwähnt werden, dass es Menschen gibt, die der Meinung sind, dass die Code-Generierung überhaupt nicht nötig ist. Der häufigste Grund für diese Sichtweise, dem ich begegnet bin, ist, dass es dann schwierig ist, die Build-Pipeline einzurichten. Ja, das ist in der Tat schwierig. Es entstehen zusätzliche Infrastrukturkosten. Wenn Sie gerade erst anfangen, mit einem bestimmten Produkt zu arbeiten (sei es jOOQ, JAXB, Hibernate usw.), benötigen Sie Zeit, um die Arbeitsumgebung einzurichten, die Sie lieber mit dem Studium der API verbringen würden, um dann den Wert daraus zu schöpfen.
Wenn die Kosten zu hoch sind, um sich mit dem Aufbau des Generators auseinanderzusetzen - dann hat das API tatsächlich schlecht an der Benutzerfreundlichkeit des Code-Generators gearbeitet (und es zeigt sich später, dass auch die Benutzereinstellungen darin kompliziert sind). Die Benutzerfreundlichkeit sollte von größter Priorität für jedes solche API sein. Aber das ist lediglich ein Argument gegen die Code-Generierung. Ansonsten ist es absolut notwendig, die lokale Darstellung der inneren oder äußeren Wahrheit vollständig manuell zu schreiben.
Viele werden sagen, dass sie keine Zeit haben, sich darum zu kümmern. Sie haben Fristen für ihr Superprodukt. Irgendwann werden wir die Build-Pipelines in Ordnung bringen, das wird schon klappen. Ich antworte ihnen:

,
Aber in Hibernate / JPA ist es so einfach, Code „für Java“ zu schreiben.
In der Tat. Für Hibernate und seine Benutzer ist es gleichzeitig ein Segen und ein Fluch. In Hibernate kann man einfach ein paar Entitäten so schreiben:
@Entity
class Book {
@Id
int id;
String title;
}Und fast alles ist bereit. Nun liegt es an Hibernate, die komplexen „Details“ zu generieren, wie diese Entität genau in DDL Ihres „Dialekts“ von SQL definiert wird:
CREATE TABLE book (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
title VARCHAR(50),
CONSTRAINT pk_book PRIMARY KEY (id)
);
CREATE INDEX i_book_title ON book (title);… und wir beginnen, die Anwendung zu starten. Tatsächlich eine großartige Möglichkeit, um schnell zu arbeiten und verschiedene Dinge auszuprobieren.
Aber lassen Sie mich. Ich habe geschummelt.
- Wird Hibernate wirklich die Definition dieses benannten Primärschlüssels anwenden?
- Wird Hibernate einen Index in TITLE erstellen? – Ich weiß, dass wir ihn brauchen werden.
- Wird Hibernate diesen Schlüssel definitiv identifizierend in der Identity Specification machen?
Wahrscheinlich nicht. Wenn Sie Ihr Projekt von Grund auf neu entwickeln, ist es immer praktisch, einfach die alte Datenbank abzulehnen und eine neue zu generieren, sobald Sie die benötigten Annotationen hinzugefügt haben. So wird die Entität Book letztendlich aussehen:
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
Cool. Neu generieren. In diesem Fall wird es zu Beginn sehr einfach sein.
Aber später müssen Sie dafür bezahlen.
Früher oder später müssen Sie in die Produktion gehen. Genau dann wird dieses Modell nicht mehr funktionieren. Denn:
In der Produktion wird es nicht mehr möglich sein, die alte Datenbank nach Bedarf abzulehnen und alles von vorne zu beginnen. Ihre Datenbank wird erbbasiert.
Von nun an müssen Sie schreiben . Was passiert in diesem Fall mit Ihren Entitäten? Sie können sie entweder manuell anpassen (und dadurch Ihre Arbeitslast verdoppeln), oder Sie lassen Hibernate sie für Sie neu generieren (wie hoch sind die Chancen, dass die so generierte Ihren Erwartungen entspricht?) In jedem Fall verlieren Sie.
So benötigen Sie, sobald Sie in die Produktion gehen, Hotfixes. Und diese müssen sehr schnell in die Produktion gebracht werden. Da Sie jedoch nicht vorbereitet sind und keinen reibungslosen Workflow für Ihre Migrationen organisiert haben, patchen Sie alles wild. Und dann schaffen Sie es nicht, alles richtig zu machen. Und beschuldigen Hibernate, denn es ist immer jemand anderes schuld, nur nicht Sie ...
Stattdessen hätte man von Anfang an alles ganz anders machen können. Zum Beispiel hätte man runde Räder an das Fahrrad setzen können.
Zuerst die Datenbank.
Die wahre „Wahrheit“ im Schema Ihrer Datenbank und die „Souveränität“ darüber liegen innerhalb der Datenbank. Das Schema wird nur in der Datenbank selbst festgelegt und nirgendwo sonst, und jeder Client hat eine Kopie dieses Schemas, weshalb es völlig sinnvoll ist, die Einhaltung des Schemas und dessen Integrität durchzusetzen, und zwar direkt in der Datenbank – dort, wo die Informationen gespeichert sind.
Das ist eine alte, oft zitierte Weisheit. Primär- und eindeutige Schlüssel sind gut. Fremdschlüssel sind gut. Einschränkungsprüfungen sind gut. sind gut.
Außerdem ist das noch nicht alles. Wenn Sie Oracle verwenden, möchten Sie wahrscheinlich angeben:
- In welchem Tablespace sich Ihre Tabelle befindet
- Was der Wert von PCTFREE ist
- Wie groß der Cache in Ihrer Sequenz (hinter der ID) ist
Vielleicht ist das alles in kleinen Systemen nicht wichtig, aber man muss nicht auf den Übergang in den Bereich „Big Data“ warten – man kann viel früher von den vom Anbieter bereitgestellten Optimierungsmöglichkeiten profitieren, wie die oben genannten. Keine der ORM, die ich bisher gesehen habe (auch jOOQ nicht), bietet Zugriff auf die vollständige Palette von DDL-Optionen, die Sie möglicherweise in Ihrer Datenbank verwenden möchten. ORM bieten einige Werkzeuge, die dabei helfen, DDL zu schreiben.
Aber letztendlich ist ein gut gestaltetes Schema manuell in DDL geschrieben. Jedes generierte DDL ist lediglich eine Annäherung daran.
Was ist mit dem Client-Modell?
Wie oben erwähnt, benötigen Sie auf dem Client eine Kopie des Schemas Ihrer Datenbank, eine Client-Darstellung. Es ist überflüssig zu erwähnen, dass diese Client-Darstellung mit dem tatsächlichen Modell synchronisiert sein muss. Wie erreichen Sie das am besten? Mit einem Code-Generator.
Alle Datenbanken stellen ihre Metainformationen über SQL bereit. So erhalten Sie alle Tabellen aus Ihrer Datenbank in verschiedenen SQL-Dialekten:
-- H2, HSQLDB, MySQL, PostgreSQL, SQL Server
SELECT table_schema, table_name
FROM information_schema.tables
-- DB2
SELECT tabschema, tabname
FROM syscat.tables
-- Oracle
SELECT owner, table_name
FROM all_tables
-- SQLite
SELECT name
FROM sqlite_master
-- Teradata
SELECT databasename, tablename
FROM dbc.tables
Diese Abfragen (oder ähnliche, je nachdem, ob auch Sichten, materialisierte Sichten, Tabellenwertfunktionen berücksichtigt werden müssen) werden ebenfalls durch den Aufruf aus JDBC oder durch das jOOQ-Metamodul ausgeführt.
Aus den Ergebnissen solcher Abfragen ist es relativ einfach, jede Client-Darstellung Ihres Datenbankmodells zu generieren, unabhängig davon, welche Technologie Sie auf dem Client verwenden.
- Wenn Sie JDBC oder Spring verwenden, können Sie eine Menge von String-Konstanten erstellen
- Wenn Sie JPA verwenden, können Sie die Entitäten selbst generieren
- Wenn Sie jOOQ verwenden, können Sie die jOOQ-Metamodel generieren
Abhängig von dem Umfang der Möglichkeiten, die Ihre Client-API (zum Beispiel jOOQ oder JPA) bietet, kann das generierte Metamodell wirklich reichhaltig und vollständig sein. Nehmen wir zum Beispiel die Möglichkeit der impliziten Joins, , die auf den generierten Metainformationen über die Beziehungen der Fremdschlüssel basiert, die zwischen Ihren Tabellen wirken.
Jetzt wird jede Änderung der Datenbank automatisch zu einer Aktualisierung des Client-Codes führen. Stellen Sie sich zum Beispiel vor:
ALTER TABLE book RENAME COLUMN title TO book_title;Würden Sie wirklich diese Arbeit zweimal machen wollen? Auf keinen Fall. Wir halten einfach das DDL fest, führen es durch Ihre Build-Pipeline und erhalten die aktualisierte Entität:
@Entity
@Table(name = "book", indexes = {
// Haben Sie darüber nachgedacht?
@Index(name = "i_book_title", columnList = "book_title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
@Column("book_title")
String bookTitle;
}Oder die aktualisierte jOOQ-Klasse. Die meisten DDL-Änderungen spiegeln sich auch in der Semantik wider und nicht nur in der Syntax. Daher ist es manchmal praktisch, im kompilierten Code zu betrachten, welcher Code durch die Änderung Ihrer Datenbank betroffen sein wird (oder sein könnte).
Die einzige Wahrheit
Unabhängig von der Technologie, die Sie verwenden, gibt es immer ein Modell, das die einzige Quelle der Wahrheit für ein bestimmtes Subsystem ist – oder zumindest sollten wir dies anstreben und vermeiden, eine Art Unternehmensverwirrung zu schaffen, in der "Wahrheit" überall und nirgendwo ist. Alles könnte viel einfacher sein. Wenn Sie lediglich XML-Dateien mit einem anderen System austauschen, verwenden Sie einfach XSD. Sehen Sie sich das Metamodell INFORMATION_SCHEMA aus jOOQ in XML-Form an:
- XSD ist gut verständlich
- XSD markiert den Inhalt von XML sehr gut und ermöglicht die Validierung in allen Client-Sprachen
- XSD lässt sich gut versionieren und hat einen ausgebauten Rückwärtskompatibilitätsgrad
- XSD kann mit XJC in Java-Code übersetzt werden
Der letzte Punkt ist wichtig. Bei der Kommunikation mit einem externen System über XML-Nachrichten möchten wir sicherstellen, dass unsere Nachrichten gültig sind. Dies lässt sich sehr leicht mit JAXB, XJC und XSD erreichen. Es wäre völliger Wahnsinn zu erwarten, dass man bei einem „Java-first“-Designansatz, bei dem wir unsere Nachrichten als Java-Objekte erstellen, diese klar in XML umwandeln und zur Verwendung in ein anderes System senden kann. XML, das auf diese Weise generiert wird, wäre von sehr schlechter Qualität, nicht dokumentiert und schwer zu entwickeln. Wenn für diese Schnittstelle eine Vereinbarung über die Servicequalität (SLA) existiert, würden wir sie sofort vermasseln.
Ehrlich gesagt passiert genau das ständig mit APIs in JSON, aber das ist eine andere Geschichte, darüber werde ich nächstes Mal meckern...
Datenbanken: Das ist alles dasselbe.
Wenn Sie mit Datenbanken arbeiten, verstehen Sie, dass alle im Grunde genommen ähnlich sind. Eine Datenbank besitzt ihre eigenen Daten und muss das Schema verwalten. Alle Änderungen, die am Schema vorgenommen werden, sollten direkt in DDL umgesetzt werden, um eine einheitliche Quelle der Wahrheit zu gewährleisten.
Sobald die Quelle aktualisiert wurde, müssen auch alle Clients ihre Modelle aktualisieren. Einige Clients könnten in Java mit jOOQ und Hibernate oder JDBC (oder alle zusammen) geschrieben werden. Andere Clients könnten in Perl geschrieben sein (da bleibt nur zu wünschen, dass ihnen Glück beschieden ist), und wieder andere in C#. Das ist nicht wichtig. Das Hauptmodell befindet sich in der Datenbank. Modelle, die mit ORM generiert werden, sind in der Regel von schlechter Qualität, schlecht dokumentiert und schwer zu entwickeln.
Machen Sie also keine Fehler. Machen Sie von Anfang an keine Fehler. Arbeiten Sie basierend auf der Datenbank. Erstellen Sie eine Bereitstellungspipeline, die automatisiert werden kann. Integrieren Sie Code-Generatoren, um das Modell Ihrer Datenbank bequem zu kopieren und auf die Clients zu übertragen. Und hören Sie auf, sich um Code-Generatoren zu sorgen. Sie sind gut. Mit ihnen werden Sie produktiver. Man muss nur von Anfang an etwas Zeit in ihre Einrichtung investieren – und dann erwarten Sie Jahre erhöhter Produktivität, aus denen die Geschichte Ihres Projekts entsteht.
Noch nicht bedanken, später.
Erläuterung
Zur Klarheit: Dieser Artikel propagiert keineswegs, dass das gesamte System (d.h. das Fachgebiet, die Geschäftslogik usw.) der Datenbankmodellierung angepasst werden sollte. In diesem Artikel spreche ich darüber, dass der Client-Code, der mit der Datenbank interagiert, von dem Datenbankmodell ausgehen sollte, sodass das Modell der Datenbank nicht selbst als "erste Klasse" im Code reproduziert wird. Diese Logik wird normalerweise auf der Datenzugriffsebene in Ihrem Client umgesetzt.
In zweistufigen Architekturen, die an einigen Orten immer noch existieren, kann ein solches Systemmodell das einzig Mögliche sein. In den meisten Systemen scheint mir jedoch die Datenzugriffsebene eine "Subsystem" zu sein, die das Datenbankmodell kapselt.
Ausnahmen
Aus jedem Regel gibt es Ausnahmen, und ich habe bereits erwähnt, dass der Ansatz mit der Vorrangstellung der Datenbank und der Generierung von Quellcode manchmal ungeeignet sein kann. Hier sind ein paar solcher Ausnahmen (vielleicht findet sich noch mehr):
- Wenn das Schema unbekannt ist und es geöffnet werden muss. Zum Beispiel, wenn Sie ein Anbieter von Tools sind, die Benutzern helfen, sich in jedem Schema zurechtzufinden. Uff. Hier kommt man ohne Codegenerierung nicht aus. Aber trotzdem – die Datenbank steht an erster Stelle.
- Wenn das Schema zur Laufzeit für die Lösung einer bestimmten Aufgabe generiert werden muss. Dieses Beispiel erscheint mir als eine etwas beeindruckende Version des Musters , d.h., Sie haben tatsächlich kein klar definiertes Schema. In diesem Fall kann man oft nicht einmal sicher sein, ob Ihnen ein RDBMS zusagt.
Ausnahmen sind ihrer Natur nach außergewöhnlich. In den meisten Fällen, die mit der Nutzung von RDBMS verbunden sind, ist das Schema im Voraus bekannt, es befindet sich innerhalb des RDBMS und stellt die einzige Quelle der "Wahrheit" dar, während alle Clients Abkömmlinge davon herstellen müssen. Idealerweise sollte dabei ein Codegenerator verwendet werden.
Quelle: habr.com
