La vérité d'abord, ou pourquoi le système doit être conçu en fonction du périphérique de base de données

Hé Habr !

Nous continuons à explorer le sujet Java и Printempsy compris au niveau de la base de données. Aujourd'hui, nous proposons de lire pourquoi, lors de la conception de grandes applications, c'est la structure de la base de données, et non le code Java, qui devrait être d'une importance décisive, comment cela se fait et quelles sont les exceptions à cette règle.

Dans cet article plutôt tardif, j'expliquerai pourquoi je pense que dans presque tous les cas, le modèle de données d'une application doit être conçu "à partir de la base de données" plutôt que "à partir des capacités de Java" (ou du langage client que vous utilisez). travailler avec). En choisissant la deuxième approche, vous entrez dans un long chemin de douleur et de souffrance une fois que votre projet commence à se développer.

L'article a été écrit sur la base une question, donné sur Stack Overflow.

Discussions intéressantes sur reddit dans les sections /r/java и /r/programmation.

Génération de code

Comme je suis surpris qu'il y ait une si petite couche d'utilisateurs qui, après s'être familiarisés avec jOOQ, n'apprécient pas le fait que jOOQ s'appuie sérieusement sur la génération de code source pour fonctionner. Personne ne vous empêche d'utiliser jOOQ comme bon vous semble, et personne ne vous oblige à utiliser la génération de code. Mais par défaut (comme décrit dans le manuel), jOOQ fonctionne comme ceci : vous commencez avec un schéma de base de données (hérité), vous le rétroconcevez avec le générateur de code jOOQ pour obtenir un ensemble de classes qui représentent vos tables, puis vous écrivez type- requêtes sécurisées sur ces tables :

	for (Record2<String, String> record : DSL.using(configuration)
//   ^^^^^^^^^^^^^^^^^^^^^^^ Информация о типах выведена на 
//   основании сгенерированного кода, на который ссылается приведенное
// ниже условие SELECT 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^ сгенерированные имена
       .from(ACTOR)
       .orderBy(1, 2)) {
    // ...
}

Le code est généré soit manuellement en dehors du build, soit manuellement à chaque build. Par exemple, une telle régénération peut suivre immédiatement après Migration de la base de données Flyway, qui peut également être effectuée manuellement ou automatiquement.

Génération de code source

Il existe diverses philosophies, avantages et inconvénients associés à ces approches de génération de code - manuelles et automatiques - dont je ne vais pas discuter en détail dans cet article. Mais, en général, tout l'intérêt du code généré est qu'il permet de reproduire en Java la « vérité » que nous tenons pour acquise, soit au sein de notre système, soit en dehors de celui-ci. Dans un sens, les compilateurs qui génèrent du bytecode, du code machine ou un autre type de code à partir du code source font la même chose - nous obtenons une représentation de notre "vérité" dans un autre langage, quelles que soient les raisons spécifiques.

Il existe de nombreux générateurs de code de ce type. Par exemple, XJC peut générer du code Java basé sur des fichiers XSD ou WSDL. Le principe est toujours le même :

  • Il existe une part de vérité (interne ou externe) - par exemple, une spécification, un modèle de données, etc.
  • Nous avons besoin d'une représentation locale de cette vérité dans notre langage de programmation.

De plus, il est presque toujours conseillé de générer une telle représentation - afin d'éviter la redondance.

Fournisseurs de types et traitement des annotations

Remarque : Une autre approche plus moderne et plus spécifique de la génération de code pour jOOQ implique l'utilisation de fournisseurs de types, tels qu'ils sont implémentés en F #. Dans ce cas, le code est généré par le compilateur, en fait au stade de la compilation. En principe, un tel code n'existe pas sous forme de codes sources. En Java, il existe des outils similaires, mais pas aussi élégants - ce sont des processeurs d'annotation, par exemple, Lombok.

Dans un certain sens, il se passe ici les mêmes choses que dans le premier cas, sauf que :

  • Vous ne voyez pas le code généré (peut-être que cette situation ne semble pas si répugnante à quelqu'un ?)
  • Vous devez vous assurer que les types peuvent être fournis, c'est-à-dire que "true" doit toujours être disponible. C'est facile dans le cas de Lombok, qui annote "vérité". C'est un peu plus difficile avec les modèles de base de données qui dépendent d'une connexion en direct toujours disponible.

Quel est le problème avec la génération de code ?

En plus de la question délicate de savoir comment il vaut mieux démarrer la génération de code - manuellement ou automatiquement, je dois mentionner qu'il y a des gens qui pensent que la génération de code n'est pas du tout nécessaire. La justification de ce point de vue, que j'ai rencontré le plus souvent, est qu'il est alors difficile de mettre en place le pipeline de build. Oui, c'est vraiment difficile. Il y a des coûts d'infrastructure supplémentaires. Si vous débutez avec un produit particulier (que ce soit jOOQ, ou JAXB, ou Hibernate, etc.), il faut du temps pour mettre en place un atelier que vous aimeriez passer à apprendre l'API elle-même pour en tirer de la valeur. .

Si les coûts associés à la compréhension de l'appareil du générateur sont trop élevés, alors, en effet, l'API a fait un mauvais travail sur la convivialité du générateur de code (et à l'avenir, il s'avère que la personnalisation est également difficile). La convivialité devrait être la priorité absolue pour une telle API. Mais ce n'est qu'un argument contre la génération de code. Sinon, écrivez entièrement à la main la représentation locale de la vérité interne ou externe.

Beaucoup diront qu'ils n'ont pas le temps de faire tout cela. Ils sont à la date limite pour leur super produit. Un jour plus tard, nous peignerons les convoyeurs d'assemblage, nous aurons le temps. Je vais leur répondre :

La vérité d'abord, ou pourquoi le système doit être conçu en fonction du périphérique de base de données
Original, Alan O'Rourke, pile d'audience

Mais dans Hibernate / JPA, il est si facile d'écrire du code "en Java".

Vraiment. Pour Hibernate et ses utilisateurs, c'est à la fois une aubaine et une malédiction. Dans Hibernate, vous pouvez simplement écrire quelques entités, comme ceci :

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

Et presque tout est prêt. Maintenant, le lot d'Hibernate est de générer des "détails" complexes sur la manière exacte dont cette entité sera définie dans le DDL de votre "dialecte" de SQL :

	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);

... et lancer l'exécution de l'application. Une fonctionnalité vraiment cool pour être rapidement opérationnel et essayer différentes choses.

Cependant, laissez-moi. Je mentais.

  • Hibernate appliquera-t-il réellement la définition de cette clé primaire nommée ?
  • Hibernate créera-t-il un index sur TITLE ? Je sais avec certitude que nous en avons besoin.
  • Hibernate fera-t-il de cette clé une clé d'identité dans la spécification d'identité ?

Probablement pas. Si vous développez votre projet à partir de zéro, il est toujours pratique de supprimer simplement l'ancienne base de données et d'en générer une nouvelle dès que vous ajoutez les annotations nécessaires. Ainsi, l'entité Book prendra éventuellement la forme :

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

Cool. Régénérer. Encore une fois, dans ce cas, ce sera très facile au début.

Mais vous devrez payer plus tard.

Tôt ou tard, vous devrez passer à la production. C'est alors que le modèle cesse de fonctionner. Parce que:

En production, il ne sera plus possible, si nécessaire, de jeter l'ancienne base de données et de tout recommencer à zéro. Votre base de données deviendra une base de données héritée.

Désormais et pour toujours tu devras écrire Scripts de migration DDL, par exemple en utilisant Flyway. Et qu'adviendra-t-il de vos entités dans ce cas ? Vous pouvez soit les adapter manuellement (et doubler votre charge de travail), soit demander à Hibernate de les régénérer pour vous (quelle est la probabilité que celui généré de cette manière réponde à vos attentes ?) Vous perdez de toute façon.

Ainsi, dès que vous passerez en production, vous aurez besoin de hot patches. Et ils doivent être mis en production très rapidement. Étant donné que vous n'avez pas préparé et organisé un pipeline fluide de vos migrations pour la production, vous corrigez énormément. Et puis vous n'avez pas le temps de tout faire correctement. Et vous grondez Hibernate, parce que c'est toujours la faute de quelqu'un, mais pas de vous...

Au lieu de cela, dès le début, tout aurait pu être fait complètement différemment. Par exemple, mettez des roues rondes sur un vélo.

Base de données d'abord

La vraie "vérité" dans votre schéma de base de données et la "souveraineté" sur celui-ci se trouvent dans la base de données. Le schéma est défini uniquement dans la base de données elle-même et nulle part ailleurs, et chacun des clients a une copie de ce schéma, il est donc parfaitement logique d'imposer le respect du schéma et de son intégrité, de le faire correctement dans la base de données - où le les informations sont stockées.
C'est une sagesse ancienne, voire éculée. Les clés primaires et uniques sont bonnes. Les clés étrangères sont correctes. La vérification des contraintes est bonne. Déclarations - Bien.

Et ce n'est pas tout. Par exemple, en utilisant Oracle, vous souhaiterez probablement spécifier :

  • Dans quel tablespace se trouve votre table
  • Quelle est sa valeur PCTFREE
  • Quelle est la taille du cache dans votre séquence (derrière l'id)

Tout cela n'a peut-être pas d'importance dans les petits systèmes, mais il n'est pas nécessaire d'attendre la transition vers le domaine du "big data" - vous pouvez commencer à bénéficier des optimisations de stockage fournies par le fournisseur, telles que celles mentionnées ci-dessus, bien plus tôt. Aucun des ORM que j'ai vus (y compris jOOQ) ne donne accès à l'ensemble complet des options DDL que vous pourriez vouloir utiliser dans votre base de données. Les ORM offrent des outils pour vous aider à écrire DDL.

Mais en fin de compte, un schéma bien conçu est écrit à la main en DDL. Tout DDL généré n'en est qu'une approximation.

Qu'en est-il du modèle client ?

Comme mentionné ci-dessus, sur le client, vous aurez besoin d'une copie de votre schéma de base de données, la vue client. Inutile de dire que cette vue client doit être synchronisée avec le modèle réel. Quelle est la meilleure façon d'y parvenir? Avec un générateur de code.

Toutes les bases de données fournissent leurs méta-informations via SQL. Voici comment obtenir toutes les tables dans différents dialectes SQL à partir de votre base de données :

	-- 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

Ces requêtes (ou similaires, selon que vous devez également considérer des vues, des vues matérialisées, des fonctions table) sont également exécutées en appelant DatabaseMetaData.getTables() depuis JDBC, ou en utilisant le méta-module jOOQ.

À partir des résultats de ces requêtes, il est relativement facile de générer une représentation côté client de votre modèle de base de données, quelle que soit la technologie que vous utilisez sur le client.

  • Si vous utilisez JDBC ou Spring, vous pouvez créer un ensemble de constantes de chaîne
  • Si vous utilisez JPA, vous pouvez générer les entités elles-mêmes
  • Si vous utilisez jOOQ, vous pouvez générer un méta modèle jOOQ

Selon la capacité de votre API client (par exemple, jOOQ ou JPA), le méta-modèle généré peut être très riche et complet. Prenons, par exemple, la possibilité de jointures implicites, introduit dans jOOQ 3.11, qui s'appuie sur les méta-informations générées sur les relations de clé étrangère entre vos tables.

Désormais, tout incrément de base de données mettra automatiquement à jour le code client. Imaginez par exemple :

ALTER TABLE book RENAME COLUMN title TO book_title;

Aimeriez-vous vraiment faire ce travail deux fois ? Dans aucun cas. Nous validons simplement le DDL, l'exécutons via votre pipeline de build et obtenons l'entité mise à jour :

@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;
}

Ou la classe jOOQ mise à jour. La plupart des modifications DDL affectent également la sémantique, pas seulement la syntaxe. Par conséquent, il peut être pratique de voir dans le code compilé quel code sera (ou pourrait être) affecté par l'incrémentation de votre base de données.

La seule vérité

Quelle que soit la technologie que vous utilisez, il y a toujours un modèle qui est la seule source de vérité pour un sous-système - ou du moins nous devrions nous efforcer d'y parvenir et d'éviter la confusion de l'entreprise où la "vérité" est partout et nulle part à la fois. Tout peut être beaucoup plus facile. Si vous échangez simplement des fichiers XML avec un autre système, utilisez simplement XSD. Jetez un œil au méta-modèle INFORMATION_SCHEMA de jOOQ sous forme XML :
https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd

  • XSD est bien compris
  • XSD balise très bien le contenu XML et permet la validation dans toutes les langues du client
  • XSD est bien versionné et hautement rétrocompatible
  • XSD peut être traduit en code Java à l'aide de XJC

Le dernier point est important. Lorsque nous communiquons avec un système externe à l'aide de messages XML, nous voulons nous assurer que nos messages sont valides. Ceci est très facile à réaliser avec JAXB, XJC et XSD. Ce serait de la pure folie de penser que, dans une approche de conception basée sur Java où nous transformons nos messages en objets Java, ils pourraient d'une manière ou d'une autre être rendus intelligiblement en XML et envoyés pour être consommés par un autre système. Le XML ainsi généré serait de très mauvaise qualité, non documenté et difficile à développer. S'il y avait un accord sur le niveau de qualité de service (SLA) sur une telle interface, on la bousillerait immédiatement.

Pour être honnête, c'est exactement ce qui se passe tout le temps avec l'API JSON, mais c'est une autre histoire, j'argumenterai la prochaine fois...

Bases de données : ce sont les mêmes

En travaillant avec des bases de données, vous comprenez qu'elles sont toutes fondamentalement les mêmes. La base de données est propriétaire de ses données et doit gérer le schéma. Toute modification apportée au schéma doit être implémentée directement dans DDL afin que la source unique de vérité soit mise à jour.

Une fois la mise à jour de la source effectuée, tous les clients doivent également mettre à jour leurs copies du modèle. Certains clients peuvent être écrits en Java en utilisant jOOQ et Hibernate ou JDBC (ou les deux). D'autres clients peuvent être écrits en Perl (souhaitons-leur bonne chance), d'autres en C#. Cela n'a pas d'importance. Le modèle principal est dans la base de données. Les modèles générés par ORM sont généralement de mauvaise qualité, mal documentés et difficiles à développer.

Alors ne faites pas d'erreurs. Ne faites pas d'erreurs dès le départ. Travailler à partir d'une base de données. Créez un pipeline de déploiement qui peut être automatisé. Activez les générateurs de code pour copier facilement votre modèle de base de données et le vider sur les clients. Et arrêtez de vous soucier des générateurs de code. Ils sont bons. Avec eux, vous deviendrez plus productif. Tout ce que vous avez à faire est de passer un peu de temps à les configurer dès le début, et vous aurez des années de performances améliorées pour construire l'histoire de votre projet.

Ne me remercie pas encore, plus tard.

Clarification

Pour être clair : cet article ne préconise en aucun cas que l'ensemble du système (c'est-à-dire le domaine, la logique métier, etc., etc.) doive être adapté pour s'adapter à votre modèle de base de données. Ce dont je parle dans cet article, c'est que le code client qui interagit avec une base de données doit agir sur la base du modèle de base de données afin qu'il ne reproduise pas le modèle de base de données dans un état "de première classe". Une telle logique est généralement située au niveau de la couche d'accès aux données sur votre client.

Dans les architectures à deux niveaux, qui sont encore conservées à certains endroits, un tel modèle de système peut être le seul possible. Cependant, dans la plupart des systèmes, la couche d'accès aux données me semble être un "sous-système" qui encapsule le modèle de base de données.

exceptions

Il y a des exceptions à chaque règle, et j'ai déjà dit que l'approche basée sur la base de données et la génération de code source peuvent parfois être inappropriées. Voici quelques-unes de ces exceptions (il y en a probablement d'autres) :

  • Lorsque le schéma est inconnu et doit être ouvert. Par exemple, vous fournissez un outil pour aider les utilisateurs à naviguer dans n'importe quel diagramme. Phew. Il n'y a pas de génération de code ici. Mais encore - la base de données tout d'abord.
  • Lorsqu'un circuit doit être généré à la volée pour résoudre un problème. Cet exemple semble être une version légèrement froncée du motif valeur d'attribut d'entité, c'est-à-dire que vous n'avez pas vraiment de schéma bien défini. Dans ce cas, vous ne pouvez souvent même pas être sûr du tout qu'un SGBDR vous conviendra.

Les exceptions sont par nature exceptionnelles. Dans la plupart des cas impliquant l'utilisation de RDBMS, le schéma est connu à l'avance, il est à l'intérieur du RDBMS et est la seule source de "vérité", et tous les clients doivent acquérir des copies dérivées de celui-ci. Idéalement, cela devrait impliquer un générateur de code.

Source: habr.com

Ajouter un commentaire