Dans cet article très récent, j'expliquerai pourquoi, à mon avis, dans la plupart des cas, lors du développement d'un modèle de données pour une application, vous devez suivre la première approche de la base de données. Au lieu de l'approche «Java [tout autre langage] d'abord», qui vous emmène sur une longue piste pleine de douleur et de souffrance, dès que le projet commence à grandir.

"Trop occupé pour être meilleur" sous licence CC par Alan O'Rourke / Audience Stack . Image originale
Cet article est inspiré d'une récente question sur StackOverflow .
Discussions reddit intéressantes / r / java et / r / programmation .
Génération de code
À ma grande surprise, un petit groupe d'utilisateurs semble avoir été choqué par le fait que jOOQ est fortement lié à la génération de code source.
Bien que vous puissiez utiliser jOOQ exactement comme vous le souhaitez, la manière préférée (selon la documentation) est de commencer avec le schéma de base de données existant, puis de générer les classes clientes nécessaires (correspondant à vos tables) à l'aide de jOOQ, et après cela, il est facile d'écrire en toute sécurité requêtes pour ces tables:
for (Record2<String, String> record : DSL.using(configuration)
Le code peut être généré soit manuellement en dehors de l'assembly, soit automatiquement avec chaque assembly. Par exemple, une telle génération peut se produire immédiatement après l'installation des migrations Flyway , qui peuvent également être démarrées manuellement ou automatiquement.
Génération de code source
Il existe différentes philosophies, avantages et inconvénients concernant ces approches de génération de code que je ne veux pas aborder dans cet article. Mais en substance, la signification du code généré est qu'il s'agit d'une représentation Java de ce que nous considérons comme une sorte de "standard" (à l'intérieur et à l'extérieur de notre système). D'une certaine manière, les compilateurs font la même chose lorsqu'ils génèrent du bytecode, du code machine ou un autre code source à partir de la source - en conséquence, nous avons une idée de notre «standard» dans un autre langage spécifique.
Il existe de nombreux générateurs de code. Par exemple, XJC peut générer du code Java à partir de fichiers XSD ou WSDL . Le principe est toujours le même:
- Il existe des normes (externes ou internes), telles que les spécifications, le modèle de données, etc.
- Il est nécessaire de se faire une idée de cette norme dans notre langage de programmation habituel.
Et presque toujours, il est logique de générer cette vue afin d'éviter un travail inutile et des erreurs inutiles.
Fournisseurs de type et traitement des annotations
Il est à noter qu'une autre approche, plus moderne, de la génération de code dans jOOQ est les fournisseurs de type ( comme cela se fait en F # ), où le code est généré par le compilateur pendant la compilation et n'existe jamais sous sa forme originale. Un outil similaire (mais moins sophistiqué) en Java est les processeurs d'annotation tels que Lombok .
Dans les deux cas, tout est identique à la génération de code normale, sauf:
- Vous ne voyez pas le code généré (peut-être pour beaucoup, c'est un gros plus?)
- Vous devez vous assurer que votre «référence» est disponible à chaque compilation. Cela ne pose aucun problème dans le cas de Lombok, qui annote directement le code source lui-même, qui est le "standard" dans ce cas. Un peu plus compliqué avec les modèles de base de données qui reposent sur une connexion en direct toujours active.
Quel est le problème avec la génération de code?
Outre la question délicate de savoir s'il faut générer le code manuellement ou automatiquement, certaines personnes pensent que le code n'a pas du tout besoin d'être généré. La raison pour laquelle j'entends le plus souvent est qu'une telle génération est difficile à implémenter dans le pipeline CI / CD. Et oui, c'est vrai, car nous obtenons des frais généraux pour créer et prendre en charge une infrastructure supplémentaire, surtout si vous êtes nouveau dans les outils utilisés (jOOQ, JAXB, Hibernate, etc.).
Si les frais généraux liés à l'étude du générateur de code sont trop élevés, il y aura vraiment peu d'avantages. Mais c'est le seul argument contre. Dans la plupart des autres cas, cela n'a absolument aucun sens d'écrire manuellement du code, qui est la représentation habituelle d'un modèle de quelque chose.
Beaucoup de gens affirment qu’ils n’ont pas le temps pour cela, car en ce moment, vous devez déployer un autre MVP dès que possible. Et ils pourront finaliser leur pipeline CI / CD un peu plus tard. Dans de tels cas, je dis généralement: "Vous êtes trop occupé pour aller mieux".
"Mais Hibernate / JPA facilite beaucoup le premier développement Java."
Oui c'est vrai. C'est à la fois une joie et une douleur pour les utilisateurs d'Hibernate. Avec lui, vous pouvez simplement écrire plusieurs objets du formulaire:
@Entity class Book { @Id int id; String title; }
Et c'est presque fini. Ensuite, Hibernate se chargera de toute la routine sur la façon de définir cet objet en DDL et dans le dialecte SQL souhaité:
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);
C'est vraiment un excellent moyen de démarrer rapidement le développement - il vous suffit de démarrer l'application.
Mais tout n'est pas si rose. Il y a encore beaucoup de questions:
- Hibernate générera-t-il le nom dont j'ai besoin pour la clé primaire?
- Vais-je créer l'index dont j'ai besoin dans le champ TITLE?
- Une valeur d'ID unique sera-t-elle générée pour chaque enregistrement?
Il semble que non. Mais pendant que le projet est en cours de développement, vous pouvez toujours jeter votre base de données actuelle et tout générer à partir de zéro en ajoutant les annotations nécessaires au modèle.
Ainsi, la classe de livre dans sa forme finale ressemblera Ă ceci:
@Entity @Table(name = "book", indexes = { @Index(name = "i_book_title", columnList = "title") }) class Book { @Id @GeneratedValue(strategy = IDENTITY) int id; String title; }
Mais tu paieras pour ça, un peu plus tard
Tôt ou tard, votre application entre en production et le schéma décrit cessera de fonctionner:
Dans un système vivant et réel, vous ne pouvez plus simplement récupérer et déposer votre base de données, car les données y sont utilisées et peuvent coûter beaucoup d'argent.
À partir de maintenant, vous devez écrire des scripts de migration pour chaque changement dans le modèle de données, par exemple, en utilisant Flyway . Mais qu'advient-il de vos classes clientes? Vous pouvez soit les adapter manuellement (ce qui entraînera un double travail), soit demander à Hibernate de les générer (mais quelle est la probabilité que les résultats d'une telle génération répondent aux attentes?). En conséquence, vous pouvez vous attendre à de gros problèmes.
Dès que le code entre en production, il est presque immédiatement nécessaire d'apporter des corrections, et le plus rapidement possible.
Et parce que l'installation des migrations de base de données n'est pas intégrée à votre chaîne de montage; vous devrez installer ces correctifs manuellement à vos risques et périls. Il n'y aura pas assez de temps pour revenir en arrière et tout faire correctement. Il suffit de blâmer Hibernate pour tous ses ennuis.
Au lieu de cela, vous auriez pu agir différemment dès le départ. À savoir, utilisez des roues rondes au lieu de celles carrées.
Allez d'abord à la base de données
La référence et le contrôle du schéma de données se trouvent dans le bureau de votre SGBD. Une base de données est le seul endroit où un schéma est défini, et tous les clients ont une copie de ce schéma, mais pas l'inverse. Les données sont dans votre base de données, et non dans votre client, il est donc logique de fournir un contrôle du schéma et de son intégrité exactement où les données se trouvent.
C'est de la vieille sagesse, rien de nouveau. Les clés primaires et uniques sont bonnes. Les clés étrangères sont belles. La vérification des contraintes côté base de données est formidable. Les assertions (quand elles sont finalement implémentées) sont géniales.
Et ce n'est pas tout. Par exemple, si vous utilisez Oracle, vous pouvez spécifier:
- Dans quel espace de table se trouve votre table?
- Quelle est la signification de PCTFREE qu'elle a
- Quelle est la taille du cache de séquence?
Peut-être que tout cela n'a pas d'importance sur les petits systèmes, mais sur les systèmes plus grands, vous n'avez pas à suivre le chemin du «big data» jusqu'à ce que vous extrayiez tous les jus de votre stockage actuel. Pas un seul ORM que j'ai jamais vu (y compris jOOQ) ne vous permettra d'utiliser l'ensemble complet des paramètres DDL fournis par votre SGBD. Les ORM ne proposent que quelques outils pour vous aider à écrire DDL.
En fin de compte, un schéma bien conçu ne doit être écrit que manuellement à l'aide d'un DDL spécifique au SGBD. Tous les DDL générés automatiquement ne sont qu'une approximation de cela.
Et le modèle client?
Comme mentionné précédemment, vous aurez besoin d'une certaine représentation du schéma de base de données côté client. Il va sans dire que cette vue doit être synchronisée avec le modèle réel. Comment faire Bien sûr, en utilisant des générateurs de code.
Toutes les bases de données donnent accès à leurs méta-informations via le bon vieux SQL. Ainsi, par exemple, vous pouvez obtenir une liste de toutes les tables de différentes bases de données:
Ce sont ces requêtes (ainsi que les requêtes similaires pour les vues, les vues matérialisées et les fonctions de table) qui sont exécutées lorsque la méthode DatabaseMetaData.getTables () d'un pilote JDBC spécifique est appelée, ou dans le module jOOQ-meta.
À partir des résultats de ces requêtes, il est relativement facile de créer une représentation client du modèle de base de données, quelle que soit la technologie d'accès aux données utilisée.
- Si vous utilisez JDBC ou Spring, vous pouvez créer un groupe de constantes String
- Si vous utilisez JPA, vous pouvez créer des objets vous-même
- Si vous utilisez jOOQ, vous pouvez créer des métamodèles jOOQ
Selon le nombre de fonctionnalités offertes par votre API d'accès aux données (jOOQ, JPA ou autre), le métamodèle généré peut être vraiment riche et complet. Par exemple, la fonction de jointure implicite dans jOOQ 3.11, qui repose sur des méta-informations sur les relations des clés étrangères entre vos tables .
Désormais, toute modification du schéma de base de données entraînera automatiquement une mise à jour du code client.
Imaginez que vous deviez renommer une colonne dans un tableau:
ALTER TABLE book RENAME COLUMN title TO book_title;
Voulez-vous vraiment effectuer ce travail deux fois? Pas question. Validez simplement ce DDL, exécutez la génération et profitez de l'objet mis à jour:
@Entity @Table(name = "book", indexes = {
De plus, le client reçu n'a pas besoin d'être compilé à chaque fois (au moins jusqu'à la prochaine modification du schéma de base de données), ce qui peut déjà être un gros plus!
La plupart des modifications DDL sont également des modifications sémantiques, pas seulement syntaxiques. Ainsi, il est formidable de voir dans le code client généré quels sont exactement les derniers changements dans la base de données affectés.
La vérité est toujours seule
Quelle que soit la technologie que vous utilisez, il ne doit y avoir qu'un seul modèle, qui est la norme pour le sous-système. Ou, du moins, nous devons nous efforcer de le faire et éviter la confusion dans les affaires, où la «norme» est partout et nulle part en même temps. Cela rend tout beaucoup plus facile. Par exemple, si vous partagez des fichiers XML avec un autre système, vous utilisez probablement XSD. En tant que métamodèle INFORMATION_SCHEMA jOOQ au format XML: https://www.jooq.org/xsd/jooq-meta-3.10.0.xsd
- XSD est bien compris
- XSD décrit parfaitement le contenu XML et permet la validation dans toutes les langues du client
- XSD rend la gestion des versions facile et rétrocompatible
- XSD peut être transformé en code Java à l'aide de XJC
Nous accordons une attention particulière au dernier point. Lors de la communication avec un système externe via des messages XML, nous devons être sûrs de la validité des messages. Et c'est vraiment très facile à faire avec des choses comme JAXB, XJC et XSD. Il serait fou de penser à la pertinence de l'approche Java-first dans ce cas. Le XML généré à partir des objets XML sera de mauvaise qualité, mal documenté et difficile à étendre. Et s'il existe un SLA pour une telle interaction, vous serez déçu.
Honnêtement, cela ressemble à ce qui se passe avec les différentes API JSON maintenant, mais c'est une histoire complètement différente ...
Qu'est-ce qui aggrave les bases de données?
Lorsque vous travaillez avec une base de données, tout est identique ici. La base de données est propriétaire des données et doit également être le maître du schéma de données. Toutes les modifications de schéma doivent être effectuées directement via DDL pour mettre à jour la référence.
Après avoir mis à jour la référence, tous les clients doivent mettre à jour leurs idées sur le modèle. Certains clients peuvent être écrits en Java à l'aide de jOOQ et / ou Hibernate ou JDBC. D'autres clients peuvent être écrits en Perl (bonne chance à eux) ou même en C #. Ce n'est pas grave. Le modèle principal est dans la base de données. Bien que les modèles créés à l'aide d'ORM soient de mauvaise qualité, peu documentés et difficiles à étendre.
Par conséquent, ne faites pas cela, et depuis le tout début du développement. Commencez plutôt avec une base de données. Créez un pipeline CI / CD automatisé. Utilisez la génération de code pour générer automatiquement un modèle de base de données pour les clients pour chaque génération. Et arrêtez de vous inquiéter, tout ira bien. Tout ce qui est requis est un petit effort initial pour mettre en place l'infrastructure, mais vous obtiendrez ainsi un gain dans le processus de développement pour le reste de votre projet pour les années à venir.
Non merci.
Explications
Pour consolider: cet article ne prétend en aucun cas que le modèle de base de données doit s'appliquer à l'ensemble de votre système (domaine, logique métier, etc.). Mes déclarations consistent uniquement dans le fait que le code client interagissant avec la base de données ne doit être qu'une représentation du schéma de la base de données, mais pas le définir et le former de quelque manière que ce soit.
Dans les architectures à deux niveaux qui ont encore leur place, le schéma de base de données peut être la seule source d'informations sur le modèle de votre système. Cependant, sur la plupart des systèmes, je vois le niveau d'accès aux données comme un «sous-système» qui encapsule un modèle de base de données. Quelque chose comme ça.
Exceptions
Comme dans toute autre bonne règle, la nôtre a également ses exceptions (et j'ai déjà averti que la première approche de la base de données et la génération de code ne sont pas toujours le bon choix). Ces exceptions (la liste n'est peut-être pas complète):
- Lorsque le circuit n'est pas connu à l'avance et doit être étudié. Par exemple, vous êtes un fournisseur d'un outil pour aider les utilisateurs à naviguer dans n'importe quel schéma. Bien sûr, il ne peut y avoir de génération de code. Mais dans tous les cas, vous devez gérer la base de données elle-même et son schéma.
- Lorsque pour une tâche, vous devez créer un schéma à la volée. Cela peut être similaire à l'une des variantes du modèle Entité-attribut-valeur , comme vous n'avez pas de modèle clairement défini. De plus, rien ne garantit que le SGBDR dans ce cas est le bon choix.
La particularité de ces exceptions est qu'elles se rencontrent rarement dans la faune. Dans la plupart des cas, lors de l'utilisation de bases de données relationnelles, le schéma est connu à l'avance et constitue la «norme» de votre modèle, et les clients doivent travailler avec une copie de ce modèle générée à l'aide de générateurs de code.