Die Wahrheit zuerst, oder warum ein System basierend auf dem Datenbankdesign entworfen werden muss

Hey Habr!

Wir forschen weiter zum Thema Javac и Feder, auch auf Datenbankebene. Heute laden wir Sie ein, zu lesen, warum beim Entwurf großer Anwendungen die Datenbankstruktur und nicht der Java-Code von entscheidender Bedeutung sein sollte, wie dies geschieht und welche Ausnahmen es von dieser Regel gibt.

In diesem ziemlich späten Artikel werde ich erklären, warum ich glaube, dass das Datenmodell in einer Anwendung in fast allen Fällen „auf der Grundlage der Datenbank“ und nicht „auf der Grundlage der Fähigkeiten von Java“ (oder einer anderen Client-Sprache) entworfen werden sollte arbeiten mit). Wenn Sie den zweiten Ansatz wählen, bereiten Sie sich auf einen langen Weg voller Schmerz und Leid vor, sobald Ihr Projekt zu wachsen beginnt.

Der Artikel wurde basierend auf geschrieben eine Frage, gegeben auf Stack Overflow.

Interessante Diskussionen auf reddit in Abschnitten /r/java и / r / Programmierung.

Codegenerierung

Wie überrascht war ich, dass es so einen kleinen Teil der Benutzer gibt, die, nachdem sie jOOQ kennengelernt haben, darüber empört sind, dass jOOQ für den Betrieb ernsthaft auf die Generierung von Quellcode angewiesen ist. Niemand hindert Sie daran, jOOQ so zu nutzen, wie Sie es für richtig halten, oder zwingt Sie, die Codegenerierung zu nutzen. Die Standardmethode (wie im Handbuch beschrieben) für die Arbeit mit jOOQ besteht jedoch darin, dass Sie mit einem (alten) Datenbankschema beginnen, es mithilfe des jOOQ-Codegenerators zurückentwickeln, um so einen Satz von Klassen zu erhalten, die Ihre Tabellen darstellen, und dann den Typ schreiben -sichere Abfragen an 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 Assembly oder manuell bei jeder Assembly generiert. Beispielsweise kann eine solche Regeneration unmittelbar danach erfolgen Flyway-Datenbankmigration, die auch manuell oder automatisch erfolgen kann.

Quellcodegenerierung

Mit diesen Ansätzen zur Codegenerierung – manuell und automatisch – sind verschiedene Philosophien sowie Vor- und Nachteile verbunden, auf die ich in diesem Artikel nicht näher eingehen werde. Aber im Allgemeinen besteht der Sinn des generierten Codes darin, dass er es uns ermöglicht, in Java die „Wahrheit“ zu reproduzieren, die wir entweder innerhalb unseres Systems oder außerhalb davon für selbstverständlich halten. In gewissem Sinne ist es das, was Compiler tun, wenn sie Bytecode, Maschinencode oder eine andere Form von Quellcode generieren: Wir erhalten eine Darstellung unserer „Wahrheit“ in einer anderen Sprache, unabhängig von den spezifischen Gründen.

Es gibt viele solcher Codegeneratoren. Zum Beispiel, XJC kann Java-Code basierend auf XSD- oder WSDL-Dateien generieren. Das Prinzip ist immer das gleiche:

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

Darüber hinaus empfiehlt es sich fast immer, eine solche Darstellung zu erstellen, um Redundanzen zu vermeiden.

Typanbieter und Anmerkungsverarbeitung

Hinweis: Ein anderer, modernerer und spezifischerer Ansatz zum Generieren von Code für jOOQ ist die Verwendung von Typanbietern. wie sie in F# implementiert sind. In diesem Fall wird der Code vom Compiler generiert, und zwar bereits in der Kompilierungsphase. Im Prinzip liegt ein solcher Code nicht in Quellform vor. Java verfügt über ähnliche, wenn auch nicht so elegante Tools – beispielsweise Annotationsprozessoren. Chili.

In gewisser Weise passieren hier die gleichen Dinge wie im ersten Fall, mit Ausnahme von:

  • Sie sehen den generierten Code nicht (vielleicht erscheint diese Situation jemandem weniger abstoßend?)
  • Sie müssen sicherstellen, dass Typen bereitgestellt werden können, d. h. „true“ muss immer verfügbar sein. Dies ist im Fall von Lombok einfach, wo „Wahrheit“ annotiert wird. Etwas komplizierter ist es bei Datenbankmodellen, die auf eine ständig verfügbare Live-Verbindung angewiesen sind.

Was ist das Problem bei der Codegenerierung?

Neben der kniffligen Frage, wie man die Codegenerierung am besten durchführt – manuell oder automatisch – müssen wir auch erwähnen, dass es Leute gibt, die glauben, dass die Codegenerierung überhaupt nicht nötig sei. Die Begründung für diese Sichtweise, die mir am häufigsten begegnet ist, ist, dass es dann schwierig sei, eine Build-Pipeline aufzubauen. Ja, es ist wirklich schwierig. Es entstehen zusätzliche Infrastrukturkosten. Wenn Sie gerade erst mit einem bestimmten Produkt beginnen (sei es jOOQ, JAXB, Hibernate usw.), nimmt die Einrichtung einer Produktionsumgebung Zeit in Anspruch, die Sie lieber mit dem Erlernen der API selbst verbringen würden, damit Sie daraus Nutzen ziehen können .

Wenn die Kosten, die mit dem Verständnis der Struktur des Generators verbunden sind, zu hoch sind, hat die API in der Tat schlechte Arbeit bei der Benutzerfreundlichkeit des Codegenerators geleistet (und später stellt sich heraus, dass die Benutzeranpassung darin ebenfalls komplex ist). Die Benutzerfreundlichkeit sollte für jede solche API höchste Priorität haben. Dies ist jedoch nur ein Argument gegen die Codegenerierung. Ansonsten ist es völlig manuell, eine lokale Darstellung der inneren oder äußeren Wahrheit zu schreiben.

Viele werden sagen, dass sie für all das keine Zeit haben. Ihnen laufen die Fristen für ihr Superprodukt aus. Irgendwann werden wir die Montagebänder aufräumen, wir werden Zeit haben. Ich werde ihnen antworten:

Die Wahrheit zuerst, oder warum ein System basierend auf dem Datenbankdesign entworfen werden muss
Original, Alan O'Rourke, Audience Stack

Aber in Hibernate/JPA ist es so einfach, Java-Code zu schreiben.

Wirklich. Für Hibernate und seine Nutzer ist dies Segen und Fluch zugleich. In Hibernate können Sie einfach ein paar Entitäten schreiben, etwa diese:

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

Und fast alles ist fertig. Jetzt liegt es an Hibernate, die komplexen „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 starten Sie die Anwendung. Eine wirklich coole Gelegenheit, schnell loszulegen und verschiedene Dinge auszuprobieren.

Bitte erlauben Sie es mir jedoch. Ich habe gelogen.

  • Wird Hibernate tatsächlich die Definition dieses benannten Primärschlüssels erzwingen?
  • Wird Hibernate einen Index in TITLE erstellen? – Ich weiß mit Sicherheit, dass wir es brauchen werden.
  • Wird Hibernate genau diesen Schlüssel in der Identitätsspezifikation identifizieren?

Wahrscheinlich nicht. Wenn Sie Ihr Projekt von Grund auf neu entwickeln, ist es immer praktisch, die alte Datenbank einfach zu verwerfen und eine neue zu erstellen, sobald Sie die erforderlichen Anmerkungen hinzugefügt haben. Somit wird die Buchentität letztendlich 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 am Anfang sehr einfach sein.

Aber Sie müssen es später bezahlen

Früher oder später müssen Sie in die Produktion gehen. Dann wird dieses Modell nicht mehr funktionieren. Weil:

In der Produktion wird es nicht mehr möglich sein, bei Bedarf die alte Datenbank zu verwerfen und von vorne zu beginnen. Ihre Datenbank wird zum Vermächtnis.

Von nun an und für immer wirst du schreiben müssen DDL-Migrationsskripte, zum Beispiel mit Flyway. Was passiert in diesem Fall mit Ihren Unternehmen? Sie können sie entweder manuell anpassen (und so Ihren Arbeitsaufwand verdoppeln), oder Sie können Hibernate anweisen, sie für Sie neu zu generieren (wie wahrscheinlich ist es, dass die auf diese Weise generierten Dateien Ihren Erwartungen entsprechen?). In jedem Fall verlieren Sie.

Sobald Sie also mit der Produktion beginnen, benötigen Sie Hot Patches. Und sie müssen sehr schnell in Produktion gehen. Da Sie Ihre Migrationen für die Produktion nicht vorbereitet und keinen reibungslosen Ablauf organisiert haben, patchen Sie wild auf alles. Und dann hat man keine Zeit mehr, alles richtig zu machen. Und Sie kritisieren Hibernate, weil immer jemand anderes schuld ist, nur nicht Sie ...

Stattdessen hätte man es von Anfang an ganz anders machen können. Befestigen Sie beispielsweise runde Räder an einem Fahrrad.

Zuerst die Datenbank

Die wahre „Wahrheit“ in Ihrem Datenbankschema und die „Souveränität“ darüber liegt in der Datenbank. Das Schema ist nur in der Datenbank selbst und nirgendwo anders definiert, und jeder Client verfügt über eine Kopie dieses Schemas. Daher ist es absolut sinnvoll, die Einhaltung des Schemas und seiner Integrität durchzusetzen und dies direkt in der Datenbank zu tun – dort, wo sich die Informationen befinden gelagert.
Das ist eine alte, sogar abgedroschene Weisheit. Primäre und eindeutige Schlüssel sind gut. Fremdschlüssel sind gut. Es ist gut, Einschränkungen zu überprüfen. Aussagen - Bußgeld.

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

  • In welchem ​​Tabellenbereich befindet sich Ihr Tisch?
  • Was ist der PCTFREE-Wert?
  • Wie groß ist der Cache in Ihrer Sequenz (hinter der ID)?

In kleinen Systemen ist das vielleicht nicht wichtig, aber Sie müssen nicht warten, bis Sie in den Big-Data-Bereich vordringen – Sie können schon viel früher von den vom Anbieter bereitgestellten Speicheroptimierungen wie den oben genannten profitieren. Keines 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 Ihnen beim Schreiben von DDL helfen.

Aber letzten Endes ist eine gut konzipierte Schaltung in DDL handgeschrieben. Jede generierte DDL ist nur eine Annäherung daran.

Wie sieht es mit dem Kundenmodell aus?

Wie oben erwähnt, benötigen Sie auf dem Client eine Kopie Ihres Datenbankschemas, die Client-Ansicht. Selbstverständlich muss diese Clientansicht mit dem tatsächlichen Modell synchronisiert sein. Was ist der beste Weg, dies zu erreichen? Verwendung eines Codegenerators.

Alle Datenbanken stellen ihre Metainformationen über SQL zur Verfügung. 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 Sie auch Ansichten, materialisierte Ansichten oder Tabellenwertfunktionen berücksichtigen müssen) werden ebenfalls per Aufruf ausgeführt DatabaseMetaData.getTables() von JDBC oder über das jOOQ-Metamodul.

Aus den Ergebnissen solcher Abfragen lässt sich relativ einfach eine clientseitige Darstellung Ihres Datenbankmodells generieren, unabhängig davon, welche Technologie Sie auf dem Client verwenden.

  • Wenn Sie JDBC oder Spring verwenden, können Sie einen Satz String-Konstanten erstellen
  • Wenn Sie JPA verwenden, können Sie die Entitäten selbst generieren
  • Wenn Sie jOOQ verwenden, können Sie das jOOQ-Metamodell generieren

Abhängig davon, wie viel Funktionalität Ihre Client-API (z. B. jOOQ oder JPA) bietet, kann das generierte Metamodell sehr umfangreich und vollständig sein. Nehmen wir zum Beispiel die Möglichkeit impliziter Joins, eingeführt in jOOQ 3.11, das auf generierten Metainformationen über die Fremdschlüsselbeziehungen basiert, die zwischen Ihren Tabellen bestehen.

Jetzt aktualisiert jedes Datenbankinkrement automatisch den Clientcode. Stellen Sie sich zum Beispiel vor:

ALTER TABLE book RENAME COLUMN title TO book_title;

Würden Sie diesen Job wirklich zweimal machen wollen? Auf keinen Fall. Übertragen Sie einfach die DDL, führen Sie sie durch Ihre Build-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 die aktualisierte jOOQ-Klasse. Die meisten DDL-Änderungen wirken sich auch auf die Semantik und nicht nur auf die Syntax aus. Daher kann es hilfreich sein, im kompilierten Code nachzusehen, welcher Code von Ihrer Datenbankinkrementierung betroffen sein wird (oder sein könnte).

Die einzige Wahrheit

Unabhängig davon, welche Technologie Sie verwenden, gibt es immer ein Modell, das die einzige Wahrheitsquelle für ein bestimmtes Subsystem darstellt – oder zumindest sollten wir danach streben und solche Unternehmensverwirrungen vermeiden, bei denen „Wahrheit“ überall und nirgends gleichzeitig ist . Alles könnte viel einfacher sein. Wenn Sie nur XML-Dateien mit einem anderen System austauschen, verwenden Sie einfach XSD. Schauen Sie sich das Metamodell INFORMATION_SCHEMA von jOOQ im XML-Format an:
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD ist gut verstanden
  • XSD tokenisiert XML-Inhalte sehr gut und ermöglicht die Validierung in allen Clientsprachen
  • XSD ist gut versioniert und verfügt über eine erweiterte Abwärtskompatibilität
  • 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 sicher sein, dass unsere Nachrichten gültig sind. Dies ist mit JAXB, XJC und XSD sehr einfach zu erreichen. Es wäre völliger Wahnsinn zu glauben, dass wir mit einem „Java First“-Entwurfsansatz, bei dem wir unsere Nachrichten als Java-Objekte erstellen, sie irgendwie kohärent auf XML abbilden und zur Verwendung an ein anderes System senden könnten. Auf diese Weise generiertes XML wäre von sehr schlechter Qualität, undokumentiert und schwer zu entwickeln. Wenn es für eine solche Schnittstelle ein Service Level Agreement (SLA) gäbe, würden wir es sofort vermasseln.

Ehrlich gesagt passiert das ständig mit JSON-APIs, aber das ist eine andere Geschichte, ich werde mich beim nächsten Mal streiten ...

Datenbanken: Sie sind dasselbe

Wenn man mit Datenbanken arbeitet, stellt man fest, dass sie alle grundsätzlich ähnlich sind. Die Basis besitzt ihre Daten und muss das Schema verwalten. Alle am Schema vorgenommenen Änderungen müssen direkt in der DDL implementiert werden, damit die Single Source of Truth aktualisiert wird.

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

Machen Sie also keine Fehler. Machen Sie keine Fehler von Anfang an. Arbeiten Sie aus der Datenbank. Erstellen Sie eine Bereitstellungspipeline, die automatisiert werden kann. Aktivieren Sie Codegeneratoren, um das Kopieren Ihres Datenbankmodells und das Speichern auf Clients zu vereinfachen. Und machen Sie sich keine Sorgen mehr über Codegeneratoren. Sie sind gut. Mit ihnen werden Sie produktiver. Sie müssen sich nur von Anfang an ein wenig Zeit für die Einrichtung nehmen – und dann erwarten Sie Jahre gesteigerter Produktivität, die die Geschichte Ihres Projekts ausmachen werden.

Danke mir noch nicht, später.

Klärung

Um es klar auszudrücken: Dieser Artikel befürwortet in keiner Weise, dass Sie das gesamte System (d. h. Domäne, Geschäftslogik usw. usw.) anpassen müssen, um es an Ihr Datenbankmodell anzupassen. Was ich in diesem Artikel sage, ist, dass der Client-Code, der mit der Datenbank interagiert, auf der Grundlage des Datenbankmodells agieren sollte, sodass er selbst das Datenbankmodell nicht in einem „erstklassigen“ Status reproduziert. Diese Logik befindet sich normalerweise auf der Datenzugriffsebene Ihres Clients.

In zweistufigen Architekturen, die mancherorts noch erhalten ist, ist ein solches Systemmodell möglicherweise das einzig mögliche. Allerdings scheint mir in den meisten Systemen die Datenzugriffsschicht ein „Subsystem“ zu sein, das das Datenbankmodell kapselt.

Ausnahmen

Zu jeder Regel gibt es Ausnahmen, und ich habe bereits gesagt, dass der Ansatz, bei dem die Datenbank an erster Stelle steht und der Quellcode generiert wird, manchmal ungeeignet sein kann. Hier sind einige solcher Ausnahmen (es gibt wahrscheinlich noch andere):

  • Wenn das Schema unbekannt ist und entdeckt werden muss. Sie sind beispielsweise Anbieter eines Tools, das Benutzern die Navigation in beliebigen Diagrammen erleichtert. Pfui. Hier findet keine Codegenerierung statt. Dennoch steht die Datenbank an erster Stelle.
  • Wenn ein Schaltkreis im laufenden Betrieb generiert werden muss, um ein Problem zu lösen. Dieses Beispiel scheint eine etwas fantasievolle Version des Musters zu sein Wert des Entitätsattributs, d. h. Sie haben nicht wirklich ein klar definiertes Schema. In diesem Fall können Sie oft nicht einmal sicher sein, ob ein RDBMS zu Ihnen passt.

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 im RDBMS und ist die einzige Quelle der „Wahrheit“, und alle Clients müssen davon abgeleitete Kopien erwerben. Idealerweise müssen Sie einen Codegenerator verwenden.

Source: habr.com

Kommentar hinzufügen