La vĂ©ritĂ© tout d'abord, ou pourquoi le systĂšme doit ĂȘtre conçu en fonction du pĂ©riphĂ©rique de base de donnĂ©es

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 
 
       .select(ACTOR.FIRST_NAME, ACTOR.LAST_NAME)
//           vvvvv ^^^^^^^^^^^^  ^^^^^^^^^^^^^^^  
       .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 Stack

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

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

All Articles