Bonjour, Habr!Nous continuons d'explorer le sujet de Java et Spring , y compris au niveau de la base de donnĂ©es. Aujourd'hui, nous vous proposons de comprendre 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 avoir une importance dĂ©cisive sur la façon dont cela est fait, et sur 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 dans l'application doit ĂȘtre conçu "sur la base de la base de donnĂ©es", et non "sur la base des capacitĂ©s de Java" (ou d'un autre langage client avec lequel vous travaillez). En choisissant la deuxiĂšme approche, vous vous lancez dans un long voyage de douleur et de souffrance dĂšs que votre projet commence Ă grandir.Cet article est basĂ© sur une question posĂ©e sur Stack Overflow.Discussions intĂ©ressantes sur reddit dans les sections / r / java et / r / programmation .GĂ©nĂ©ration de code
Combien je suis surpris qu'il y ait une si petite couche d'utilisateurs qui, aprĂšs avoir fait connaissance avec jOOQ, sont scandalisĂ©s par le fait que lorsqu'ils travaillent jOOQ s'appuie sĂ©rieusement sur la gĂ©nĂ©ration de code source. Personne ne vous dĂ©range pour utiliser jOOQ comme bon vous semble et ne vous oblige pas Ă utiliser la gĂ©nĂ©ration de code. Mais par dĂ©faut (comme dĂ©crit dans le manuel), travailler avec jOOQ se passe comme suit: vous commencez avec le schĂ©ma de base de donnĂ©es (hĂ©ritĂ©), vous le rĂ©tro-concevez avec le gĂ©nĂ©rateur de code jOOQ, de sorte que vous obtenez un ensemble de classes reprĂ©sentant vos tables, puis Ă©crire des requĂȘtes de type sĂ©curisĂ© dans ces tables: for (Record2<String, String> record : DSL.using(configuration)
.select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
.from(ACTOR)
.orderBy(1, 2)) {
}
Le code est gĂ©nĂ©rĂ© soit manuellement en dehors de l'assembly, soit manuellement avec chaque assembly. Par exemple, une telle rĂ©gĂ©nĂ©ration peut suivre immĂ©diatement aprĂšs la migration de la base de donnĂ©es Flyway, qui peut Ă©galement ĂȘtre effectuĂ©e manuellement ou automatiquement .GĂ©nĂ©ration de code source
Diverses philosophies, avantages et inconvĂ©nients sont associĂ©s Ă de telles approches de gĂ©nĂ©ration de code - manuelle et automatique - 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 nous permet de reproduire en Java cette «vĂ©rité» que nous tenons pour acquise, que ce soit Ă l'intĂ©rieur ou Ă l'extĂ©rieur de notre systĂšme. Dans un sens, les compilateurs qui gĂ©nĂšrent du bytecode, du code machine ou un autre type de code basĂ© sur la source font la mĂȘme chose - nous obtenons une reprĂ©sentation de notre «vĂ©rité» dans une autre langue, indĂ©pendamment de 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 y a une certaine vĂ©ritĂ© (interne ou externe) - par exemple, la spĂ©cification, le 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, générer une telle représentation est presque toujours conseillé - pour éviter les redondances.Fournisseurs de types et traitement des annotations
Remarque: une autre approche plus moderne et spĂ©cifique de la gĂ©nĂ©ration de code pour jOOQ est associĂ©e Ă l'utilisation de fournisseurs de types, sous la forme dans laquelle 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. Sous forme de sources, un tel code, en principe, n'existe pas. Il existe des outils similaires, quoique moins Ă©lĂ©gants, en Java - ce sont des processeurs d'annotation tels que Lombok .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 semble Ă quelqu'un pas si rĂ©pugnante?)
- , , , «» . Lombok, ââ. , .
?
En plus de la question dĂ©licate de savoir comment il est prĂ©fĂ©rable de dĂ©marrer la gĂ©nĂ©ration de code - manuellement ou automatiquement, il est nĂ©cessaire de 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 configurer le pipeline d'assemblage. Oui, vraiment dur. Il y a des coĂ»ts d'infrastructure supplĂ©mentaires. Si vous commencez tout juste Ă travailler avec un certain produit (que ce soit jOOQ, ou JAXB, ou Hibernate, etc.), il faut du temps pour configurer l'environnement de travail que vous souhaitez consacrer Ă l'apprentissage de l'API lui-mĂȘme, puis en extraire de la valeur.Si les coĂ»ts associĂ©s Ă la comprĂ©hension du dispositif gĂ©nĂ©rateur sont trop importants, alors, en effet, l'API a fait un peu de travail sur l'utilisabilitĂ© du gĂ©nĂ©rateur de code (et Ă l'avenir, il s'avĂšre que la configuration utilisateur est compliquĂ©e). La facilitĂ© d'utilisation devrait ĂȘtre la prioritĂ© la plus Ă©levĂ©e pour une telle API. Mais ce n'est qu'un argument contre la gĂ©nĂ©ration de code. Pour le reste, il est complĂštement entiĂšrement manuel d'Ă©crire une reprĂ©sentation locale de la vĂ©ritĂ© interne ou externe.Beaucoup diront quâils nâont pas le temps de faire tout cela. Ils ont des dĂ©lais pour leur super produit. Un peu plus tard, nous peignerons les convoyeurs d'assemblage, ce sera dans le temps. Je vais y rĂ©pondre:
Original , Alan O'Rourke, Audience StackMais dans Hibernate / JPA, il est si simple d'écrire du code «pour Java».Vraiment. Pour Hibernate et ses utilisateurs, c'est à la fois une bénédiction 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. DĂ©sormais, le destin d'Hibernate est de gĂ©nĂ©rer des «dĂ©tails» complexes sur la façon dont cette entitĂ© sera dĂ©finie sur la 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 à piloter l'application. Une opportunité vraiment cool de commencer rapidement et d'essayer différentes choses.Cependant, permettez. Je trompais.- Hibernate applique-t-il vraiment la définition de cette clé primaire nommée?
- Hibernate créera-t-il un index dans TITLE? "Je sais avec certitude que nous aurons besoin de lui."
- Hibernate rend-il exactement cette clé identifiable dans la spécification d'identité?
Probablement pas. Si vous développez votre projet à partir de zéro, il est toujours pratique de simplement supprimer 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é Livre prendra finalement 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, au début, ce sera trÚs facile.Mais alors il faut payer pour ça
TĂŽt ou tard, vous devez entrer en production. Ă ce moment-lĂ , un tel modĂšle cessera de fonctionner. Parce que:En production, il ne sera plus possible, si nĂ©cessaire, de supprimer l'ancienne base de donnĂ©es et de tout recommencer Ă zĂ©ro. Votre base de donnĂ©es deviendra une ancienne.Ă partir de maintenant, vous devrez Ă©crire des scripts de migration DDL, par exemple, en utilisant Flyway . Et qu'advient-il alors de vos entitĂ©s? Vous pouvez soit les adapter manuellement (et ainsi doubler votre charge de travail), soit commander Ă Hibernate de les rĂ©gĂ©nĂ©rer pour vous (quelles sont les chances que celle gĂ©nĂ©rĂ©e de cette maniĂšre rĂ©ponde Ă vos attentes?) Vous perdez quand mĂȘme.Ainsi, dĂšs que vous entrerez en production, vous aurez besoin de correctifs Ă chaud. Et ils doivent ĂȘtre mis en production trĂšs rapidement. Comme vous n'avez pas prĂ©parĂ© et organisĂ© un convoyeur fluide de vos migrations pour la production, vous corrigez tout de maniĂšre extravagante. Et puis vous nâavez pas le temps de tout faire correctement. Et rĂ©primander Hibernate, parce que tout le monde est toujours Ă blĂąmer, mais pas vous ...Au lieu de cela, dĂšs le dĂ©but, tout pourrait ĂȘtre fait d'une maniĂšre complĂštement diffĂ©rente. Par exemple, placez des roues rondes sur un vĂ©lo.Base de donnĂ©es d'abord
La vĂ©ritable «vĂ©rité» dans le schĂ©ma de votre base de donnĂ©es et la «souveraineté» sur elle rĂ©side Ă l'intĂ©rieur de la base de donnĂ©es. Un 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 conseillĂ© d'imposer la conformitĂ© avec le schĂ©ma et son intĂ©gritĂ©, de le faire directement dans la base de donnĂ©es - oĂč les informations sont stockĂ©es.C'est mĂȘme une vieille sagesse usĂ©e. Les clĂ©s primaires et uniques sont bonnes. Les clĂ©s Ă©trangĂšres sont bonnes. VĂ©rifier les restrictions est une bonne chose. Les dĂ©clarations sont bonnes.De plus, ce n'est pas tout. Par exemple, en utilisant Oracle, vous souhaiterez 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)
Peut-ĂȘtre que tout cela n'est pas important dans les petits systĂšmes, mais il n'est pas nĂ©cessaire d'attendre la transition vers le domaine des «mĂ©gadonnĂ©es» - il est possible et bien plus tĂŽt de commencer Ă bĂ©nĂ©ficier des optimisations de stockage fournies par le fournisseur, telles que celles mentionnĂ©es ci-dessus. Aucun des ORM que j'ai vus (y compris jOOQ) ne donne accĂšs Ă l'ensemble complet des options DDL que vous voudrez peut-ĂȘtre utiliser dans votre base de donnĂ©es. Les ORM offrent quelques outils qui aident Ă Ă©crire DDL.Mais au final, un circuit bien conçu est Ă©crit manuellement en DDL. Tout DDL gĂ©nĂ©rĂ© n'est qu'une approximation de celui-ci.Et le 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. Il va sans dire que cette vue client doit ĂȘtre synchronisĂ©e avec le modĂšle rĂ©el. Quelle est la meilleure façon d'y parvenir? Utilisation d'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:
SELECT table_schema, table_name
FROM information_schema.tables
SELECT tabschema, tabname
FROM syscat.tables
SELECT owner, table_name
FROM all_tables
SELECT name
FROM sqlite_master
SELECT databasename, tablename
FROM dbc.tables
Ces requĂȘtes (ou similaires, selon que vous devez Ă©galement prendre en compte les reprĂ©sentations, les reprĂ©sentations matĂ©rialisĂ©es, les fonctions avec une valeur de table) sont Ă©galement effectuĂ©es Ă l'aide d'un appel DatabaseMetaData.getTables()
de JDBC ou Ă l'aide du mĂ©ta-module jOOQ.Ă partir des rĂ©sultats de ces requĂȘtes, il est relativement facile de gĂ©nĂ©rer une vue client de votre modĂšle de base de donnĂ©es, quelle que soit la technologie utilisĂ©e sur votre 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 des entitĂ©s elles-mĂȘmes
- Si vous utilisez jOOQ, vous pouvez générer le méta-modÚle jOOQ
Selon le nombre de fonctionnalitĂ©s offertes par votre API client (par exemple jOOQ ou JPA), le mĂ©ta-modĂšle gĂ©nĂ©rĂ© peut ĂȘtre vraiment riche et complet. Prenons, par exemple , la possibilitĂ© de jointures implicites apparues dans jOOQ 3.11 , qui s'appuie sur les mĂ©ta-informations gĂ©nĂ©rĂ©es sur les relations des clĂ©s Ă©trangĂšres entre vos tables.DĂ©sormais, tout incrĂ©ment 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;
Aimeriez-vous vraiment faire ce travail deux fois? Dans aucun cas. Corrigez simplement le DDL, exécutez-le dans votre pipeline d'assemblage et obtenez 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 une 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 peut ĂȘ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 existe toujours un modĂšle qui est la seule source de vĂ©ritĂ© pour certains sous-systĂšmes - ou, du moins, nous devrions nous efforcer de le faire et Ă©viter une telle confusion d'entreprise, oĂč la «vĂ©rité» est partout et nulle part. Tout peut ĂȘtre beaucoup plus simple. Si vous Ă©changez simplement des fichiers XML avec un autre systĂšme, utilisez simplement XSD. Regardez le 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 XML
- XSD
- XSD Java XJC
Le dernier point est important. Lorsque nous communiquons avec un systĂšme externe Ă l'aide de messages XML, nous voulons ĂȘtre sĂ»rs de la validitĂ© de nos messages. Ceci est trĂšs facile Ă rĂ©aliser avec JAXB, XJC et XSD. Il serait complĂštement insensĂ© de s'attendre Ă ce que, lorsque nous approchons de la conception de «Java d'abord», oĂč nous faisons nos messages sous forme d'objets Java, ils puissent en quelque sorte ĂȘtre clairement affichĂ©s en XML et envoyĂ©s pour consommation Ă un autre systĂšme. Le XML gĂ©nĂ©rĂ© de cette maniĂšre serait de trĂšs mauvaise qualitĂ©, non documentĂ©, et serait difficile Ă dĂ©velopper. S'il y avait un accord sur le niveau de qualitĂ© de service (SLA) sur une telle interface, nous le ruinerions immĂ©diatement.HonnĂȘtement, c'est exactement ce qui se passe tout le temps de l'API Ă JSON, mais c'est une autre histoire, je le jure la prochaine fois ...Bases de donnĂ©es: c'est la mĂȘme chose
Lorsque vous travaillez avec des bases de donnĂ©es, vous comprenez qu'elles sont, en principe, similaires. La base est propriĂ©taire de ses donnĂ©es et doit gĂ©rer le schĂ©ma. Toutes les modifications apportĂ©es au circuit doivent ĂȘtre implĂ©mentĂ©es directement sur DDL afin de mettre Ă jour une seule source de vĂ©ritĂ©.Lorsqu'une mise Ă jour 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 (il reste Ă leur souhaiter bonne chance), et d'autres en C #. Ce n'est pas important. Le modĂšle principal est dans la base de donnĂ©es. Les modĂšles gĂ©nĂ©rĂ©s Ă l'aide d'ORM, gĂ©nĂ©ralement de mauvaise qualitĂ©, sont mal documentĂ©s et difficiles Ă dĂ©velopper.Par consĂ©quent, ne vous trompez pas. Ne faites aucune erreur dĂšs le dĂ©part. Travaillez Ă 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 faciliter la copie de 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. Il vous suffit de passer un peu de temps dĂšs le dĂ©but pour les configurer - et vous aurez alors des annĂ©es de productivitĂ© accrue qui constitueront l'historique de votre projet.D'ici lĂ , merci.Explication
Pour plus de clartĂ©: cet article ne prĂ©conise en aucun cas que, selon le modĂšle de votre base de donnĂ©es, vous devez plier l'ensemble du systĂšme (c'est-Ă -dire le domaine, la logique mĂ©tier, etc., etc.). Dans cet article, je dis que le code client qui interagit avec la 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 l'Ă©tat "premiĂšre classe". Cette logique est gĂ©nĂ©ralement situĂ©e au niveau de l'accĂšs aux donnĂ©es sur votre client.Dans les architectures Ă deux niveaux, qui sont encore prĂ©servĂ©es Ă certains endroits, un tel modĂšle de 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 un "sous-systĂšme" encapsulant un modĂšle de base de donnĂ©es.Exceptions
Il existe des exceptions Ă toute rĂšgle, et j'ai dĂ©jĂ dit qu'une approche avec la primautĂ© de la base de donnĂ©es et la gĂ©nĂ©ration de code source peut parfois ĂȘtre inappropriĂ©e. Voici quelques exceptions (il y en a probablement d'autres):- Lorsque le circuit est inconnu et qu'il doit ĂȘtre ouvert. Par exemple, vous ĂȘtes un fournisseur d'un outil pour aider les utilisateurs Ă naviguer dans n'importe quel schĂ©ma. phew Il n'y a pas de gĂ©nĂ©ration de code. Mais encore - la base de donnĂ©es est avant tout.
- Quand un circuit doit ĂȘtre gĂ©nĂ©rĂ© Ă la volĂ©e pour rĂ©soudre un certain problĂšme. Cet exemple semble ĂȘtre une version lĂ©gĂšrement fantaisiste du modĂšle de valeur d'attribut d'entitĂ© , c'est-Ă -dire que vous n'avez vraiment pas de schĂ©ma bien dĂ©fini. Dans ce cas, il est souvent impossible dâĂȘtre certain que le SGBDR vous convient.
Les exceptions sont intrinsÚquement exceptionnelles. Dans la plupart des cas impliquant l'utilisation d'un SGBDR, le schéma est connu à l'avance, il est situé à l'intérieur du SGBDR et est la seule source de «vérité», et tous les clients doivent acquérir des copies qui en sont dérivées. Idéalement, vous devez utiliser un générateur de code.