Peut-être que vous savez déjà quelque chose sur la bibliothèque open source Celesta . Sinon, peu importe, maintenant nous allons tout vous dire. Une autre année s'est écoulée, la version 7.x est sortie, beaucoup de choses ont changé, et il est temps de résumer les changements, et en même temps de rappeler ce qu'est Celesta en général.
Si vous n'avez encore rien entendu parler de Celesta, et lorsque vous lisez cet article, vous voulez savoir pour quelles tâches commerciales son application est la plus efficace, je peux recommander la première partie de l' ancien article ou cette vidéo d'une demi-heure (à l'exception des mots sur l'utilisation du langage Python). Mais mieux encore, lisez d'abord cet article. Je vais commencer par les changements survenus dans la version 7, puis je passerai en revue un exemple technique complet d'utilisation de la version moderne de Celesta pour écrire un petit service backend pour une application Java utilisant Spring Boot.
Qu'est-ce qui a changé dans la version 7.x?
- Nous avons refusé d'utiliser Jython comme langage intégré à Celesta. Si plus tôt nous avons commencé à parler de Celesta avec le fait que la logique métier est écrite en Python, maintenant ... tout langage Java peut servir de langage de logique métier: Java, Groovy, JRuby ou le même Jython. Désormais, Celesta n'appelle pas le code logique métier, mais le code logique métier utilise Celesta et ses classes d'accès aux données comme bibliothèque Java la plus courante. Oui, la compatibilité descendante a été violée à cause de cela, mais c'est le prix que nous étions prêts à payer. Malheureusement, notre pari sur Jython a été perdu. Lorsque nous avons commencé à utiliser Jython il y a quelques années, c'était un projet vivant et prometteur, mais au fil des ans, son développement a ralenti, le retard de la spécification du langage s'est accumulé, les problèmes de compatibilité pour la plupart des bibliothèques pip n'ont pas été résolus. La dernière goutte a été les nouveaux bogues dans les dernières versions linguistiques, qui se sont manifestés lors du travail sur une charge de production. Nous-mêmes n'avons pas les ressources nécessaires pour soutenir le projet Jython, et nous avons décidé de nous en séparer. Celesta ne dépend plus de Jython.
- Les classes d'accès aux données sont désormais générées en code en langage Java (et non en Python, comme auparavant) à l'aide du plugin Maven. Et depuis que nous sommes passés de la frappe dynamique à la frappe statique à cause de cela, il y a eu plus d'opportunités de refactoring et il est devenu plus facile d'écrire du code subjectivement correct.
- L'extension pour JUnit5 est apparue, il est donc devenu très pratique d'écrire des tests de logique qui fonctionnent avec la base de données dans JUnit5 (qui seront discutés plus tard).
- Un projet distinct est apparu - spring-boot-starter-celesta , qui, comme son nom l'indique, est le starter de Celesta dans Spring Boot. La possibilité d'intégrer des applications Celesta dans des services Spring Boot facilement déployables a compensé la perte de la possibilité de mettre à jour l'application sur le serveur en changeant simplement le dossier avec des scripts Python.
- Nous avons transféré toute la documentation du Wiki au format AsciiDoctor , l' avons placée dans le contrôle de version avec le code, et nous avons maintenant une documentation à jour pour chaque version de Celesta. Pour la dernière version, la documentation en ligne est disponible ici: https://courseorchestra.imtqy.com/celesta/
- On nous a souvent demandé s'il était possible d'utiliser la migration de base de données via DDL idempotent séparément de Celesta. Maintenant, il existe une telle opportunité en utilisant l'outil 2bass .
Qu'est-ce que Celesta et que peut-elle faire?
En bref, Celesta est:
- une couche intermédiaire entre la base de données relationnelle et le code logique métier, basée sur l'approche de conception basée sur la base de données ,
- mécanisme de migration de la structure de la base de données,
- cadre pour tester le code qui fonctionne avec les données.
Nous prenons en charge quatre types de bases de données relationnelles: PostgreSQL, MS SQL Server, Oracle et H2.
Caractéristiques clés de Celesta:
- Un principe très similaire au principe de base de Java: "Écrivez une fois, exécutez sur tous les SGBDR pris en charge." Le code logique métier ne sait pas sur quel type de base de données il s'exécutera. Vous pouvez écrire le code de logique métier et l'exécuter dans MS SQL Server, puis passer à PostgreSQL, et cela se produira sans complications (enfin, presque :)
- Restructuration automatique sur une base de données live. La plupart du cycle de vie des projets Celesta se produit lorsque la base de données de travail est déjà là et qu'elle est remplie de données qui doivent être enregistrées, mais il est également nécessaire de changer constamment leur structure. L'une des principales caractéristiques de Celesta est la possibilité d'adapter automatiquement la structure de la base de données à votre modèle de données.
- Test. Une grande attention est accordée à la possibilité de tester le code de Celesta, afin que nous puissions tester automatiquement les méthodes qui modifient les données dans la base de données, en le faisant facilement, rapidement et avec élégance, sans utiliser d'outils externes tels que DbUnit et les conteneurs.
Pourquoi avez-vous besoin d'indépendance vis-à-vis du type de SGBD?
L'indépendance du code de logique métier par rapport au type de SGBD n'était pas le premier point que nous avons mis: le code écrit pour Celesta ne sait pas du tout sur quel SGBD il s'exécute. Pourquoi?
Tout d'abord, du fait que le choix d'un type de SGBD n'est pas un problème technologique, mais politique. En venant à un nouveau client commercial, nous constatons souvent qu'il a déjà un type de SGBD préféré dans lequel les fonds sont investis, et le client veut voir d'autres solutions sur l'infrastructure existante. Le paysage technologique est en train de changer: PostgreSQL est de plus en plus présent dans les agences gouvernementales et les entreprises privées, bien que MS SQL Server ait prévalu dans notre pratique il y a quelques années. Celesta prend en charge les SGBD les plus courants, et nous ne sommes pas inquiets de ces changements.
Deuxièmement, je voudrais transférer le code déjà créé pour résoudre les problèmes standard d'un projet à un autre, pour créer une bibliothèque réutilisable. Des éléments comme les répertoires hiérarchiques ou les modules de distribution de notification par courrier électronique sont intrinsèquement standard, et pourquoi devons-nous prendre en charge plusieurs versions pour les clients ayant des relations différentes?
Troisièmement, le dernier mais non le moindre, la possibilité d'exécuter des tests unitaires sans utiliser DbUnit et les conteneurs à l'aide d'une base de données H2 en mémoire. Dans ce mode, la base H2 démarre instantanément. Celesta y crée très rapidement un schéma de données, après quoi vous pouvez effectuer les tests nécessaires et «oublier» la base de données. Puisque le code de logique métier ne sait vraiment pas sur quelle base il est exécuté, alors en conséquence, s'il fonctionne sans erreur sur H2, alors sans erreur, il fonctionnera sur PostgreSQL. Bien sûr, la tâche des développeurs du système Celesta lui-même est de faire tous les tests en utilisant de vrais SGBD pour s'assurer que notre plate-forme exécute également son API sur différentes relations. Et nous le faisons. Mais le développeur de logique métier n'est plus requis.
CelestaSQL
Comment le cross-basementisme est-il atteint? Bien sûr, au prix de travailler avec des données uniquement via une API spéciale qui isole la logique des spécificités de la base de données. Celesta génère des classes Java pour accéder aux données, d'une part, et au code SQL et à certains objets auxiliaires à l'intérieur de la base de données, d'autre part.
Celesta ne fournit pas de mappage relationnel objet dans sa forme la plus pure, car lors de la conception d'un modèle de données, nous ne venons pas des classes, mais de la structure de la base de données. Autrement dit, nous créons d'abord un modèle ER de tables, puis, sur la base de ce modèle, Celesta génère lui-même des classes de curseur pour accéder aux données.
Vous pouvez réaliser le même travail sur tous les SGBD pris en charge uniquement pour la fonctionnalité qui est implémentée de manière à peu près égale dans chacun d'eux. Si nous décrivons conditionnellement l'ensemble des capacités fonctionnelles de chacune des bases soutenues par nous sous la forme de «cercles d'Euler», alors nous obtenons l'image suivante:

Si nous fournissons une indépendance totale par rapport au type de base de données, la fonctionnalité que nous ouvrons aux programmeurs de logique métier doit se situer à l'intersection de toutes les bases. À première vue, il semble que ce soit une limitation importante. Oui: certaines fonctionnalités spécifiques, par exemple, nous ne pouvons pas utiliser SQL Server. Mais sans exception, les bases de données relationnelles prennent en charge les tables, les clés étrangères, les vues, les séquences, les requêtes SQL avec JOIN et GROUP BY. En conséquence, nous pouvons donner ces opportunités aux développeurs. Nous fournissons aux développeurs du «SQL dépersonnalisé», que nous appelons «CelestaSQL», et dans le processus, nous générons des requêtes SQL pour les dialectes des bases de données correspondantes.
Le langage CelestaSQL inclut DDL pour définir les objets de base de données et les requêtes SELECT pour les vues et les filtres, mais ne contient pas de commandes DML: les curseurs sont utilisés pour modifier les données, qui doivent encore être discutées.
Chaque base de données possède son propre ensemble de types de données. CelestaSQL possède également son propre ensemble de types. Au moment de la rédaction, il y en avait neuf, et ce tableau les compare aux types réels dans diverses bases de données et types de données Java.
Il peut sembler que neuf types ne suffisent pas (par rapport à ce que PostgreSQL prend en charge , par exemple), mais en réalité ce sont les types qui sont suffisants pour stocker des informations financières, commerciales et logistiques: chaînes, entiers, fractionnaires , les dates, les valeurs booléennes et les blobs sont toujours suffisants pour représenter de telles données.
Le langage CelestaSQL lui-même est décrit dans la documentation avec un grand nombre de diagrammes de syntaxe.
Modification de la structure de la base de données. Idempotent DDL
Une autre caractéristique clé de Celesta est son approche de la migration de la structure de la base de données de travail au fur et à mesure du développement du projet. Pour ce faire, l'approche intégrée à Celesta à l'aide de DDL idempotent est utilisée.
En résumé, lorsque nous écrivons dans CelestaSQL le texte suivant:
CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) );
- ce texte n'est pas interprété par Celesta comme «créer un tableau, mais s'il y a déjà un tableau, alors donner une erreur», mais «amener le tableau à la structure souhaitée». C'est-à-dire: «s'il n'y a pas de table, créez-la, s'il y a une table, voyez quels champs s'y trouvent, avec quels types, quels index, quelles clés étrangères, quelles valeurs par défaut, etc., et si quelque chose doit être changé dans ce tableau pour l'amener à la bonne sorte. "
Avec cette approche, nous implémentons la possibilité de refactoriser et de contrôler les versions des scripts pour déterminer la structure de la base de données:
- nous voyons dans le script "l'image souhaitée" actuelle de la structure,
- ce qui, par qui et pourquoi la structure a changé au fil du temps, nous pouvons regarder à travers le système de contrôle de version,
- quant aux commandes ALTER, Celesta les génère et les exécute automatiquement «sous le capot» selon les besoins.
Bien sûr, cette approche a ses limites. Celesta met tout en œuvre pour que la migration automatique soit indolore et transparente, mais cela n'est pas possible dans tous les cas. La motivation, les possibilités et les limites de cette approche ont été décrites dans cet article (sa version anglaise est également disponible).
Afin d'accélérer le processus de vérification / mise à jour de la structure de la base de données, Celesta applique le stockage des sommes de contrôle de script DDL dans la base de données (jusqu'à ce que la somme de contrôle soit modifiée, le processus de vérification et de mise à jour de la structure de la base de données ne démarre pas). Pour que le processus de mise à jour se déroule sans problèmes liés à l'ordre de changement des objets dépendants les uns des autres, un tri topologique des dépendances entre les schémas par des clés étrangères est appliqué. Le processus de migration automatique est décrit plus en détail dans la documentation .
Création d'un projet et d'un modèle de données Celesta
Le projet de démonstration, que nous considérerons, est disponible sur le github . Voyons comment utiliser Celesta lors de l'écriture d'une application Spring Boot. Voici les dépendances Maven dont vous avez besoin:
org.springframework.boot:spring-boot-starter-web
et ru.curs:spring-boot-starter-celesta
(pour plus de détails, ru.curs:spring-boot-starter-celesta
documentation).- Si vous n'utilisez pas Spring Boot, vous pouvez connecter directement la dépendance
ru.curs:celesta-system-services
. - Pour la génération de code de classes d'accès aux données basées sur des scripts Celesta-SQL,
ru.curs:celesta-maven-plugin
nécessaire ru.curs:celesta-maven-plugin
- le code source d'un exemple de démonstration ou de la documentation décrit comment le connecter. - Pour profiter de la possibilité d'écrire des tests unitaires JUnit5 pour les méthodes qui modifient les données, vous devez connecter
ru.curs:celesta-unit
dans la portée du test.
Créez maintenant un modèle de données et compilez des classes d'accès aux données.
Supposons que nous réalisons un projet pour une société de commerce électronique qui a récemment fusionné avec une autre société. Chacun a sa propre base de données. Ils collectent les commandes, mais jusqu'à ce qu'ils fusionnent leurs bases de données, ils ont besoin d'un point d'entrée unique pour collecter les commandes de l'extérieur.
La mise en œuvre de ce «point d'entrée» devrait être assez classique: un service HTTP avec des opérations CRUD qui stockent des données dans une base de données relationnelle.
Étant donné que Celesta implémente l'approche de conception basée sur la base de données, nous devons d'abord créer une structure de table qui stocke les commandes. Une commande, comme vous le savez, est une entité composite: elle se compose d'un en-tête où sont stockées les informations sur le client, la date de la commande et d'autres attributs de la commande, ainsi que de nombreuses lignes (articles de base).
Donc, pour le travail: créer
src/main/celestasql
- par défaut, c'est le chemin d'accès aux scripts de projet CelestaSQL- il contient des sous-dossiers qui répètent la structure des dossiers des packages java (
ru/curs/demo
dans notre cas). - dans le dossier du package, créez un fichier
.sql
avec le contenu suivant:
CREATE SCHEMA demo VERSION '1.0'; CREATE TABLE OrderHeader( id VARCHAR(30) NOT NULL, date DATETIME, customer_id VARCHAR(30), customer_name VARCHAR(50), manager_id VARCHAR(30), CONSTRAINT Pk_OrderHeader PRIMARY KEY (id) ); CREATE TABLE OrderLine( order_id VARCHAR(30) NOT NULL, line_no INT NOT NULL, item_id VARCHAR(30) NOT NULL, item_name VARCHAR(100), qty INT NOT NULL DEFAULT 0, cost REAL NOT NULL DEFAULT 0.0, CONSTRAINT Idx_OrderLine PRIMARY KEY (order_id, line_no) ); ALTER TABLE OrderLine ADD CONSTRAINT fk_OrderLine FOREIGN KEY (order_id) REFERENCES OrderHeader(id); CREATE VIEW OrderedQty AS SELECT item_id, sum(qty) AS qty FROM OrderLine GROUP BY item_id;
Nous avons décrit ici deux tables reliées par une clé étrangère et une vue qui renverra une quantité récapitulative pour les marchandises présentes dans toutes les commandes. Comme vous pouvez le voir, ce n'est pas différent du SQL standard, à l'exception de la commande CREATE SCHEMA
, dans laquelle nous avons déclaré la version du schéma de demo
(pour savoir comment le numéro de version affecte la migration automatique, consultez la documentation ). Mais il y a aussi des fonctionnalités. Par exemple, tous les noms des tables et des champs que nous utilisons ne peuvent être tels qu'ils peuvent être transformés en noms de classe et de variable valides dans le langage Java. Par conséquent, les espaces et les caractères spéciaux sont exclus. Vous pouvez également remarquer que les commentaires que nous avons mis sur les noms des tables et certains champs, nous n'avons pas commencé avec / *, comme d'habitude, mais avec / **, comment les commentaires JavaDoc commencent - et ce n'est pas un hasard! Un commentaire défini sur une entité commençant par / ** sera disponible à l'exécution dans la propriété .getCelestaDoc()
de cette entité. Cela est utile lorsque nous voulons fournir aux éléments de base de données des méta-informations supplémentaires: par exemple, des noms de champs lisibles par l'homme, des informations sur la façon de représenter les champs dans l'interface utilisateur, etc.
Le script CelestaSQL remplit deux tâches tout aussi importantes: d'une part, pour le déploiement / modification de la structure d'une base de données relationnelle, et d'autre part, pour la génération de code des classes d'accès aux données.
Nous pouvons générer des classes d'accès aux données maintenant, il suffit d'exécuter la commande mvn generate-sources
ou, si vous travaillez dans IDEA, cliquez sur le bouton `` Générer les sources et mettre à jour les dossiers '' dans le panneau de configuration Maven. Dans le second cas, IDEA « target/generated-sources/celesta
dossier créé dans target/generated-sources/celesta
et rend son contenu disponible pour importation dans les codes sources du projet. Le résultat de la génération de code se présentera comme suit - une classe pour chaque objet de la base de données:
La connexion à la base de données est spécifiée dans les paramètres de l'application, dans notre cas, dans le fichier src/main/resources/application.yml
. Lors de l'utilisation de spring-boot-starter-celesta, IDEA vous indiquera les options de code disponibles dans la complétion de code.
Si nous ne voulons pas nous soucier du "vrai" SGBDR à des fins de démonstration, nous pouvons faire fonctionner Celesta avec la base de données H2 intégrée en mode en mémoire en utilisant la configuration suivante:
celesta: h2: inMemory: true
Pour connecter une "vraie" base de données, changez la configuration en quelque chose comme
celesta: jdbc: url: jdbc:postgresql://127.0.0.1:5432/celesta username: <your_username> password: <your_password>
(dans ce cas, vous devrez également ajouter un pilote JDBC PostgreSQL à votre application via la dépendance Maven).
Lorsque vous lancez une application Celesta avec une connexion à un serveur de base de données, vous pouvez constater que les tables, vues, index, etc. nécessaires sont créés pour une base de données vide et pour une base non vide, ils sont mis à jour selon les structures spécifiées dans la DDL.
Création de méthodes de manipulation de données
Une fois que vous avez compris comment créer une structure de base de données, vous pouvez commencer à écrire la logique métier.
Afin de pouvoir mettre en œuvre les exigences de distribution des droits d'accès et des actions de journalisation, toute opération sur les données dans Celesta est effectuée pour le compte d'un utilisateur, il n'y a pas d'opérations «anonymes». Par conséquent, tout code Celesta est exécuté dans le contexte de l'appel décrit dans la classe CallContext .
- Avant de démarrer une opération pouvant modifier les données de la base de données,
CallContext
activé. - Au moment de l'activation, une connexion à la base de données est prise à partir du pool de connexions et la transaction commence.
- Une fois l'opération
CallContext
exécute soit commit()
si l'opération a réussi, soit rollback()
si une exception non CallContext
s'est produite pendant l'exécution, CallContext
ferme et la connexion à la base de données est renvoyée au pool.
Si nous utilisons spring-boot-starter-celesta, alors ces actions sont effectuées automatiquement pour toutes les méthodes annotées par @CelestaTransaction
.
Supposons que nous voulons écrire un gestionnaire qui enregistre le document dans la base de données. Son code au niveau du contrôleur pourrait ressembler à ceci:
@RestController @RequestMapping("/api") public class DocumentController { private final DocumentService srv; public DocumentController(DocumentService srv) { this.srv = srv; } @PutMapping("/save") public void saveOrder(@RequestBody OrderDto order) { CallContext ctx = new CallContext("user1");
En règle générale, au niveau de la méthode du contrôleur (c'est-à-dire lorsque l'authentification est déjà passée), nous connaissons l'ID utilisateur et pouvons l'utiliser lors de la création du CallContext
. La liaison d'un utilisateur à un contexte détermine les autorisations d'accès aux tables et permet également de consigner les modifications apportées en son nom. Certes, dans ce cas, pour l'opérabilité du code interagissant avec la base de données, les droits de l'utilisateur "user1" doivent être indiqués dans les tables système . Si vous ne souhaitez pas utiliser le système de distribution d'accès Celesta et accorder au contexte de session tous les droits sur toutes les tables, vous pouvez créer un objet SystemCallContext
.
La méthode d'enregistrement de la facture au niveau du service peut ressembler à ceci:
@Service public class DocumentService { @CelestaTransaction public void postOrder(CallContext context, OrderDto doc) { try (OrderHeaderCursor header = new OrderHeaderCursor(context); OrderLineCursor line = new OrderLineCursor(context)) { header.setId(doc.getId()); header.setDate(Date.from(doc.getDate().atStartOfDay(ZoneId.systemDefault()).toInstant())); header.setCustomer_id(doc.getCustomerId()); header.setCustomer_name(doc.getCustomerName()); header.insert(); int lineNo = 0; for (OrderLineDto docLine : doc.getLines()) { lineNo++; line.setLine_no(lineNo); line.setOrder_id(doc.getId()); line.setItem_id(docLine.getItemId()); line.setQty(docLine.getQty()); line.insert(); } } }
Notez l'annotation @CelestaTransaction
. Grâce à lui, l'objet proxy DocumentService
effectuera toutes ces actions de service avec le paramètre CallContext ctx
décrit ci-dessus. Autrement dit, au début de la méthode, il sera déjà lié à la connexion à la base de données et la transaction sera prête à commencer. Nous pouvons nous concentrer sur l'écriture de la logique métier. Dans notre cas, lire l'objet OrderDto
et l'enregistrer dans la base de données.
Pour ce faire, nous utilisons les curseurs dits - classes générées à l'aide celesta-maven-plugin
. Nous avons déjà vu ce qu'ils sont. Une classe est créée pour chacun des objets de schéma - deux tables et une vue. Et maintenant, nous pouvons utiliser ces classes pour accéder aux objets de base de données dans notre logique métier.
Pour créer un curseur sur la table de commande et sélectionner le premier enregistrement, vous devez écrire le code suivant:
OrderHeaderCursor header = new OrderHeaderCursor(context); header.tryFirst();
Après avoir créé l'objet d'en-tête, nous pouvons accéder aux champs de l'entrée de table via des getters et setters:
Lors de la création d'un curseur, nous devons utiliser le contexte d'appel actif - c'est la seule façon de créer un curseur. Le contexte d'appel contient des informations sur l'utilisateur actuel et ses droits d'accès.
Avec l'objet curseur, nous pouvons faire différentes choses: filtrer, parcourir les enregistrements et aussi, naturellement, insérer, supprimer et mettre à jour des enregistrements. L'API entière du curseur est décrite en détail dans la documentation .
Par exemple, le code de notre exemple pourrait être développé comme suit:
OrderHeaderCursor header = new OrderHeaderCursor(context); header.setRange("manager_id", "manager1"); header.tryFirst(); header.setCounter(header.getCounter() + 1); header.update();
Dans cet exemple, nous définissons le filtre par le champ manager_id, puis nous trouvons le premier enregistrement à l'aide de la méthode tryFirst.
(pourquoi "essayer")Les méthodes get
, first
, insert
, update
ont deux options: sans le préfixe try (juste get(...)
, etc.) et avec le préfixe try ( tryGet(...)
, tryFirst()
, etc.) . Les méthodes sans le préfixe try lèvent une exception si la base de données ne dispose pas des données appropriées pour effectuer l'action. Par exemple, first () lèvera une exception si aucun enregistrement n'entre dans le filtre défini sur le curseur. En même temps, les méthodes avec le préfixe try ne lèvent pas d'exception, mais renvoient à la place une valeur booléenne qui signale le succès ou l'échec de l'opération correspondante. La pratique recommandée consiste à utiliser des méthodes sans le préfixe try dans la mesure du possible. De cette façon, un code «d'auto-test» est créé, signalant des erreurs dans le temps dans les données de logique et / ou de base de données.
Lorsque tryFirst
déclenché, les variables du tryFirst
sont remplies avec les données d'un enregistrement, nous pouvons les lire et leur affecter des valeurs. Et lorsque les données du curseur sont entièrement préparées, nous exécutons update()
, et il stocke le contenu du curseur dans la base de données.
Quel problème ce code pourrait-il être affecté? Bien sûr, l'émergence de la condition de course / mise à jour perdue! Parce qu'entre le moment où nous avons reçu les données dans la ligne avec "tryFirst" et le moment où nous essayons de mettre à jour ces données au point "update", quelqu'un d'autre peut déjà recevoir, modifier et mettre à jour ces données dans la base de données. Une fois les données lues, le curseur ne bloque en aucun cas leur utilisation par d'autres utilisateurs! Pour se protéger contre les mises à jour perdues, Celesta utilise le principe de verrouillage optimiste. Dans chaque table, par défaut, Celesta crée un champ de recversion
, et au niveau ON du déclencheur UPDATE, il incrémente le numéro de version et vérifie que les données mises à jour ont la même version que la table. Si un problème se produit, lève une exception. Vous pouvez en savoir plus à ce sujet dans l'article de l'article « Protection contre les mises à jour perdues ».
Rappelons à nouveau qu'une transaction est associée à l'objet CallContext. Si la procédure Celesta réussit, un commit se produit. Si la méthode Celesta se termine par une exception non gérée, une restauration se produit. Ainsi, si une erreur se produit dans une procédure compliquée, la transaction entière liée au contexte d'appel est annulée, comme si nous n'avions rien commencé à faire avec les données, les données ne sont pas corrompues. Si, pour une raison quelconque, vous avez besoin d'une validation au milieu, par exemple, d'une sorte de procédure volumineuse, une validation explicite peut être exécutée en appelant context.commit()
.
Test des méthodes de données
Créons un test unitaire qui vérifie l'exactitude de la méthode de service qui stocke OrderDto
dans la base de données.
Lorsque vous utilisez JUnit5 et l'extension pour JUnit5 disponible dans le module Celesta celesta-unit
, c'est très simple. La structure du test est la suivante:
@CelestaTest public class DocumentServiceTest { DocumentService srv = new DocumentService(); @Test void documentIsPutToDb(CallContext context) { OrderDto doc =... srv.postOrder(context, doc);
Grâce à l'annotation @CelestaTest
, qui est une extension pour JUnit5, nous pouvons déclarer le paramètre de CallContext context
dans les méthodes de test. Ce contexte est déjà activé et lié à la base de données (H2 en mémoire), et donc nous n'avons pas besoin d'envelopper la classe de service dans un proxy - nous la créons en utilisant new
, et non en utilisant Spring. Cependant, si nécessaire, injectez le service dans le test à l'aide des outils Spring, il n'y a aucun obstacle à cela.
Nous créons des tests unitaires en supposant qu'au moment de leur exécution, la base de données sera complètement vide, mais avec la structure dont nous avons besoin, et après leur exécution, nous ne pouvons pas nous inquiéter du fait que nous ayons laissé des «ordures» dans la base de données. Ces tests sont effectués à très grande vitesse.
Créons une deuxième procédure qui retourne JSON avec des valeurs agrégées indiquant le nombre de produits que nous avons commandés.
Le test écrit deux commandes dans la base de données, après quoi il vérifie la valeur totale retournée par la nouvelle méthode getAggregateReport
:
@Test void reportReturnsAggregatedQuantities(CallContext context) { srv.postOrder(context, . . .); srv.postOrder(context, . . .); Map<String, Integer> result = srv.getAggregateReport(context); assertEquals(5, result.get("A").intValue()); assertEquals(7, result.get("B").intValue()); }
Pour implémenter la méthode getAggregateReport
nous utiliserons la vue OrderedQty, qui, je le rappelle, dans le fichier CelestaSQL ressemble à ceci:
create view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id;
La demande est standard: nous résumons les lignes de commande par quantité et regroupons par code produit. Un curseur OrderedQtyCursor a déjà été créé pour la vue, que nous pouvons utiliser. Nous déclarons ce curseur, le parcourons et collectons la Map<String, Integer>
souhaitée:
@CelestaTransaction public Map<String, Integer> getAggregateReport(CallContext context) { Map<String, Integer> result = new HashMap<>(); try (OrderedQtyCursor ordered_qty = new OrderedQtyCursor(context)) { for (OrderedQtyCursor line : ordered_qty) { result.put(ordered_qty.getItem_id(), ordered_qty.getQty()); } } return result; }
Vues Celesta matérialisées
Pourquoi l'utilisation d'une vue est-elle mauvaise pour obtenir des données agrégées? Cette approche est tout à fait réalisable, mais en réalité elle met une bombe à retardement dans tout notre système: après tout, une vue, qui est une requête SQL, s'exécute de plus en plus lentement à mesure que les données s'accumulent dans le système. Il devra résumer et regrouper de plus en plus de lignes. Comment éviter cela?
Celesta essaie de mettre en œuvre toutes les tâches standard auxquelles les programmeurs de logique métier sont constamment confrontés au niveau de la plate-forme.
MS SQL Server a le concept de vues matérialisées (indexées), qui sont stockées sous forme de tables et sont mises à jour rapidement à mesure que les données des tables source changent. Si nous travaillions dans un serveur MS SQL «propre», dans notre cas, le remplacement de la vue par une vue indexée serait exactement ce dont nous avions besoin: la récupération du rapport agrégé ne ralentirait pas à mesure que les données s'accumulaient et le travail de mise à jour du rapport agrégé serait effectué pour le moment insérer des données dans le tableau des lignes de commande et n'augmenterait pas non plus beaucoup avec une augmentation du nombre de lignes.
Mais si nous travaillons avec PostgreSQL via Celesta, que pouvons-nous faire? Redéfinissez la vue en ajoutant le mot matérialisé:
create materialized view OrderedQty as select item_id, sum(qty) as qty from OrderLine group by item_id;
Commençons le système et voyons ce qui est arrivé à la base de données.
Nous remarquerons que la OrderedQty
a disparu et que la table OrderedQty
est apparue à la OrderedQty
. De plus, comme la table OrderLine est remplie de données, les informations de la table OrderedQty seront mises à jour "comme par magie", comme si OrderedQty était une vue.
Il n'y a pas de magie ici si nous regardons les déclencheurs construits sur la table OrderLine
. Celesta, ayant reçu la tâche de créer une «vue matérialisée», a analysé la requête et créé des déclencheurs sur la table OrderLine
qui mettent à jour OrderedQty
. En insérant un seul mot-clé - materialized
- dans le fichier CelestaSQL, nous avons résolu le problème de dégradation des performances, et le code logique métier n'a même pas eu besoin d'être changé!
, , , . «» Celesta , , JOIN-, GROUP BY. , , , , . . .
Conclusion
Celesta. — .