Salut, Habr !
Nous continuons à explorer le sujet et , y compris au niveau des bases de données. Aujourd'hui, nous vous proposons de lire pourquoi, lors de la conception de grandes applications, la structure de la base de données, et non le code Java, devrait avoir une importance primordiale, comment cela se fait, et quelles sont les exceptions à cette rÚgle.
Dans cet article quelque peu tardif, j'expliquerai pourquoi je crois que dans presque tous les cas, le modĂšle de donnĂ©es dans une application doit ĂȘtre conçu « en fonction de la base de donnĂ©es », et non « en fonction des capacitĂ©s de Java » (ou d'un autre langage client avec lequel vous travaillez). En choisissant la deuxiĂšme approche, vous vous engagez sur un long chemin de douleur et de souffrance dĂšs que votre projet commence Ă croĂźtre.
Cet article est inspiré par , posée sur Stack Overflow.
Des discussions intéressantes sur reddit dans les sections et .
Génération de code
Je suis tellement surpris qu'il existe une si petite couche d'utilisateurs qui, aprĂšs avoir rencontrĂ© jOOQ, s'offusquent du fait qu'avec jOOQ, on s'appuie sĂ©rieusement sur la gĂ©nĂ©ration de code. Personne ne vous empĂȘche d'utiliser jOOQ comme vous le souhaitez et ne vous force Ă utiliser la gĂ©nĂ©ration de code. Mais par dĂ©faut (comme dĂ©crit dans le manuel), travailler avec jOOQ se dĂ©roule ainsi : vous partez d'un schĂ©ma de base de donnĂ©es (hĂ©ritĂ©), vous effectuez une rĂ©tro-conception Ă l'aide du gĂ©nĂ©rateur de code jOOQ pour obtenir un ensemble de classes reprĂ©sentant vos tables, puis vous Ă©crivez des requĂȘtes type-safes Ă ces tables :
for (Record2 record : DSL.using(configuration)
// ^^^^^^^^^^^^^^^^^^^^^^^ Informations sur les types déduites de
// code généré auquel se réfÚre la condition SELECT ci-dessous
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
// vvvvv ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^ noms générés
.from(ACTOR)
.orderBy(1, 2)) {
// ...
}Le code est généré soit manuellement en dehors de la construction, soit manuellement à chaque construction. Par exemple, une telle régénération peut se produire juste aprÚs .
Génération de code source
Avec ces approches de génération de code - manuelles et automatiques - sont associées différentes philosophies, avantages et inconvénients, que je ne vais pas détailler dans cet article. Mais, en général, toute l'essence du code généré réside dans le fait qu'il permet de reproduire en Java cette "vérité" que nous considérons comme acquise, soit dans le cadre de notre systÚme, soit en dehors de celui-ci. D'une certaine maniÚre, c'est ce que font les compilateurs, générant du bytecode, du code machine ou tout autre type de code à partir des sources - nous obtenons une représentation de notre "vérité" dans un autre langage, indépendamment des raisons spécifiques.
Il existe de nombreux gĂ©nĂ©rateurs de code. Par exemple, Le principe est toujours le mĂȘme :
- Il existe une certaine 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.
Il est presque toujours judicieux de générer une telle représentation - pour éviter la redondance.
Providers de types et traitement des annotations
à noter : une autre approche, plus moderne et spécifique, de génération de code pour jOOQ implique l'utilisation de providers de types, Dans ce cas, le code est généré par le compilateur, précisément au stade de la compilation. Sous forme de sources, ce code n'existe fondamentalement pas. En Java, il existe des outils similaires, bien que moins élégants - ce sont les processeurs d'annotations, par exemple, .
Dans un certain sens, les mĂȘmes choses se produisent ici que dans le premier cas, Ă l'exception de :
- Vous ne voyez pas le code gĂ©nĂ©rĂ© (peut-ĂȘtre que cette situation ne semble pas si repoussante Ă quelqu'un ?)
- Vous devez garantir que les types peuvent ĂȘtre fournis, c'est-Ă -dire que la "vĂ©ritĂ©" doit toujours ĂȘtre accessible. Cela est facile dans le cas de Lombok, qui annotent la "vĂ©ritĂ©". Cela devient un peu plus compliquĂ© avec des modĂšles de bases de donnĂ©es, dont le fonctionnement dĂ©pend d'une connexion active constamment disponible.
Quel est le problÚme avec la génération de code ?
En plus de la question dĂ©licate de savoir comment il est prĂ©fĂ©rable de lancer la gĂ©nĂ©ration de code â manuellement ou automatiquement, il convient Ă©galement de mentionner qu'il y a des personnes qui pensent que la gĂ©nĂ©ration de code n'est tout simplement pas nĂ©cessaire. Le raisonnement derriĂšre ce point de vue, que j'ai rencontrĂ© le plus souvent, est que cela rend difficile la configuration du pipeline de construction. Oui, c'est effectivement compliquĂ©. Cela entraĂźne des coĂ»ts d'infrastructure supplĂ©mentaires. Si vous dĂ©butez avec un produit particulier (qu'il s'agisse de jOOQ, JAXB, Hibernate, etc.), le temps consacrĂ© Ă la configuration de l'environnement de travail est du temps que vous prĂ©fĂ©reriez passer Ă Ă©tudier l'API elle-mĂȘme afin d'en tirer de la valeur par la suite.
Si les coĂ»ts liĂ©s Ă la comprĂ©hension du fonctionnement du gĂ©nĂ©rateur sont trop Ă©levĂ©s, alors effectivement, cela signifie que l'API n'a pas suffisamment travaillĂ© sur l'ergonomie du gĂ©nĂ©rateur de code (et il s'avĂšre par la suite que la personnalisation utilisateur y est Ă©galement complexe). La facilitĂ© d'utilisation doit ĂȘtre la prioritĂ© absolue pour toute API de ce type. Mais c'est juste un argument contre la gĂ©nĂ©ration de code. Par ailleurs, il est totalement possible d'Ă©crire manuellement la reprĂ©sentation locale de la vĂ©ritĂ© interne ou externe.
Beaucoup diront qu'ils n'ont pas le temps de s'occuper de tout cela. Ils ont des délais serrés pour leur Super Produit. On s'occupera des pipelines de construction plus tard, il y aura le temps. Je leur réponds :

,
Mais avec Hibernate / JPA, il est si facile d'écrire du code « sous Java ».
Effectivement. Pour Hibernate et ses utilisateurs, c'est à la fois une bénédiction et une malédiction. Avec Hibernate, on peut simplement écrire quelques entités comme ceci :
@Entity
class Book {
@Id
int id;
String title;
}Et presque tout est prĂȘt. Maintenant, la tĂąche d'Hibernate est de gĂ©nĂ©rer les dĂ©tails complexes de la maniĂšre dont cette entitĂ© sera dĂ©finie dans le DDL de votre dialecte 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 nous commençons à faire fonctionner l'application. C'est vraiment une fonctionnalité incroyable pour commencer rapidement à travailler et à essayer différentes choses.
Cependant, permettez-moi. J'ai exagéré.
- Est-ce qu'Hibernate appliquera vraiment la définition de cette clé primaire nommée ?
- Est-ce qu'Hibernate va crĂ©er un index dans TITLE ? â je sais pertinemment qu'il nous sera nĂ©cessaire.
- Est-ce qu'Hibernate fera vraiment de cette clé une clé identifiant dans la spécification d'identité ?
Probablement pas. Si vous développez votre projet à partir de zéro, il est toujours pratique de simplement abandonner l'ancienne base de données et de générer une nouvelle dÚs que vous ajoutez les annotations nécessaires. Ainsi, l'entité Book finira par avoir la forme suivante :
@Entity
@Table(name = "book", indexes = {
@Index(name = "i_book_title", columnList = "title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
String title;
}
Génial. Générer à nouveau. Encore une fois, dans ce cas, cela sera trÚs facile au départ.
Mais par la suite, cela aura un coût.
TĂŽt ou tard, il faudra passer en production. C'est Ă ce moment-lĂ que ce modĂšle cessera de fonctionner. Parce que :
En production, vous ne pourrez plus abandonner l'ancienne base de données et tout recommencer à zéro si nécessaire. Votre base de données sera considérée comme héritée.
à partir de maintenant et pour toujours, vous devrez écrire . Que se passera-t-il alors avec vos entités ? Vous pourrez soit les adapter manuellement (et ainsi doubler votre volume de travail), soit demander à Hibernate de les générer à nouveau pour vous (quelles sont les chances que ce soit conforme à vos attentes ?). Dans tous les cas, vous perdez.
Ainsi, dĂšs que vous serez en production, vous aurez besoin de patches Ă chaud. Et il faut dĂ©ployer ceux-ci en production trĂšs rapidement. Ătant donnĂ© que vous ne vous ĂȘtes pas prĂ©parĂ© et n'avez pas organisĂ© un flux de migration fluide pour la production, vous appliquerez tous des patchs qui ne seront pas bien faits. Ensuite, vous n'aurez pas le temps de tout faire correctement. Et vous blĂąmerez Hibernate, parce que quelqu'un d'autre est toujours responsable, sauf vousâŠ
Au lieu de cela, tout aurait pu ĂȘtre fait complĂštement diffĂ©remment dĂšs le dĂ©but. Par exemple, mettre des roues rondes sur le vĂ©lo.
D'abord la base de données.
La vĂ©ritable "vĂ©ritĂ©" dans le schĂ©ma de votre base de donnĂ©es et la "souverainetĂ©" sur celui-ci se trouvent dans la base de donnĂ©es elle-mĂȘme. Le schĂ©ma n'est dĂ©fini que dans la base de donnĂ©es elle-mĂȘme et nulle part ailleurs, et chaque client a une copie de ce schĂ©ma, il est donc tout Ă fait logique d'imposer le respect du schĂ©ma et de son intĂ©gritĂ©, en le faisant directement dans la base de donnĂ©es - lĂ oĂč les informations sont stockĂ©es.
C'est une vieille sagesse mĂȘme usĂ©e. Les clĂ©s primaires et uniques - c'est bien. Les clĂ©s Ă©trangĂšres - c'est bien. La vĂ©rification des contraintes - c'est bien. â c'est bien.
Cependant, ce n'est pas tout. Par exemple, en utilisant Oracle, vous voudrez probablement spécifier :
- Dans quel espace de table se trouve votre table
- Quelle est sa valeur PCTFREE
- Quelle est la taille du cache dans votre séquence (derriÚre l'identifiant)
Il se peut que tout cela ne soit pas important dans les petites systĂšmes, mais il n'est pas nĂ©cessaire d'attendre de passer au domaine du « big data » â il est possible de commencer Ă tirer parti des optimisations de stockage des donnĂ©es fournies par le fournisseur beaucoup plus tĂŽt, comme celles mentionnĂ©es ci-dessus. Aucune des ORM que j'ai pu voir (y compris jOOQ) ne donne accĂšs Ă l'ensemble des options DDL que vous souhaiterez peut-ĂȘtre utiliser dans votre base de donnĂ©es. Les ORM offrent des outils qui aident Ă rĂ©diger du DDL.
Mais au final, un schéma bien conçu est écrit manuellement en DDL. Tout DDL généré n'est qu'une approximation de celui-ci.
Qu'en est-il du modĂšle client ?
Comme mentionnĂ© prĂ©cĂ©demment, du cĂŽtĂ© client, vous aurez besoin d'une copie du schĂ©ma de votre base de donnĂ©es, une vue client. Il est superflu de mentionner que cette vue client doit ĂȘtre synchronisĂ©e avec le modĂšle rĂ©el. Comment y parvenir au mieux ? Avec un gĂ©nĂ©rateur de code.
Toutes les bases de données fournissent leurs métadonnées via SQL. Voici comment obtenir toutes les tables de votre base de données sur différents dialectes SQL :
-- 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 qu'il faille aussi considĂ©rer les vues, les vues matĂ©rialisĂ©es, ou les fonctions Ă valeur tabulaire) sont Ă©galement exĂ©cutĂ©es via l'appel de JDBC, ou via le module mĂ©ta jOOQ.
Ă partir des rĂ©sultats de telles requĂȘtes, il est relativement facile de gĂ©nĂ©rer n'importe quelle vue client du modĂšle de votre base de donnĂ©es, peu importe la technologie que vous utilisez cĂŽtĂ© 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 vous-mĂȘme les entitĂ©s
- Si vous utilisez jOOQ, vous pouvez générer la méta-modÚle de jOOQ
Selon le volume de fonctionnalitĂ©s proposĂ© par votre API client (par exemple, jOOQ ou JPA), le mĂ©ta-modĂšle gĂ©nĂ©rĂ© peut ĂȘtre vĂ©ritablement riche et complet. Prenons, par exemple, la possibilitĂ© de jointures implicites, , qui repose sur les mĂ©tadonnĂ©es gĂ©nĂ©rĂ©es concernant les relations de clĂ©s Ă©trangĂšres entre vos tables.
Désormais, toute augmentation de la base de données entraßnera automatiquement la mise à jour du code client. Imaginez par exemple :
ALTER TABLE book RENAME COLUMN title TO book_title;Voudriez-vous vraiment faire ce travail deux fois ? Absolument pas. Il suffit de fixer le DDL, de le faire passer par votre pipeline de construction et d'obtenir l'entité mise à jour :
@Entity
@Table(name = "book", indexes = {
// Y avez-vous déjà pensé ?
@Index(name = "i_book_title", columnList = "book_title")
})
class Book {
@Id
@GeneratedValue(strategy = IDENTITY)
int id;
@Column("book_title")
String bookTitle;
}Ou bien la classe jOOQ mise Ă jour. La plupart des modifications DDL se reflĂštent Ă©galement sur la sĂ©mantique et pas seulement sur la syntaxe. Il est donc pratique de consulter le code compilĂ© pour voir quel code sera (ou pourrait ĂȘtre) impactĂ© par l'augmentation de votre base de donnĂ©es.
La seule vérité
Qu'importe la technologie que vous utilisez, il existe toujours un modĂšle qui constitue la seule source de vĂ©ritĂ© pour un certain sous-systĂšme â ou, au minimum, nous devons viser cela et Ă©viter cette confusion d'entreprise oĂč "la vĂ©ritĂ©" est Ă la fois partout et nulle part. Tout pourrait ĂȘtre beaucoup plus simple. Si vous ne faites qu'Ă©changer des fichiers XML avec un autre systĂšme, utilisez simplement des XSD. Regardez le mĂ©ta-modĂšle INFORMATION_SCHEMA de jOOQ en forme XML :
- L'XSD est bien comprise
- L'XSD marque trĂšs bien le contenu XML et permet de valider dans tous les langages clients
- L'XSD est bien versionnée et possÚde une rétrocompatibilité avancée
- L'XSD peut ĂȘtre transformĂ©e en code Java Ă l'aide de XJC
Le dernier point est important. Lors de la communication avec un systĂšme externe Ă l'aide de messages XML, nous voulons ĂȘtre sĂ»rs de la validitĂ© de nos messages. Cela peut ĂȘtre facilement rĂ©alisĂ© grĂące Ă JAXB, XJC et XSD. Ce serait complĂštement insensĂ© de penser qu'avec une approche de conception « Java d'abord », oĂč nous faisons nos messages sous forme d'objets Java, nous pourrions les mapper de maniĂšre claire sur XML et les envoyer pour consommation dans un autre systĂšme. XML gĂ©nĂ©rĂ© de cette maniĂšre serait de trĂšs mauvaise qualitĂ©, non documentĂ©, et difficile Ă faire Ă©voluer. Si un accord sur le niveau de service (SLA) existait pour cette interface, nous l'aurions immĂ©diatement compromis.
HonnĂȘtement, c'est exactement ce qui se passe constamment avec les API au format JSON, mais c'est une autre histoire, la prochaine fois je me fĂącheraiâŠ
Bases de donnĂ©es : c'est la mĂȘme chose
En travaillant avec des bases de donnĂ©es, vous comprenez qu'elles sont toutes fondamentalement similaires. Une base possĂšde ses donnĂ©es et doit gĂ©rer le schĂ©ma. Toute modification apportĂ©e au schĂ©ma doit ĂȘtre mise en Ćuvre directement sur le DDL, afin de mettre Ă jour la source unique de vĂ©ritĂ©.
Lorsque la mise Ă jour de la source a eu lieu, 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 tous en mĂȘme temps). D'autres clients peuvent ĂȘtre Ă©crits en Perl (je leur souhaite bonne chance), et d'autres encore en C#. Peu importe. Le modĂšle principal se trouve dans la base de donnĂ©es. Les modĂšles gĂ©nĂ©rĂ©s Ă l'aide de l'ORM sont gĂ©nĂ©ralement de mauvaise qualitĂ©, mal documentĂ©s, et difficiles Ă faire Ă©voluer.
Alors ne faites pas d'erreurs. Ne faites pas d'erreurs dĂšs le dĂ©part. Travaillez en vous basant sur la base de donnĂ©es. Construisez un pipeline de dĂ©ploiement qui peut ĂȘtre automatisĂ©. Incluez des gĂ©nĂ©rateurs de code pour faciliter la copie de votre modĂšle de base de donnĂ©es et le dĂ©ploiement sur les clients. Et arrĂȘtez de vous soucier des gĂ©nĂ©rateurs de code. Ils sont bons. Avec eux, vous serez plus productif. Il suffit de consacrer un peu de temps au dĂ©but pour les configurer â et par la suite, vous bĂ©nĂ©ficierez de plusieurs annĂ©es dâaugmentation de productivitĂ©, qui formeront lâhistoire de votre projet.
Ne me remerciez pas encore, plus tard.
Explication
Pour plus de clartĂ© : Cet article ne prĂ©conise en aucun cas que l'ensemble du systĂšme (c'est-Ă -dire le domaine, la logique mĂ©tier, etc.) doit ĂȘtre adaptĂ© au modĂšle de votre base de donnĂ©es. Dans cet article, je mentionne que le code client interagissant avec la base de donnĂ©es doit fonctionner en se basant sur le modĂšle de donnĂ©es, de sorte que lui-mĂȘme ne reproduise pas le modĂšle de base de donnĂ©es au statut de « premier classe ». Cette logique est gĂ©nĂ©ralement situĂ©e au niveau d'accĂšs aux donnĂ©es de votre client.
Dans les architectures Ă deux niveaux, qui existent encore par endroits, ce type de modĂšle systĂšme peut ĂȘtre le seul possible. Cependant, dans la plupart des systĂšmes, le niveau d'accĂšs aux donnĂ©es me semble ĂȘtre une « sous-systĂšme », encapsulant le modĂšle de la base de donnĂ©es.
Exceptions
Il existe des exceptions Ă toute rĂšgle, et j'ai dĂ©jĂ mentionnĂ© que l'approche de prioritĂ© de la base de donnĂ©es et de gĂ©nĂ©ration de code source peut parfois ĂȘtre inappropriĂ©e. Voici quelques-unes de ces exceptions (d'autres peuvent Ă©ventuellement exister) :
- Lorsque le schĂ©ma est inconnu et doit ĂȘtre ouvert. Par exemple, vous ĂȘtes un fournisseur d'outils qui aide les utilisateurs Ă naviguer dans n'importe quel schĂ©ma. Ouf. Ici, il n'y a pas de gĂ©nĂ©ration de code. Mais, tout de mĂȘme - la base de donnĂ©es avant tout.
- Lorsque le schĂ©ma doit ĂȘtre gĂ©nĂ©rĂ© Ă la volĂ©e pour rĂ©soudre un certain problĂšme. Cet exemple semble ĂȘtre une version lĂ©gĂšrement enjouĂ©e du pattern , c'est-Ă -dire que vous n'avez vraiment pas de schĂ©ma clairement dĂ©fini. Dans ce cas, il est souvent mĂȘme impossible d'ĂȘtre certain qu'un SGBD vous conviendra.
Les exceptions sont par nature exceptionnelles. Dans la plupart des cas liĂ©s Ă l'utilisation d'un SGBD, le schĂ©ma est connu Ă l'avance, il est Ă l'intĂ©rieur du SGBD et constitue la seule source de « vĂ©ritĂ© », et tous les clients doivent avoir des copies dĂ©rivĂ©es de celui-ci. IdĂ©alement, un gĂ©nĂ©rateur de code devrait ĂȘtre impliquĂ©.
Source : habr.com
