Wahrheit zuerst: Warum Systeme unter BerĂŒcksichtigung der Datenbankstruktur entworfen werden mĂŒssen

Hallo, Habr!

Wir setzen unsere Erforschung des Themas fort Java und Spring, einschließlich auf Datenbankebene. Heute möchten wir Ihnen erlĂ€utern, warum bei der Gestaltung großer Anwendungen die Struktur der Datenbank, nicht der Java-Code, entscheidend sein sollte, wie dies umgesetzt wird und welche Ausnahmen es von dieser Regel gibt.

In diesem etwas verspÀteten Artikel erklÀre ich, warum ich denke, dass das Datenmodell in einer Anwendung in den meisten FÀllen "von der Datenbank aus" und nicht "von den Möglichkeiten von Java" (oder einer anderen Programmiersprache, mit der Sie arbeiten) entworfen werden sollte. Wenn Sie den zweiten Ansatz wÀhlen, begeben Sie sich auf einen langen Weg voller Schmerzen und Herausforderungen, sobald Ihr Projekt zu wachsen beginnt.

Dieser Artikel basiert auf einer Frage, die auf Stack Overflow gestellt wurde.

Interessante Diskussionen auf reddit in den Bereichen /r/java und /r/programming.

Code-Generierung

Ich war wirklich ĂŒberrascht, dass es eine so kleine Gruppe von Nutzern gibt, die sich ĂŒber die Tatsache empören, dass jOOQ ernsthaft auf die Codegenerierung angewiesen ist. Niemand hindert Sie daran, jOOQ so zu verwenden, wie Sie es fĂŒr richtig halten, und niemand zwingt Sie zur Codegenerierung. Aber standardmĂ€ĂŸig (wie im Handbuch beschrieben) funktioniert die Arbeit mit jOOQ so: Sie beginnen mit einem (vererbbare) Datenbankschema, fĂŒhren dessen Reverse Engineering mit dem jOOQ-Codegenerator durch, um so eine Reihe von Klassen zu erhalten, die Ihre Tabellen reprĂ€sentieren, und dann schreiben Sie typensichere Abfragen an diese Tabellen:

	for (Record2 record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Typinformationen stammen aus 
//   dem generierten Code, auf den sich die
//   folgende SELECT-Anweisung bezieht

       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ generierte Namen
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Der Code wird entweder manuell außerhalb des Builds oder manuell bei jedem Build generiert. Zum Beispiel kann eine solche Regenerierung sofort nach Datenbankmigration mit Flyway, die sowohl manuell als auch automatisch durchgefĂŒhrt werden kann..

Generierung von Quellcode

Mit diesen AnsĂ€tzen zur Code-Generierung – sowohl manuell als auch automatisch – sind verschiedene Philosophien, Vorteile und Nachteile verbunden, die ich in diesem Artikel nicht im Detail erörtern möchte. Im Grunde genommen geht es bei generiertem Code darum, dass er es ermöglicht, in Java jene 'Wahrheit' zu reproduzieren, die wir als gegeben akzeptieren, sei es innerhalb oder außerhalb unseres Systems. In gewisser Weise leisten Compiler, die Bytecode, Maschinencode oder eine andere Art von Code aus den Quellcodes generieren, dasselbe – wir erhalten eine Darstellung unserer 'Wahrheit' in einer anderen Sprache, unabhĂ€ngig von den spezifischen GrĂŒnden.

Es gibt eine Vielzahl solcher Code-Generatoren. Zum Beispiel, XJC kann Java-Code basierend auf XSD- oder WSDL-Dateien generieren.Das Prinzip ist immer dasselbe:

  • Es gibt eine gewisse Wahrheit (intern oder extern) – beispielsweise 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.

Typen-Provider und Annotationenverarbeitung

Hinweis: Ein weiterer, modernerer und spezifischerer Ansatz zur Codegenerierung fĂŒr jOOQ bezieht sich auf die Nutzung von Typen-Providern, so wie sie in F# umgesetzt sind.In diesem Fall wird der Code vom Compiler wĂ€hrend des Kompilierungsprozesses generiert. In Form von Quellcode existiert dieser Code grundsĂ€tzlich nicht. In Java gibt es Ă€hnliche, wenn auch weniger elegante Werkzeuge – diese sind die Annotation-Prozessoren, zum Beispiel, Lombok.

In gewissem Sinne geschieht hier dasselbe wie im ersten Fall, mit der Ausnahme:

  • Sie sehen den generierten Code nicht (vielleicht erscheint diese Situation fĂŒr einige nicht so abweisend?).
  • Sie mĂŒssen garantieren, dass Typen bereitgestellt werden können, das heißt, 'Wahrheit' muss immer verfĂŒgbar sein. Das ist einfach im Falle von Lombok, das 'Wahrheit' annotiert. Etwas komplizierter wird es mit Datenbankmodellen, deren Funktion auf einer stĂ€ndig verfĂŒgbaren, aktiven Verbindung beruht.

Was ist das Problem bei der Codegenerierung?

Neben der komplexen Frage, wie man die Codegenerierung am besten startet – manuell oder automatisch –, muss auch erwĂ€hnt werden, dass es Menschen gibt, die der Meinung sind, dass die Codegenerierung ĂŒberhaupt nicht notwendig ist. Die am hĂ€ufigsten gehörte BegrĂŒndung fĂŒr diese Sichtweise ist, dass es dann schwierig ist, die Build-Pipeline einzurichten. Ja, das ist in der Tat eine Herausforderung. Es entstehen zusĂ€tzliche infrastrukturelle Kosten. Wenn Sie gerade erst mit einem bestimmten Produkt beginnen (sei es jOOQ, JAXB oder Hibernate usw.), benötigt die Einrichtung der Entwicklungsumgebung Zeit, die Sie lieber in das Erlernen der API investieren wĂŒrden, um spĂ€ter davon profitieren zu können.

Wenn die Kosten fĂŒr das VerstĂ€ndnis der Funktionsweise des Generators zu hoch sind, dann wurde in der API tatsĂ€chlich schlecht an der Benutzerfreundlichkeit des Code-Generators gearbeitet (was sich spĂ€ter als auch schwierig fĂŒr die Benutzeranpassung herausstellt). Die Benutzerfreundlichkeit sollte die oberste PrioritĂ€t fĂŒr jede solche API haben. Aber das ist nur ein Argument gegen die Code-Generierung. Ansonsten ist es absolut notwendig, die lokale Darstellung interner oder externer Wahrheiten vollstĂ€ndig manuell zu erstellen.

Viele werden sagen, dass sie keine Zeit dafĂŒr haben. Ihre Fristen fĂŒr das Super-Produkt brennen. Irgendwann werden wir die MontagebĂ€nder ĂŒberarbeiten, dazu bleibt Zeit. Ich antworte ihnen:

Wahrheit zuerst: Warum Systeme unter BerĂŒcksichtigung der Datenbankstruktur entworfen werden mĂŒssen
Original, Alan O’Rourke, Audience Stack

Aber in Hibernate / JPA ist es so einfach, Code „fĂŒr Java“ zu schreiben.

In der Tat. FĂŒr Hibernate und seine Nutzer ist das sowohl Segen als auch Fluch. In Hibernate kann man einfach ein paar EntitĂ€ten schreiben, so wie:

	@Entity
class Book {
  @Id
  int id;
  String title;
}

Und fast alles ist bereit. Nun ist es die Aufgabe von Hibernate, die komplexen „Details“ zu generieren, wie genau diese EntitĂ€t in DDL Ihres SQL-Dialekts definiert wird:

	ERSTELLEN TABLE book (
  id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  title VARCHAR(50),
 
  CONSTRAINT pk_book PRIMARY KEY (id)
);
 
ERSTELLEN INDEX i_book_title ON book (title);


 und wir beginnen, die Anwendung zu starten. Eine wirklich tolle Möglichkeit, schnell loszulegen und verschiedene Dinge auszuprobieren.

Aber warten Sie. Ich habe geschummelt.

  • Wird Hibernate tatsĂ€chlich die Definition dieses benannten PrimĂ€rschlĂŒssels anwenden?
  • Wird Hibernate einen Index in TITLE erstellen? – Ich weiß genau, dass wir ihn brauchen werden.
  • Wird Hibernate diesen SchlĂŒssel in der Identity-Spezifikation als identifizierend festlegen?

Wahrscheinlich nicht. Wenn Sie Ihr Projekt von Grund auf neu entwickeln, ist es immer praktisch, die alte Datenbank einfach abzulehnen und eine neue zu generieren, sobald Sie die erforderlichen Annotationen hinzugefĂŒgt haben. So wird die EntitĂ€t Book letztendlich folgendermaßen 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. Wieder einmal wird es in diesem Fall 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 ein solches Modell nicht mehr funktionieren. Denn:

Im Produktionsbetrieb können Sie nicht mehr einfach die alte Datenbank fallen lassen und von Grund auf neu beginnen. Ihre Datenbank wird zu einer veralteten Version.

Von nun an werden Sie DDL-Migrationsskripte, beispielsweise mithilfe von Flyway, schreiben mĂŒssen.Was passiert dann mit Ihren EntitĂ€ten? Sie können entweder manuell Anpassungen vornehmen (was Ihren Arbeitsaufwand verdoppelt), oder Sie lassen Hibernate fĂŒr Sie neu generieren (wie hoch ist die Wahrscheinlichkeit, dass das generierte Ergebnis Ihren Erwartungen entspricht?). In jedem Fall haben Sie verloren.

Sobald Sie in die Produktion ĂŒbergehen, benötigen Sie Hotfixes. Diese mĂŒssen sehr schnell in die Produktion gebracht werden. Da Sie sich jedoch nicht vorbereitet haben und keine reibungslose Pipeline fĂŒr Ihre Migrationen eingerichtet haben, mĂŒssen Sie alles wild patchen. Und dann schaffen Sie es nicht, alles richtig zu machen. Und beschuldigen Hibernate, denn immer ist jemand anders schuld, nur nicht Sie


Stattdessen hÀtte man von Anfang an alles ganz anders gestalten können. Zum Beispiel hÀtte man runde RÀder an das Fahrrad montieren können.

Zuerst die Datenbank

Die wahre "Wahrheit" ĂŒber das Schema Ihrer Datenbank und die "SouverĂ€nitĂ€t" darĂŒber liegt innerhalb der Datenbank. Das Schema wird ausschließlich in der Datenbank selbst definiert und nirgendwo anders, und jeder Kunde hat eine Kopie dieses Schemas. Daher ist es vollkommen sinnvoll, die Einhaltung des Schemas und seiner IntegritĂ€t direkt in der Datenbank durchzusetzen – dort, wo die Informationen gespeichert sind.
Das ist eine alte, beinahe abgedroschene Weisheit. PrimĂ€r- und SekundĂ€rschlĂŒssel sind gut. FremdschlĂŒssel sind gut. EinschrĂ€nkungen zu ĂŒberprĂŒfen ist gut. Aussagen – gut.

Und das ist noch nicht alles. Wenn Sie Oracle verwenden, möchten Sie wahrscheinlich angeben:

  • In welchem Tabellenraum sich Ihre Tabelle befindet
  • Welchen PCTFREE-Wert sie hat
  • 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 zu "Big Data" warten – man kann bereits viel frĂŒher von den vom Anbieter bereitgestellten Optimierungen fĂŒr die Datenspeicherung profitieren, wie die oben genannten. Keine der ORM-Frameworks, die ich gesehen habe (einschließlich jOOQ), bietet Zugriff auf das vollstĂ€ndige Spektrum der DDL-Optionen, die Sie möglicherweise in Ihrer Datenbank verwenden möchten. ORM bieten einige Werkzeuge, die beim Schreiben von DDL helfen.

Aber letztendlich wird ein gut gestaltetes Schema manuell in DDL geschrieben. Jeder generierte DDL ist lediglich eine AnnÀherung daran.

Wie sieht es mit dem Client-Modell aus?

Wie oben erwÀhnt, benötigen Sie auf dem Client eine Kopie des Schemas Ihrer Datenbank, ein Client-View. Es ist unnötig zu erwÀhnen, dass dieses Client-View mit dem tatsÀchlichen Modell synchronisiert werden muss. Wie erreichen Sie das am besten? Mit einem Code-Generator.

Alle Datenbanken stellen ihre Metainformationen ĂŒber SQL bereit. So können Sie alle Tabellen aus Ihrer Datenbank in verschiedenen SQL-Dialekten abrufen:

	-- 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 Sicht, materialisierte Sichten und tabellarische Funktionen berĂŒcksichtigt werden mĂŒssen) werden ebenfalls ĂŒber einen Aufruf ausgefĂŒhrt DatabaseMetaData.getTables() aus JDBC oder ĂŒber das Meta-Modul jOOQ.

Aus den Ergebnissen solcher Abfragen lÀsst sich relativ einfach eine klientenspezifische Darstellung Ihres Datenbankmodells generieren, unabhÀngig davon, welche Technologie auf Ihrer Client-Seite verwendet wird.

  • Wenn Sie JDBC oder Spring verwenden, können Sie eine Reihe von String-Konstanten erstellen.
  • Wenn Sie JPA verwenden, können Sie die EntitĂ€ten selbst generieren.
  • Wenn Sie jOOQ verwenden, können Sie das jOOQ-Meta-Modell generieren.

Je nachdem, welche Funktionen Ihre Client-API (z. B. jOOQ oder JPA) bietet, kann das generierte Meta-Modell wirklich umfangreich und detailliert sein. Nehmen wir zum Beispiel die Möglichkeit von impliziten Joins, die in jOOQ 3.11 eingefĂŒhrt wurde., die auf generierten Metainformationen zu den Beziehungen zwischen Ihren Tabellen basiert.

Jetzt fĂŒhrt jede Änderung in der Datenbank automatisch zu einem Update des Client-Codes. Stellen Sie sich beispielsweise Folgendes vor:

ALTER TABLE book RENAME COLUMN title TO book_title;

Möchten Sie diese Arbeit wirklich zweimal machen? Auf keinen Fall. Wir halten einfach DDL fest, fĂŒhren es durch Ihre Build-Pipeline und erhalten das aktualisierte Entity:

@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 beeinflussen auch die Semantik, nicht nur die Syntax. Daher kann es sinnvoll sein, im kompilieren Code zu sehen, welcher Code durch die Änderungen in Ihrer Datenbank betroffen sein könnte.

Die einzige Wahrheit

UnabhĂ€ngig von der verwendeten Technologie gibt es stets ein Modell, das die einzige Quelle der Wahrheit fĂŒr ein bestimmtes Teilsystem darstellt – oder zumindest sollten wir dieses Ziel anstreben, um das Chaos im Unternehmen zu vermeiden, wo die „Wahrheit“ ĂŒberall und nirgendwo gleichzeitig existiert. Alles könnte viel einfacher sein. Wenn Sie lediglich XML-Dateien mit einem anderen System austauschen, nutzen Sie einfach XSD. Werfen Sie einen Blick auf das INFORMATION_SCHEMA-Meta-Modell von jOOQ im XML-Format:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD ist gut verstĂ€ndlich.
  • XSD kennzeichnet den XML-Inhalt sehr prĂ€zise und ermöglicht die Validierung in allen Client-Sprachen.
  • XSD verfĂŒgt ĂŒber eine gute Versionierung und bietet umfangreiche AbwĂ€rtskompatibilitĂ€t.
  • XSD kann mithilfe von XJC in Java-Code ĂŒbersetzt werden.

Der letzte Punkt ist wichtig. Bei der Kommunikation mit externen Systemen ĂŒber XML-Nachrichten möchten wir sicherstellen, dass unsere Nachrichten gĂŒltig sind. Das lĂ€sst sich sehr einfach mit JAXB, XJC und XSD erreichen. Es wĂ€re absolut verrĂŒckt zu glauben, dass wir bei einem "Java-zuerst"-Ansatz, bei dem wir unsere Nachrichten als Java-Objekte erstellen, diese irgendwie sinnvoll nach XML abbilden und zur Verwendung in ein anderes System senden können. XML, das auf diese Weise generiert wird, wĂ€re von sehr schlechter QualitĂ€t, schlecht dokumentiert und schwer weiterzuentwickeln. Sollte ein solcher Schnittstelle ein Service Level Agreement (SLA) zugrunde liegen, wĂŒrden wir es sofort ruinieren.

Ehrlich gesagt passiert genau das stĂ€ndig mit JSON-APIs, aber das ist eine andere Geschichte, beim nĂ€chsten Mal werde ich mich darĂŒber aufregen


Datenbanken: das ist dasselbe

Wenn Sie mit Datenbanken arbeiten, verstehen Sie, dass sie alle im Grunde Àhnlich sind. Eine Datenbank besitzt ihre Daten und muss das Schema verwalten. Alle Modifikationen am Schema sollten direkt in DDL umgesetzt werden, damit die einzige Wahrheit aktualisiert wird.

Wenn die Quelle aktualisiert wird, mĂŒssen auch alle Kunden ihre Modellkopien aktualisieren. Einige Kunden könnten in Java mit jOOQ und Hibernate oder JDBC (oder sogar allen zusammen) geschrieben sein. Andere könnten in Perl geschrieben sein (da bleibt nur, ihnen viel GlĂŒck zu wĂŒnschen), und wieder andere in C#. Das spielt keine Rolle. Das Hauptmodell befindet sich in der Datenbank. Durch ORM generierte Modelle sind oft von minderer QualitĂ€t, schlecht dokumentiert und schwer weiterzuentwickeln.

Machen Sie daher keine Fehler. Begehen 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 Ihr Datenbankmodell bequem kopieren und an die Clients ĂŒbergeben zu können. Und hören Sie auf, sich ĂŒber Code-Generatoren zu sorgen. Sie sind gut. Mit ihnen werden Sie produktiver. Sie mĂŒssen nur von Anfang an etwas Zeit in ihre Einrichtung investieren – und schon erwarten Sie Jahre gesteigerter ProduktivitĂ€t, die die Geschichte Ihres Projekts prĂ€gen werden.

Noch nicht danken, spÀter.

ErklÀrung

Zur Klarheit: Dieser Artikel propagiert keineswegs, dass das gesamte System (d.h. das Fachgebiet, die GeschÀftslogik usw.) sich nach dem Modell Ihrer Datenbank richten sollte. In diesem Artikel erklÀre ich, dass der Client-Code, der mit der Datenbank interagiert, basierend auf dem Modell der Datenbank handeln sollte, sodass das Modell der Datenbank nicht als "First-Class" im Client selbst reproduziert wird. Eine solche Logik befindet sich normalerweise auf der Datenzugriffsebene in Ihrem Client.

In den nach wie vor gelegentlich anzutreffenden Zwei-Schichten-Architekturen könnte ein solches Systemmodell die einzige Möglichkeit darstellen. Allerdings erscheint mir in den meisten Systemen die Datenzugriffsebene als eine "Subsystem", die das Modell der Datenbank kapselt.

Ausnahmen

Jede Regel hat Ausnahmen, und ich habe bereits erwÀhnt, dass der Ansatz, die Datenbank als primÀr zu betrachten und den Quellcode zu generieren, manchmal ungeeignet sein kann. Hier sind ein paar solcher Ausnahmen (es gibt wahrscheinlich noch weitere):

  • Wenn das Schema unbekannt ist und es geöffnet werden muss. Zum Beispiel sind Sie ein Anbieter eines Tools, das den Nutzern hilft, sich in jedem Schema zurechtzufinden. Uff. Das geht hier ohne Codegenerierung. Aber dennoch – die Datenbank steht an erster Stelle.
  • Wenn das Schema zur Laufzeit generiert werden soll, um eine bestimmte Aufgabe zu lösen. Dieses Beispiel scheint eine etwas ausgefallene Version des Musters zu sein. Entity-Attribut-Wert, d.h., Sie haben wirklich kein klar definiertes Schema. In diesem Fall kann man oft nicht einmal sicher sein, dass eine relationale Datenbank geeignet ist.

Ausnahmen sind von Natur aus außergewöhnlich. In den meisten FĂ€llen, die mit der Nutzung von relationalen Datenbanken verbunden sind, ist das Schema im Voraus bekannt, es befindet sich innerhalb der Datenbank und stellt die einzige Quelle der 'Wahrheit' dar, wĂ€hrend alle Clients Kopien davon erwerben mĂŒssen, die von ihm abgeleitet sind. Idealerweise sollte dabei ein Codegenerator verwendet werden.

Quelle: habr.com

ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen đŸ”„ ZuverlĂ€ssiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster