Die Wahrheit zuallererst oder warum das System basierend auf dem Datenbankgerät entworfen werden muss

Hallo Habr!

Wir beschäftigen uns weiterhin mit Java und Spring , auch auf Datenbankebene. Heute empfehlen wir zu lesen, warum beim Entwerfen großer Anwendungen die Datenbankstruktur und nicht der Java-Code eine entscheidende Bedeutung dafür haben sollte, wie dies geschieht und welche Ausnahmen von dieser Regel bestehen.

In diesem eher verspäteten Artikel werde ich erklären, warum ich der Meinung bin, dass das Datenmodell in der Anwendung in fast allen Fällen "basierend auf der Datenbank" und nicht "basierend auf den Fähigkeiten von Java" (oder einer anderen Client-Sprache, mit der Sie arbeiten) entworfen werden sollte. Wenn Sie sich für den zweiten Ansatz entscheiden, begeben Sie sich auf eine lange Reise voller Schmerzen und Leiden, sobald Ihr Projekt zu wachsen beginnt.

Dieser Artikel basiert auf einer Frage zum Stapelüberlauf.

Interessante Diskussionen zu reddit in den Abschnitten / r / java und / r / Programmierung .

Codegenerierung


Wie überrascht ich bin, dass es eine so kleine Anzahl von Benutzern gibt, die sich mit jOOQ vertraut gemacht haben und empört darüber sind, dass jOOQ bei der Arbeit ernsthaft auf die Generierung von Quellcode angewiesen ist. Niemand stört Sie daran, jOOQ nach Belieben zu verwenden, und zwingt Sie nicht zur Codegenerierung. Standardmäßig (wie im Handbuch beschrieben) geschieht die Arbeit mit jOOQ folgendermaßen: Sie beginnen mit dem (geerbten) Datenbankschema, entwickeln es mit dem jOOQ-Codegenerator zurück, sodass Sie eine Reihe von Klassen erhalten, die Ihre Tabellen darstellen, und dann Schreiben Sie typsichere Abfragen in diese Tabellen:

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^      
//     ,    
//   SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^  
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Der Code wird entweder manuell außerhalb der Baugruppe oder manuell mit jeder Baugruppe generiert. Eine solche Regeneration kann beispielsweise unmittelbar nach der Migration der Flyway-Datenbank erfolgen, was auch manuell oder automatisch erfolgen kann .

Quellcode-Generierung


Mit solchen Ansätzen zur manuellen und automatischen Codegenerierung sind verschiedene Philosophien, Vor- und Nachteile verbunden, auf die ich in diesem Artikel nicht näher eingehen werde. Im Allgemeinen besteht der springende Punkt des generierten Codes darin, dass wir in Java die „Wahrheit“ reproduzieren können, die wir für selbstverständlich halten, entweder innerhalb oder außerhalb unseres Systems. In gewisser Weise tun Compiler, die Bytecode, Maschinencode oder eine andere Art von Quellcode generieren, dasselbe - wir erhalten eine Darstellung unserer „Wahrheit“ in einer anderen Sprache, unabhängig von bestimmten Gründen.

Es gibt viele solcher Codegeneratoren. Beispielsweise kann XJC Java-Code basierend auf XSD- oder WSDL-Dateien generieren . Das Prinzip ist immer dasselbe:

  • Es gibt eine Wahrheit (intern oder extern) - zum Beispiel Spezifikation, Datenmodell usw.
  • Wir brauchen eine lokale Darstellung dieser Wahrheit in unserer Programmiersprache.

Darüber hinaus ist es fast immer ratsam, eine solche Darstellung zu erstellen, um Redundanz zu vermeiden.

Typanbieter und Anmerkungsverarbeitung


Hinweis: Ein anderer, modernerer und spezifischerer Ansatz zur Codegenerierung für jOOQ ist mit der Verwendung von Typanbietern in der Form verbunden, in der sie in F # implementiert sind . In diesem Fall wird der Code vom Compiler tatsächlich in der Kompilierungsphase generiert. In Form von Quellen existiert ein solcher Code grundsätzlich nicht. In Java gibt es ähnliche, wenn auch weniger elegante Tools - dies sind Anmerkungsprozessoren wie Lombok .

In gewissem Sinne passieren hier die gleichen Dinge wie im ersten Fall, mit Ausnahme von:

  • Sie sehen den generierten Code nicht (vielleicht scheint diese Situation jemandem nicht so abstoßend zu sein?)
  • , , , «» . Lombok, “”. , .

?


Neben der kniffligen Frage, wie es besser ist, die Codegenerierung manuell oder automatisch zu starten, muss erwähnt werden, dass es Menschen gibt, die glauben, dass die Codegenerierung überhaupt nicht erforderlich ist. Der Grund für diesen Standpunkt, auf den ich am häufigsten gestoßen bin, ist, dass es dann schwierig ist, die Assembly-Pipeline zu konfigurieren. Ja, wirklich schwer. Es fallen zusätzliche Infrastrukturkosten an. Wenn Sie gerade erst mit einem bestimmten Produkt arbeiten (z. B. jOOQ, JAXB, Hibernate usw.), dauert es einige Zeit, bis Sie die Arbeitsumgebung eingerichtet haben, die Sie für das Erlernen der API selbst verwenden möchten, und dann den Wert daraus extrahieren.

Wenn die mit dem Verständnis des Generatorgeräts verbundenen Kosten zu hoch sind, hat die API tatsächlich ein wenig an der Benutzerfreundlichkeit des Codegenerators gearbeitet (und in Zukunft stellt sich heraus, dass die Benutzerkonfiguration darin kompliziert ist). Benutzerfreundlichkeit sollte für eine solche API die höchste Priorität haben. Dies ist jedoch nur ein Argument gegen die Codegenerierung. Im Übrigen ist es vollständig manuell, eine lokale Darstellung der internen oder externen Wahrheit zu schreiben.

Viele werden sagen, dass sie keine Zeit haben, dies alles zu tun. Sie haben Fristen für ihr Superprodukt. Einige Zeit später kämmen wir die Montageförderer, es wird rechtzeitig sein. Ich werde sie beantworten:


Original , Alan O'Rourke, Audience Stack

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

Ja wirklich. Für Hibernate und seine Benutzer ist dies sowohl ein Segen als auch ein Fluch. In Hibernate können Sie einfach einige Entitäten wie folgt schreiben:

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

Und fast alles ist fertig. Das Ziel von Hibernate besteht nun darin, komplexe „Details“ zu generieren, wie genau diese Entität in der DDL Ihres SQL- „Dialekts“ 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 starten die Anwendung. Eine wirklich coole Gelegenheit, schnell loszulegen und verschiedene Dinge auszuprobieren.

Erlauben Sie jedoch. Ich habe getäuscht.

  • Wendet der Ruhezustand wirklich die Definition dieses benannten Primärschlüssels an?
  • Wird Hibernate einen Index in TITLE erstellen? "Ich weiß sicher, dass wir ihn brauchen werden."
  • Macht Hibernate diesen Schlüssel in der Identitätsspezifikation genau identifizierbar?

Wahrscheinlich nicht. Wenn Sie Ihr Projekt von Grund auf neu entwickeln, ist es immer praktisch, einfach die alte Datenbank zu löschen und eine neue zu generieren, sobald Sie die erforderlichen Anmerkungen hinzufügen. Die Buchentität wird also irgendwann die Form annehmen:

	@Entity
@Table(name = "book", indexes = {
  @Index(name = "i_book_title", columnList = "title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
  String title;
}

Cool. Regenerieren. Auch in diesem Fall wird es zu Beginn sehr einfach sein.

Aber dann muss man dafür bezahlen


Früher oder später muss man in Produktion gehen. In diesem Moment funktioniert ein solches Modell nicht mehr. Denn:

In der Produktion ist es bei Bedarf nicht mehr möglich, die alte Datenbank zu verwerfen und von vorne zu beginnen. Ihre Datenbank wird zu einer Legacy-Datenbank.

Von nun an müssen Sie DDL-Migrationsskripte schreiben , beispielsweise mit Flyway . Und was passiert dann mit Ihren Entitäten? Sie können sie entweder manuell anpassen (und damit Ihre Arbeitslast verdoppeln) oder Hibernate anweisen, sie für Sie neu zu generieren (wie groß sind die Chancen, dass die auf diese Weise generierte Ihre Erwartungen erfüllt?). Sie verlieren trotzdem.

Sobald Sie in Produktion gehen, benötigen Sie daher Hot Patches. Und sie müssen sehr schnell in Produktion gehen. Da Sie keinen reibungslosen Ablauf Ihrer Migrationen für die Produktion vorbereitet und organisiert haben, patchen Sie alles wild. Und dann haben Sie keine Zeit, alles richtig zu machen. Und schimpfen Sie mit Hibernate, denn immer ist jemand schuld, aber nicht Sie ...

Stattdessen könnte von Anfang an alles auf eine ganz andere Art und Weise geschehen. Stellen Sie beispielsweise runde Räder auf ein Fahrrad.

Datenbank zuerst


Die wahre "Wahrheit" im Schema Ihrer Datenbank und die "Souveränität" darüber liegt in der Datenbank. Das Schema wird nur in der Datenbank selbst und nirgendwo anders definiert, und jeder der Clients verfügt über eine Kopie dieses Schemas. Es ist daher ratsam, die Einhaltung des Schemas und seiner Integrität direkt in der Datenbank festzulegen - wo die Informationen gespeichert sind.
Das ist sogar alte, abgenutzte Weisheit. Primär- und eindeutige Schlüssel sind gut. Fremdschlüssel sind gut. Die Überprüfung auf Einschränkungen ist gut. Aussagen sind gut.

Darüber hinaus ist dies nicht alles. Wenn Sie beispielsweise Oracle verwenden, möchten Sie wahrscheinlich Folgendes angeben:

  • In welchem ​​Tablespace befindet sich Ihre Tabelle?
  • Was ist der PCTFREE-Wert?
  • Wie groß ist der Cache in Ihrer Sequenz (hinter dem Bezeichner)?

Vielleicht ist all dies in kleinen Systemen nicht wichtig, aber es ist nicht notwendig, auf den Übergang zum Bereich "Big Data" zu warten - es ist möglich und viel früher, von der Optimierung der Datenspeicherung durch den Lieferanten zu profitieren, wie oben erwähnt. Keine der ORMs, die ich gesehen habe (einschließlich jOOQ), bietet Zugriff auf alle DDL-Optionen, die Sie möglicherweise in Ihrer Datenbank verwenden möchten. ORMs bieten einige Tools, die beim Schreiben von DDL helfen.

Am Ende wird eine gut gestaltete Schaltung manuell in DDL geschrieben. Jede generierte DDL ist nur eine Annäherung daran.

Was ist mit dem Kundenmodell?


Wie oben erwähnt, benötigen Sie auf dem Client eine Kopie Ihres Datenbankschemas, die Client-Ansicht. Diese Client-Ansicht muss natürlich mit dem realen Modell synchronisiert werden. Was ist der beste Weg, um dies zu erreichen? Verwenden eines Codegenerators.

Alle Datenbanken stellen ihre Metainformationen über SQL bereit. So rufen Sie alle Tabellen in verschiedenen SQL-Dialekten aus Ihrer Datenbank ab:

	-- 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 Sie auch Darstellungen, materialisierte Darstellungen, Funktionen mit einem Tabellenwert berücksichtigen müssen) werden auch mithilfe eines Aufrufs DatabaseMetaData.getTables()von JDBC oder mithilfe des jOOQ-Metamoduls ausgeführt.

Aus den Ergebnissen solcher Abfragen ist es relativ einfach, eine Client-Ansicht Ihres Datenbankmodells zu generieren, unabhängig davon, welche Technologie auf Ihrem Client verwendet wird.

  • Wenn Sie JDBC oder Spring verwenden, können Sie eine Reihe von Zeichenfolgenkonstanten erstellen
  • Wenn Sie JPA verwenden, können Sie Entitäten selbst generieren
  • Wenn Sie jOOQ verwenden, können Sie das jOOQ-Metamodell generieren

Abhängig davon, wie viele Funktionen Ihre Client-API bietet (z. B. jOOQ oder JPA), kann das generierte Metamodell wirklich umfangreich und vollständig sein. Nehmen Sie zum Beispiel die Möglichkeit impliziter Verknüpfungen , die in jOOQ 3.11 erschienen sind und auf den generierten Metainformationen über die Beziehungen von Fremdschlüsseln zwischen Ihren Tabellen beruhen.

Jetzt führt jedes Inkrementieren der Datenbank automatisch zur Aktualisierung des Client-Codes. Stellen Sie sich zum Beispiel vor:

ALTER TABLE book RENAME COLUMN title TO book_title;

Möchten Sie diesen Job wirklich zweimal machen? Auf keinen Fall. Korrigieren Sie einfach die DDL, führen Sie sie durch Ihre Assembly-Pipeline und erhalten Sie die aktualisierte Entität:

@Entity
@Table(name = "book", indexes = {
 
  //    ?
  @Index(name = "i_book_title", columnList = "book_title")
})
class Book {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  int id;
 
  @Column("book_title")
  String bookTitle;
}

Oder eine aktualisierte jOOQ-Klasse. Die meisten DDL-Änderungen wirken sich auch auf die Semantik aus, nicht nur auf die Syntax. Daher kann es praktisch sein, im kompilierten Code zu sehen, welcher Code vom Inkrement Ihrer Datenbank betroffen ist (oder sein kann).

Die einzige Wahrheit


Unabhängig davon, welche Technologie Sie verwenden, gibt es immer ein Modell, das für ein Subsystem die einzige Wahrheitsquelle darstellt - oder zumindest sollten wir uns darum bemühen und solche Unternehmensverwirrungen vermeiden, bei denen die „Wahrheit“ überall und nirgendwo ist. Alles kann viel einfacher sein. Wenn Sie nur XML-Dateien mit einem anderen System austauschen, verwenden Sie einfach XSD. Sehen Sie sich das Metamodell INFORMATION_SCHEMA von jOOQ in XML-Form an:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD ist gut verstanden
  • XSD XML
  • XSD
  • XSD Java XJC

Der letzte Punkt ist wichtig. Bei der Kommunikation mit einem externen System über XML-Nachrichten möchten wir die Gültigkeit unserer Nachrichten sicherstellen. Dies ist mit JAXB, XJC und XSD sehr einfach zu erreichen. Es wäre völlig verrückt zu erwarten, dass unsere Nachrichten, wenn wir uns dem Design von „Java first“ nähern, bei dem wir unsere Nachrichten in Form von Java-Objekten erstellen, irgendwie klar in XML angezeigt und zum Verbrauch an ein anderes System gesendet werden könnten. Das auf diese Weise erzeugte XML wäre von sehr schlechter Qualität, nicht dokumentiert und schwer zu entwickeln. Wenn es eine Vereinbarung über das Niveau der Servicequalität (SLA) für eine solche Schnittstelle gäbe, würden wir sie sofort ruinieren.

Ehrlich gesagt, genau das passiert die ganze Zeit von der API bis zu JSON, aber das ist eine andere Geschichte, ich werde das nächste Mal schwören ...

Datenbanken: es ist das gleiche


Wenn Sie mit Datenbanken arbeiten, verstehen Sie, dass diese im Prinzip ähnlich sind. Die Basis besitzt ihre Daten und muss das Schema verwalten. Alle an der Schaltung vorgenommenen Änderungen müssen direkt in DDL implementiert werden, um eine einzelne Wahrheitsquelle zu aktualisieren.

Wenn eine Quellaktualisierung stattgefunden hat, müssen alle Clients auch ihre Kopien des Modells aktualisieren. Einige Clients können mit jOOQ und Hibernate oder JDBC (oder alle gleichzeitig) in Java geschrieben werden. Andere Kunden können in Perl geschrieben werden (es bleibt ihnen viel Glück zu wünschen) und andere in C #. Das ist nicht wichtig. Das Hauptmodell befindet sich in der Datenbank. Mit ORMs generierte Modelle, die normalerweise von schlechter Qualität sind, sind schlecht dokumentiert und schwer zu entwickeln.

Machen Sie deshalb keine Fehler. Machen Sie von Anfang an keine Fehler. Arbeiten Sie aus einer Datenbank. Erstellen Sie eine Bereitstellungspipeline, die automatisiert werden kann. Aktivieren Sie Codegeneratoren, damit Sie Ihr Datenbankmodell bequem kopieren und auf Clients sichern können. Und hör auf, dir Sorgen um Codegeneratoren zu machen. Sie sind gut. Mit ihnen werden Sie produktiver. Sie müssen von Anfang an nur ein wenig Zeit aufwenden, um sie zu konfigurieren - und dann haben Sie jahrelange Produktivitätssteigerungen, die den Verlauf Ihres Projekts bestimmen.

Bis dahin danke.

Erläuterung


Zur Verdeutlichung: Dieser Artikel befürwortet in keiner Weise, dass Sie unter dem Modell Ihrer Datenbank das gesamte System (d. H. Themenbereich, Geschäftslogik usw. usw.) biegen müssen. In diesem Artikel sage ich, dass Client-Code, der mit der Datenbank interagiert, auf der Grundlage des Datenbankmodells handeln muss, damit das Datenbankmodell nicht im Status "First Class" reproduziert wird. Diese Logik befindet sich normalerweise auf der Datenzugriffsebene Ihres Clients.

In zweistufigen Architekturen, die an einigen Stellen noch erhalten sind, kann ein solches Systemmodell das einzig mögliche sein. In den meisten Systemen scheint mir die Ebene des Datenzugriffs jedoch ein "Subsystem" zu sein, das ein Datenbankmodell kapselt.

Ausnahmen


Es gibt Ausnahmen zu jeder Regel, und ich habe bereits gesagt, dass ein Ansatz mit Datenbankprimat und Quellcodegenerierung manchmal unangemessen sein kann. Hier sind einige Ausnahmen (es gibt wahrscheinlich andere):

  • Wenn der Stromkreis unbekannt ist und geöffnet werden muss. Sie sind beispielsweise Anbieter eines Tools, mit dem Benutzer in jedem Schema navigieren können. Puh Es erfolgt keine Codegenerierung. Aber trotzdem - die Datenbank ist vor allem.
  • Wenn eine Schaltung im laufenden Betrieb generiert werden soll, um ein bestimmtes Problem zu lösen. Dieses Beispiel scheint eine etwas phantasievolle Version des Wertmusters für Entitätsattribute zu sein , d. H. Sie haben wirklich kein genau definiertes Schema. In diesem Fall ist es oft unmöglich, überhaupt sicher zu sein, dass ein RDBMS für Sie geeignet ist.

Ausnahmen sind von Natur aus außergewöhnlich. In den meisten Fällen, in denen ein RDBMS verwendet wird, ist das Schema im Voraus bekannt, es befindet sich innerhalb des RDBMS und ist die einzige Quelle für „Wahrheit“, und alle Kunden müssen Kopien erwerben, die von diesem abgeleitet sind. Idealerweise müssen Sie einen Codegenerator verwenden.

All Articles