Recherche

Effacer le filtre
Article
Lorenzo Scalese · Mai 30, 2022

Modèle entité-attribut-valeur dans les bases de données relationnelles. Faut-il émuler les globales dans les tables ? Partie 1

## Introduction Dans le premier article de cette série, nous examinerons le [modèle entité-attribut-valeur (EAV)](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model) dans les bases de données relationnelles pour voir comment il est utilisé et à quoi il sert. Ensuite, nous comparerons les concepts du modèle EAV aux globales. Parfois, on dispose d'objets comportant un nombre inconnu de champs, ou peut-être des champs hiérarchiquement imbriqués, pour lesquels, en règle générale, il faut effectuer une recherche. Par exemple, voici une boutique en ligne avec divers groupes de produits. Chaque groupe de produits a son propre ensemble de propriétés uniques et a également des propriétés communes. Par exemple, les disques SSD et les disques durs ont la propriété commune "capacité", mais tous deux ont également des propriétés uniques, "Endurance, TBW" pour les SSD et "temps moyen de positionnement de la tête" pour les disques durs. Dans certaines situations, le même produit, fabriqué par différents fabricants, possède des propriétés uniques. Ainsi, imaginons que nous ayons une boutique en ligne qui vend 50 groupes de marchandises différents. Chaque groupe de produits a ses cinq propriétés uniques, qui peuvent être numériques ou textuelles. Si nous créons une table dans lequel chaque produit possède 250 propriétés, alors que seules cinq d'entre elles sont réellement utilisées, non seulement nous augmentons considérablement (50 fois !) les exigences en matière d'espace disque, mais nous réduisons aussi considérablement les caractéristiques de vitesse de la base de données, puisque le cache sera encombré de propriétés inutiles et vides. Mais ce n'est pas tout. Chaque fois que nous ajoutons une nouvelle famille de produits avec ses propriétés propres, nous devons modifier la structure du tableau à l'aide de la commande **ALTER TABLE**. Sur les tables de grande taille, cette opération peut prendre des heures ou des jours, ce qui est inacceptable pour les entreprises. "Oui", remarquera le lecteur attentif, "mais nous pouvons utiliser une table différente pour chaque groupe de produits." Bien sûr, vous avez raison, mais cette approche nous donne une base de données avec des dizaines de milliers de tables pour un grand magasin, ce qui est difficile à administrer. De plus, le code, qui doit être pris en charge, devient de plus en plus complexe. D'autre part, il n'est pas nécessaire de modifier la structure de la base de données lors de l'ajout d'un nouveau groupe de produits. Il suffit d'ajouter une nouvelle table pour un nouveau groupe de produits. Dans tous les cas, les utilisateurs doivent être capables de rechercher facilement les produits dans un magasin, d'obtenir une table pratique des marchandises indiquant leurs propriétés actuelles et de comparer les produits. Comme vous pouvez l'imaginer, un formulaire de recherche comportant 250 champs serait extrêmement gênant pour l'utilisateur, tout comme le fait de voir 250 colonnes de propriétés diverses dans la table des produits alors que seulement cinq propriétés pour le groupe sont nécessaires. Il en va de même pour les comparaisons de produits. Une base de données marketing pourrait également servir comme un autre exemple utile. Pour chaque personne stockée dans la base, de nombreuses propriétés (souvent imbriquées) doivent être ajoutées, modifiées ou supprimées en permanence. Dans le passé, une personne peut avoir acheté quelque chose pour un certain coût, ou avoir acheté certains groupes de produits, avoir participé à un événement, avoir travaillé quelque part, avoir de la famille, vivre dans une certaine ville, appartenir à une certaine classe sociale, et ainsi de suite. Il pourrait y avoir des milliers de champs possibles, en constante évolution. Les spécialistes du marketing réfléchissent sans cesse à la manière de distinguer différents groupes de clients et de leur proposer des offres spéciales convaincantes. Pour résoudre ces problèmes et disposer en même temps d'une structure de base de données précise et définie, l'approche entité-attribut-valeur a été développée. ## Approche EAV L'essence de l'approche EAV est le stockage séparé des entités, des attributs et des valeurs d'attributs. En général, pour illustrer l'approche EAV, on utilise seulement trois tables, appelés Entité, Attribut et Valeur : ![](/sites/default/files/inline/images/images/simpleeav.png) La structure des données de démonstration que nous allons stocker. ![](/sites/default/files/inline/images/images/data_structure1.png) ## Implémentation de l'approche EAV à l'aide de tables Considérons maintenant un exemple plus complexe utilisant cinq tables (quatre si vous choisissez de consolider les deux derniers tables pour en faire un seul). ![](/sites/default/files/inline/images/images/db_struct.png) La première table est `Сatalog`: CREATE TABLE Catalog ( id INT, name VARCHAR (128), parent INT ); Cette table correspond en fait à l'Entité dans l'approche EAV. Elle permettra de stocker les sections du catalogue hiérarchique des marchandises. La deuxième table est ****`Field` : CREATE TABLE Field ( id INT, name VARCHAR (128), typeOf INT, searchable INT, catalog_id INT, table_view INT, sort INT ); Dans cette table, nous spécifions le nom de l'attribut, son type, et si l'attribut est recherchable. Nous indiquons également la section du catalogue qui contient les marchandises auxquelles ces propriétés appartiennent. Tous les produits de la section du catalogue de catalog_id ou inférieur peuvent avoir des propriétés différentes qui sont stockées dans cette table. La troisième table est `Good`.EIle est conçue pour stocker les marchandises, avec leurs prix, la quantité totale des marchandises, la quantité réservée des marchandises, et le nom des marchandises. En principe, vous n'avez pas vraiment besoin de cette table mais, à mon avis, il est utile d'avoir une table séparée pour les marchandises. CREATE TABLE Good ( id INT, name VARCHAR (128), price FLOAT, item_count INT, reserved_count, catalog_id INT ); La quatrième table (`TextValues`) et la cinquième table (`NumberValues`) sont conçues pour stocker les valeurs du texte et les attributs numériques des marchandises, et elles ont une structure similaire. CREATE TABLE TextValues ​​( good_id INT, field_id INT, fValue TEXT ); CREATE TABLE NumberValues ​​( good_id INT, field_id INT, fValue INT ); Au lieu des tables de valeurs textuelles et numériques, vous pouvez utiliser une seule table CustomValues avec une structure de ce type : CREATE TABLE CustomValues ​​( good_id INT, field_id INT, text_value TEXT, number_value INT ); Je préfère stocker les différents types de données séparément car cela augmente la vitesse et économise de l'espace. ## Accès aux données à l'aide de l'approche EAV Commençons par afficher le mappage de la structure du catalogue à l'aide de SQL : SELECT * FROM Catalog ORDER BY id; Afin de former un arbre à partir de ces valeurs, un code distinct est nécessaire. En PHP, cela ressemblerait à quelque chose comme ceci : $stmt = $ pdo-> query ('SELECT * FROM Catalog ORDER BY id'); $aTree = []; $idRoot = NULL; while ($row = $ stmt->fetch()) {     $aTree [$row ['id']] = ['name' => $ row ['name']];     if (! $row['parent'])       $idRoot = $row ['id'];     else       $aTree [$row['parent']] ['sub'] [] = $row['id']; } À l'avenir, nous pourrons simplement dessiner l'arbre si nous partons du nœud racine $aTree[$ idRoot]. Maintenant, nous allons obtenir les propriétés d'un produit spécifique.  Tout d'abord, nous allons obtenir une liste de propriétés spécifiques à ce produit, puis y attacher les propriétés qui sont dans la base de données. Dans la vie réelle, toutes les propriétés indiquées ne sont pas renseignées et nous sommes donc obligés d'utiliser LEFT JOIN : SELECT * FROM ( SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g INNER JOIN Field as f ON f.catalog_id = g.catalog_id LEFT JOIN TextValues ​​as val ON tv.good = g.id AND f.id = val.field_id WHERE g.id = $ nGood AND f.type_of = 'text' UNION SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g INNER JOIN Field as f ON f.catalog_id = g.catalog_id LEFT JOIN NumberValues ​​as val ON val.good = g.id AND f.id = val.field_id WHERE g.id = $nGood AND f.type_of = 'number' ) t ORDER BY t.sort; Si nous utilisons une seule table pour stocker les valeurs numériques et textuelles, la requête est considérablement simplifiée : SELECT g. *, F.name, f.type_of, val.text_value, val.number_value, f.sort FROM Good as g INNER JOIN Field as f ON f.catalog = g.catalog LEFT JOIN CustomValues ​​as val ON tv.good = g.id AND f.id = val.field_id WHERE g.id = $nGood ORDER BY f.sort; Maintenant, nous allons obtenir les produits sous la forme de table contenue dans la section du catalogue $nCatalog. Tout d'abord, nous obtenons une liste de propriétés qui doivent être reflétées dans la vue de la table pour cette section du catalogue : SELECT f.id, f.name, f.type_of FROM Catalog as c INNER JOIN Field as f ON f.catalog_id = c.id WHERE c.id = $nCatalog AND f.table_view = 1 ORDER BY f.sort; Ensuite, nous construisons la requête pour créer la table. Supposons que pour une vue tabulaire, nous ayons besoin de trois propriétés supplémentaires (sans compter celles de la table Good). Pour simplifier la requête, nous supposons que : SELECT g.if, g.name, g.price,             f1.fValue as f1_val,             f2.fValue as f2_val,             f3.fValue as f3_val, FROM Good LEFT JOIN TextValue as f1 ON f1.good_id = g.id LEFT JOIN NumberValue as f2 ON f2.good_id = g.id LEFT JOIN NumberValue as f3 ON f3.good_id = g.id WHERE g.catalog_id = $nCatalog; ## Les avantages et les inconvénients de l'approche EAV L'avantage évident de l'approche EAV est sa flexibilité. Avec des structures de données fixes telles que les tables, nous pouvons nous permettre de stocker une grande variété d'ensembles de propriétés pour les objets. Et nous pouvons stocker différentes structures de données sans modifier le schéma de la base de données.  Nous pouvons également utiliser SQL, qui est familier à un grand nombre de développeurs.  Le défaut le plus évident est l'inadéquation entre la structure logique des données et leur stockage physique, qui entraîne diverses difficultés.  En outre, la programmation implique souvent des requêtes SQL très complexes. Le débogage peut être difficile car vous devez créer des outils non-standards pour visualiser les données EAV. Enfin, vous pouvez être amené à utiliser des requêtes LEFT JOIN, qui ralentissent la base de données. ## Globales : Une alternative à EAV Comme je suis familier à la fois du monde SQL et du monde des globales, j'ai eu l'idée que l'utilisation des globales pour les tâches résolues par l'approche EAV serait beaucoup plus intéressante. Les globales sont des structures de données qui vous permettent de stocker des informations dispersées et hiérarchiques. Un point très important est que les globales sont soigneusement optimisées pour le stockage d'informations hiérarchiques. Les globales sont elles-mêmes des structures de niveau inférieur aux tables, ce qui leur permet de travailler beaucoup plus rapidement que ces derniers. Dans le même temps, la structure de globale elle-même peut être sélectionnée en fonction de la structure des données, ce qui rend le code très simple et clair. ## Structure de globale pour le stockage des données démographiques Une globale représente une structure tellement flexible et élégante pour le stockage des données que nous pourrions nous débrouiller avec une seule globale pour le stockage des données dans les sections du catalogue, les propriétés et les produits, par exemple, de la manière suivante : ![](/sites/default/files/inline/images/images/1global.png) Remarquez à quel point la structure de globale est similaire à la structure de données. Cette conformité simplifie grandement le codage et le débogage. En pratique, il est préférable d'utiliser plusieurs globales, bien que la tentation de stocker toutes les informations dans une seule globale soit assez forte. Il est judicieux de créer des globales distinctes pour les indices. Vous pouvez également séparer le stockage de la structure de la partition du répertoire des marchandises. ## Quelle est la suite ? Dans le deuxième article de cette série, nous aborderons les détails et les avantages du stockage des données dans des globales InterSystems Iris au lieu de suivre le modèle EAV.
Article
Iryna Mykhailova · Juil 1, 2022

Transmission de fichiers via REST pour les stocker en Property, partie 3

Dans le premier article de cette série, nous avons vu comment lire un "gros" volume de données dans le corps brut d'une méthode HTTP POST et l'enregistrer dans une base de données en tant que propriété de flux d'une classe. Le deuxième article explique comment enregistrer des fichiers et leurs noms dans un format JSON. Examinons maintenant de plus près l'idée d'envoyer des fichiers volumineux par parties au niveau du serveur. Il existe plusieurs approches que nous pouvons utiliser pour y parvenir. Cet article traite de l'utilisation de l'en-tête Transfer-Encoding pour indiquer un transfert par blocs. La spécification HTTP/1.1 a introduit l'en-tête Transfer-Encoding, et RFC 7230, section 4.1 l'a décrit, mais il n'est pas mentionné dans la spécification HTTP/2.  Transfer-Encoding  L'objectif de l'en-tête Transfer-Encoding est de spécifier la forme d'encodage utilisée pour transférer le corps de la charge utile à l'utilisateur en toute sécurité. Vous utilisez cet en-tête principalement pour délimiter avec précision une charge utile générée dynamiquement et pour distinguer les codages de charge utile pour l'efficacité du transport ou la sécurité des caractéristiques de la ressource sélectionnée.   Vous pouvez utiliser les valeurs suivantes dans cet en-tête :   Chunked (en blocs)   Compress (compresser)   Deflate (dégonfler)   gzip  Le codage de transfert est égal à l'encodage par blocs   Lorsque vous définissez le codage de transfert par blocs, le corps du message est constitué d'un nombre non spécifié de blocs réguliers, d'un bloc de fin, d'un trailer et du caractère retour chariot (CRLF).   Chaque partie commence par une dimension de bloc représentée par un nombre hexadécimal, suivie d'une extension facultative et d'un CRLF. Ensuite vient le corps du bloc avec CRLF à la fin. Les extensions contiennent les métadonnées du bloc. Par exemple, les métadonnées peuvent inclure une signature, un hachage, des informations de contrôle à mi-message, etc. Le bloc de fin de message est un bloc régulier de longueur nulle. Un trailer, qui consiste en des champs d'en-tête (éventuellement vides), suit le block de fin.   Pour rendre tout cela plus facile à imaginer, voici la structure d'un message avec Transfer-Encoding = par blocs :  Voici un exemple de message court et découpé en blocs :  13\r\n Transferring Files \r\n 4\r\n on\r\n 1A\r\n community.intersystems.com 0\r\n \r\n Ce corps de message se compose de trois blocs significatifs. Le premier bloc a une longueur de dix-neuf octets, le deuxième en a quatre et le troisième en a vingt-six. Vous pouvez constater que les CRLF de fin qui marquent la fin des blocs ne sont pas pris en compte dans la taille du bloc. Mais si vous utilisez le CRLF comme marqueur de fin de ligne (EOL), alors le CRLF est compté comme une partie du message et prend deux octets. Le message décodé ressemble à ceci :  Transferring Files on community.intersystems.com Formation de messages groupés dans IRIS  Pour ce tutoriel, nous allons utiliser la méthode sur le serveur créé dans le premier article. Cela signifie que nous allons envoyer le contenu du fichier directement dans le corps de la méthode POST. Comme nous envoyons le contenu du fichier dans le corps, nous envoyons le POST à http://webserver/RestTransfer/file.  Maintenant, voyons comment nous pouvons former un message en bloc dans IRIS. Comme indiqué dans Envoi de requêtes HTTP, à la section Envoi d'une requête par blocs, vous pouvez envoyer une requête HTTP par blocs si vous utilisez HTTP/1.1. La meilleure partie de ce processus est que %Net.HttpRequest calcule automatiquement la longueur du contenu de l'ensemble du corps du message côté serveur, de sorte qu'il n'est pas nécessaire de modifier le côté serveur du tout. Par conséquent, pour envoyer une requête en bloc, vous devez suivre ces étapes dans le client uniquement.  La première étape consiste à créer une sous-classe de %Net.ChunkedWriter et à implémenter la méthode OutputStream. Cette méthode doit recevoir un flux de données, l'examiner, décider s'il faut le diviser en parties ou non, et comment le diviser, et invoquer les méthodes héritées de la classe pour écrire la sortie. Dans notre cas, nous appellerons la classe RestTransfer.ChunkedWriter.  Ensuite, dans la méthode côté client responsable de l'envoi des données (appelée ici "SendFileChunked"), vous devez créer une instance de la classe RestTransfer.ChunkedWriter et la remplir avec les données demandées que vous souhaitez envoyer. Comme nous envoyons des fichiers, nous ferons tout le travail dans la classe RestTransfer.ChunkedWriter. Nous ajoutons une propriété nommée Filename As %String et un paramètre nommé “MAXSIZEOFCHUNK = 10000.” Bien sûr, vous pouvez décider de définir une dimension maximale autorisée pour le bloc en tant que propriété et la définir pour chaque fichier ou message.  Enfin, définissez la propriété EntityBody de %Net.HttpRequest comme étant égale à l'instance créée de la classe RestTransfer.ChunkedWriter.  Ces étapes correspondent simplement au nouveau code que vous devez écrire et remplacer dans votre méthode existante qui envoie des fichiers à un serveur.  La méthode ressemble à ceci :  ClassMethod SendFileChunked(aFileName) As %Status { Set sc = $$$OK Set request = ..GetLink() set cw = ##class(RestTransfer.ChunkedWriter).%New() set cw.Filename = aFileName set request.EntityBody = cw set sc = request.Post("/RestTransfer/file") Quit:$System.Status.IsError(sc) sc Set response=request.HttpResponse do response.OutputToDevice() Quit sc } La classe %Net.ChunkedWriter est une classe de flux abstraite qui fournit une interface et possède quelques méthodes et propriétés implémentées. Ici, nous utilisons les propriétés et méthodes suivantes :  La propriété TranslateTable en tant que %String force la traduction automatique des blocs lors de leur écriture dans le flux de sortie (EntityBody). Nous nous attendons à recevoir des données brutes, nous devons donc définir TranslateTable sur "RAW".  La méthode OutputStream est une méthode abstraite surchargée par une sous-classe pour faire tout le découpage en blocs.  La méthode WriteSingleChunk(buffer As %String) écrit l'en-tête HTTP Content-Length suivi du corps de l'entité en un seul bloc. Nous vérifions si la dimension du fichier est inférieure à la méthode MAXSIZEOFCHUNK, auquel cas, nous utilisons cette méthode.  La méthode WriteFirstChunk(buffer As %String) écrit l'en-tête Transfer-Encoding suivi du premier bloc. Il doit toujours être présent. Il peut être suivi de zéro ou plusieurs appels pour écrire d'autres blocs, puis d'un appel obligatoire pour écrire le dernier bloc avec la chaîne vide. Nous vérifions que la longueur du fichier est supérieure à la méthode MAXSIZEOFCHUNK et appelons cette méthode.  La méthode WriteChunk(buffer As %String) écrit des blocs conséquents. Vérifiez si le reste du fichier après le premier bloc est toujours supérieur à MAXSIZEOFCHUNK puis utilisez cette méthode pour envoyer les données. Nous continuons à le faire jusqu'à ce que la taille de la dernière partie du fichier soit inférieure à MAXSIZEOFCHUNK.   La méthode WriteLastChunk(buffer As %String) écrit le dernier bloc suivi d'un bloc de longueur zéro pour marquer la fin des données.   Sur la base de tout ce qui précède, notre classe RestTransfer.ChunkedWriter est comme suit :  Class RestTransfer.ChunkedWriter Extends %Net.ChunkedWriter { Parameter MAXSIZEOFCHUNK = 10000; Property Filename As %String; Method OutputStream() { set ..TranslateTable = "RAW" set cTime = $zdatetime($Now(), 8, 1) set fStream = ##class(%Stream.FileBinary).%New() set fStream.Filename = ..Filename set size = fStream.Size if size < ..#MAXSIZEOFCHUNK { set buf = fStream.Read(.size, .st) if $$$ISERR(st) { THROW st } else { set ^log(cTime, ..Filename) = size do ..WriteSingleChunk(buf) } } else { set ^log(cTime, ..Filename, 0) = size set len = ..#MAXSIZEOFCHUNK set buf = fStream.Read(.len, .st) if $$$ISERR(st) { THROW st } else { set ^log(cTime, ..Filename, 1) = len do ..WriteFirstChunk(buf) } set i = 2 While 'fStream.AtEnd { set len = ..#MAXSIZEOFCHUNK set temp = fStream.Read(.len, .sc) if len<..#MAXSIZEOFCHUNK { do ..WriteLastChunk(temp) } else { do ..WriteChunk(temp) } set ^log(cTime, ..Filename, i) = len set i = $increment(i) } } } } Pour voir comment ces méthodes divisent le fichier en parties, nous ajoutons une globale ^log avec la structure suivante :  //for transfer in a single chunk ^log(time, filename) = size_of_the_file //pour un transfert en plusieurs blocs ^log(time, filename, 0) = size_of_the_file ^log(time, filename, idx) = size_of_the_idx’s_chunk Maintenant que la programmation est terminée, voyons comment ces trois approches fonctionnent pour différents fichiers. Nous écrivons une simple méthode de classe pour faire des appels au serveur :  ClassMethod Run() { // D'abord, je supprime les globales. kill ^RestTransfer.FileDescD kill ^RestTransfer.FileDescS // Ensuite, je forme une liste des fichiers que je veux envoyer for filename = "D:\Downloads\wiresharkOutput.txt", // 856 bytes "D:\Downloads\wiresharkOutput.pdf", // 60 134 bytes "D:\Downloads\Wireshark-win64-3.4.7.exe", // 71 354 272 bytes "D:\Downloads\IRIS_Community-2021.1.0.215.0-win_x64.exe" //542 370 224 bytes { write !, !, filename, !, ! // Et je lance les trois méthodes d'envoi de données au serveur. set resp1=##class(RestTransfer.Client).SendFileChunked(filename) if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1) set resp1=##class(RestTransfer.Client).SendFile(filename) if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1) set resp1=##class(RestTransfer.Client).SendFileDirect(filename) if $$$ISERR(resp1) do $System.OBJ.DisplayError(resp1) } } Après avoir exécuté la méthode de classe Run, dans la sortie des trois premiers fichiers, l'état était correct. Mais pour le dernier fichier, alors que les premier et dernier appels ont fonctionné, celui du milieu a renvoyé une erreur : 5922, Dépassement de délai en attente de réponse. Si nous examinons notre méthode des globales, nous voyons que le code n'a pas enregistré le onzième fichier. Cela signifie que ##class(RestTransfer.Client).SendFile(filename) a échoué - ou pour être précis, la méthode qui déballe les données de JSON n'a pas réussi.   Maintenant, si nous regardons nos flux, nous voyons que tous les fichiers enregistrés avec succès ont des dimensions correctes.  Si nous regardons la globale ^log, nous voyons combien de blocs le code a créé pour chaque fichier :  Vous aimeriez probablement voir le corps des messages réels. Eduard Lebedyuk a suggéré dans son article Debugging Web qu'il est possible d'utiliser CSP Gateway Logging and Tracing.  Si nous examinons le journal des événements pour le deuxième fichier découpé en blocs, nous constatons que la valeur de l'en-tête Transfer-Encoding est effectivement "découpé en blocs". Malheureusement, le serveur a déjà collé le message ensemble, donc nous ne voyons pas le découpage en blocks réel.  L'utilisation de la fonction Trace ne montre pas beaucoup plus d'informations, mais elle permet de clarifier la présence d'un écart entre l'avant-dernière et la dernière demande.  Pour voir les parties réelles des messages, nous copions le client sur un autre ordinateur pour utiliser un renifleur. Ici, nous avons choisi d'utiliser Wireshark car il est gratuit et possède les fonctions nécessaires. Pour mieux vous montrer comment le code divise le fichier en blocs, nous pouvons changer la valeur de MAXSIZEOFCHUNK à 100 et choisir d'envoyer un petit fichier. Ainsi, nous pouvons maintenant voir le résultat suivant :  Nous constatons que la longueur de tous les blocs, sauf les deux derniers, est égale à 64 en HEX (100 en DEC), que le dernier bloc contenant des données est égal à 21 DEC (15 en HEX) et que la dimension du dernier bloc est égale à zéro. Tout semble correct et conforme à la spécification. La longueur totale du fichier est égale à 421 (4x100+1x21), ce que nous pouvons également voir dans les globales :  Coonclusion  Dans l'ensemble, nous pouvons constater que cette approche fonctionne et permet d'envoyer sans problème de gros fichiers au serveur. En outre, si vous envoyez de grandes quantités de données à un client, vous pouvez vous familiariser avec Fonctionnement et configuration de la passerelle Web, section Paramètres de configuration du chemin d'application, paramètre Notification de la dimension de la réponse. Celui-ci spécifie le comportement de la passerelle Web lors de l'envoi de grandes quantités de données en fonction de la version de HTTP utilisée.  Le code de cette approche est ajouté à la version précédente de cet exemple sur GitHub et InterSystems Open Exchange.  À propos de l'envoi de fichiers en blocs, il est également possible d'utiliser l'en-tête Content-Range avec ou sans l'en-tête Transfer-Encoding pour indiquer quelle partie exacte des données est transférée. En outre, vous pouvez utiliser un tout nouveau concept de flux disponible avec la spécification HTTP/2.  Comme toujours, si vous avez des questions ou des suggestions, n'hésitez pas à les écrire dans la section des commentaires.
Article
Iryna Mykhailova · 23 hr il y a

Exemple de remplacement du processus de transformation SDA en FHIR pour inclure le paramètre « RequestMethod »

Lors de la création d'un bundle à partir de données héritées, je (et d'autres) souhaitais pouvoir contrôler si les ressources étaient générées avec une méthode de requête FHIR PUT plutôt qu'avec la méthode POST codée en dur. J'ai étendu les deux classes responsables de la transformation de SDA en FHIR dans une production d'interopérabilité afin de prendre en charge un paramètre permettant à l'utilisateur de contrôler la méthode de requête. La première classe est la classe Processus métier. Elle inclut un nouveau paramètre exposé dans l'onglet « Paramètres » de l'interface d'interopérabilité, appelé FHIRRequestMethod. Elle doit également transmettre la propriété FHIRRequestMethod à la méthode de classe de transformation en tant que paramètre. Class Demo.FHIR.DTL.Util.HC.SDA.FHIR.ProcessV2 Extends HS.FHIR.DTL.Util.HC.SDA3.FHIR.Process { Parameter SETTINGS = "FHIRRequestMethod:Basic"; /// Cette propriété peut remplacer la méthode de requête générée avec chaque ressource FHIR. <br> /// Cette propriété s'applique uniquement aux nouvelles ressources qui ne possèdent pas d'identifiant issu des données sources. Property FHIRRequestMethod As %String(MAXLEN = 10) [ InitialExpression = "POST" ]; /// Il s'agit d'une méthode d'instance car elle doit envoyer SendSync à un hôte professionnel et obtenir la réponse de l'hôte. Method ProcessSDARequest(pSDAStream, pSessionApplication As %String, pSessionId As %String, pPatientResourceId As %String = "") As %Status { New %HSIncludeTimeZoneOffsets Set %HSIncludeTimeZoneOffsets = 1 Set tSC = $$$OK Try { // Vérifiez la classe de base de l'hôte métier cible. Déterminez s'il s'agit d'un hôte métier FHIRServer Interop ou non. If '$Data(%healthshare($$$CurrentClass, "isInteropHost"))#10 { $$$ThrowOnError(##class(HS.Director).OpenCurrentProduction(.tProdObj)) Set tClassName = "" For i = 1:1:tProdObj.Items.Count() { If tProdObj.Items.GetAt(i).Name = ..TargetConfigName { Set tClassName = tProdObj.Items.GetAt(i).ClassName Quit } } Kill tProdObj Set tIsInteropHost = 0 Set tRequiredHostBases("HS.FHIRServer.Interop.Operation") = "" Set tRequiredHostBases("HS.FHIRServer.Interop.HTTPOperation") = "" Set tHostBase = "" For { Set tHostBase = $Order(tRequiredHostBases(tHostBase)) If tHostBase="" Quit If $ClassMethod(tClassName, "%IsA", tHostBase) { Set tIsInteropHost = 1 Quit } } Set %healthshare($$$CurrentClass, "isInteropHost") = tIsInteropHost } Else { Set tIsInteropHost = %healthshare($$$CurrentClass, "isInteropHost") } // Obtenir l'hôte et le port du serveur Web de l'instance actuelle, à utiliser pour renseigner l'en-tête // HOST du message de requête FHIR. L'en-tête HOST est nécessaire dans le message de requête FHIR lorsque // le message est acheminé pour traitement en production locale, contrairement à sa transmission à un serveur externe. Do ..GetHostAndPort(.tHost, .tPort) Set tLocalHostAndPort = tHost_$Select(tPort'="":":",1:"")_tPort If ..FHIRFormat="JSON" { Set tMessageContentType = "application/fhir+json" } ElseIf ..FHIRFormat="XML" { Set tMessageContentType = "application/fhir+xml" } Set tFHIRMetadataSetKey = $ZStrip($Piece(..FHIRMetadataSet, "/", 1), "<>W") Set tSchema = ##class(HS.FHIRServer.Schema).LoadSchema(tFHIRMetadataSetKey) If '..FormatFHIROutput { Set tIndentChars = "" Set tLineTerminator = "" Set tFormatter = "" } Else { Set tIndentChars = $Char(9) Set tLineTerminator = $Char(13,10) Set tFormatter = ##class(%JSON.Formatter).%New() Set tFormatter.IndentChars = tIndentChars Set tFormatter.LineTerminator = tLineTerminator } #dim tTransformObj As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR Set tTransformObj = $ClassMethod(..TransformClass, "TransformStream", pSDAStream, "HS.SDA3.Container", tFHIRMetadataSetKey, pPatientResourceId, "", ..FHIRRequestMethod) // tTransformObj.bundle est a %DynamicObject. Set tBundleObj = tTransformObj.bundle $$$HSTRACE("Bundle object", "tBundleObj", tBundleObj.%ToJSON()) // « individual » n'est pas un type de transaction ni une interaction. // Ce mode entraîne l'envoi de chaque entrée du Bundle // à TargetConfigName individuellement, et non en tant que transaction. If ..TransmissionMode="individual" { For i = 0:1:tBundleObj.entry.%Size()-1 { If tIsInteropHost { Set tSC = ..CreateAndSendInteropMessage(tBundleObj.entry.%Get(i), tSchema, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId) } Else { Set tSC = ..CreateAndSendFHIRMessage(tBundleObj.entry.%Get(i), tSchema, tLocalHostAndPort, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId) } } } Else { If tIsInteropHost { Set tSC = ..CreateAndSendInteropMessage(tBundleObj, tSchema, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId) } Else { Set tSC = ..CreateAndSendFHIRMessage(tBundleObj, tSchema, tLocalHostAndPort, tMessageContentType, tFormatter, tIndentChars, tLineTerminator, pSessionApplication, pSessionId) } } } Catch eException { Set tSC = eException.AsStatus() } Quit tSC } Storage Default { <Data name="ProcessV2DefaultData"> <Subscript>"ProcessV2"</Subscript> <Value name="1"> <Value>FHIRRequestMethod</Value> </Value> </Data> <DefaultData>ProcessV2DefaultData</DefaultData> <Type>%Storage.Persistent</Type> } } La deuxième classe est la classe de transformation, pour laquelle nous devons également ajouter une nouvelle propriété pour stocker le paramètre FHIRRequestMethod. La valeur de FHIRRequestMethod provient de l'appel de la méthode de classe à ..TransformStream. Une fois que ce paramètre du processus métier est passé à ..TransformStream, je le stocke dans la propriété de classe afin que toutes les méthodes de cette classe de transformation aient accès à la valeur. Class Demo.FHIR.DTL.Util.API.Transform.SDA3ToFHIRV2 Extends HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR { /// Propriété permettant de remplacer la méthode de requête pour les ressources non identifiées Property FHIRRequestMethod As %String(MAXLEN = 10); /// Transforme un flux SDA (conteneur ou classe SDA) vers la version FHIR spécifiée. Renvoie une instance /// de cette classe contenant une propriété « bundle ». Cette propriété contiendra un bundle FHIR avec /// toutes les ressources générées lors de la transformation et toutes les références résolues. Si /// <var>patientId</var> ou <var>encounterId</var> sont spécifiés, ces valeurs seront intégrées à toutes /// les références Patient et Encounter applicables. /// @API.Method /// @Argument stream %Stream representation of an SDA object or Container /// @Argument SDAClassname Classname for the object contained in the stream (eg. HS.SDA3.Container) /// @Argument fhirVersion Version of FHIR used by the resource, eg. "STU3", "R4" /// @Argument patientId (optional) FHIR resource id to be assigned to the Patient resource /// @Argument encounterId (optional) FHIR resource id to be assigned to the Encounter resource, if not transforming a Container ClassMethod TransformStream(stream As %Stream.Object, SDAClassname As %String, fhirVersion As %String, patientId As %String = "", encounterId As %String = "", FHIRRequestMethod As %String) As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR { set source = $classmethod(SDAClassname, "%New") if SDAClassname = "HS.SDA3.Container" { $$$ThrowOnError(source.InitializeXMLParse(stream, "SDA3")) } else { $$$ThrowOnError(source.XMLImportSDAString(stream.Read(3700000))) } return ..TransformObject(source, fhirVersion, patientId, encounterId, FHIRRequestMethod) } /// Transforme un objet SDA (conteneur ou classe SDA) vers la version FHIR spécifiée. Renvoie une instance /// de cette classe contenant une propriété « bundle ». Cette propriété contiendra un bundle FHIR avec /// toutes les ressources générées lors de la transformation et toutes les références résolues. Si /// <var>patientId</var> ou <var>encounterId</var> sont spécifiés, ces valeurs seront intégrées à toutes /// les références Patient et Encounter applicables. /// @API.Method /// @Argument source SDA object or Container /// @Argument fhirVersion Version of FHIR used by the resource, eg. "STU3", "R4" /// @Argument patientId (optional) FHIR resource id to be assigned to the Patient resource /// @Argument encounterId (optional) FHIR resource id to be assigned to the Encounter resource, if not transforming a Container ClassMethod TransformObject(source, fhirVersion As %String, patientId As %String = "", encounterId As %String = "", FHIRRequestMethod As %String) As HS.FHIR.DTL.Util.API.Transform.SDA3ToFHIR { set schema = ##class(HS.FHIRServer.Schema).LoadSchema(fhirVersion) set transformer = ..%New(schema) // Mise à jour à partir de la classe parent pour définir la méthode FHIRRequestMethod pour l'utilisation de n'importe quelle méthode de classe Set transformer.FHIRRequestMethod = FHIRRequestMethod //SDA obtient l'identifiant du patient et de la rencontre, tandis que le conteneur obtient uniquement l'identifiant du patient. //Parce qu'un conteneur peut avoir plusieurs rencontres et nous ne pouvons pas déterminer à laquelle il fait référence. if source.%ClassName(1) = "HS.SDA3.Container" { do transformer.TransformContainer(source, patientId) } else { do transformer.TransformSDA(source, patientId, encounterId) } return transformer } /// Vérifie que la ressource est FHIR valide, l'ajoute au bundle de sortie et renvoie une référence à cette ressource. /// La ressource est également générée sous forme de %DynamicObject. /// @Inputs /// source SDA object which created this resource /// resource Object model version of the resource /// resourceJson %DynamicObject version of the resource /// One of <var>resource</var> or <var>resourceJson</var> must be provided. If both are provided, /// the %DynamicObject representation will be given precedence Method AddResource(source As HS.SDA3.SuperClass, resource As %RegisteredObject = "", ByRef resourceJson As %DynamicObject = "") As HS.FHIR.DTL.vR4.Model.Base.Reference [ Internal ] { if '$isobject(resourceJson) { set resourceJson = ##class(%DynamicObject).%FromJSON(resource.ToJSON()) } try { do ..%resourceValidator.ValidateResource(resourceJson) } catch ex { do ..HandleInvalidResource(resourceJson, ex) return "" } set entry = ##class(%DynamicObject).%New() set entry.request = ##class(%DynamicObject).%New() set id = ..GetId(source, resourceJson) if id '= "" { set resourceJson.id = id } //Vérifiez un mappage identifiant SDA->id pour maintenir les références //Remarque : Provenance attribue un GUIDE à l'ID externe pour une utilisation interne, il ne s'agit pas d'un ID externe // et il ne devrait pas influencer l'attribution de l'ID set sourceIdentifier = "" if resourceJson.resourceType = "Encounter" { set sourceIdentifier = source.EncounterNumber } elseif ((source.%Extends("HS.SDA3.SuperClass")) && (resourceJson.resourceType '= "Provenance")) { set sourceIdentifier = source.ExternalId } if id = "" { if (resourceJson.resourceType = "Patient") && (..%patientId '= "") { set id = ..%patientId } elseif $get(..%resourceIds(resourceJson.resourceType)) '= "" { set id = ..%resourceIds(resourceJson.resourceType) } elseif (sourceIdentifier '= "") && $data(..%resourceIds(resourceJson.resourceType, sourceIdentifier)) { set id = ..%resourceIds(resourceJson.resourceType, sourceIdentifier) } if id '= "" { set resource.id = id set resourceJson.id = id } } if resourceJson.id '= "" { set id = resourceJson.id set entry.fullUrl = $select(..GetBaseURL()'="":..GetBaseURL() _ "/", 1:"") _ resourceJson.resourceType _ "/" _ resourceJson.id set entry.request.method = "PUT" set entry.request.url = resourceJson.resourceType _ "/" _ resourceJson.id } else { set id = $zconvert($system.Util.CreateGUID(), "L") set entry.fullUrl = "urn:uuid:" _ id // changed from parent class to accept parameter as input instead of hard coding "POST" set entry.request.method = ..FHIRRequestMethod set entry.request.url = resourceJson.resourceType } //Enregistrer les mappages d'identifiants pour un accès ultérieur if resourceJson.resourceType = "Patient" { set ..%patientId = id } elseif sourceIdentifier '= "" { set ..%resourceIds(resourceJson.resourceType, sourceIdentifier) = id } set duplicate = ..IsDuplicate(resourceJson, id) if duplicate '= "" { return duplicate } //Index pour la recherche O(1) si nécessaire pour le post-traitement set ..%resourceIndex(resourceJson.resourceType, id) = resourceJson set entry.resource = resourceJson do ..%bundle.entry.%Push(entry) return ..CreateReference(resourceJson.resourceType, id) } } Ces classes sont conçues pour être utilisées dans une production d'interopérabilité. La démonstration qui met en évidence la version de base de ces classes peut être trouvée ici : Learning Services: Converting Legacy Data to HL7 FHIR R4 in InterSystems IRIS for Health & Github Repo for Legacy To FHIR Transformation Demo
Article
Lorenzo Scalese · Avr 15, 2022

APM - Surveillance des performances des requêtes SQL

Depuis Caché 2017, le moteur SQL comprend un nouvel ensemble de statistiques. Celles-ci enregistrent le nombre de fois qu'une requête est exécutée et le temps qu'elle prend pour s'exécuter. C'est une mine d'or pour quiconque surveille et tente d'optimiser les performances d'une application qui comprend de nombreuses instructions SQL, mais il n'est pas aussi facile d'accéder aux données que certaines personnes le souhaitent. Cet article et l'exemple de code associé expliquent comment utiliser ces informations et comment extraire de manière routinière un résumé des statistiques quotidiennes et conserver un historique des performances SQL de votre application. Qu'est-ce qui est enregistré ? Chaque fois qu'une instruction SQL est exécutée, le temps pris est enregistré. Ce système est très léger et vous ne pouvez pas le désactiver. Pour minimiser les coûts, les statistiques sont conservées en mémoire et écrites sur disque périodiquement. Les données comprennent le nombre de fois qu'une requête a été exécutée dans la journée et le temps moyen et total nécessaire. Les données ne sont pas écrites sur le disque immédiatement, et après qu'elles aient été écrites, les statistiques sont mises à jour par une tâche "Update SQL query statistics" qui est généralement programmée pour s'exécuter une fois par heure. Cette tâche peut être lancée manuellement, mais si vous souhaitez voir les statistiques en temps réel tout en testant une requête, l'ensemble du processus requiert un peu de patience. Avertissement : Dans InterSystems IRIS 2019 et les versions antérieures, ces statistiques ne sont pas collectées pour le embedded SQL dans les classes ou les routines qui ont été déployées à l'aide du mécanisme %Studio.Project:Deploy . Rien ne sera cassé avec l'exemple de code, mais il pourrait vous tromper (il m'a trompé) en pensant que tout était OK parce que rien n'est apparu comme coûteux. Comment voyez-vous les informations ? Vous pouvez voir la liste des requêtes dans le portail de gestion. Passez à la page SQL et cliquez sur l'onglet "SQL Statements". C'est une bonne chose pour une nouvelle requête que vous exécutez et regardez, mais si des milliers de requêtes sont exécutées, cela peut devenir ingérable. L'alternative est d'utiliser SQL pour rechercher les requêtes. Les informations sont stockées dans des tableaux du schéma INFORMATION_SCHEMA. Ce schéma comporte un certain nombre de tableaux et j'ai inclus quelques exemples de requêtes SQL à la fin de cet article. Quand les statistiques sont-elles retirées ? Les données d'une requête sont supprimées chaque fois qu'elle est recompilée. Ainsi, pour les requêtes dynamiques, cela peut signifier que les requêtes en cache sont purgées. Pour le embedded SQL, cela signifie que la classe ou la routine dans laquelle le embedded SQL se trouve est recompilée. Sur un site actif, il est raisonnable de croire que les statistiques seront conservées pendant plus d'une journée, mais les tableaux contenant les statistiques ne peuvent pas être utilisés comme source de référence à long terme pour l'exécution de rapports ou d'analyses à long terme. Comment pouvez-vous résumer l'information ? Je recommande d'extraire les données chaque nuit dans des tableaux permanents avec lesquels il est plus facile de travailler pour générer des rapports de performance. Il est possible que certaines informations soient perdues si les classes sont compilées pendant la journée, mais il est peu probable que cela fasse une réelle différence dans votre analyse des requêtes lentes. Le code ci-dessous est un exemple illustrant comment vous pourriez extraire les statistiques dans un résumé quotidien pour chaque requête. Il comprend trois classes courtes : * Une tâche qui doit être exécutée chaque nuit. * DRL.MonitorSQL est une classe principale qui extrait et stocke les données des tableaux INFORMATION_SCHEMA. La troisième classe DRL.MonitorSQLText est une optimisation qui permet de stocker le texte de la requête (potentiellement long) une seule fois et de ne stocker que le hashage de la requête dans les statistiques pour chaque jour. Notes concernant l'exemple La tâche extrait le jour précédent et doit donc être programmée peu après minuit. Vous pouvez exporter plus de données historiques si elles existent. Pour extraire les 120 derniers jours Faites ##class(DRL.MonitorSQL).Capture($h-120,$h-1) Le code d'exemple lit directement le global ^rIndex car les premières versions des statistiques n'exposaient pas la Date à SQL. La variante que j'ai incluse boucle sur tous les espaces de noms de l'instance, mais cela n'est pas toujours approprié. Comment faire une requête sur les données extraites Une fois les données extraites, vous pouvez trouver les requêtes les plus lourdes en exécutant SELECT top 20 S.RunDate,S.RoutineName,S.TotalHits,S.SumpTIme,S.Hash,t.QueryText from DRL.MonitorSQL S left join DRL.MonitorSQLText T on S.Hash=T.Hash where RunDate='08/25/2019' order by SumpTime desc De plus, si vous choisissez le hachage pour une requête coûteuse, vous pouvez voir l'historique de cette requête avec SELECT S.RunDate,S.RoutineName,S.TotalHits,S.SumpTIme,S.Hash,t.QueryText from DRL.MonitorSQL S left join DRL.MonitorSQLText T on S.Hash=T.Hash where S.Hash='CgOlfRw7pGL4tYbiijYznQ84kmQ=' order by RunDate Au début de l'année, j'ai analysé les données d'un site en direct et j'ai examiné les requêtes les plus coûteuses. Une requête durait en moyenne moins de 6 secondes mais était appelée 14 000 fois par jour, ce qui représentait près de 24 heures de temps écoulé chaque jour. En fait, un noyau était entièrement occupé par cette seule requête. Pire encore, la deuxième requête qui prend une heure était une variation de la précédente requête. RunDate RoutineName Nombre total de visites Total Time Hash QueryText (en abrégé) 03/16/2019 14,576 85,094 5xDSguu4PvK04se2pPiOexeh6aE= DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4) … 03/16/2019 15,552 3,326 rCQX+CKPwFR9zOplmtMhxVnQxyw= DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4) , … 03/16/2019 16,892 597 yW3catzQzC0KE9euvIJ+o4mDwKc= DECLARE C CURSOR FOR SELECT * INTO :%col(1) , :%col(2) , :%col(3) , :%col(4) , :%col(5) , :%col(6) , :%col(7) , 03/16/2019 16,664 436 giShyiqNR3K6pZEt7RWAcen55rs= DECLARE C CURSOR FOR SELECT * , TKGROUP INTO :%col(1) , :%col(2) , :%col(3) , .. 03/16/2019 74,550 342 4ZClMPqMfyje4m9Wed0NJzxz9qw= DECLARE C CURSOR FOR SELECT … Tableau 1: Résultats réels sur le site du client Les tableaux du schéma INFORMATION_SCHEMA En plus des statistiques, les tableaux de ce schéma gardent la trace de l'endroit où les requêtes, les colonnes, les indices, etc. sont utilisés. En général, l'instruction SQL est un tableau de départ et il est joint par quelque chose comme "Statements.Hash=OtherTable.Statement". La requête équivalente pour accéder directement à ces tableaux afin de trouver les requêtes les plus coûteuses pour une journée serait... SELECT DS.Day,Loc.Location,DS.StatCount,DS.StatTotal,S.Statement,S.Hash FROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS DS left join INFORMATION_SCHEMA.STATEMENTS S on S.Hash=DS.Statement left join INFORMATION_SCHEMA.STATEMENT_LOCATIONS Loc on S.Hash=Loc.Statement where Day='08/26/2019' order by DS.stattotal desc Que vous envisagiez ou non de mettre en place un processus plus systématique, je recommande à tous ceux qui ont une grande application utilisant SQL de lancer aujourd'hui cette requête. Si une requête particulière apparaît comme coûteuse, vous pouvez obtenir l'historique en exécutant SELECT DS.Day,Loc.Location,DS.StatCount,DS.StatTotal,S.Statement,S.Hash FROM INFORMATION_SCHEMA.STATEMENT_DAILY_STATS DS left join INFORMATION_SCHEMA.STATEMENTS S on S.Hash=DS.Statement left join INFORMATION_SCHEMA.STATEMENT_LOCATIONS Loc on S.Hash=Loc.Statement where S.Hash='jDqCKaksff/4up7Ob0UXlkT2xKY=' order by DS.Day Exemple de code pour l'extraction quotidienne de statistiques Clause de non-responsabilité standard - cet exemple est fourni à titre d'illustration uniquement. Son fonctionnement n'est ni pris en charge ni garanti. Class DRL.MonitorSQLTask Extends %SYS.Task.Definition{Parameter TaskName = "SQL Statistics Summary";Method OnTask() As %Status{ set tSC=$$$OK TRY { do ##class(DRL.MonitorSQL).Run() } CATCH exp { set tSC=$SYSTEM.Status.Error("Error in SQL Monitor Summary Task") } quit tSC }} Class DRL.MonitorSQLText Extends %Persistent{/// Hash of query textProperty Hash As %String; /// query text for hashProperty QueryText As %String(MAXLEN = 9999);Index IndHash On Hash [ IdKey, Unique ];} /// Summary of very low cost SQL query statistics collected in Cache 2017.1 and later. /// Refer to documentation on "SQL Statement Details" for information on the source data. /// Data is stored by date and time to support queries over time. /// Typically run to summarise the SQL query data from the previous day.Class DRL.MonitorSQL Extends %Persistent{/// RunDate and RunTime uniquely identify a runProperty RunDate As %Date;/// Time the capture was started/// RunDate and RunTime uniquely identify a runProperty RunTime As %Time;/// Count of total hits for the time period for Property TotalHits As %Integer;/// Sum of pTimeProperty SumPTime As %Numeric(SCALE = 4);/// Routine where SQL is foundProperty RoutineName As %String(MAXLEN = 1024);/// Hash of query textProperty Hash As %String;Property Variance As %Numeric(SCALE = 4);/// Namespace where queries are runProperty Namespace As %String;/// Default run will process the previous days data for a single day./// Other date range combinations can be achieved using the Capture method.ClassMethod Run(){ //Each run is identified by the start date / time to keep related items together set h=$h-1 do ..Capture(+h,+h)}/// Captures historic statistics for a range of datesClassMethod Capture(dfrom, dto){ set oldstatsvalue=$system.SQL.SetSQLStatsJob(-1) set currNS=$znspace set tSC=##class(%SYS.Namespace).ListAll(.nsArray) set ns="" set time=$piece($h,",",2) kill ^||TMP.MonitorSQL do { set ns=$o(nsArray(ns)) quit:ns="" use 0 write !,"processing namespace ",ns zn ns for dateh=dfrom:1:dto { set hash="" set purgedun=0 do { set hash=$order(^rINDEXSQL("sqlidx",1,hash)) continue:hash="" set stats=$get(^rINDEXSQL("sqlidx",1,hash,"stat",dateh)) continue:stats="" set ^||TMP.MonitorSQL(dateh,ns,hash)=stats &SQL(SELECT Location into :tLocation FROM INFORMATION_SCHEMA.STATEMENT_LOCATIONS WHERE Statement=:hash) if SQLCODE'=0 set Location="" set ^||TMP.MonitorSQL(dateh,ns,hash,"Location")=tLocation &SQL(SELECT Statement INTO :Statement FROM INFORMATION_SCHEMA.STATEMENTS WHERE Hash=:hash) if SQLCODE'=0 set Statement="" set ^||TMP.MonitorSQL(dateh,ns,hash,"QueryText")=Statement } while hash'="" } } while ns'="" zn currNS set dateh="" do { set dateh=$o(^||TMP.MonitorSQL(dateh)) quit:dateh="" set ns="" do { set ns=$o(^||TMP.MonitorSQL(dateh,ns)) quit:ns="" set hash="" do { set hash=$o(^||TMP.MonitorSQL(dateh,ns,hash)) quit:hash="" set stats=$g(^||TMP.MonitorSQL(dateh,ns,hash)) continue:stats="" // The first time through the loop delete all statistics for the day so it is re-runnable // But if we run for a day after the raw data has been purged, it will wreck eveything // so do it here, where we already know there are results to insert in their place. if purgedun=0 { &SQL(DELETE FROM websys.MonitorSQL WHERE RunDate=:dateh ) set purgedun=1 } set tObj=##class(DRL.MonitorSQL).%New() set tObj.Namespace=ns set tObj.RunDate=dateh set tObj.RunTime=time set tObj.Hash=hash set tObj.TotalHits=$listget(stats,1) set tObj.SumPTime=$listget(stats,2) set tObj.Variance=$listget(stats,3) set tObj.Variance=$listget(stats,3) set queryText=^||TMP.MonitorSQL(dateh,ns,hash,"QueryText") set tObj.RoutineName=^||TMP.MonitorSQL(dateh,ns,hash,"Location") &SQL(Select ID into :TextID from DRL.MonitorSQLText where Hash=:hash) if SQLCODE'=0 { set textref=##class(DRL.MonitorSQLText).%New() set textref.Hash=tObj.Hash set textref.QueryText=queryText set sc=textref.%Save() } set tSc=tObj.%Save() //avoid dupicating the query text in each record because it can be very long. Use a lookup //table keyed on the hash. If it doesn't exist add it. if $$$ISERR(tSc) do $system.OBJ.DisplayError(tSc) if $$$ISERR(tSc) do $system.OBJ.DisplayError(tSc) } while hash'="" } while ns'="" } while dateh'="" do $system.SQL.SetSQLStatsJob(0)}Query Export(RunDateH1 As %Date, RunDateH2 As %Date) As %SQLQuery{SELECT S.Hash,RoutineName,RunDate,RunTime,SumPTime,TotalHits,Variance,RoutineName,T.QueryText FROM DRL.MonitorSQL S LEFT JOIN DRL.MonitorSQLText T on S.Hash=T.Hash WHERE RunDate>=:RunDateH1 AND RunDate<=:RunDateH2}}
Article
Lorenzo Scalese · Mai 16, 2022

Création d'index personalisé dans Caché

Les modèles de données objet et relationnel de la base de données Caché supportent trois types d'index, à savoir standard, [bitmap](http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_indices#GSQL_indices_bitmap) et [bitslice](http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_indices#GSQL_indices_bitslice). En plus de ces trois types natifs, les développeurs peuvent déclarer leurs propres types d'index personnalisés et les utiliser dans toutes les classes depuis la version 2013.1. Par exemple, les index de texte iFind utilisent ce mécanisme. Un Custom Index Type est une classe qui implémente les méthodes de l'interface _%Library.FunctionalIndex_ pour effectuer des insertions, des mises à jour et des suppressions. Vous pouvez spécifier une telle classe comme type d'index lorsque vous déclarez un nouvel index. Exemple: Property A As %String; Property B As %String; Index someind On (A,B) As CustomPackage.CustomIndex; La classe _CustomPackage.CustomIndex_ est la classe même qui implémente les index personnalisés. Par exemple, analysons le petit prototype d'un index à base de quadtrees pour les données spatiales qui a été développé pendant le [Hackathon](http://writeimagejournal.com/?p=1912) par notre équipe : [Andrey Rechitsky](https://github.com/ARechitsky), [Aleksander Pogrebnikov](https://github.com/APogrebnikov) et [moi-même](https://github.com/adaptun). (Le Hackathon a été organisé dans le cadre de la formation annuelle de l'école d'innovation d'InterSystems Russie, et nous remercions tout particulièrement le principal inspirateur du Hackathon, [Timur Safin](https://github.com/tsafin/).) Dans cet article, je ne vais pas parler des [quadtrees] (https://en.wikipedia.org/wiki/Quadtree) et de la façon de les utiliser. Nous allons plutôt examiner comment créer une nouvelle classe qui implémente l'interface [_%Library.FunctionalIndex_](http://docs.intersystems.com/ens20152/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Library.FunctionalIndex) pour l'implémentation de l'algorithme quadtree existant. Dans notre équipe, cette tâche a été confiée à Andrey. Andrey a créé la classe _SpatialIndex.Indexer_ avec deux méthodes : * Insert(x, y, id) * Delete(x, y, id) Lors de la création d'une nouvelle instance de la classe _SpatialIndex.Indexer_, il était nécessaire de définir un nom de nœud global dans lequel nous stockons les données d'index. Tout ce que j'avais à faire était de créer la classe _SpatialIndex.Index_ avec les méthodes _InsertIndex_, _UpdateIndex_, _DeleteIndex_ et _PurgeIndex_. Les trois premières méthodes acceptent l'_Id_ de la chaîne à modifier et les valeurs indexées exactement dans le même ordre que celui dans lequel elles ont été définies dans la déclaration de l'index au sein de la classe correspondante. Dans notre exemple, les arguments d'entrée sont _pArg(1)_ — _A_ and _pArg(2)_ — _B_. Class SpatialIndex.Index Extends %Library.FunctionalIndex [ System = 3 ] { ClassMethod InsertIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)") } } ClassMethod UpdateIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Delete(pArg(3),pArg(4),pID)") $$$GENERATE($C(9)_"do indexer.Insert(pArg(1),pArg(2),pID)") } } ClassMethod DeleteIndex(pID As %CacheString, pArg... As %Binary) [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"set indexer = ##class(SpatialIndex.Indexer).%New($Name("_IndexGlobal_"))") $$$GENERATE($C(9)_"do indexer.Delete(pArg(1),pArg(2),pID)") } } ClassMethod PurgeIndex() [ CodeMode = generator, ServerOnly = 1 ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"kill " _ IndexGlobal) } } ClassMethod IndexLocation(className As %String, indexName As %String) As %String { set storage = ##class(%Dictionary.ClassDefinition).%OpenId(className).Storages.GetAt(1).IndexLocation quit $Name(@storage@(indexName)) } } _IndexLocation_ est une méthode supplémentaire qui renvoie le nom du nœud dans le global où la valeur de l'index est enregistrée. Analysons maintenant la classe de test dans laquelle l'index du type _SpatialIndex.Index_ est utilisé : Class SpatialIndex.Test Extends %Persistent { Property Name As %String(MAXLEN = 300); Property Latitude As %String; Property Longitude As %String; Index coord On (Latitude, Longitude) As SpatialIndex.Index; } Lorsque la classe _SpatialIndex.Test_ est compilée, le système génère les méthodes suivantes dans le code INT pour chaque index du type _SpatialIndex.Index_ : zcoordInsertIndex(pID,pArg...) public { set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord"))) do indexer.Insert(pArg(1),pArg(2),pID) } zcoordPurgeIndex() public { kill ^SpatialIndex.TestI("coord") } zcoordSegmentInsert(pIndexBuffer,pID,pArg...) public { do ..coordInsertIndex(pID, pArg...) } zcoordUpdateIndex(pID,pArg...) public { set indexer = ##class(SpatialIndex.Indexer).%New($Name(^SpatialIndex.TestI("coord"))) do indexer.Delete(pArg(3),pArg(4),pID) do indexer.Insert(pArg(1),pArg(2),pID) } Les méthodes _%SaveData_, _%DeleteData_, _%SQLInsert_, _%SQLUpdate_ et _%SQLDelete_ appellent les méthodes de notre index. Par exemple, le code suivant fait partie de la méthode _%SaveData_ : if insert { ... do ..coordInsertIndex(id,i%Latitude,i%Longitude,"") ... } else { ... do ..coordUpdateIndex(id,i%Latitude,i%Longitude,zzc27v3,zzc27v2,"") ... } Un exemple pratique est toujours mieux que la théorie, vous pouvez donc télécharger les fichiers depuis notre entrepôt : . Ceci est un lien vers une branche sans l'interface web. Pour utiliser ce code : 1. Importez les classes 2. Décompresser RuCut.zip 3. Importez les données en utilisant les appels suivants : do $system.OBJ.LoadDir("c:\temp\spatialindex","ck") do ##class(SpatialIndex.Test).load("c:\temp\rucut.txt") Le fichier rucut.txt contient des données sur 100 000 villes et villages de Russie, avec leur nom et leurs coordonnées. La méthode Load lit chaque chaîne de caractères du fichier, puis l'enregistre comme une instance distincte de la classe SpatialIndex.Test. Une fois la méthode Load exécutée, le fichier global ^SpatialIndex.TestI("coord") contient un quadtree avec les coordonnées de latitude et de longitude. ## Et maintenant, exécutons des requêtes ! La construction des index n'est pas la partie la plus intéressante. Nous voulons utiliser notre index dans diverses requêtes. Dans Caché, il existe une syntaxe standard pour les index non standard : SELECT * FROM SpatialIndex.Test WHERE %ID %FIND search_index(coord, 'window', 'minx=56,miny=56,maxx=57,maxy=57') _%ID %FIND search_index_ est la partie fixe de la syntaxe. Ensuite, il y a le nom de l'index, _coord_ - et notez qu'aucun guillemet n'est nécessaire. Tous les autres paramètres _('window', 'minx=56,miny=56,maxx=57,maxy=57')_ sont transmis à la méthode Find, qui doit également être définie dans la classe du type d'index (qui, dans notre exemple, est _SpatialIndex.Index_) : ClassMethod Find(queryType As %Binary, queryParams As %String) As %Library.Binary [ CodeMode = generator, ServerOnly = 1, SqlProc ] { if %mode'="method" { set IndexGlobal = ..IndexLocation(%class,%property) set IndexGlobalQ = $$$QUOTE(IndexGlobal) $$$GENERATE($C(9)_"set result = ##class(SpatialIndex.SQLResult).%New()") $$$GENERATE($C(9)_"do result.PrepareFind($Name("_IndexGlobal_"), queryType, queryParams)") $$$GENERATE($C(9)_"quit result") } } Dans cet exemple de code, nous avons seulement deux paramètres - _queryType_ et _queryParams_, mais vous pouvez ajouter autant de paramètres que vous le souhaitez. Lorsque vous compilez une classe dans laquelle la méthode _SpatialIndex.Index_ est utilisée, la méthode _Find_ génère une méthode supplémentaire appelée _z<IndexName>Find_, qui est ensuite utilisée pour exécuter des requêtes SQL : zcoordFind(queryType,queryParams) public { Set:'$isobject($get(%sqlcontext)) %sqlcontext=##class(%Library.ProcedureContext).%New() set result = ##class(SpatialIndex.SQLResult).%New() do result.PrepareFind($Name(^SpatialIndex.TestI("coord")), queryType, queryParams) quit result } La méthode _Find_ doit retourner une instance de la classe qui implémente l'interface [_%SQL.AbstractFind_](http://docs.intersystems.com/ens20152/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SQL.AbstractFind). Les méthodes de cette interface, _NextChunk_ et _PreviousChunk_, renvoient des chaînes de bits par tranches de 64 000 bits chacune. Lorsqu'un enregistrement avec un certain _ID_ répond aux critères de sélection, le bit correspondant (chunk\_number * 64000 + position\_number\_within\_chunk) est mis à 1. Class SpatialIndex.SQLResult Extends %SQL.AbstractFind { Property ResultBits [ MultiDimensional, Private ]; Method %OnNew() As %Status [ Private, ServerOnly = 1 ] { kill i%ResultBits kill qHandle quit $$$OK } Method PrepareFind(indexGlobal As %String, queryType As %String, queryParams As %Binary) As %Status { if queryType = "window" { for i = 1:1:4 { set item = $Piece(queryParams, ",", i) set IndexGlobal = ..IndexLocation(%class,%property) $$$GENERATE($C(9)_"kill " _ IndexGlobal) set param = $Piece(item, "=", 1) set value = $Piece(item, "=" ,2) set arg(param) = value } set qHandle("indexGlobal") = indexGlobal do ##class(SpatialIndex.QueryExecutor).InternalFindWindow(.qHandle,arg("minx"),arg("miny"),arg("maxx"),arg("maxy")) set id = "" for { set id = $O(qHandle("data", id),1,idd) quit:id="" set tChunk = (idd\64000)+1, tPos=(idd#64000)+1 set $BIT(i%ResultBits(tChunk),tPos) = 1 } } quit $$$OK } Method ContainsItem(pItem As %String) As %Boolean { set tChunk = (pItem\64000)+1, tPos=(pItem#64000)+1 quit $bit($get(i%ResultBits(tChunk)),tPos) } Method GetChunk(pChunk As %Integer) As %Binary { quit $get(i%ResultBits(pChunk)) } Method NextChunk(ByRef pChunk As %Integer = "") As %Binary { set pChunk = $order(i%ResultBits(pChunk),1,tBits) quit:pChunk="" "" quit tBits } Method PreviousChunk(ByRef pChunk As %Integer = "") As %Binary { set pChunk = $order(i%ResultBits(pChunk),-1,tBits) quit:pChunk="" "" quit tBits } } Comme le montre l'exemple de code ci-dessus, la méthode _InternalFindWindow_ de la classe _SpatialIndex.QueryExecutor_ recherche les points situés dans le rectangle spécifié. Ensuite, les ID des lignes correspondantes sont écrits dans les bitsets dans la boucle FOR. Dans notre projet Hackathon, Andrey a également implémenté la fonctionnalité de recherche pour les ellipses : SELECT * FROM SpatialIndex.Test WHERE %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') and name %StartsWith 'Z' ## Un peu plus à propos de %FIND Le prédicat _%FIND_ possède un paramètre supplémentaire, _SIZE_, qui aide le moteur SQL à estimer le nombre de lignes correspondantes. En fonction de ce paramètre, le moteur SQL décide d'utiliser ou non l'index spécifié dans le prédicat _%FIND_. Par exemple, ajoutons l'index suivant dans la classe SpatialIndex.Test : Index ByName on Name; Maintenant, recompilons la classe et construisons cet index : write ##class(SpatialIndex.Test).%BuildIndices($LB("ByName")) Et enfin, lancez TuneTable : do $system.SQL.TuneTable("SpatialIndex.Test", 1) Voici le plan de la requête : SELECT * FROM SpatialIndex.Test WHERE name %startswith 'za' and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((10)) ![](https://lh5.googleusercontent.com/qJYDnZ9OmUQ9ZzRvwB3ZdnOYx0FXLobAh5JxMuHGIvO1rWGC7z1S9VZBy1WKKXzqX8q7gDFBSDokG_egrFuU88T9N5HvemUBtIVw1wc_i6hQ1oZ774XZV2UQYDt4j4v9wG3_utOD) Comme l'index coord est susceptible de retourner peu de lignes, le moteur SQL n'utilise pas l'index sur la propriété _Name_. Il y a un plan différent pour la requête suivante : SELECT * FROM SpatialIndex.Test WHERE name %startswith 'za' and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((1000)) ![](https://lh3.googleusercontent.com/eS64e9v_Suc2GWeaUwM_O2dg5WX4iPGsJeWx2UfASqnCceBBLg-TmpHl0C0BFlSKBkNEXVMFdugRjq-JtJHZFoPI5QMbHmdT6zHS9j-oAJdIgZ1uDr1hlNSWPTLy6ljuKSKBQqb3) Le moteur SQL utilise les deux index pour exécuter cette requête. Et, comme dernier exemple, créons une requête qui utilise uniquement l'index sur le champ _Name_, puisque l'index _coord_ renverra probablement environ 100 000 lignes et sera donc très peu utilisable : SELECT * FROM SpatialIndex.Test WHERE name %startswith 'za' and %ID %FIND search_index(coord,'radius','x=55,y=55,radiusX=2,radiusY=2') size ((100000)) ![](https://lh4.googleusercontent.com/xw0oNiI7_qQmSioTApOOVydSf3pgFFOXChiMQodDh9UavcSQfdz7w_6su65rs4-m_06100eyr2oHBa1Th0J0TV9BOx7DCO0XNIzIyOD0w64HZOzpwXyHEUlW26U9yzuB_F4VptQs) Merci à tous ceux qui ont lu ou au moins parcouru cet article. Outre les liens de documentation ci-dessous, vous pouvez également trouver utile d'examiner les implémentations alternatives des interfaces _%Library.FunctionalIndex_ et _%SQL.AbstractFind_. Pour visualiser ces implémentations, ouvrez l'une de ces classes dans Caché Studio et choisissez **Class > Inherited Classes** dans le menu. Liens: * [%FIND](http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_find) * [search_index](http://docs.intersystems.com/cache20152/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_searchindex) * [%SQL.AbstractFind](http://docs.intersystems.com/cache20152/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SQL.AbstractFind) * [%Library.FunctionalIndex](http://docs.intersystems.com/cache20152/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Library.FunctionalIndex)
Article
Irène Mykhailova · Mai 25, 2022

Traitement des opérations sur les dates et les heures dans Caché

Voici quelques exemples de conversions et d'opérations dont vous pourriez avoir besoin, ainsi que des liens vers la documentation où vous pourrez en apprendre davantage. Au moment où j'ai écrit ces lignes, l'heure d'été était en vigueur pour mon système Caché. # Comment Caché conserve l'heure et la date Caché a un format d'heure simple, avec une plus grande gamme de dates reconnues par rapport à certaines autres technologies. L'heure actuelle est conservée dans une variable spéciale $HOROLOG ($H) : USER>WRITE $H 64146,54027 USER> Le premier nombre entier est le nombre de jours écoulés depuis le 31 décembre 1840. Le second nombre entier est le nombre de secondes écoulées depuis minuit le jour actuel. Vous pouvez également obtenir l'heure et la date actuelles avec $SYSTEM.SYS.Horolog(). # Comment établir un horodatage $HOROLOG comptabilise le temps avec une précision de l'ordre de la seconde. $ZTIMESTAMP a une forme similaire à $HOROLOG, mais il suit les fractions de seconde dans la partie temps et conserve le temps universel coordonné (UTC), plutôt que l'heure locale. La précision des fractions de seconde dépend de votre plate-forme. Par conséquent, $ZTIMESTAMP fournit un horodatage qui est uniforme dans tous les fuseaux horaires. L'horodatage que vous voyez à un moment donné peut avoir une date et une heure différentes de votre heure locale actuelle. Dans cet exemple, mon heure locale est l'heure avancée de l'Est, soit quatre heures de moins que l'heure UTC. WRITE !,"$ZTIMESTAMP: "_$ZTIMESTAMP_" $HOROLOG: "_$HOROLOG $ZTIMESTAMP: 64183,53760.475 $HOROLOG: 64183,39360 La différence (sans compter les secondes fractionnées) est de 14400 secondes. Mon $HOROLOG est donc quatre heures "derrière" $ZTIMESTAMP. # Comment convertir le format interne en format d'affichage Vous pouvez utiliser $ZDATETIME. La conversion de la date et de l'heure actuelles à partir du format interne peut être aussi simple que suit WRITE !, "Avec le format de date et d'heure par défaut : ",$ZDATETIME($HOROLOG) Avec le format de date et d'heure par défaut : 09/22/2016 10:56:00 Cela prend les paramètres par défaut des paramètres locaux Caché et NLS. Les deuxième et troisième arguments (facultatifs) servent à spécifier le format de la date et le format de l'heure. WRITE !, "With dformat 5 and tformat 5: ", $ZDATETIME($HOROLOG,5,5) Avec dformat 5 et tformat 5: Sep 22, 2016T10:56:00-04:00 Le format horaire 7, par exemple, affiche l'heure en temps universel coordonné comme vous le voyez ici. WRITE !, "With dformat 5 and tformat 7: ", $ZDATETIME($HOROLOG,5,7) Avec dformat 5 et tformat 7: Sep 22, 2016T14:56:00Z Outre les formats de date et d'heure, il existe de nombreux autres arguments facultatifs qui vous permettent de contrôler l'affichage. Par exemple, vous pouvez * Spécifier les limites inférieure et supérieure des dates valides si elles sont également autorisées par les paramètres régionaux actuels. * Contrôler si les années sont affichées avec deux ou quatre chiffres. * Contrôler l'affichage des erreurs. # Comment convertir un format d'affichage en un format interne à Caché Utilisez $ZDATETIMEH pour passer du format d'affichage au format interne, comme $HOROLOG. Le H à la fin est un rappel que vous finirez avec le format $HOROLOG. De nombreux formats d'entrée différents peuvent être utilisés. SET display = "09/19/2016 05:05 PM" WRITE !, display_" is "_$ZDATETIMEH(display)_" in internal format" WRITE !, "Suffixes AM, PM, NOON, MIDNIGHT can be used" SET startdate = "12/31/1840 12:00 MIDNIGHT" WRITE !, startdate_" is "_$ZDATETIMEH(startdate)_" in internal format" 09/19/2016 05:05 PM est 64180,61500 en format interne Les suffixes AM, PM, NOON, MIDNIGHT peuvent être utilisés 12/31/1840 12:00 MIDNIGHT est 0,0 en format interne # Comment convertir l'heure UTC en heure locale et vice versa au format interne Vous pouvez utiliser $ZDATETIME et $ZDATETIMEH avec un spécificateur spécial de format de date (-3) pour le deuxième argument. La meilleure façon de convertir l'heure UTC en heure locale au format interne de Caché est d'utiliser la fonction $ZDATETIMEH(datetime, -3). Ici, le premier argument contient l'heure UTC au format interne. SET utc1 = $ZTIMESTAMP SET loctime1 = $ZDATETIMEH(utc1, -3) WRITE !, "$ZTIMESTAMP returns a UTC time in internal format: ", utc1 WRITE !, "$ZDATETIMEH( ts,-3) converts UTC to local time: ", loctime1 WRITE !, "$ZDATETIME converts this to display formats: ", $ZDATETIME(utc1) WRITE !, "which is "_$ZDATETIME(loctime1)_" in local time" $ZTIMESTAMP renvoie une heure UTC au format interne : 64183,53760.475 $ZDATETIMEH( ts,-3) convertit l'UTC en heure locale : 64183,39360.475 $ZDATETIME le convertit en format d'affichage : 09/22/2016 14:56:00 qui est 09/22/2016 10:56:00 en heure locale Si vous avez besoin de passer de l'heure locale à UTC, toujours au format interne, utilisez $ZDATETIME(datetime, -3). Ici, le paramètre datetime contient l'heure reflétant votre fuseau horaire local au format interne. SET loctime2 = $HOROLOG SET utc2 = $ZDATETIME(loctime2, -3) WRITE !, "$HOROLOG returns a local time in internal format: ", loctime2 WRITE !, "$ZDATETIME(ts, -3) converts this to UTC: ", utc2 WRITE !, "$ZDATETIME converts this to display formats:" WRITE !, "Local: ", $ZDATETIME(loctime2) WRITE !, "UTC: ", $ZDATETIME(utc2) $HOROLOG renvoie une heure locale au format interne : 64183,39360 $ZDATETIME(ts, -3) le convertit en UTC : 64183,53760 $ZDATETIME le convertit en format d'affichage : Local: 09/22/2016 10:56:00 UTC: 09/22/2016 14:56:00 Gardez ces points à l'esprit lorsque vous effectuez des conversions d'heure locale et UTC : * Les conversions entre l'heure locale et l'UTC doivent utiliser les règles de fuseau horaire en vigueur pour la date et le lieu spécifiés. Caché dépend du système d'exploitation pour suivre ces changements au cours du temps. Si le système d'exploitation ne le fait pas correctement, les conversions ne seront pas correctes. * Les conversions de dates et d'heures futures utilisent les règles actuelles gérées par le système d'exploitation. Cependant, les règles pour les années futures peuvent changer. # Comment déterminer le fuseau horaire du système Vous pouvez obtenir le décalage du fuseau horaire actuel en examinant la valeur de $ZTIMEZONE ou de %SYSTEM.SYS.TimeZone(). La valeur par défaut est définie par le système d'exploitation. WRITE !, "$ZTIMEZONE is set to "_$ZTIMEZONE WRITE !, "%SYSTEM.SYS.TimeZone() returns "_$System.SYS.TimeZone() $ZTIMEZONE est réglé sur 300 %SYSTEM.SYS.TimeZone() renvoie 300 Vous ne devez pas modifier la valeur de $ZTIMEZONE. Si vous le faites, vous affecterez les résultats de IsDST(), $ZDATETIME, et $ZDATETIMEH, parmi de nombreux autres effets. L'heure pour le processus ne changera pas l'heure correctement pour l'heure d'été.  La modification de $ZTIMEZONE n'est pas un moyen cohérent de changer le fuseau horaire utilisé par Caché. À partir de la version 2016.1, Caché fournit la méthode $System.Process.TimeZone() qui vous permet de définir et de récupérer le fuseau horaire pour un processus spécifique en utilisant la variable d'environnement TZ. Elle renvoie -1 si TZ n'est pas défini. WRITE !,$System.Process.TimeZone() WRITE !, "Current Time: "_$ZDT($H) WRITE !, "Set Central Time" DO $System.Process.TimeZone("CST6CDT") WRITE !, "New current time: "_$ZDT($H) WRITE !, "Current Time Zone: "_$System.Process.TimeZone() -1 L'heure actuelle : 10/03/2016 15:46:04 Réglage de l'heure centrale Nouvelle heure actuelle : 10/03/2016 14:46:04 Le fuseau horaire actuel : CST6CDT # Comment déterminer si l'heure d'été est en vigueur Utilisez $SYSTEM.Util.IsDST(). Ici aussi, Caché s'appuie sur le système d'exploitation pour appliquer les règles correctes permettant de déterminer si l'heure d'été est en vigueur. SET dst = $System.Util.IsDST() IF (dst = 1) {WRITE !, "DST is in effect"} ELSEIF (dst = 0) { WRITE !, "DST is not in effect" } ELSE { WRITE !, "DST cannot be determined" } # Comment effectuer l'arithmétique des dates Puisque le format interne de Caché maintient un compte des jours et un compte des secondes dans chaque jour, vous pouvez faire de l'arithmétique de date d'une manière directe. La fonction $PIECE vous permet de séparer les parties date et heure du format interne. Voici une courte routine qui utilise $ZDATE et $ZDATEH pour déterminer le dernier jour de l'année dernière afin de pouvoir compter le jour de l'année d'aujourd'hui. Cette routine utilise les méthodes de la classe %SYS.NLS pour définir le format de date que nous voulons, obtenir le séparateur de date et rétablir les valeurs par défaut. DATECALC ; Exemple d'arithmétique de date. W !, "Extracting date and time from $H using $PIECE" W !, "---------------------------------------------" set curtime = $H set today = $PIECE(curtime,",",1) set now = $PIECE(curtime,",",2) W !, "Curtime: "_curtime_" Today: "_today_" Now: "_now W !, "Counting the days of the year" W !, "-----------------------------" ; set to US format SET rtn = ##class(%SYS.NLS.Format).SetFormatItem("DateFormat",1) set sep = ##class(%SYS.NLS.Format).GetFormatItem("DateSeparator") SET lastyear = ($PIECE($ZDATE($H),sep,3) - 1) SET start = $ZDATEH("12/31/"_lastyear) W !, "Today is day "_(today - start)_" of the year" ; put back the original date format SET rtn=##class(%SYS.NLS.Format).SetFormatItem("DateFormat",rtn) # Comment obtenir et définir d'autres paramètres NLS Utilisez la classe %SYS.NLS.Format pour des paramètres tels que le format de la date, les dates maximum et minimum et d'autres paramètres. Les paramètres initiaux proviennent des paramètres régionaux actuels et les modifications que vous apportez à cette classe n'affectent que le processus actuel. # Heure et date en SQL Caché fournit une variété de fonctions SQL pour travailler avec les dates et les heures. Celles-ci sont également disponibles en ObjectScript via la classe $System.SQL. TO_DATE : Convertit une date au format CHAR ou VARCHAR2 en une date. Ceci est disponible en ObjectScript en utilisant $System.SQL.TODATE("string", "format") DAYOFYEAR : Renvoie le jour de l'année pour une expression d'année donnée, qui peut être dans plusieurs formats, comme un entier de date de $HOROLOG. DAYNAME : renvoie le nom du jour qui correspond à une date spécifiée. W $ZDT($H) 10/12/2016 11:39:19 w $System.SQL.TODATE("2016-10-12","YYYY-MM-DD") 64203 W $System.SQL.DAYOFYEAR(+$H) 286 W $System.SQL.DAYNAME(+$H) Wednesday Il y a beaucoup plus d'informations dans la documentation de Cache' sur la façon d'utiliser ces fonctions (et bien d'autres). Référez-vous aux références de la section suivante. # Références Liens vers la documentation en ligne d'InterSystems pour les éléments abordés dans le présent article. [$HOROLOG](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_vhorolog) [$PIECE](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fpiece) [SQL Functions reference](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_FUNCTIONS) [%SYS.NLS.Format](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYS.NLS.Format) [%SYSTEM.Process.TimeZone()](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.Process#METHOD_TimeZone) [%SYSTEM.SQL](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.SQL) [%SYSTEM.SYS.Horolog](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.SYS) [%SYSTEM.Util.IsDST()](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.Util#IsDST) [$ZDATE](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdate) [$ZDATEH](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdateh) [$ZDATETIME](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdatetime) [$ZDATETIMEH](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdatetimeh) Commentaire de @Marcel.DenOuden : Pour mes clients en Hollande, notre fuseau horaire est POSIX "CET-1CEST". Fonctionne également pour la plupart des voisins. (2016+) write !, "Current time: "_$ZDT($H) do $System.Process.TimeZone("CET-1CEST") write !, "New Current time: "_$ZDT($H)
Article
Lorenzo Scalese · Juin 8, 2022

Analyse de la génération de code avec la Method Generator

En tant que développeur, vous avez probablement passé au moins un certain temps à écrire un code répétitif. Vous vous êtes peut-être même retrouvé à souhaiter pouvoir générer ce code de manière programmatique. Si vous êtes dans cette situation, cet article est pour vous ! Nous allons commencer par un exemple. Note : les exemples suivants utilisent l'interface `%DynamicObject`, qui nécessite Caché 2016.2 ou une version supérieure. Si vous n'êtes pas familier avec cette classe, consultez la documentation ici : [Utiliser JSON dans Caché](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GJSON_intro). C'est vraiment génial ! ##Exemple Vous avez une classe `%Persistent` que vous utilisez pour stocker des données. Maintenant, supposons que vous allez saisir des données au format JSON en utilisant l'interface `%DynamicObject`. Comment faire correspondre la structure `%DynamicObject` à votre classe ? Une solution consiste à écrire du code pour copier directement les valeurs : Class Test.Generator Extends %Persistent { Property SomeProperty As %String; Property OtherProperty As %String; ClassMethod FromDynamicObject(dynobj As %DynamicObject) As Test.Generator { set obj = ..%New() set obj.SomeProperty = dynobj.SomeProperty set obj.OtherProperty = dynobj.OtherProperty quit obj } } Cependant, si les propriétés sont nombreuses ou si vous utilisez ce modèle pour plusieurs classes, cela devient fastidieux (et difficile à maintenir). C'est là que les Method Generators peuvent vous aider ! En termes simples, lorsqu'on utilise une Method Generator, au lieu d'écrire le code d'une méthode donnée, on écrit du code que le compilateur de la classe exécutera pour générer le code de la méthode. Cela vous semble-t-il gênant ? Non, pas du tout. Prenons un exemple : Class Test.Generator Extends %Persistent { ClassMethod Test() As %String [ CodeMode = objectgenerator ] { do %code.WriteLine(" write ""This is a method Generator!"",!") do %code.WriteLine(" quit ""Done!""") quit $$$OK } } Nous utilisons le paramètre `CodeMode = objectgenerator` pour indiquer que la méthode courante est une Method Generator, et non une méthode classique. Comment fonctionne cette méthode ? Afin de déboguer les Method Generators, il est utile de regarder le code généré pour la classe. Dans notre cas, il s'agit d'une routine INT nommée Test.Generator.1.INT. Vous pouvez l'ouvrir dans Studio en tapant Ctrl+Shift+V, ou vous pouvez simplement ouvrir la routine depuis la boîte de dialogue "Open" de Studio, ou depuis l'Atelier. Dans le code INT, vous pouvez trouver l'implémentation de cette méthode : zTest() public { write "This is a method Generator!",! quit "Done!" } Comme vous pouvez le voir, l'implémentation de la méthode contient simplement le texte qui est écrit dans l'objet `%code`. `%code` est un objet de type spécial de flux (`%Stream.MethodGenerator`). Le code écrit dans ce flux peut contenir n'importe quel code valide dans une routine MAC, y compris des macros, des directives de préprocesseur, et du SQL intégré. Il y a deux choses à garder à l'esprit quand on travaille avec des Method Generators : * La signature de la méthode s'applique à la méthode cible que vous allez générer. Le code du générateur doit toujours renvoyer un code d'état indiquant soit un succès, soit une erreur. * Le code écrit dans %code doit être un ObjectScript valide (les générateurs de méthodes avec d'autres modes de langage ne sont pas concernés par cet article). Cela signifie, entre autres, que les lignes contenant des commandes doivent commencer par un espace. Notez que les deux appels `WriteLine()` dans l'exemple commencent par un espace. En plus de la variable `%code` (représentant la méthode générée), le compilateur rend les métadonnées de la classe courante disponibles dans les variables suivantes : * `%class` * `%method` * `%compiledclass` * `%compiledmethod` * `%parameter` Les quatre premières variables sont des instances de `%Dictionary.ClassDefinition`, `%Dictionary.MethodDefinition`, `%Dictionary.CompiledClass` `%Dictionary.CompiledMethod`, respectivement. `%parameter` est un tableau souscrit de noms et de valeurs de paramètres définis dans la classe. La principale différence (pour nos besoins) entre `%class` et `%compiledclass` est que `%class` ne contient que les métadonnées des membres de la classe (propriétés, méthodes, etc.) définis dans la classe courante. `%compiledclass` contiendra ces membres, mais aussi les métadonnées de tous les membres hérités. De plus, les informations de type référencées à partir de `%class` apparaîtront exactement comme spécifié dans le code de la classe, alors que les types dans `%compiledclass` (et `%compiledmethod`) seront étendus au nom complet de la classe. Par exemple, `%String` sera développé en `%Library.String`, et les noms de classes sans package spécifié seront développés en nom complet `Package.Class`. Vous pouvez consulter la référence de ces classes pour plus d'informations. En utilisant ces informations, nous pouvons construire une Method Generator pour notre exemple `%DynamicObject` : ClassMethod FromDynamicObject(dynobj As %DynamicObject) As Test.Generator [ CodeMode = objectgenerator ] { do %code.WriteLine(" set obj = ..%New()") for i=1:1:%class.Properties.Count() { set prop = %class.Properties.GetAt(i) do %code.WriteLine(" if dynobj.%IsDefined("""_prop.Name_""") {") do %code.WriteLine(" set obj."_prop.Name_" = dynobj."_prop.Name) do %code.WriteLine(" }") } do %code.WriteLine(" quit obj") quit $$$OK } Le code suivant est ainsi créé : zFromDynamicObject(dynobj) public { set obj = ..%New() if dynobj.%IsDefined("OtherProperty") { set obj.OtherProperty = dynobj.OtherProperty } if dynobj.%IsDefined("SomeProperty") { set obj.SomeProperty = dynobj.SomeProperty } quit obj } Comme vous pouvez le voir, cela génère du code pour configurer chaque propriété définie dans cette classe. Notre implémentation exclut les propriétés héritées, mais nous pourrions facilement les inclure en utilisant `%compiledclass.Properties` au lieu de `%class.Properties`. Nous avons également ajouté une vérification pour voir si la propriété existe dans le `%DynamicObject` avant de tenter de la définir. Ce n'est pas strictement nécessaire, puisque la référence à une propriété qui n'existe pas dans un `%DynamicObject` n'entraînera pas d'erreur, mais c'est utile si l'une des propriétés de la classe définit une valeur par défaut. Si nous n'effectuons pas cette vérification, la valeur par défaut sera toujours surchargée par cette méthode. Les Method Generators peuvent être très puissants lorsqu'ils sont combinés à l'héritage. Nous pouvons prendre le générateur de méthodes FromDynamicObject() et le placer dans une classe abstraite. Maintenant, si nous voulons écrire une nouvelle classe qui doit être capable d'être désérialisée à partir d'un `%DynamicObject`, tout ce que nous devons faire est d'étendre cette classe pour activer cette fonctionnalité. Le compilateur de classes exécutera le code de la Method Generator lors de la compilation de chaque sous-classe, créant ainsi une implémentation personnalisée pour cette classe. ## Débogage des générateurs de méthodes ### Débogage de base L'utilisation de Method Generator permet d'ajouter un niveau d'indirection à votre programmation. Cela peut poser quelques problèmes lorsqu'on essaie de déboguer le code du générateur. Prenons un exemple. Considérons la méthode suivante : Method PrintObject() As %Status [ CodeMode = objectgenerator ] { if (%class.Properties.Count()=0)&&($get(%parameter("DISPLAYEMPTY"),0)) { do %code.WriteLine(" write ""{}"",!") } elseif %class.Properties.Count()=1 { set pname = %class.Properties.GetAt(1).Name do %code.WriteLine(" write ""{ "_pname_": ""_.."_pname_"_""}"",!") } elseif %class.Properties.Count()>1 { do %code.WriteLine(" write ""{"",!") for i=1:1:%class.Properties.Count() { set pname = %class.Properties.GetAt(i).Name do %code.WriteLine(" write """_pname_": ""_.."_pname_",!") } do %code.WriteLine(" write ""}""") } do %code.WriteLine(" quit $$$OK") quit $$$OK } Il s'agit d'une méthode simple conçue pour imprimer le contenu d'un objet. Elle affiche les objets dans un format différent selon le nombre de propriétés : un objet avec plusieurs propriétés sera imprimé sur plusieurs lignes, tandis qu'un objet avec zéro ou une propriété sera imprimé sur une ligne. De plus, l'objet contient un paramètre DISPLAYEMTPY, qui permet de supprimer ou non l'affichage des objets ayant zéro propriété. Cependant, il y a un problème avec le code. Pour une classe avec zéro propriété, l'objet n'est pas affiché correctement : TEST>set obj=##class(Test.Generator).%New() TEST>do obj.PrintObject() TEST> Nous nous attendons à ce que cela produise un objet vide "{}", et non un rien. Pour déboguer cela, nous pouvons regarder dans le code INT pour voir ce qui se passe. Cependant, en ouvrant le code INT, vous découvrez qu'il n'y a pas de définition pour zPrintObject() ! Ne me croyez pas sur parole, compilez le code et regardez par vous-même. Allez-y... Je vais attendre. OK. Retour ? Qu'est-ce qui se passe ici ? Les lecteurs astucieux ont peut-être trouvé le problème initial : il y a une faute de frappe dans la première clause de l'instruction IF. La valeur par défaut du paramètre DISPLAYEMPTY devrait être 1 et non 0. Il devrait être le suivant : `$get(%parameter("DISPLAYEMPTY"),1)` not `$get(%parameter("DISPLAYEMPTY"),0)`. Ceci explique le comportement. Mais pourquoi la méthode n'était-elle pas dans le code INT ? Il était encore exécutable. Nous n'avons pas eu d'erreur `` ; la méthode n'a simplement rien fait. Maintenant que nous voyons l'erreur, regardons ce que le code _aurait_ été s'il avait été dans le code INT. Puisque nous n'avons satisfait à aucune des conditions de la construction if ... elseif ..., le code aurait été simplement comme suit : zPrintObject() public { quit 1 } Remarquez que ce code ne fonctionne pas réellement ; il renvoie simplement une valeur littérale. Il s'avère que le compilateur de classe Caché est assez intelligent. Dans certaines situations, il peut détecter que le code d'une méthode n'a pas besoin d'être exécuté, et peut optimiser le code INT de la méthode. Il s'agit d'une excellente optimisation, car la répartition du noyau vers le code INT peut impliquer une quantité considérable de surcharge, en particulier pour les méthodes simples. Notez que ce comportement n'est pas spécifique aux Method Generators. Essayez de compiler la méthode suivante, et cherchez-la dans le code INT : ClassMethod OptimizationTest() As %Integer { quit 10 } Il peut être très utile de vérifier le code INT pour déboguer le code de votre Method Generator. Cela vous permettra de savoir ce que le générateur a réellement produit. Cependant, vous devez être attentif au fait qu'il y a des cas où le code généré n'apparaîtra pas dans le code INT. Si cela se produit de manière inattendue, il y a probablement un bug dans le code du générateur qui l'empêche de générer un code significatif. ### Utilisation du débogueur Comme nous l'avons vu, s'il y a un problème avec le code généré, nous pouvons le voir en regardant le code INT. Nous pouvons également déboguer la méthode normalement en utilisant ZBREAK ou le débogueur de Studio. Vous vous demandez peut-être s'il existe un moyen de déboguer le code de la Method Generator elle-même. Bien sûr, vous pouvez toujours ajouter des instructions "write" à la Method Generator ou définir des globaux de débogage comme un homme des cavernes. Mais il doit bien y avoir un meilleur moyen, n'est-ce pas ? La réponse est "Oui", mais pour comprendre la manière dont cela se passe, nous devons connaître le fonctionnement du compilateur de classes. En gros, lorsque le compilateur de classes compile une classe, il va d'abord analyser la définition de la classe et générer les métadonnées de la classe. Il s'agit essentiellement de générer les données pour les variables `%class` et `%compiledclass` dont nous avons parlé précédemment. Ensuite, il génère le code INT pour toutes les méthodes. Au cours de cette étape, il va créer une routine séparée pour contenir le code de génération de tous les Method Generators. Cette routine est nommée `.G1.INT`. Il exécute ensuite le code dans la routine *.G1 pour générer le code des méthodes, et les stocke dans la routine `.1.INT` avec le reste des méthodes de la classe. Il peut ensuite compiler cette routine et voilà ! Nous avons notre classe compilée ! Il s'agit bien sûr d'une simplification énorme d'un logiciel très complexe, mais cela suffira pour nos besoins. Cette routine *.G1 semble intéressante. Jetons-y un coup d'œil ! ;Test.Generator3.G1 ;(C)InterSystems, method generator for class Test.Generator3. Do NOT edit. Quit ; FromDynamicObject(%class,%code,%method,%compiledclass,%compiledmethod,%parameter) public { do %code.WriteLine(" set obj = ..%New()") for i=1:1:%class.Properties.Count() { set prop = %class.Properties.GetAt(i) do %code.WriteLine(" if dynobj.%IsDefined("""_prop.Name_""") {") do %code.WriteLine(" set obj."_prop.Name_" = dynobj."_prop.Name) do %code.WriteLine(" }") } do %code.WriteLine(" quit obj") quit 1 Quit 1 } Vous êtes peut-être habitué à modifier le code INT d'une classe et à ajouter du code de débogage. Normalement, c'est bien, même si c'est un peu primitif. Cependant, cela ne va pas fonctionner ici. Afin d'exécuter ce code, nous devons recompiler la classe. (C'est le compilateur de la classe qui l'appelle, après tout.) Mais recompiler la classe régénérera cette routine, effaçant toutes les modifications que nous avons apportées. Heureusement, nous pouvons utiliser ZBreak ou le débogueur de Studio pour parcourir ce code. Puisque nous connaissons maintenant le nom de la routine, l'utilisation de ZBreak est assez simple : TEST>zbreak FromDynamicObject^Test.Generator.G1 TEST>do $system.OBJ.Compile("Test.Generator","ck") La compilation a commencé le 14/11/2016 17:13:59 avec les qualificatifs 'ck' Compiling class Test.Generator FromDynamicObject(%class,%code,%method,%compiledclass,%compiledmethod,%parameter) publ ^ ic { FromDynamicObject^Test.Generator.G1 TEST 21e1>write %class.Name Test.Generator TEST 21e1> L'utilisation du débogueur de Studio est également simple. Vous pouvez définir un point de contrôle dans la routine *.G1.MAC, et configurer la cible de débogage pour qu'elle invoque $System.OBJ.Compile() sur la classe : $System.OBJ.Compile("Test.Generator","ck") Et maintenant vous vous lancez dans le débogage. # Conclusion Cet article a été un bref aperçu des générateurs de méthodes. Pour de plus amples informations, veuillez consulter la documentation ci-dessous : * [Defining Method and Trigger Generators](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_generators#GOBJ_C2395) * Pour plus d'informations sur les objets `%class` et `%compiledclass`, consultez : * [Using the %Dictionary Classes](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_dictionary) * [%Dictionary.ClassDefinition class reference](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Dictionary.ClassDefinition) * [%Dictionary.CompiledClass class reference](http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Dictionary.CompiledClass)
Article
Guillaume Rongier · Mars 6, 2023

Interopérabilité avec Embedded Python

Cette preuve de concept vise à montrer comment le cadre d'interopérabilité de l'iris peut être utilisé avec d'Embedded Python. 1.1. Table des matières 1. interoperability-embedded-python 1.1. Table des matières 1.2. Exemple 1.3. Enregistrer un composant 2. Démo 3. Conditions préalables 4. Installation 4.1. Avec Docker 4.2. Sans Docker 4.3. Avec ZPM 5. Comment exécuter l'échantillon 5.1. Conteneurs Docker 5.2. Portail de gestion et VSCode 5.3. Ouverture de la production 6. Que trouve-t-on dans le référentiel ? 6.1. Dockerfile 6.2. .vscode/settings.json 6.3. .vscode/launch.json 6.4. .vscode/extensions.json 6.5. dossier src 7. Comment ça marche 7.1. Le fichier__init__.py 7.2. La classe common 7.3. La classe business_host 7.4. La classe inbound_adapter 7.5. La classe outbound_adapter 7.6. Le classebusiness_service 7.7. La classe business_process 7.8. La classe business_operation 7.8.1. Le système de répartition 7.8.2. Les méthodes 7.9. La classe director 7.10. Les objets 7.11. Les messages 7.12. Comment enregistrer un composant 7.12.1. register_component 7.12.2. register_file 7.12.3. register_folder 7.13. Utilisation directe de Grongier.PEX 8. Générique 1.2. Exemple from grongier.pex import BusinessOperation,Message @dataclass class MyRequest(Message): request_string:str = None @dataclass class MyResponse(Message): my_string:str = None class MyBusinessOperation(BusinessOperation): def on_init(self): #Cette méthode est appelée lorsque le composant devient actif dans la production. self.log_info("[Python] ...MyBusinessOperation:on_init() is called") return def on_teardown(self): #Cette méthode est appelée lorsque le composant devient inactif dans la production. self.log_info("[Python] ...MyBusinessOperation:on_teardown() is called") return def on_message(self, message_input:MyRequest): # Appelé depuis le service/process/opération, le message est de type MyRequest avec la propriété request_string self.log_info("[Python] ...MyBusinessOperation:on_message() is called with message:"+message_input.request_string) response = MyResponse("...MyBusinessOperation:on_message() echos") return response 1.3. Enregistrer un composant Grâce à la méthode grongier.pex.Utils.register_component() : Lancer un shell python intégré : /usr/irissys/bin/irispython Utilisez ensuite cette méthode de classe pour ajouter une classe python à la liste des composants pour l'interopérabilité. from grongier.pex import Utils Utils.register_component(<ModuleName>,<ClassName>,<PathToPyFile>,<OverWrite>,<NameOfTheComponent>) par exemple : from grongier.pex import Utils Utils.register_component("MyCombinedBusinessOperation","MyCombinedBusinessOperation","/irisdev/app/src/python/demo/",1,"PEX.MyCombinedBusinessOperation") C'est un hack, ce n'est pas pour la production. 2. Démo La démo se trouve dans src/python/demo/reddit/ et se compose de : Un fichier adapter.py contenant un RedditInboundAdapter qui, étant donné un service, récupérera les messages récents de Reddit. Un fichier bs.py contient trois services qui font la même chose, ils appelleront notre Processus et l'enverront à reddit post. Le premier travaille seul, le second utilise l'adaptateur RedditInBoundAdapter dont nous avons parlé précédemment et le troisième utilise un adaptateur reddit inbound codé en ObjectScript. Un fichier bp.py contenant un processus FilterPostRoutingRule qui va analyser nos posts reddit et les envoyer à nos opérations s'ils contiennent certains mots. Un fichier bo.py contenant : Deux opérations de courrier électronique qui enverront un courriel à une certaine entreprise selon les mots analysés précédemment, l'une fonctionne seule et l'autre avec un OutBoundAdapter. Deux opérations de fichier qui écriront dans un fichier texte selon les mots analysés auparavant, l'une fonctionne seule et l'autre avec un OutBoundAdapter. Nouvelle trace json pour les messages natifs de python : 3. Conditions préalables Assurez-vous que vous avez installé git et Docker desktop. 4. Installation 4.1. Avec Docker Clone/git extrait le référentiel dans n'importe quel répertoire local. git clone https://github.com/grongierisc/interpeorability-embedded-python Ouvrez le terminal dans ce répertoire et lancez: docker-compose build Lancez le conteneur IRIS avec votre projet: docker-compose up -d 4.2. Sans Docker Avec PyPi : pip3 install iris_pex_embedded_python Puis dans un shell python : from grongier.pex import Utils Utils.setup() 4.3. Avec ZPM zpm "install pex-embbeded-python" 5. Comment exécuter l'échantillon 5.1. Conteneurs Docker Afin d'avoir accès aux images d'InterSystems, nous devons nous rendre à l'adresse suivante URL :http://container.intersystems.com. Après avoir connecté avec nos informations d'identification InterSystems, nous obtiendrons notre mot de passe pour nous connecter au registre. Dans l'extension docker VScode, dans l'onglet image, en appuyant sur "connect registry" (connecter le registre) et en entrant la même url que précédemment (http://container.intersystems.com) comme registre générique, il nous sera demandé de donner nos informations d'identification. L'identifiant est le même que d'habitude mais le mot de passe est celui que nous avons obtenu du site web. À partir de là, nous devrions être en mesure de construire et de composer nos conteneurs (avec les fichiers docker-compose.yml et donnés de Dockerfile). 5.2. Portail de gestion et VSCode Ce dépôt est prêt pour VS Code. Ouvrez le dossier interoperability-embedeed-python cloné localement dans VS Code. Si vous y êtes invité (dans le coin inférieur droit), installez les extensions recommandées. IMPORTANT: Lorsque vous y êtes invité, rouvrez le dossier à l'intérieur du conteneur afin de pouvoir utiliser les composants python qu'il contient. La première fois que vous effectuez cette opération, cela peut prendre plusieurs minutes, le temps que le conteneur soit préparé. En ouvrant le dossier à distance, vous permettez à VS Code et à tous les terminaux ouverts dans ce dossier d'utiliser les composants python dans le conteneur. Configurez-les pour utiliser /usr/irissys/bin/irispython 5.3. Ouverture de la production Pour ouvrir la production, vous pouvez aller à production.Vous pouvez également cliquer en bas sur le bouton 127.0.0.1:52773 [IRISAPP] et sélectionner Open Management Portal et cliquer sur les menus d'Interopérabilité [Interoperability] et configuration [Configurer] puis cliquer sur [productions] et [Go]. La production a déjà quelques exemples de code. Ici, nous pouvons voir la production et nos services et opérations en python pur : Nouvelle trace json pour les messages natifs de Python : 6. Que trouve-t-on dans le référentiel ? 6.1. Dockerfile Un dockerfile qui installe quelques dépendances de python (pip, venv) et sudo dans le conteneur pour les besoins. Puis il crée le répertoire dev et y copie ce dépôt git. Il démarre IRIS et active %Service_CallIn pour Python Shell. Utilisez le fichier docker-compose.yml correspondant pour configurer facilement des paramètres supplémentaires tels que le numéro de port et l'emplacement des clés et des dossiers d'hôte. Ce dockerfile se termine par l'installation des exigences pour les modules python. Utilisez le fichier .env/ pour ajuster le dockerfile utilisé dans docker-compose. 6.2. .vscode/settings.json Fichier de configuration pour vous permettre de coder immédiatement en VSCode avec le plugin VSCode ObjectScript. 6.3. .vscode/launch.json Fichier de configuration si vous voulez déboguer avec VSCode ObjectScript Découvrez tous les fichiers dans cet article 6.4. .vscode/extensions.json Fichier de recommandation pour ajouter des extensions si vous voulez fonctionner avec VSCode dans le conteneur. Plus d'informations ici Ceci est très utile pour travailler avec python intégré. 6.5. dossier src src ├── Grongier │ └── PEX // Classes d'ObjectScript qui enveloppent le code python │ ├── BusinessOperation.cls │ ├── BusinessProcess.cls │ ├── BusinessService.cls │ ├── Common.cls │ ├── Director.cls │ ├── InboundAdapter.cls │ ├── Message.cls │ ├── OutboundAdapter.cls │ ├── Python.cls │ ├── Test.cls │ └── _utils.cls ├── PEX // Quelques exemples de classes enveloppées │ └── Production.cls └── python ├── demo // Code python actuel pour exécuter cette démo | `-- reddit | |-- adapter.py | |-- bo.py | |-- bp.py | |-- bs.py | |-- message.py | `-- obj.py ├── dist // Roue utilisée pour pour la mise en œuvre des composants d'interopérabilité python │ └── grongier_pex-1.2.4-py3-none-any.whl ├── grongier │ └── pex // Classes d'aide pour la mise en œuvre des composants d'interopérabilité │ ├── _business_host.py │ ├── _business_operation.py │ ├── _business_process.py │ ├── _business_service.py │ ├── _common.py │ ├── _director.py │ ├── _inbound_adapter.py │ ├── _message.py │ ├── _outbound_adapter.py │ ├── __init__.py │ └── _utils.py ─ ─ ─ setup.py // configuration pour construire la roue 7. Comment ça marche 7.1. Le fichier __init__.py Ce fichier va nous permettre de créer les classes à importer dans le code.Il récupère les classes dans les multiples fichiers vus précédemment et les transforme en classes appelables. Ainsi, lorsque vous souhaitez créer une opération métier, par exemple, il vous suffit de faire ceci : from grongier.pex import BusinessOperation 7.2. La classe common La classe "common" ne doit pas être appelée par l'utilisateur, elle définit presque toutes les autres classes.Cette classe définit le suivant: on_init: La méthode on_init() est appelée lorsque le composant est lancé.Utilisez la méthode on_init() pour initialiser toutes les structures nécessaires au composant. on_tear_down: La méthode est appelée avant que le composant ne soit terminé.Utilisez-la pour libérer toutes les structures. on_connected: La méthode on_connected() est appelée lorsque le composant est connecté ou reconnecté après avoir été déconnecté.Utilisez la méthode on_connected() pour initialiser toutes les structures nécessaires au composant. log_info: Rédigez une entrée de journal de type "info". :log - ces entrées de journal peuvent être consultées dans le portail de gestion. log_alert: Rédigez une entrée de journal de type "alerte". :log - ces entrées de journal peuvent être consultées dans le portail de gestion. log_warning: Rédigez une entrée de journal de type "avertissement". :log - ces entrées de journal peuvent être consultées dans le portail de gestion. log_error: Rédigez une entrée de journal de type "erreur". :log - ces entrées de journal peuvent être consultées dans le portail de gestion. 7.3. La classe business_host La classe hôte métier ne doit pas être appelée par l'utilisateur, c'est la classe de base pour toutes les classes d'entreprise.Cette classe définit le suivant: send_request_sync: Envoyer le message spécifié au processus d'entreprise ou à l'opération métier cible de manière synchrone.Paramètre: target: une chaîne de caractères qui spécifie le nom du processus métier ou de l'opération devant recevoir la requête. La cible est le nom du composant tel que spécifié dans la propriété Item Name de la définition de production, et non le nom de la classe du composant. request: spécifie le message à envoyer à la cible. La requête est soit une instance d'une classe appartenant à la classe Message soit une instance de la classe IRISObject. Si la cible est un composant ObjectScript intégré, vous devez utiliser la classe IRISObject. La classe IRISObject permet au cadre PEX de convertir le message en une classe prise en charge par la cible. timeouts: un nombre entier facultatif qui spécifie le nombre de secondes à attendre avant de traiter la requête d'envoi comme un échec. La valeur par défaut est -1, ce qui signifie attendre indéfiniment. description : un paramètre facultatif de type chaîne qui définit une propriété de description dans l'en-tête du message. La valeur par défaut est None. Retour : l'objet de réponse de la cible. Renvoie : TypeError: si la requête n'est pas de type Message ou IRISObject. send_request_async: Envoyez le message spécifié au processus métier ou à l'opération d'entreprise cible de manière asynchrone. Paramètre: target: une chaîne de caractères qui spécifie le nom du processus métier ou de l'opération devant recevoir la requête. La cible est le nom du composant tel que spécifié dans la propriété Item Name de la définition de production, et non le nom de la classe du composant. request: spécifie le message à envoyer à la cible. La requête est une instance d' IRISObject ou d'une sous-classe de Message. Si la cible est un composant ObjectScript intégré, vous devez utiliser la classe IRISObject. La classe IRISObject permet au cadre PEX de convertir le message en une classe prise en charge par la cible. description: un paramètre facultatif de type chaîne qui définit une propriété de description dans l'en-tête du message. La valeur par défaut est None. Renvoie : TypeError: si la requête n'est pas de type Message ou IRISObject. get_adapter_type: Nom de l'adaptateur enregistré. 7.4. La classe inbound_adapter Inbound Adapter en Python sont des sous-classes de grongier.pex.InboundAdapter en Python, qui héritent de toutes les fonctions de la common class.Cette classe est chargée de recevoir les données du système externe, de les valider et de les envoyer au service métier en appelant la méthode process_input de BusinessHost. Cette classe définit le suivant: on_task: Appelé par le cadre de production à des intervalles déterminés par la propriété CallInterval du service métier.Le message peut avoir n'importe quelle structure convenue par l'adaptateur entrant et le service métier. Exemple d'un adaptateur entrant (situé dans le fichier src/python/demo/reddit/adapter.py): from grongier.pex import InboundAdapter import requests import iris import json class RedditInboundAdapter(InboundAdapter): """ Cet adaptateur utilise des requêtes pour récupérer les messages de self.limit en tant que données de l'API reddit avant d'appeler process_input pour chaque message. """ def on_init(self): if not hasattr(self,'feed'): self.feed = "/new/" if self.limit is None: raise TypeError('no Limit field') self.last_post_name = "" return 1 def on_task(self): self.log_info(f"LIMIT:{self.limit}") if self.feed == "" : return 1 tSC = 1 # Requête HTTP try: server = "https://www.reddit.com" request_string = self.feed+".json?before="+self.last_post_name+"&limit="+self.limit self.log_info(server+request_string) response = requests.get(server+request_string) response.raise_for_status() data = response.json() updateLast = 0 for key, value in enumerate(data['data']['children']): if value['data']['selftext']=="": continue post = iris.cls('dc.Reddit.Post')._New() post._JSONImport(json.dumps(value['data'])) post.OriginalJSON = json.dumps(value) if not updateLast: self.LastPostName = value['data']['name'] updateLast = 1 response = self.BusinessHost.ProcessInput(post) except requests.exceptions.HTTPError as err: if err.response.status_code == 429: self.log_warning(err.__str__()) else: raise err except Exception as err: self.log_error(err.__str__()) raise err return tSC 7.5. La classe outbound_adapter Outbound Adapter en Python sont des sous-classes de grongier.pex.OutboundAdapter en Python, qui héritent de toutes les fonctions de la common class.Cette classe est responsable de l'envoi des données au système externe. L'adaptateur entrant Outbound Adapter donne à l'opération la possibilité d'avoir une notion de battement de cœur. Pour activer cette option, le paramètre CallInterval de l'adaptateur doit impérativement être supérieur à 0. Exemple d'un adaptateur sortant (situé dans le fichier src/python/demo/reddit/adapter.py): class TestHeartBeat(OutboundAdapter): def on_keepalive(self): self.log_info('beep') def on_task(self): self.log_info('on_task') 7.6. La classe business_service Cette classe est responsable de la réception des données provenant d'un système externe et de leur envoi aux processus ou aux opérations métier dans la production.Le service métier peut utiliser un adaptateur pour accéder au système externe, qui est spécifié en surchargeant la méthode get_adapter_type.Il y a trois façons de mettre en œuvre un service métier : Service métier au sondage avec un adaptateur - Le cadre de production appelle à intervalles réguliers la méthode OnTask() de l'adaptateur, qui envoie les données entrantes à la méthode ProcessInput() du service métier, qui, à son tour, appelle la méthode OnProcessInput avec votre code. Service métier au sondage qui utilise l'adaptateur par défaut - Dans ce cas, le cadre appelle la méthode OnTask de l'adaptateur par défaut sans données. La méthode OnProcessInput() joue alors le rôle de l'adaptateur et se charge d'accéder au système externe et de recevoir les données.. Service métier sans sondage - Le cadre de production ne lance pas le service métier. Au lieu de cela, le code personnalisé d'un processus à long terme ou d'un processus lancé à intervalles réguliers lance le service métier en appelant la méthode Director.CreateBusinessService(). Les services métier en Python sont des sous-classes de grongier.pex.BusinessService en Python, qui héritent de toutes les fonctions de l'hôte métier.Cette classe définit le suivant: on_process_input: Reçoit le message de l'adaptateur entrant via la méthode PRocessInput et est chargé de le transmettre aux processus ou opérations cibles.Si le service métier ne spécifie pas d'adaptateur, alors l'adaptateur par défaut appelle cette méthode sans message et le service métier est chargé de recevoir les données du système externe et de les valider. Paramètre : message_input: une instance d'IRISObject ou une sous-classe de Message contenant les données que l'adaptateur entrant transmet. Le message peut avoir n'importe quelle structure convenue par l'adaptateur entrant et le service métier. Exemple de service métier (situé dans le fichier src/python/demo/reddit/bs.py): from grongier.pex import BusinessService import iris from message import PostMessage from obj import PostClass class RedditServiceWithPexAdapter(BusinessService): """ Ce service utilise notre python Python.RedditInboundAdapter pour recevoir le message du site reddit et appeler le processus FilterPostRoutingRule. """ def get_adapter_type(): """ Nom de l'adaptateur enregistré """ return "Python.RedditInboundAdapter" def on_process_input(self, message_input): msg = iris.cls("dc.Demo.PostMessage")._New() msg.Post = message_input return self.send_request_sync(self.target,msg) def on_init(self): if not hasattr(self,'target'): self.target = "Python.FilterPostRoutingRule" return 7.7. La classe business_process Contient généralement la majeure partie de la logique d'une production.Un processus métier peut recevoir des messages d'un service métier, d'un autre processus métier ou d'une opération métier.Il peut modifier le message, le convertir dans un format différent ou l'acheminer en fonction de son contenu.Le processus métier peut acheminer un message vers une opération métier ou un autre processus métier.Le processus métier en Python sont des sous-classes de grongier.pex.BusinessProcess en Python, qui héritent de toutes les fonctions de l'hôte métier.Cette classe définit le suivant: on_request: Gère les requêtes envoyées au processus métier. Une production appelle cette méthode chaque fois qu'une requête initiale pour un processus métier spécifique arrive dans la file d'attente appropriée et se voit attribuer un travail à exécuter.Paramètre: request: Une instance d'IRISObject ou une sous-classe de Message qui contient le message de requête envoyé au processus métier. Retour : Une instance d'IRISObject ou une sous-classe de Message qui contient le message de réponse que ce processus métier peut renvoyer au composant de production qui a envoyé le message initial. on_response: Gère les réponses envoyées au processus métier en réponse aux messages que celui-ci a envoyés à la cible.Une production appelle cette méthode chaque fois qu'une réponse pour un processus métier spécifique arrive dans la file d'attente appropriée et se voit attribuer un travail à exécuter.Il s'agit généralement d'une réponse à une requête asynchrone faite par le processus métier où le paramètre responseRequired a une valeur vraie.Paramètre: request: Une instance d'IRISObject ou une sous-classe de Message qui contient le message initiale de requête envoyé au processus métier. response: Une instance d'IRISObject ou une sous-classe de Message qui contient le message de réponse que ce processus métier peut renvoyer au composant de production qui a envoyé le message initial. callRequest: Une instance d'IRISObject ou une sous-classe de Message qui contient la requête que le processus métier a envoyée à sa cible. callResponse: Une instance d'IRISObject ou une sous-classe de Message qui contient la réponse entrante. completionKey: Une chaîne qui contient la clé d'achèvement completionKey spécifiée dans le paramètre completionKey de la méthode sortante SendAsync(). Retour : Une instance d'IRISObject ou une sous-classe de Message qui contient le message de réponse que ce processus métier peut renvoyer au composant de production qui a envoyé le message initial. on_complète: Appelé après que le processus métier a reçu et traité toutes les réponses aux requêtes qu'il a envoyées aux cibles.Paramètre: request: Une instance d'IRISObject ou une sous-classe de Message qui contient le message initiale de requête envoyé au processus métier. response: Une instance d'IRISObject ou une sous-classe de Message qui contient le message de réponse que ce processus métier peut renvoyer au composant de production qui a envoyé le message initial. Retour : Une instance d'IRISObject ou une sous-classe de Message qui contient le message de réponse que ce processus métier peut renvoyer au composant de production qui a envoyé le message initial. Exemple de service métier (situé dans le fichers src/python/demo/reddit/bp.py): from grongier.pex import BusinessProcess from message import PostMessage from obj import PostClass class FilterPostRoutingRule(BusinessProcess): """ Ce processus reçoit un PostMessage contenant un post reddit. Il reconnaît alors si le message parle d'un chien, d'un chat ou de rien et remplit les informations appropriées dans le PostMessage avant de l'envoyer à l'opération FileOperation. """ def on_init(self): if not hasattr(self,'target'): self.target = "Python.FileOperation" return def on_request(self, request): if 'dog'.upper() in request.post.selftext.upper(): request.to_email_address = 'dog@company.com' request.found = 'Dog' if 'cat'.upper() in request.post.selftext.upper(): request.to_email_address = 'cat@company.com' request.found = 'Cat' if request.found is not None: return self.send_request_sync(self.target,request) else: return 7.8. La classe business_operation Cette classe est chargée d'envoyer les données à un système externe ou à un système local tel qu'une base de données d'iris.L'opération métier peut utiliser comme une option un adaptateur pour traîter le message sortant, qui est spécifié en surchargeant la méthode get_adapter_type.Si la transaction métier dispose d'un adaptateur, elle l'utilise pour envoyer le message au système externe.L'adaptateur peut être un adaptateur PEX, un adaptateur ObjectScript ou un adaptateur python.L'opération métier en Python sont des sous-classes de grongier.pex.BusinessOperation en Python, qui héritent de toutes les fonctions de l'hôte métier. 7.8.1. Le système de répartition Dans une opération métier, il est possible de créer un certain nombre de fonctions similaires à la méthode on_message qui prendront comme argument une requête typée comme ceci my_special_message_method(self,request: MySpecialMessage). Le système de répartition analysera automatiquement toute requête arrivant à l'opération et répartira les requêtes en fonction de leur type. Si le type de la requête n'est pas reconnu ou n'est pas spécifié dans une fonction similaire à la fonction type on_message, le système de répartition l'envoie à la fonction on_message. 7.8.2. Les méthodes Cette classe définit le suivant: on_message: Appelé lorsque l'opération métier reçoit un message d'un autre composant de production qui ne peut pas être distribué à une autre fonction.En règle générale, l'opération envoie le message au système externe ou le transmet à un processus métier ou à une autre opération métier. Si l'opération a un adaptateur, elle utilise la méthode Adapter.invoke() pour appeler la méthode sur l'adaptateur qui envoie le message au système externe. Si l'opération transmet le message à un autre composant de production, elle utilise la méthode SendRequestAsync() ou SendRequestSync().Paramètre: request: Une instance d'une sous-classe de Message ou d'IRISObject contenant le message entrant pour l'opération métier. Retour : L'objet de réponse Exemple d'une opération métier (située dans le fichier src/python/demo/reddit/bo.py): from grongier.pex import BusinessOperation from message import MyRequest,MyMessage import iris import os import datetime import smtplib from email.mime.text import MIMEText class EmailOperation(BusinessOperation): """ Cette opération reçoit un PostMessage et envoie un courriel contenant toutes les informations importantes à l'entreprise concernée (entreprise de chiens ou de chats). """ def my_message(self,request:MyMessage): sender = 'admin@example.com' receivers = 'toto@example.com' port = 1025 msg = MIMEText(request.toto) msg['Subject'] = 'MyMessage' msg['From'] = sender msg['To'] = receivers with smtplib.SMTP('localhost', port) as server: server.sendmail(sender, receivers, msg.as_string()) print("Successfully sent email") def on_message(self, request): sender = 'admin@example.com' receivers = [ request.to_email_address ] port = 1025 msg = MIMEText('This is test mail') msg['Subject'] = request.found+" found" msg['From'] = 'admin@example.com' msg['To'] = request.to_email_address with smtplib.SMTP('localhost', port) as server: # server.login('username', 'password') server.sendmail(sender, receivers, msg.as_string()) print("Successfully sent email") Si cette opération est appelée en utilisant un message MyRequest, la fonction my_message sera appelée grâce au distributeur, sinon la fonction on_message sera appelée. 7.9. La classe director La classe Directorclass est utilisée pour les services métier sans sondage, c'est-à-dire les services métier qui ne sont pas automatiquement appelés par le cadre de production (via l'adaptateur entrant) à l'intervalle d'appel.Au lieu de cela, ces services métier sont créés par une application personnalisée en appelant la méthode Director.create_business_service().Cette classe définit le suivant: create_business_service: La méthode create_business_service() lance le service métier spécifié.Paramètre: connection: un objet IRISConnection qui spécifie la connexion à une instance IRIS pour Java. target: une chaîne qui spécifie le nom du service métier dans la définition de production. Retour : un objet qui contient une instance d' IRISBusinessService Exemple de TEC 7.10. Les objets Nous utiliserons dataclass pour contenir les informations dans nos messages dans un fichier obj.py. Exemple d'un objet (situé dans le fichier src/python/demo/reddit/obj.py): from dataclasses import dataclass @dataclass class PostClass: title: str selftext : str author: str url: str created_utc: float = None original_json: str = None 7.11. Les messages Les messages contiendront un ou plusieurs objets, situé dans le fichier obj.py.Les messages, les requêtes et les réponses sont tous hérités de la classe grongier.pex.Message. Ces messages nous permettront de transférer des informations entre n'importe quel service/processus/opération métier. Exemple d'un message (situé dans le fichier src/python/demo/reddit/message.py): from grongier.pex import Message from dataclasses import dataclass from obj import PostClass @dataclass class PostMessage(Message): post:PostClass = None to_email_address:str = None found:str = None TEC Il est à noter qu'il est nécessaire d'utiliser des types lorsque vous définissez un objet ou un message. 7.12. Comment enregistrer un composant Vous pouvez enregistrer un composant dans iris de plusieurs manières : Un seul composant avec register_component Tous les composants dans un fichier avec register_file Tous les composants dans un dossier avec register_folder 7.12.1. register_component Lancer un shell python intégré : /usr/irissys/bin/irispython Utilisez ensuite cette méthode de classe pour ajouter un nouveau fuchier py à la liste des composants pour l'interopérabilité. from grongier.pex import Utils Utils.register_component(<ModuleName>,<ClassName>,<PathToPyFile>,<OverWrite>,<NameOfTheComponent>) par exemple : from grongier.pex import Utils Utils.register_component("MyCombinedBusinessOperation","MyCombinedBusinessOperation","/irisdev/app/src/python/demo/",1,"PEX.MyCombinedBusinessOperation") 7.12.2. register_file Lancer un shell python intégré : /usr/irissys/bin/irispython Utilisez ensuite cette méthode de classe pour ajouter un nouveau fuchier py à la liste des composants pour l'interopérabilité. from grongier.pex import Utils Utils.register_file(<File>,<OverWrite>,<PackageName>) par exemple : from grongier.pex import Utils Utils.register_file("/irisdev/app/src/python/demo/bo.py",1,"PEX") 7.12.3. register_folder Lancer un shell python intégré : /usr/irissys/bin/irispython Utilisez ensuite cette méthode de classe pour ajouter un nouveau fuchier py à la liste des composants pour l'interopérabilité. from grongier.pex import Utils Utils.register_folder(<Path>,<OverWrite>,<PackageName>) par exemple : from grongier.pex import Utils Utils.register_folder("/irisdev/app/src/python/demo/",1,"PEX") 7.13. Utilisation directe de Grongier.PEX Si vous ne souhaitez pas utiliser l'utilitaire register_component, Vous pouvez ajouter un composant Grongier.PEX.BusinessService directement dans le portail de gestion et configurer les propriétés : %module : Nom du module de votre code python %classname : Nom de classe de votre composant %classpaths Chemin où se trouve votre composant. Cela peut nécessiter un chemin ou plusieurs chemins de classe (séparés par le caractère '|') en plus de PYTHON_PATH par exemple : 8. Générique La plupart du code provient de PEX for Python de Mo Cheng et Summer Gerry. Fonctionne uniquement sur IRIS 2021.2 +
Article
Lorenzo Scalese · Oct 5, 2022

Utilisation de SUSHI pour la création de profils FHIR, partie 2

Bonjour, chers développeurs ! Cet article est le deuxième d'une série sur la façon d'utiliser SUSHI, un outil de création de profils FHIR, en tant que technologie associée à FHIR. Six mois se sont écoulés avant cette deuxième partie. Dans la précédente [partie 1](https://fr.community.intersystems.com/post/cr%C3%A9ons-un-profil-fhir-%C3%A0-laide-de-sushi-partie-1), nous avons abordé les questions suivantes : qu'est-ce que FHIR, qu'est-ce que le profil FHIR, qu'est-ce que FHIR Shorthand ? Et quel genre d'outil est SUSHI ? Que peut-il produire ? Avec des captures d'écran pour des exemples de résultats. Cet article présente un exemple d'utilisation réelle d'un profil créé avec SUSHI, dans lequel une **Extension** est ajoutée à une ressource Patient à l'aide de SUSHI, et un nouveau **SearchParameter** est défini pour l'élément de cette Extension, jusqu'à ce que le nouveau SearchParameter puisse être utilisé dans l'IRIS for Health's FHIR Repositoy jusqu'à ce que le nouveau SearchParameter puisse être utilisé. ## Mise à jour de SUSHI Je suis désolé de m'écarter du sujet principal, mais si vous êtes comme moi et que vous n'avez pas touché à SUSHI depuis un moment, vous devriez mettre à jour SUSHI. Au cours des six derniers mois, SUSHI a fait l'objet d'une importante mise à jour, et la version 2.0.0 est sortie en août. La dernière version au moment de la rédaction de cet article était [SUSHI 2.1.1](https://github.com/FHIR/sushi/releases). Comme décrit dans ce lien, la mise à jour est la commande suivante de même que l'installation. ```Bash $ npm install -g fsh-sushi ``` Vous pouvez vérifier la version en exécutant sushi -version. De même, l'outil IG Publisher, qui crée un ensemble de fichiers HTML pour le Guide de mise en œuvre sur la base des Profils générés par SUSHI, peut être mis à jour en exécutant la commande **_updatePublisher**. ## Création de fichiers FISH Tout d'abord, créez un projet en utilisant la commande ``sushi --init`` comme précédemment. Dans cet article, nous allons modifier le fichier **patient.fsh** généré par le modèle. Cette fois, nous ajouterons une extension de type **lieu de naissance**, qui est une chaîne de caractères String représentant le lieu de naissance du patient, et nous définirons également un SearchParameter pour ce lieu de naissance, afin de pouvoir effectuer une recherche par lieu de naissance du patient ! # Ajout d'Extension Tout d'abord, ajoutez la définition suivante pour ajouter Extension. Comme dans US Core et JP Core, le type Adresse est habituellement utilisé, mais ici il s'agit simplement du type String ``` Extension: BirthPlace Id: birthPlace Title: "出身地" Description: "生まれた場所をstring型で表現する" * ^url = "http://isc-demo/fhir/StructureDefinition/patient-birthPlace" * value[x] only string ``` Chaque élément correspond à la StructureDefinition d'Extension comme suit. Certains éléments sont placés à plusieurs endroits. Certaines informations, comme la version du fhir de base et la version de cette Extension elle-même, proviennent du fichier `sushi-config.yml`. | Entrée SUSHI | Entrée StructureDefinition correspondante | |:----------|:------------| | Extensions | nom | | Id | id | | Titre | title/differencial.element[id=Extension].short | | Desctiption | description/differencial.element[id=Extension].definition | | ^url | url//differencial.element[id=Extension.url].fixedUri | | valeur[x] | differencial.element[id=Extension.value[x]].type.code | La StructureDefinition réelle d'Extension générée. Il est difficile de créer cela à partir de rien et à la main, mais avec SUSHI, c'est relativement facile. ```json { "resourceType": "StructureDefinition", "id": "birthPlace", "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace", "version": "0.1.0", "name": "BirthPlace", "title": "出身地", "status": "active", "description": "生まれた場所をstring型で表現する", "fhirVersion": "4.0.1", "mapping": [ { "identity": "rim", "uri": "http://hl7.org/v3", "name": "RIM Mapping" } ], "kind": "complex-type", "abstract": false, "context": [ { "type": "element", "expression": "Element" } ], "type": "Extension", "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Extension", "derivation": "constraint", "differential": { "element": [ { "id": "Extension", "path": "Extension", "short": "出身地", "definition": "生まれた場所をstring型で表現する" }, { "id": "Extension.extension", "path": "Extension.extension", "max": "0" }, { "id": "Extension.url", "path": "Extension.url", "fixedUri": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace" }, { "id": "Extension.value[x]", "path": "Extension.value[x]", "type": [ { "code": "string" } ] } ] } } ``` Les données d'Extension aux ressources Patient ajoutées avec cette Extension ressembleront à ceci. ```json "extension": [ { "url": "http://isc-demo/fhir/StructureDefinition/patient-birthPlace", "valueString": "鹿児島" } ], ``` # Ajout de SearchParamter Ensuite, ajoutez un **SearchParamter** afin de pouvoir rechercher des ressources en utilisant l'entrée Extension que vous venez d'ajouter comme clé, **mais seules les entrées (≒elements) définies dans le SearchParamter peuvent être recherchées**. Ceci est un peu différent des tables SQL. Le nom de SearchParamter est défini séparément du nom de l'élément, et certains éléments correspondent au nom de l'élément = nom de SearchParameter, comme le sexe dans la ressource Patient, alors que d'autres ne correspondent pas, comme le nom de l'élément = adresse. country -> nom de SearchParamter = Certains ne correspondent pas aux éléments structurés, comme address-country. Naturellement, les éléments ajoutés à l'extension ne sont pas des SearchParameters par défaut (car vous ne savez pas ce qui sera inclus), mais les extensions qui osent définir l'extension et définir une politique pour les stocker sont souvent des éléments importants. Ajoutez ce qui suit au fichier patient.fsh pour créer la définition de SearchParameter ``` Instance: BirthPlaceSearchParameter InstanceOf: SearchParameter Usage: #definition * url = "http://isc-demo/fhir/SearchParameter/patient-birthPlace" * version = "0.0.1" * name = "birthPlace" * status = #active * description = "出身地検索のパラメータ" * code = #birthPlace * base = #Patient * type = #string * expression = "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value" * comparator = #eq ``` Voici la StructureDefinition générée par SearchParameter. Comme il s'agit d'une définition relativement simple, le mappage avec l'information SUSHI ci-dessus devrait être facile à comprendre. ```json { "resourceType": "SearchParameter", "id": "BirthPlaceSearchParameter", "url": "http://isc-demo/fhir/SearchParameter/patient-birthPlace", "version": "0.0.1", "name": "birthPlace", "status": "active", "description": "出身地検索のパラメータ", "code": "birthPlace", "base": [ "Patient" ], "type": "string", "expression": "Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value", "comparator": [ "eq" ] } ``` Les principales composantes de la définition de SearchParameter sont l' **expression** et le **comparateur**. L'élément **expression** décrit l'expression **FHIRPath** pour le SearchParameter cible. Si vous êtes intéressé par FHIRPath, veuillez vous référer à [cette page officielle](. html). Définition utilisée cette fois-ci `Patient.extension.where(url='http://isc-demo/fhir/StructureDefinition/patient-birthPlace').value"` Cette expression spécifie Patient.extension dans un ordre hiérarchique selon la structure Json de la ressource Patient, et réduit l'extension avec url=(omitted) par rapport aux multiples extensions qui peuvent exister, et spécifie la valeur de l'extension. comparator spécifie le type d'expressions de comparaison qui peuvent être utilisées. Pour plus d'informations, voir [ici](https://www.hl7.org/fhir/valueset-search-comparator.html). # Ajout de la définition d'Extension créée dans Patient Il y a un autre changement important : l'ajout de l'Extension BirthPlace créée dans la ressource Patient. Modifiez la définition de profil MyProfile dans la ressource Patient générée automatiquement à l'origine comme suit : les modifications apportées au paramètre Cardinality de l'élément Name ont été commentées. ```comparator spécifie le type d'expressions de comparaison qui peuvent être utilisées. Pour plus d'informations, voir [ici](https://www.hl7.org/fhir/valueset-search-comparator.html). Profile: MyPatient Parent: Patient Description: "An example profile of the Patient resource." //* name 1..* MS * extension contains BirthPlace named birthPlace 0..1 ``` L'Extension nommée "BirthPlace" qui a été ajoutée précédemment est ajoutée dans la ressource Patient avec le nom birthPlace dans le paramètre Cardinality 0..1. # Création de ressources pour des tests en plus de ce qui précède SUSHI vous permet également de créer des Instances de ressources qui peuvent être utilisées à des fins d'illustration ou autres. Vous pouvez également les utiliser à des fins de test. Vous pouvez également y inclure l'extension que vous venez de définir. ``` Instance: KamiExample InstanceOf: MyPatient Description: "Exemples de ressources Patient " * nom.family = "Yamada" * extension[BirthPlace].valueString = "Kagoshima" ``` Vous verrez quel type de données a été produit dans le test final. ## Essayons SUSHI ! Le fichier FSH est prêt ! Maintenant, utilisons la commande SUSHI pour générer chaque fichier de définition à partir du fichier fsh ! Exécutez la commande **sushi** et elle est réussie si deux Profils (Patient et Extension étendus) et deux Instances (SearchParameter et ressource de modèle) sont générés comme suit. ```PowerShell C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject>sushi . info Running SUSHI v2.1.1 (implements FHIR Shorthand specification v1.2.0) info Arguments: info C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject info No output path specified. Output to . info Using configuration file: C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject\sushi-config.yaml info Importing FSH text... info Preprocessed 1 documents with 0 aliases. info Imported 2 definitions and 2 instances. info Checking local cache for hl7.fhir.r4.core#4.0.1... info Found hl7.fhir.r4.core#4.0.1 in local cache. info Loaded package hl7.fhir.r4.core#4.0.1 (node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency (Use `node --trace-warnings ...` to show where the warning was created) (node:27132) Warning: Accessing non-existent property 'INVALID_ALT_NUMBER' of module exports inside circular dependency info Converting FSH to FHIR resources... info Converted 2 FHIR StructureDefinitions. info Converted 2 FHIR instances. info Exporting FHIR resources as JSON... info Exported 4 FHIR resources as JSON. info Assembling Implementation Guide sources... info Generated ImplementationGuide-myprofileproject.json info Assembled Implementation Guide sources; ready for IG Publisher. ╔════════════════════════ SUSHI RESULTS ══════════════════════════╗ ║ ╭───────────────┬──────────────┬──────────────┬───────────────╮ ║ ║ │ Profiles │ Extensions │ Logicals │ Resources │ ║ ║ ├───────────────┼──────────────┼──────────────┼───────────────┤ ║ ║ │ 1 │ 1 │ 0 │ 0 │ ║ ║ ╰───────────────┴──────────────┴──────────────┴───────────────╯ ║ ║ ╭────────────────────┬───────────────────┬────────────────────╮ ║ ║ │ ValueSets │ CodeSystems │ Instances │ ║ ║ ├────────────────────┼───────────────────┼────────────────────┤ ║ ║ │ 0 │ 0 │ 2 │ ║ ║ ╰────────────────────┴───────────────────┴────────────────────╯ ║ ║ ║ ╠═════════════════════════════════════════════════════════════════╣ ║ FSHing for compliments? Super job! 0 Errors 0 Warnings ║ ╚═════════════════════════════════════════════════════════════════╝ C:\Users\kaminaka\Documents\Work\FHIR\SUSHI\TestProject\MyProfileProject> ``` Les artefacts suivants ont été créés dans le dossier `fsh-generated\resource`. |nom de fichier | contenu | |:----------|:------------| | ImplementationGuide-myprofileproject.json | ImplemamtionGuide, qui résume l'ensemble des contenus présentés ici. | | StructureDefinition-MyPatient.json | StructureDefinition avec Extension ajoutée au Patient | | StructureDefinition-birthPlace.json | StructureDefinition contenant la définition d'Extension birthPlace | | SearchParameter-BirthPlaceSearchParameter.json | Fichier de définition de SearchParameter pour le lieu de naissance (birthPlace) | | Patient-KamiExample.json | Exemples d'instances de Patient | ## Importation et test du profil FHIR dans IRIS for Health # Application d'IRIS for Health au référentiel FHIR Dans l'article précédent, nous avons exécuté _updatePublisher pour générer un groupe de fichiers IG. Dans cet article, nous allons voir comment le fichier StructureDefinitino/SearchParameter peut être importé dans le référentiel FHIR d'IRIS for Health et faire l'objet d'une recherche avec le nouveau SearchParameter. Pour plus d'informations sur l'importation des profils FHIR, etc., veuillez vous référer à [l'article de la communauté des développeurs sur les profils FHIR](https://jp.community.intersystems.com/node/495321). Vous pouvez également vous référer à [cet article](https://jp.community.intersystems.com/node/480231) pour plus d'informations sur la façon de construire un référentiel FHIR. L'importation est ciblée sur les cinq fichiers qui viennent d'être générés. - StructureDefinition-MyPatient.json - StructureDefinition-birthPlace.json - SearchParameter-BirthPlaceSearchParameter.json Il y a trois fichiers. Copiez-les dans un autre dossier, et préparez également un fichier `package.json` pour gérer l'ensemble des informations du paquet. **package.json** ```json { "name": "SUSHI Demo", "title": "SUSHI Demo", "version": "0.0.1", "author": { "name": "ISC" }, "fhirVersions": [ "4.0.1" ], "bundleDependencies": false, "date": "20201208205547", "dependencies": { "hl7.fhir.r4.core": "4.0.1" }, "deprecated": false } ``` Vous pouvez modifier le nom, le titre, l'auteur, la date et d'autres éléments comme vous le souhaitez. (Remarque : lorsque chaque profil est modifié et réimporté dans IRIS, la version doit être modifiée (augmentée) en conséquence. (La version actuelle 2021.1 du référentiel FHIR n'a pas de fonction de suppression des profils, il faut donc veiller à ce que le nombre de profils n'augmente pas trop dans l'environnement de production, etc., en vérifiant le bon fonctionnement dans l'environnement de test et en ne les appliquant ensuite qu'un nombre minimum de fois dans l'environnement de production.) À partir du portail de gestion IRIS, allez Health -> FHIR Configuration -> Package Configuration et sélectionnez le dossier contenant les quatre fichiers ci-dessus dans Import Package, et vous verrez l'écran suivant. ![image](/sites/default/files/inline/images/sushi_part2_ss1.jpg) Cliquez sur Import pour terminer l'importation dans IRIS. Ensuite, créez un nouveau référentiel FHIR sur l'écran de configuration du serveur Server Configuration. (Vous pouvez également ajouter à un référentiel FHIR existant). ![image](/sites/default/files/inline/images/sushi_part2_ss2.jpg) ## Test de POSTMAN POSTEZ la ressource de test qui vient d'être générée par SUSHI. À des fins de vérification, il peut être préférable de générer des données qui incluent d'autres valeurs de birthPlace, ou une ressource Patient qui n'inclut pas de birthPlace en premier lieu. ![image](/sites/default/files/inline/images/sushi_part2_ss3.jpg) Si birthPlace a été correctement ajouté au SearchParameter dans le référentiel FHIR, la requête GET suivante devrait permettre de récupérer ces informations sur le patient ! ```http GET http://localhost:52785/csp/healthshare/sushi/fhir/r4/Patient?birthPlace=Kagoshima ``` Obtenez-vous maintenant les bons résultats ? Si le nouveau SearchParameter, birthPlace, n'a pas été ajouté correctement, la première réponse à la requête GET contiendra la ressource OperationOutcome suivante qui contient les informations d'erreur suivantes : "Le paramètre birthPlace n'a pas été reconnu. Vérifiez le message de réponse pour ce message. ```json { "resource": { "resourceType": "OperationOutcome", "issue": [ { "severity": "error", "code": "invalid", "diagnostics": "ParameterNotSupported", "details": { "text": "Unrecognized parameter 'birthPlace'. 鹿児島" } } ] }, "search": { "mode": "outcome" } }, ``` # Résumé Vous avez vu le processus de création d'un profil (StructureDefinition/SearchParameter) pour FHIR à l'aide de SUSHI et son importation dans le référentiel FHIR d'IRIS for Health pour étendre ses fonctionnalités. Dans ce cas, les éléments ajoutés à Extension ont été ajoutés à SearchParameter, mais il est également possible d'ajouter SearchParameter à des éléments qui existent dans la spécification standard FHIR mais qui ne sont pas encore des SearchParameters. Bien que le développement très flexible de FHIR permette d'étendre les fonctionnalités de cette manière, il est également important de partager des informations sur le type d'extensions réalisées pour assurer l'interopérabilité, c'est-à-dire de créer des guides de mise en œuvre, etc. Comme nous l'avons vu dans les parties 1 et 2 de cette série, SUSHI est un outil open source très unique et puissant qui couvre les deux côtés de la question. On espère que ces outils seront combinés avec IRIS for Health pour créer une nouvelle solution FHIR. Le fichier fsh SUSHI utilisé dans cet article et les fichiers modèles StructureDefinition/SearchParameter générés sont disponibles [ici](https://github.com/Intersystems-jp/FHIR_SUSHI).
Article
Irène Mykhailova · Juin 16, 2023

Tutoriel : Déploiement de votre application dockerisée sur AWS

Aujourd'hui, la plupart des applications sont déployées sur des services de cloud public. Cela présente de nombreux avantages, notamment des économies de ressources humaines et matérielles, la possibilité de se développer rapidement et à moindre coût, une plus grande disponibilité, une plus grande fiabilité, une évolutivité élastique et des options permettant d'améliorer la protection des actifs numériques. L'une des options les plus populaires est AWS. Nous pouvons y déployer nos applications à l'aide de machines virtuelles (service EC2), de conteneurs Docker (service ECS) ou de Kubernetes (service EKS). La première solution, au lieu d'utiliser Docker, emploie une machine virtuelle avec Windows ou Linux où vous pouvez installer votre serveur et déployer votre application. Cependant, la dernière correspond mieux aux applications à grande échelle avec de nombreuses instances Docker en cours d'exécution grâce à l'option auto-scale. La deuxième solution (ECS), en revanche, est le meilleur choix pour les applications de petite et moyenne échelle.Cet article vous montrera comment utiliser, configurer et exécuter des applications Docker sur AWS à l'aide du service ECS. ## Obtention d'un exemple d'application Docker à déployer Pour notre exemple, nous allons utiliser une application Docker prête du catalogue InterSystems Open Exchange. Pour commencer, suivez les étapes suivantes : 1. Assurez-vous que Git est installé. 2. Allez sur https://openexchange.intersystems.com/package/iris-rest-api-template. 3. Clonez/git pull le référentiel dans n'importe quel répertoire local:   git <span class="hljs-built_in">clone</span> git@github.com:intersystems-community/iris-rest-api-template.git Le modèle iris-rest-api-template est une application backend avec une base de données IRIS et une API REST d'IRIS écrite en ObjectScript. Nous allons déployer cette application sur le service AWS ECS. ## Obtention de vos références AWS Pour commencer, vous aurez besoin d'un compte AWS et d'un utilisateur disposant d'une clé d'accès. Pour ce faire, procédez comme suit : 1. Allez sur https://aws.amazon.com/console et cliquez sur le bouton de connexion en haut à droite Sign in : ![](/sites/default/files/inline/images/images/image-20230510101315-1.png)   2. Si vous disposez d'un compte AWS, il vous suffit de vous connecter avec celui-ci. Si vous n'en possédez pas, cliquez sur le bouton Create a new AWS account (créer un nouveau compte AWS). Après avoir complété votre profil, connectez-vous avec vos nouvelles données. 3. Dans le champ de recherche supérieur, écrivez IAM ("outil de gestion des identités et des accès AWS"), puis cliquez sur IAM : ![](/sites/default/files/inline/images/images/image-20230510101329-2.png)   4. Dans le menu de gauche, cliquez sur Users (utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101354-3.png)   5. Cliquez sur le bouton Add users (ajouter des utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101405-4.png)   6. Remplissez le champ qui est apparu avec les valeurs mentionnées ci-dessous : * Nom d'utilisateur : iris * Cochez la case Provide user access to the AWS Management Console (fournir un accès à l'utilisateur à la console de gestion AWS). * Choisissez I want to create an IAM user (Je veux créer un utilisateur IAM) * Sélectionnez Custom password (mot de passe personnalisé) et saisissez Iris@2023 * Décochez la case Users must create a new password at next sign-in (Les utilisateurs doivent créer un nouveau mot de passe lors de la prochaine connexion) * Cliquez sur le bouton Next (suivant)     ![](/sites/default/files/inline/images/images/image-20230510101424-5.png)   ![](/sites/default/files/inline/images/images/image-20230510101443-6.png)   7. Dans les options de permissions, choisissez Attach policies directly (attacher les politiques directement), sélectionnez AdministratorAccess (accès administrateur) et cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510101504-7.png)   8. Dans Review and Create, cliquez sur le bouton Create user (créer un utilisateur) dans le pied de page : ![](/sites/default/files/inline/images/images/image-20230510101513-8.png)   9. Cliquez sur le bouton Download .csv file (télécharger le fichier .csv) pour enregistrer les nouvelles références de l'utilisateur. 10. Dans la barre Search de recherche supérieure, recherchez IAM et cliquez sur IAM : ![](/sites/default/files/inline/images/images/image-20230510101533-9.png)   11. Dans le menu de gauche, sélectionnez Users (Utilisateurs) : ![](/sites/default/files/inline/images/images/image-20230510101551-10.png)   12. Cliquez sur le lien de l'utilisateur Iris : ![](/sites/default/files/inline/images/images/image-20230510101604-11.png)   13. Cliquez sur l'onglet Security Credentials (références de sécurité) : ![](/sites/default/files/inline/images/images/image-20230510101618-12.png)   14. Allez dans la sous-section Access keys (clés d'accès) (faites défiler l'écran pour la trouver) et cliquez sur le bouton Create access key (créer une clé d'accès) : ![](/sites/default/files/inline/images/images/image-20230510101633-13.png)   15. Sélectionnez Command Line Interface (interface de ligne de commande), cochez la case "I understand the above recommendation and want to proceed to create an access key" (Je comprends la recommandation ci-dessus et je souhaite procéder à la création d'une clé d'accès), puis cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510101651-14.png)   ![](/sites/default/files/inline/images/images/image-20230510101704-15.png)   16. Cliquez maintenant sur le bouton Create access key (créer une clé d'accès) : ![](/sites/default/files/inline/images/images/image-20230510101730-16.png)   17. Copiez votre clé d'accès et votre clé d'accès secrète dans un fichier sur votre ordinateur. Utilisez le bouton Télécharger le fichier .csv et enfin cliquez sur le bouton Done (terminé) : ![](/sites/default/files/inline/images/images/image-20230510101749-17.png)   ## Installation de l'outil AWS CLI et y attribuer l'utilisateur créé L'outil AWS CLI est utilisé pour tirer l'image Docker vers AWS ECR (c'est une sorte de Docker Hub pour les images Docker AWS). Pour l'installer, procédez comme suit : 1. Allez sur https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install... et choisissez les instructions d'installation correspondant au système d'exploitation de votre ordinateur. 2. Après l'installation, si vous ne l'avez pas encore fait, suivez les étapes suivantes : a. Sur votre terminal, mettez :   aws configure b. Définissez la clé d'accès créée ci-dessus : ![](/sites/default/files/inline/images/images/image-20230510102005-18.png)   c. Définissez la clé secrète assemblée précédemment : ![](/sites/default/files/inline/images/images/image-20230510102013-19.png)   d. Ne modifiez pas les valeurs restantes. Acceptez simplement les valeurs par défaut : ##![](/sites/default/files/inline/images/images/image-20230510102020-20.png)   ## Téléchargement de votre application Docker sur l'ECR AWS 1. Dans le champ de recherche de la console AWS, recherchez ECR et sélectionnez Elastic Container Registry : ![](/sites/default/files/inline/images/images/image-20230510102053-21.png)   2. Cliquez sur le bouton Get Started (commencer) dans la section Create Repository (créer le référentiel) : ![](/sites/default/files/inline/images/images/image-20230510102103-22.png)   3. Dans Create Repository, mettez les valeurs suivantes : * Paramètres de visibilité : Public * Nom du référentiel : iris-repo iris-repo * Cliquez sur le bouton Create repository (créer le référentiel) ![](/sites/default/files/inline/images/images/image-20230510102122-23.png)   ![](/sites/default/files/inline/images/images/image-20230510102145-24.png)   4. Le référentiel est maintenant créé. Sélectionnez iris-repo et cliquez sur le bouton View push commands (afficher les commandes push) : ![](/sites/default/files/inline/images/images/image-20230510102337-25.png)   5. Copiez la valeur de l'URI du référentiel (deuxième colonne - URI) et stockez-la dans un fichier. Vous en aurez besoin plus tard au cours de cet article. 6. Exécutez les 4 commandes de la boîte de dialogue dans votre terminal dans le dossier où vous avez cloné le projet Git : ![](/sites/default/files/inline/images/images/image-20230510102356-26.png)   a. Première commande : connectez-vous avec l'utilisateur IRIS : ![](/sites/default/files/inline/images/images/image-20230510102416-27.png)   b. Deuxième commande : docker build -t iris-repo . ![](/sites/default/files/inline/images/images/image-20230510102434-28.png)        c. Troisième commande : docker tag iris-repo:latest public.ecr.aws/e7i6j8j1/iris-repo:latest ![](/sites/default/files/inline/images/images/image-20230510102451-29.png)   d. Dernière commande : docker push public.ecr.aws/e7i6j8j1/iris-repo:latest ![](/sites/default/files/inline/images/images/image-20230510102507-30.png)   Félicitations ! Votre projet Docker est maintenant une image Docker public sur AWS ECR.. ## Création de l'instance Docker sur AWS ECS pour votre nouvelle image AWS ECR Voici venue l'heure des dernières étapes. Nous allons créer une instance Docker fonctionnant sur AWS à ce stade. Pour ce faire, procédez comme suit 1. Accédez à la console AWS et recherchez ECS dans la barre de recherche supérieure. Cliquez ensuite sur le lien Elastic Container Service : ![](/sites/default/files/inline/images/images/image-20230510102539-31.png)   2. Dans le menu de gauche, sélectionnez Clusters : ![](/sites/default/files/inline/images/images/image-20230510102551-32.png)   3. Cliquez sur le bouton Create a cluster (créer un cluster) : ![](/sites/default/files/inline/images/images/image-20230510102604-33.png)   4. Sur Create Cluster, ajoutez la valeur iriscluster au champ Cluster name (nom du cluster). Acceptez les valeurs restantes pour les autres champs et cliquez sur le bouton Create (créer) : ![](/sites/default/files/inline/images/images/image-20230510102624-34.png)   ![](/sites/default/files/inline/images/images/image-20230510102637-35.png)   ![](/sites/default/files/inline/images/images/image-20230510102658-36.png)   5. Attendez quelques secondes, et vous aurez un nouveau cluster listé :     ![](/sites/default/files/inline/images/images/image-20230510102736-37.png)   6. Dans le menu de gauche, sélectionnez Task definitions (définitions de tâches) et allez à Create new task definition (créer une nouvelle définition de tâche) : ![](/sites/default/files/inline/images/images/image-20230510102754-38.png)   7. Dans Configure task definition and containers (configuration de la définition de la tâche et des conteneurs), définissez les valeurs indiquées ci-dessous et cliquez sur le bouton Next (suivant) : * Famille de définition des tâches : iristask * Détails du conteneur - Nom : irisrepo * Détails du conteneur - URI de l'image : URI que vous avez stocké dans un fichier lorsque vous avez créé l'image avec ECR. Dans mon cas, il s'agit de public.ecr.aws/e7i6j8j1/iris-repo * Port de Mappage – Port à conteneurs : 52773, Protocole : TCP. ![](/sites/default/files/inline/images/images/image-20230510102820-39.png)   ![](/sites/default/files/inline/images/images/image-20230510102827-40.png)   ![](/sites/default/files/inline/images/images/image-20230510102852-41.png)   8. Dans Configure environment, storage, monitoring, and tags (configuration de l'environnement, le stockage, la surveillance et les balises), modifiez la mémoire pour qu'elle soit de 4 Go. Le rôle de la tâche doit être modifié en ecsTaskExecutionRole, et Stockage - Quantité en 30. Pour les autres paramètres, acceptez les valeurs par défaut et cliquez sur le bouton Next (suivant) : ![](/sites/default/files/inline/images/images/image-20230510102922-42.png)   ![](/sites/default/files/inline/images/images/image-20230510102929-43.png)   ![](/sites/default/files/inline/images/images/image-20230510103004-44.png)   ![](/sites/default/files/inline/images/images/image-20230510103034-45.png)   ![](/sites/default/files/inline/images/images/image-20230510103039-46.png)   9. Dans Review (révision) and Create (créer), cliquez sur le bouton Create : ![](/sites/default/files/inline/images/images/image-20230510103146-47.png)   ![](/sites/default/files/inline/images/images/image-20230510103212-48.png)   ![](/sites/default/files/inline/images/images/image-20230510103235-49.png)   ![](/sites/default/files/inline/images/images/image-20230510103257-50.png)   ![](/sites/default/files/inline/images/images/image-20230510103341-51.png)     10. Cliquez sur le bouton Deploy > Run Task (déployer > exécuter une tâche) en haut de la page : ![](/sites/default/files/inline/images/images/image-20230510103412-52.png)   11. Dans Create (créer), définissez les valeurs mentionnées ci-dessous et cliquez sur le bouton Create : * Cluster existant : iriscluster * Options de calcul : Type de lancement Launch * Type d'application : Task (Tâche) 12. Développez la section Networking (mise en réseau) et choisissez : * Groupe de sécurité : sélectionnez Create a new security group (créer un nouveau groupe de sécurité) * Nom du groupe de sécurité : irissec * Description du groupe de sécurité : irissec * Règles d'entrée - Type : TCP personnalisé, plage de ports : 52773 ![](/sites/default/files/inline/images/images/image-20230510103454-53.png)   ![](/sites/default/files/inline/images/images/image-20230510103513-54.png)   ![](/sites/default/files/inline/images/images/image-20230510103536-55.png)   ![](/sites/default/files/inline/images/images/image-20230510103614-56.png)   ![](/sites/default/files/inline/images/images/image-20230510103650-57.png)   13. Attendez un certain temps pour voir l'état de la création (cliquez sur le bouton pour vérifier l'état actuel) : ![](/sites/default/files/inline/images/images/image-20230510103758-58.png)   14. . Lorsque l'état devient "Running" (en cours d'exécution), cliquez sur le lien Task (tâche) : ![](/sites/default/files/inline/images/images/image-20230510103832-59.png)   15. Copiez l'IP public : ![](/sites/default/files/inline/images/images/image-20230510103856-60.png)   16. Ouvrez votre navigateur et tapez (dans mon cas, il s'agit de 54.226.128.138) : http://<public ip>:52773/csp/sys/%25CSP.Portal.Home.zen 17. Le portail de gestion IRIS (avec l'utilisateur _SYSTEM et le mot de passe SYS) est maintenant actif, et les services REST pour l'application fonctionnent également (authentification de base avec _SYSTEM et SYS) : ![](/sites/default/files/inline/images/images/image-20230510103925-61.png)   ![](/sites/default/files/inline/images/images/image-20230510103959-62.png)   ![](/sites/default/files/inline/images/images/image-20230510104024-63.png)   Vous avez réussi ! Vous avez maintenant votre IRIS sur AWS. N'OUBLIEZ PAS D'ARRÊTER LA TÂCHE, POUR NE PAS ÊTRE FACTURÉ. Pour ce faire, cliquez sur le bouton Stop : ![](/sites/default/files/inline/images/images/image-20230510104054-64.png)   Profitez-en !
Article
Guillaume Rongier · Avr 4, 2022

Les globales sont des épées magiques pour stocker des données. Arbres (partie 2)

![](/sites/default/files/inline/images/old-sword-small.jpg) ## 3. Variantes des structures lors de l'utilisation de globales Une structure, telle qu'un arbre ordonné, présente plusieurs cas particuliers. Examinons ceux qui ont une valeur pratique pour le travail avec les globales. ### 3.1 Cas particulier 1. Un nœud sans branches Les globales peuvent être utilisées non seulement comme une liste de données, mais aussi comme des variables ordinaires. Par exemple, pour créer un compteur :   Set ^counter = 0 ; setting counter Set id=$Increment(^counter) ; atomic incrementation En même temps, une globale peut avoir des branches outre sa valeur. L'un n'exclut pas l'autre. ### 3.2 Cas particulier 2. Un nœud et plusieurs branches En fait, il s'agit d'une base classique clé-valeur. Et si nous enregistrons des tuples de valeurs au lieu de valeurs, nous obtiendrons une table ordinaire avec une clé primaire. ![](/sites/default/files/inline/images/key_value_table.png) Afin d'implémenter une table basé sur des globales, nous devrons former des chaînes de caractères à partir des valeurs des colonnes, puis les enregistrer dans une globale par la clé primaire. Afin de pouvoir diviser la chaîne en colonnes lors de la lecture, nous pouvons utiliser ce qui suit : 1. Caractère de délimitation. Set ^t(id1) = "col11/col21/col31" Set ^t(id2) = "col12/col22/col32" 2. Un schéma fixe, dans lequel chaque champ occupe un nombre particulier d'octets. C'est ainsi qu'on procède généralement dans les bases de données relationnelles. 3. Une fonction spéciale [$LB](http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flistbuild) (introduite dans Caché) qui compose une chaîne de caractères à partir de valeurs. Set ^t(id1) = $LB("col11", "col21", "col31") Set ^t(id2) = $LB("col12", "col22", "col32") Ce qui est intéressant, c'est qu'il n'est pas difficile de faire quelque chose de similaire aux clés étrangères dans les bases de données relationnelles en utilisant des globales. Appelons ces structures des index globaux. Un index global est un arbre supplémentaire permettant d'effectuer des recherches rapides sur des champs qui ne font pas partie intégrante de la clé primaire de la globale principale. Vous devez écrire un code supplémentaire pour le remplir et l'utiliser. Nous créons un index global basé sur la première colonne. Set ^i("col11", id1) = 1 Set ^i("col12", id2) = 1 Pour effectuer une recherche rapide par la première colonne, vous devrez regarder dans la ^i globale et trouver les clés primaires (id) correspondant à la valeur nécessaire dans la première colonne. Lors de l'insertion d'une valeur, nous pouvons créer à la fois des valeurs et des index globaux pour les champs nécessaires. Pour plus de fiabilité, nous allons l'intégrer dans une transaction. TSTART Set ^t(id1) = $LB("col11", "col21", "col31") Set ^i("col11", id1) = 1 TCOMMIT Plus d'informations sont disponibles ici [making tables in M using globals and emulation of secondary keys.](http://gradvs1.mgateway.com/download/extreme1.pdf) Ces tables fonctionneront aussi rapidement que dans les bases de données traditionnelles (ou même plus rapidement) si les fonctions d'insertion/mise à jour/suppression sont écrites en COS/M et compilées. J'ai vérifié cette affirmation en appliquant un grand nombre d'opérations INSERT et SELECT à une seule table à deux colonnes, en utilisant également les commandes TSTART et TCOMMIT (transactions). Je n'ai pas testé de scénarios plus complexes avec des accès concurrents et des transactions parallèles. Sans utiliser de transactions, la vitesse d'insertion pour un million de valeurs était de 778 361 insertions/seconde. Pour 300 millions de valeurs, la vitesse était de 422 141 insertions/seconde. Lorsque des transactions ont été utilisées, la vitesse a atteint 572 082 insertions/seconde pour 50 millions de valeurs. Toutes les opérations ont été exécutées à partir du code M compilé. J'ai utilisé des disques durs ordinaires, pas des SSD. RAID5 avec Write-back. Le tout fonctionnant sur un processeur Phenom II 1100T. Pour effectuer le même test pour une base de données SQL, il faudrait écrire une procédure stockée qui effectuerait les insertions en boucle. En testant MySQL 5.5 (stockage InnoDB) avec la même méthode, je n'ai jamais obtenu plus de 11K insertions par seconde. En effet, l'implémentation de tables avec des globales est plus complexe que de faire la même chose dans des bases de données relationnelles. C'est pourquoi les bases de données industrielles basées sur les globales ont un accès SQL pour simplifier le travail avec les données tabulaires. En général, si le schéma de données ne change pas souvent, que la vitesse d'insertion n'est pas critique et que l'ensemble de la base de données peut être facilement représenté par des tables normalisées, il est plus facile de travailler avec SQL, car il offre un niveau d'abstraction plus élevé. Dans ce cas, je voulais montrer que les globales peuvent être utilisées comme un constructeur pour créer d'autres bases de données. Comme le langage assembleur qui peut être utilisé pour créer d'autres langages. Et voici quelques exemples d'utilisation des globales pour créer des contreparties de [key-values, lists, sets, tabular, document-oriented DB's.](http://gradvs1.mgateway.com/docs/nosql_in_globals.pdf) Si vous devez créer une base de données non standard avec un minimum d'efforts, vous devriez envisager d'utiliser les globales. ### 3.3 Cas particulier 3. Un arbre à deux niveaux dont chaque nœud de deuxième niveau a un nombre fixe de branches Vous l'avez probablement deviné : il s'agit d'une implémentation alternative des tables utilisant des globales. Comparons-la avec la précédente. Tables dans un arborescence deux niveaux vs. Tables dans un arborescence mono niveau. Cons Pros Insertions plus lentes, car le nombre de nœuds doit être égal au nombre de colonnes. Une plus grande consommation d'espace sur le disque dur, car les index globaux (comme les index de table) avec les noms de colonne occupent de l'espace sur le disque dur et sont dupliqués pour chaque ligne.   Un accès plus rapide aux valeurs de certaines colonnes, puisque vous n'avez pas besoin d'analyser la chaîne de caractères. D'après mes tests, c'est 11,5 % plus rapide pour 2 colonnes et encore plus rapide pour plus de colonnes. Il est plus facile de modifier le schéma de données et de lire le code. **Conclusion:** Rien d'extraordinaire. Les performances étant l'un des principaux avantages des globales, il n'y a pratiquement aucun intérêt à utiliser cette approche, car il est peu probable qu'elle soit plus rapide que les tables ordinaires des bases de données relationnelles. ### 3.4 Cas général. Arbres et clés ordonnées Toute structure de données qui peut être représentée comme un arbre s'adapte parfaitement aux globales. #### 3.4.1 Objets avec des sous-objets ![](/sites/default/files/inline/images/json_opt.png) C'est dans ce domaine que les globales sont traditionnellement utilisées. Il existe de nombreuses maladies, médicaments, symptômes et méthodes de traitement dans le domaine médical. Il est irrationnel de créer une table avec un million de champs pour chaque patient, d'autant plus que 99% d'entre eux seront vides. Imaginez une base de données SQL composée des tables suivants : " Patient " ~ 100 000 champs, " Médicament " 100 000 champs, " Thérapie " 100 000 champs, " Complications " 100 000 champs et ainsi de suite. Comme alternative, vous pouvez créer une BD avec des milliers de tableaux, chacun pour un type de patient particulier (et ils peuvent aussi se superposer !), un traitement, un médicament, ainsi que des milliers de tables pour les relations entre ces tables. Les globales s'adaptent parfaitement aux soins de santé, puisqu'elles permettent à chaque patient de disposer d'un dossier complet, de la liste des thérapies, des médicaments administrés et de leurs effets, le tout sous la forme d'un arbre, sans gaspiller trop d'espace disque en colonnes vides, comme ce serait le cas avec les bases de données relationnelles. **Les globales fonctionnent bien pour les bases de données contenant des données personnelles**, lorsque la tâche consiste à accumuler et à systématiser le maximum de données personnelles diverses sur un client. C'est particulièrement important dans les domaines de la santé, de la banque, du marketing, de l'archivage et autres. Il est évident que SQL permet également d'émuler un arbre en utilisant seulement quelques tables ([EAV](https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model), [1](https://en.wikipedia.org/wiki/Hierarchical_database_model),[2](http://mikehillyer.com/articles/managing-hierarchical-data-in-mysql/),[3](https://stackoverflow.com/questions/4048151/what-are-the-options-for-storing-hierarchical-data-in-a-relational-database),[4](https://www.simple-talk.com/sql/performance/the-performance-of-traversing-a-sql-hierarchy/),[5](http://moinne.com/blog/ronald/mysql/manage-hierarchical-data-with-mysql-stored-procedures),[6](https://coderwall.com/p/ohomlg/nested-set-model-the-best-approach-to-deal-with-hierarchical-data), [7](http://tdan.com/modeling-hierarchies/5400),[8](https://www.sitepoint.com/hierarchical-data-database/)), mais c'est beaucoup plus complexe et plus lent. En fait, nous devrions écrire une globale basé sur des tables et cacher toutes les routines liées aux tables sous une couche d'abstraction. Il n'est pas correct d'émuler une technologie de niveau inférieur (les globales) à l'aide d'une technologie de niveau supérieur (SQL). C'est tout simplement injustifié. Ce n'est pas un secret que la modification d'un schéma de données dans des tableaux gigantesques (ALTER TABLE) peut prendre un temps considérable. MySQL, par exemple, effectue l'opération ALTER TABLE ADD|DROP COLUMN en copiant toutes les données de l'ancienne tableau vers la nouvelle (je l'ai testé sur MyISAM et InnoDB). Cela peut bloquer une base de données de production contenant des milliards d'enregistrements pendant des jours, voire des semaines. **Si nous utilisons des globales, la modification de la structure des données ne nous coûte rien** Nous pouvons ajouter de nouvelles propriétés à n'importe quel objet, à n'importe quel niveau de la hiérarchie et à n'importe quel moment. Les changements qui nécessitent de renommer les branches peuvent être appliqués en arrière-plan avec la base de données en fonctionnement. Par conséquent, lorsqu'il s'agit de stocker des objets comportant un grand nombre de propriétés facultatives, les globales fonctionnent parfaitement. Je vous rappelle que l'accès à l'une des propriétés est instantané, puisque dans une globale, tous les chemins sont un B-Arbre. Dans le cas général, les bases de données basées sur des globales sont un type de bases de données orientées documents qui supportent le stockage d'informations hiérarchiques. Par conséquent, les bases de données orientées documents peuvent concurrencer efficacement les globales dans le domaine du stockage des cartes médicales. Mais ce n'est pas encore le cas. Prenons MongoDB, par exemple. Dans ce champ, il perd face aux globales pour les raisons suivantes : Taille du document. L'unité de stockage est un texte au format JSON (BSON, pour être exact) dont la taille maximale est d'environ 16 Mo. Cette limitation a été introduite dans le but de s'assurer que la base de données JSON ne devienne pas trop lente lors de l'analyse syntaxique, lorsqu'un énorme document JSON y est enregistré et que des valeurs de champ particulières sont traitées. Ce document est censé contenir des informations complètes sur un patient. Nous savons tous à quel point les cartes de patient peuvent être épaisses. Si la taille maximale de la carte est plafonnée à 16 Mo, cela permet de filtrer immédiatement les patients dont les cartes contiennent des IRM, des radiographies et d'autres documents. Une seule branche d'une entreprise mondiale peut contenir des gigaoctets, des pétaoctets ou des téraoctets de données. Tout est dit, mais laissez-moi vous en dire plus. Le temps nécessaire à la création/modification/suppression de nouvelles propriétés de la carte du patient. Une telle base de données devrait copier la carte entière dans la mémoire (beaucoup de données !), analyser les données BSON, ajouter/modifier/supprimer le nouveau nœud, mettre à jour les index, remballer le tout en BSON et sauvegarder sur le disque. Une globale n'aurait besoin que d'adresser la propriété nécessaire et d'effectuer l'opération nécessaire. La vitesse d'accès à des propriétés particulières. Si le document possède de nombreuses propriétés et une structure à plusieurs niveaux, l'accès à des propriétés particulières sera plus rapide car chaque chemin dans la globale est le B-Arbre. En BSON, vous devrez analyser linéairement le document pour trouver la propriété nécessaire. #### 3.3.2 Tables associatives Les tables associatives (même avec les tables imbriquées) fonctionnent parfaitement avec les globales. Par exemple, cette table PHP ressemblera à la première illustration en 3.3.1. $a = array( "name" => "Vince Medvedev", "city" => "Moscow", "threatments" => array( "surgeries" => array("apedicectomy", "biopsy"), "radiation" => array("gamma", "x-rays"), "physiotherapy" => array("knee", "shoulder") ) ); #### 3.3.3 Documents hiérarchiques : XML, JSON Ils peuvent également être facilement stockés dans des globales et décomposés de manières différentes. **XML** La méthode la plus simple pour décomposer le XML en globales consiste à stocker les attributs des balises dans les nœuds. Et si vous avez besoin d'un accès rapide aux attributs des attributs, nous pouvons les placer dans des branches séparées. <note id=5> <to>Alex</to> <from>Sveta</from> <heading>Reminder</heading> <body>Call me tomorrow!</body> </note> Dans COS, le code ressemblera à ceci : Set ^xml("note")="id=5" Set ^xml("note","to")="Alex" Set ^xml("note","from")="Sveta" Set ^xml("note","heading")="Reminder" Set ^xml("note","body")="Call me tomorrow!" **Note:** Pour XML, JSON et les tables associatives, vous pouvez imaginer un certain nombre de méthodes pour les afficher dans les globales. Dans ce cas particulier, nous n'avons pas reflété l'ordre des balises imbriquées dans la balise "note". Dans la globale **^xml**, les balises imbriquées seront affichés dans l'ordre alphabétique. Pour un affichage précis de l'ordre, vous pouvez utiliser le modèle suivant, par exemple : ![](/sites/default/files/inline/images/xml_sort.png) **JSON.** Le contenu de ce document JSON est présenté dans la première illustration de la section 3.3.1 : var document = { "name": "Vince Medvedev", "city": "Moscow", "threatments": { "surgeries": ["apedicectomy", "biopsy"], "radiation": ["gamma", "x-rays"], "physiotherapy": ["knee", "shoulder"] }, }; #### 3.3.4 Structures identiques liées par des relations hiérarchiques Exemples : structure des bureaux de vente, positions des personnes dans une structure MLM, base des débuts aux échecs. **Base de données des débuts.** Vous pouvez utiliser une évaluation de la force du mouvement comme valeur de l'indice de nœud d'une globale. Dans ce cas, vous devrez sélectionner une branche ayant le poids le plus élevé pour déterminer le meilleur déplacement. Dans la globale, toutes les branches de chaque niveau seront triées en fonction de la force du mouvement. ![](/sites/default/files/inline/images/debut.png) **La structure des bureaux de vente, des personnes dans une société MLM.** Les noeuds peuvent stocker certaines valeurs de cache reflétant les caractéristiques de la sous-arborescence entière. Par exemple, les ventes de cette sous-arborescence particulière. Nous pouvons obtenir des informations exactes sur les réalisations de n'importe quelle branche à tout moment. ![](/sites/default/files/inline/images/sales.png) ## 4. Situations où l'utilisation des globales est avantageuse La première colonne contient une liste de cas où l'utilisation des globales vous donnera un avantage considérable en termes de performance, et la seconde - une liste de situations où elles simplifieront le développement ou le modèle de données. Vitesse Commodité du traitement/de la présentation des données 1. Insertion [avec tri automatique à chaque niveau], [indexation par la clé primaire] 2. Suppression de sous-arbres 3. Objets comportant de nombreuses propriétés imbriquées auxquelles vous devez accéder individuellement 4. Structure hiérarchique avec possibilité de parcourir les branches enfant à partir de n'importe quelle branche, même inexistante 5. Parcours en profondeur de l'arbre 1. Objets/instances avec un grand nombre de propriétés/instances non requises [et/ou imbriquées] 2. Données sans schéma - de nouvelles propriétés peuvent souvent être ajoutées et d'anciennes supprimées 3. Vous devez créer une BD non standard.Bases de données de chemins et arbres de solutions 4. Lorsque les chemins peuvent être représentés de manière pratique sous forme d'arbre 5. On doit supprimer les structures hiérarchiques sans utiliser la récursion   Clause de non-responsabilité: cet article et les commentaires le concernant reflètent uniquement mon opinion et n'ont rien à voir avec la position officielle de la société InterSystems.
Article
Irène Mykhailova · Mai 9, 2022

Connaissez vos indexes

Cet article est le premier d'une série d'articles sur les indexes SQL. Partie 1 - Découvrez vos indexes Qu'est-ce qu'un index, en fait ? Imaginez la dernière fois où vous êtes allé à la bibliothèque. En général, les livres y sont classés par sujet (puis par auteur et par titre), et chaque étagère comporte une étiquette avec un code décrivant le sujet de ses livres. Si vous voulez collectionner des livres d'un certain sujet, au lieu de traverser chaque allée et de lire la couverture intérieure de chaque livre, vous pouvez vous diriger directement vers l'étagère étiquetée avec le sujet désiré et choisir vos livres. Un index SQL a la même fonction générale : améliorer les performances en donnant une référence rapide à la valeur des champs pour chaque ligne de la table. La mise en place d'index est l'une des principales étapes de la préparation de vos classes pour une performance SQL optimale. Dans cet article, nous allons examiner les questions suivantes : 1. Qu'est-ce qu'un index et pourquoi/quand dois-je l'utiliser ?2. Quels types d'indexes existent et pour quels scénarios sont-ils parfaitement adaptés ?3. Qu'est-ce qu'un index ?4. Comment le créer ? Et si j'ai des index, qu'est-ce que j'en fais ? Je vais me référer aux classes de notre schéma Sample. Celles-ci sont disponibles dans le stockage Github suivant, et elles sont également fournies dans l'espace de noms Samples dans les installations de Caché et Ensemble : https://github.com/intersystems/Samples-Data Les principes de base Vous pouvez indexer chaque propriété persistante et chaque propriété qui peut être calculée de manière fiable à partir de données persistantes. Disons que nous voulons indexer la propriété TaxID dans Sample.Company. Dans Studio ou Atelier, nous ajouterions ce qui suit à la définition de la classe : Index TaxIDIdx On TaxID; L'instruction SQL DDL équivalente ressemblerait à ceci : CREATE INDEX TaxIDIdx ON Sample.Company (TaxID); La structure globale de l'index par défaut est la suivante : ^Sample.CompanyI("TaxIDIdx ",<TaxIDValueAtRowID>,<RowID>) = "" Notez qu'il y a moins d'index inférieurs à lire que de champs dans une globale de données typique. Considérons la requête SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349' C'est logiquement simple et le plan de requête pour l'exécution de cette requête le reflète : Ce plan indique essentiellement que nous vérifions l'index global pour les lignes avec la valeur TaxID donnée, puis nous nous référons à la globale de données ("carte principale") pour récupérer la ligne correspondante. Considérons maintenant la même requête sans index sur TaxIDX. Le plan de requête résultant est, comme prévu, moins efficace : Sans index, l'exécution de la requête sous-jacente d'IRIS repose sur la lecture en mémoire et l'application de la condition de la clause WHERE à chaque ligne de la table. Et comme nous ne nous attendons logiquement pas à ce qu'une société partage TaxID, nous faisons tout ce travail pour une seule ligne ! Bien sûr, avoir des indexes signifie avoir des données d'index et de ligne sur le disque. En fonction de ce sur quoi nous avons une condition et de la quantité de données que notre table contient, cela peut s'avérer avoir ses propres défis lorsque nous créons et alimentons un index. Alors, quand ajoutons-nous un index à une propriété ? Dans le cas général, nous avons fréquemment à remettre une propriété en état. Des exemples sont des informations d'identification telles que le SSN d'une personne ou un numéro de compte bancaire. Vous pouvez également considérer les dates de naissance ou les fonds d'un compte. Pour en revenir à Sample.Company, la classe bénéficierait peut-être de l'indexation de la propriété Revenue si nous voulions collecter des données sur les organisations à hauts revenus. À l'inverse, les propriétés sur lesquelles il est peu probable que nous remettions des conditions sont moins appropriées pour être indexées : disons un slogan ou une description d'entreprise. Facile - sauf qu'il faut aussi considérer quel type d'index est le meilleur ! Types d'indexes Il existe six principaux types d'index que je vais aborder ici : standard, bitmap, compound, collection, bitslice et data. Je vais également aborder brièvement les index iFind, qui sont basés sur les flux. Il y a des chevauchements possibles ici et nous avons déjà abordé les indexes standards avec l'exemple ci-dessus. Je vais présenter des exemples sur la façon de créer des indexes dans votre définition de classe, mais l'ajout de nouveaux index à une classe est plus complexe que le simple ajout d'une ligne dans votre définition de classe. Nous aborderons des considérations supplémentaires dans la partie suivante. Prenons l'exemple de Sample.Person. Notez que Person a une sous-classe Employee, ce qui sera utile pour comprendre certains exemples. Employee partage son stockage global de données avec Person, et tous les indexes de Person sont hérités par Employee - ce qui signifie qu'Employee utilise l'index global de Person pour ces indexes hérités. Si vous n'êtes pas familier avec ces classes, voici un aperçu général de celles-ci : Person a les propriétés SSN, DOB, Name, Home (un objet d'adresse intégré contenant l'état et la ville), Office (également une adresse), et la collection de listes FavoriteColors. Employee a une propriété supplémentaire Salary (que j'ai moi-même définie). Standard Index DateIDX On DOB; J'utilise ici le terme "standard" pour désigner les indexes qui stockent la valeur brute d'une propriété (par opposition à une représentation binaire). Si la valeur est une chaîne de caractères, elle sera stockée sous une certaine collation - celle de SQLUPPER par défaut. Par rapport aux index bitmap ou bitslice, les indexes standard sont plus compréhensibles pour les humains et relativement faciles à maintenir. Nous avons un nœud global pour chaque ligne de la table. Voici comment DateIDX est stocké au niveau global. ^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81 Notez que le premier index inférieur après le nom de l'index est la valeur de la date, le dernier index inférieur est l'ID de la personne ayant cette date de naissance, et la valeur stockée sur ce noeud global indique que cette personne est également membre de la sous-classe Sample.Employee. Si cette personne n'était membre d'aucune sous-classe, la valeur du noeud serait une chaîne vide. Cette structure de base sera cohérente avec la plupart des indexes non binaires, où les indexes sur plus d'une propriété créent plus d'indexes inférieurs dans la globale, et où le fait d'avoir plus d'une valeur stockée au nœud produit un objet $listbuild, par exemple : ^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2) Bitmap - Une représentation binaire de l'ensemble des ID-codes correspondant à une valeur de propriété. Index HomeStateIDX On Home.State [ Type = bitmap]; Les indexes bitmap sont stockés par valeur unique, contrairement aux indexes standard, qui sont stockés par ligne. Pour aller plus loin dans l'exemple ci-dessus, disons que la personne avec l'ID 1 vit dans le Massachusetts, avec l'ID 2 à New York, avec l'ID 3 dans le Massachusetts et avec l'ID 4 à Rhode Island. HomeStateIDX est essentiellement stocké comme suit : ID 1 2 3 4 (…) (…) 0 0 0 0 - MA 1 0 1 0 - NY 0 1 0 0 - RI 0 0 0 1 - (…) 0 0 0 0 - Si nous voulions qu'une requête renvoie les données des personnes vivant en Nouvelle-Angleterre, le système effectue un bitwise OR sur les lignes pertinentes de l'index bitmap. On voit rapidement que nous devons charger en mémoire des objets Personne avec les ID 1, 3 et 4 au minimum. Les bitmaps peuvent être efficaces pour les opérateurs AND, RANGE et OR dans vos clauses WHERE. Bien qu'il n'y ait pas de limite officielle au nombre de valeurs uniques que vous pouvez avoir pour une propriété avant qu'un index bitmap soit moins efficace qu'un index standard, la règle générale est d'environ 10 000 valeurs distinctes. Ainsi, si un index bitmap peut être efficace pour un état des États-Unis, un index bitmap pour une ville ou un comté des États-Unis ne serait pas aussi utile. Un autre concept à prendre en compte est l'efficacité du stockage. Si vous prévoyez d'ajouter et de supprimer fréquemment des lignes de votre table, le stockage de votre index bitmap peut devenir moins efficace. Prenons l'exemple ci-dessus : supposons que nous ayons supprimé de nombreuses lignes pour une raison quelconque et que notre table ne contienne plus de personnes vivant dans des états moins peuplés tels que le Wyoming ou le Dakota du Nord. Le bitmap comporte donc plusieurs lignes contenant uniquement des zéros. D'un autre côté, la création de nouvelles lignes dans les grandes tables peut finir par devenir plus lente, car le stockage bitmap doit accueillir un plus grand nombre de valeurs uniques. Dans ces exemples, j'ai environ 150 000 lignes dans Sample.Person. Chaque nœud global stocke jusqu'à 64 000 ID, de sorte que l'index bitmap global à la valeur MA est divisé en trois parties : ^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...)) ^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…)) ^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…)) Cas particulier : Bitmap étendu Un bitmap étendue, souvent appelé $<ClassName>, est un index bitmap sur les ID d'une classe - cela donne à IRIS un moyen rapide de savoir si une ligne existe et peut être utile pour les requêtes COUNT ou les requêtes sur les sous-classes. Ces indexes sont générés automatiquement lorsqu'un index bitmap est ajouté à la classe ; vous pouvez également créer manuellement un index bitmap d'étendue dans une définition de classe comme suit : Index Company [ Extent, SqlName = "$Company", Type = bitmap ]; Ou via le mot-clé DDL appelé BITMAPEXTENT : CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company Composés - Les indexes basés sur deux ou plusieurs propriétés Index OfficeAddrIDX On (Office.City, Office.State); Le cas général d'utilisation des index composés est le conditionnement de requêtes fréquentes sur deux propriétés ou plus. L'ordre des propriétés dans un index composé est important en raison de la manière dont l'index est stocké au niveau global. Le fait d'avoir la propriété la plus sélective en premier est plus efficace en termes de performances car cela permet d'économiser les lectures initiales du disque de l'index global ; dans cet exemple, Office.City est en premier car il y a plus de villes uniques que d'états aux États-Unis. Le fait d'avoir une propriété moins sélective en premier est plus efficace en termes d'espace. En termes de structure globale, l'arbre d'indexation serait plus équilibré si State était placé en premier. Pensez-y : chaque état contient de nombreuses villes, mais certains noms de ville n'appartiennent qu'à un seul état. Vous pouvez également vous demander si vous vous attendez à exécuter des requêtes fréquentes ne conditionnant qu'une seule de ces propriétés - cela peut vous éviter de définir un autre index. Voici un exemple de la structure globale des indexes composés : ^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~" Commentaires : Index composé ou index bitmap ? Pour les requêtes comportant des conditions sur plusieurs propriétés, vous pouvez également vous demander si des indexes bitmap séparés seraient plus efficaces qu'un seul index composé. Les opérations par bit sur deux indexes différents peuvent être plus efficaces à condition que les indexes bitmap conviennent à chaque propriété. Il est également possible d'avoir des indexes bitmap composés, c'est-à-dire des indexes bitmap dont la valeur unique est l'intersection de plusieurs propriétés sur lesquelles vous effectuez l'indexation. Considérez la table donnée dans la section précédente, mais au lieu des états, nous avons toutes les paires possibles d'un état et d'une ville (par exemple, Boston, MA, Cambridge, MA, même Los Angeles, MA, etc.), et les cellules obtiennent des 1 pour les lignes qui adhèrent aux deux valeurs. Collection - Les index basés sur les propriétés de la collection Nous avons ici la propriété FavoriteColors définie comme suit : Property FavoriteColors As list Of %String; Avec chacun des indexes suivants définis à titre de démonstration : Index fcIDX1 On FavoriteColors(ELEMENTS);Index fcIDX2 On FavoriteColors(KEYS); J'utilise ici le terme "collection" pour désigner plus largement les propriétés à cellule unique contenant plus d'une valeur. Les propriétés List Of et Array Of sont pertinentes ici, et si vous le souhaitez, même les chaînes de caractères délimitées. Les propriétés de la collection sont automatiquement analysées pour construire leurs indexes. Pour les propriétés délimitées, comme un numéro de téléphone, vous devez définir cette méthode, <PropertyName>BuildValueArray(value, .valueArray), explicitement. Compte tenu de l'exemple ci-dessus pour FavoriteColors, fcIDX1 ressemblerait à ceci pour une personne dont les couleurs préférées sont le bleu et le blanc : ^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~" (…) ^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~" fcIDX2 ressemblerait à : ^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~" ^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~" Dans ce cas, puisque FavoriteColors est une collection de listes, un index basé sur ses clés est moins utile qu'un index basé sur ses éléments. Veuillez vous référer à notre documentation pour des considérations plus approfondies sur la création et la gestion des indexes sur les propriétés des collections. Bitslice - Représentation en bitmap de la représentation en chaîne de bits des données numériques Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee Contrairement aux indexes bitmap, qui contiennent des balises indiquant quelles lignes contiennent une valeur spécifique, les indexes bitslice convertissent d'abord les valeurs numériques de la décimale à la binaire, puis créent un bitmap sur chaque chiffre de la valeur binaire. Reprenons l'exemple ci-dessus et, par souci de réalisme, simplifions le salaire en unités de 1 000 dollars. Ainsi, si le salaire d'un employé est enregistré sous la forme 65, il est compris comme représentant 65 000 dollars. Disons que nous avons un employé avec l'ID 1 qui a un salaire de 15, l'ID 2 un salaire de 40, l'ID 3 un salaire de 64 et l'ID 4 un salaire de 130. Les valeurs binaires correspondantes sont : 15 0 0 0 0 1 1 1 1 40 0 0 1 0 1 0 0 0 64 0 1 0 0 0 0 0 0 130 1 0 0 0 0 0 1 0 Notre chaîne de bits s'étend sur 8 chiffres. La représentation bitmap correspondante - les valeurs d'indexes bitslice - est essentiellement stockée comme suit : ^Sample.PersonI("SalaryIDX",1,1) = "1000" ; La ligne 1 a une valeur à la place 1 ^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Les lignes 1 et 4 ont des valeurs à la place 2 ^Sample.PersonI("SalaryIDX",3,1) = "1000" ; La ligne 1 a une valeur à la place 4 ^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Les lignes 1 et 2 ont des valeurs à la place 8 ^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc… ^Sample.PersonI("SalaryIDX",6,1) = "0100" ^Sample.PersonI("SalaryIDX",7,1) = "0010" ^Sample.PersonI("SalaryIDX",8,1) = "0001" Notez que les opérations modifiant Sample.Employee ou les salaires dans ses lignes, c'est-à-dire les INSERTs, UPDATESs et DELETEs, nécessitent maintenant la mise à jour de chacun de ces nœuds globaux, ou bitslices. L'ajout d'un index bitslice à plusieurs propriétés d'une table ou à une propriété fréquemment modifiée peut présenter des risques pour les performances. En général, la maintenance d'un index bitslice est plus coûteuse que celle des indexes standard ou bitmap. Les indexes Bitslice sont hautement spécialisés et ont donc des cas d'utilisation spécifiques : les requêtes qui doivent effectuer des calculs agrégés, par exemple SUM, COUNT ou AVG. En outre, ils ne peuvent être utilisés efficacement que sur des valeurs numériques - les chaînes de caractères sont converties en un 0 binaire. Notez que si la table de données, et non les index, doit être lu pour vérifier la condition d'une requête, les indexes bitslice ne seront pas choisis pour exécuter la requête. Supposons que Sample.Person ne possède pas d'index sur Name. Si nous calculions le salaire moyen des employés portant le nom de famille Smith : SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' nous aurions besoin de lire des lignes de données pour appliquer la condition WHERE, et donc l'index bitslice ne serait pas utilisé en pratique. Des problèmes de stockage similaires se posent pour les indexes bitslice et bitmap sur les tables où des lignes sont fréquemment créées ou supprimées. Data - Index dont les données sont stockées dans leurs nœuds globaux. Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ]; Dans plusieurs des exemples précédents, vous avez peut-être observé la chaîne “~Sample.Employee~” stockée comme valeur au niveau du noeud lui-même. Rappelez-vous que Sample.Employee hérite des indexes de Sample.Person. Lorsque nous effectuons une requête sur les employés en particulier, nous lisons la valeur aux nœuds d'index correspondant à notre condition de propriété pour vérifier que ladite personne est également un employé. On peut aussi définir explicitement les valeurs à stocker. Le fait d'avoir des données définies au niveau des nœuds globaux de l'index permet d'éviter la lecture de l'ensemble des données globales ; cela peut être utile pour les requêtes sélectives ou les requêtes ordonnées fréquentes. Considérons l'index ci-dessus comme un exemple. Si nous voulions extraire des informations d'identification sur une personne à partir de tout ou une partie de son nom (par exemple, pour rechercher des informations sur les clients dans une application de réception), nous pourrions avoir une requête telle que SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name Puisque les conditions de notre requête sur le nom et les valeurs que nous récupérons sont toutes contenues dans les nœuds globaux QuickSearchIDX, il nous suffit de lire notre I globale pour exécuter cette requête. Notez que les valeurs de données ne peuvent pas être stockées avec des indexes de bitmap ou de bitslice. ^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.") iFind Indexes Vous en avez déjà entendu parler ? Moi non plus. Les indexes iFind sont utilisés sur les propriétés des flux, mais pour les utiliser vous devez spécifier leurs noms avec des mots-clés dans la requête. Je pourrais vous en dire plus, mais Kyle Baxter a déjà rédigé un article utile à ce sujet.
Article
Sylvain Guilbaud · Mars 31, 2023

Prédictions de Covid-19 ICU via ML vs. IntegratedML (Partie II)

Mots-clés:  IRIS, IntegratedML, apprentissage automatique, Covid-19, Kaggle  Continuation de la [précédente Partie I](https://community.intersystems.com/post/run-some-covid-19-icu-predictions-ml-vs-integratedml-part-i) ... Dans la partie I, nous avons parcouru les approches ML traditionnelles sur ce jeu de données Covid-19 sur Kaggle. Dans cette partie II, nous allons exécuter les mêmes données et la même tâche, dans sa forme la plus simple possible, à travers IRIS integratedML qui est une interface SQL agréable et élégante pour les options AutoML du backend. Cette interface utilise le même environnement.    ## Approche IntegratedML ? ### **Comment charger des données dans IRIS** [integredML-demo-template](https://openexchange.intersystems.com/package/integratedml-demo-template) a défini plusieurs façons de charger des données dans IRIS. Par exemple, je peux définir une classe IRIS personnalisée spécifique à ce fichier xls au format CSV, puis le charger dans un tableau IRIS. Cela permet un meilleur contrôle pour les volumes de données importants.  Cependant, dans cet article, j'opte pour une méthode simplifiée et légère, en me contentant de [charger le jeux des données dans un tableau IRIS via une fonction Python personnalisée que j'ai créée](https://community.intersystems.com/post/save-pandas-dataframe-iris-quick-note).  Cela nous permet de sauvegarder à tout moment les différentes étapes des dataframes brutes ou traitées dans IRIS, pour des comparaisons similaires avec l'approche ML précédente. def to_sql_iris(cursor, dataFrame, tableName, schemaName='SQLUser', drop_table=False ): """" Insertion dynamique d'un dataframe dans un tableau IRIS via SQL par "excutemany" Inputs: cursor: Curseur Python JDBC ou PyODBC à partir d'une connexion DB valide et établie dataFrame: Pandas dataframe tablename: Tableau SQL IRIS à créer, à insérer ou à modifier schemaName: IRIS schemaName, par défaut pour "SQLUser" drop_table: Si le tableau existe déjà, le supprimer et le recréer si True ; sinon, le sauvegarder et l'appliquer Output: True en cas de succès ; False en cas d'exception. """ if drop_table: try: curs.execute("DROP TABLE %s.%s" %(schemaName, tableName)) except Exception: pass try: dataFrame.columns = dataFrame.columns.str.replace("[() -]", "_") curs.execute(pd.io.sql.get_schema(dataFrame, tableName)) except Exception: pass curs.fast_executemany = True cols = ", ".join([str(i) for i in dataFrame.columns.tolist()]) wildc =''.join('?, ' * len(dataFrame.columns)) wildc = '(' + wildc[:-2] + ')' sql = "INSERT INTO " + tableName + " ( " + cols.replace('-', '_') + " ) VALUES" + wildc #print(sql) curs.executemany(sql, list(dataFrame.itertuples(index=False, name=None)) ) return True ### **Configuration de la connexion Python JDBC** import numpy as np import pandas as pd from sklearn.impute import SimpleImputer import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score, roc_curve import seaborn as sns sns.set(style="whitegrid") import jaydebeapi url = "jdbc:IRIS://irisimlsvr:51773/USER" driver = 'com.intersystems.jdbc.IRISDriver' user = "SUPERUSER" password = "SYS" jarfile = "./intersystems-jdbc-3.1.0.jar" conn = jaydebeapi.connect(driver, url, [user, password], jarfile) curs = conn.cursor()   ### **Définition du point de départ des données** Pour les comparaisons à l'identique, j'ai commencé par le dataframe après les sélections de caractéristiques dans le post précédent (dans la section "Sélection de caractéristiques - Sélection finale"), où "DataS" est le dataframe exact que nous commençons ici. data = dataS data = pd.get_dummies(data) data.ÂGE_AU-DESSUS65 = data.ÂGE_AU-DESSUS65.astype(int) data.ICU = data.ICU.astype(int) data_new = data data_new   ÂGE_AU-DESSUS65 GENRE HTN AUTRES CALCIUM_MÉDIAN CALCIUM_MIN CALCIUM_MAX CRÉATININE_MÉDIANE CRÉATININE_MOYENNE CRÉATININE_MIN ... DIFFÉRENCE_DU_RYTHME_CARDIAQUE_REL DIFFÉRENCE_DE_TAUX_RESPIRATOIRE_REL DIFFÉRENCE_DE_TEMPÉRATURE_REL DIFFÉRENCE_DE_SATURATION_D'OXYGÈNE_REL USI FENÊTRE_0-2 FENÊTRE_2-4 FENÊTRE_4-6 FENÊTRE_6-12 FENÊTRE_AU-DESSUS_12 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 2 1 0.0 0.0 1.0 0.183673 0.183673 0.183673 -0.868365 -0.868365 -0.868365 ... -0.817800 -0.719147 -0.771327 -0.886982 1 3 1 0.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -0.817800 -0.719147 -1.000000 -1.000000 1 4 1 0.0 0.0 1.0 0.326531 0.326531 0.326531 -0.926398 -0.926398 -0.926398 ... -0.230462 0.096774 -0.242282 -0.814433 1 1 ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 1920 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1921 1.0 0.0 1.0 0.244898 0.244898 0.244898 -0.934890 -0.934890 -0.934890 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1922 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1923 1.0 0.0 1.0 0.330359 0.330359 0.330359 -0.891078 -0.891078 -0.891078 ... -1.000000 -1.000000 -1.000000 -1.000000 1 1924 1.0 0.0 1.0 0.306122 0.306122 0.306122 -0.944798 -0.944798 -0.944798 ... -0.763868 -0.612903 -0.551337 -0.835052 1 1925 lignes × 62 colonnes Ce qui précède indique que nous disposons de 58 caractéristiques sélectionnées plus 4 autres caractéristiques converties à partir de la colonne non numérique précédente ("FENÊTRE").     ### **Sauvegarder les données dans le tableau IRIS** Nous utilisons la fonction **to\_sql\_iris** ci-dessus pour sauvegarder les données dans le tableau IRIS "CovidPPP62" : iris_schema = 'SQLUser' iris_table = 'CovidPPP62' to_sql_iris(curs, data_new, iris_table, iris_schema, drop_table=True) df2 = pd.read_sql("SELECT COUNT(*) from %s.%s" %(iris_schema, iris_table),conn) display(df2)  Sauvegarder les données dans le tableau IRIS Aggregate_1 1925 Définissez ensuite le nom de la vue de formation, le nom du modèle et la colonne cible de la formation, qui est ici " USI ".   dataTable = iris_table dataTableViewTrain = dataTable + 'Train1' dataTablePredict = dataTable + 'Predict1' dataColumn = 'ICU' dataColumnPredict = 'ICUPredicted' modelName = "ICUP621" #choisir un nom - doit être unique du côté serveur Nous pouvons ensuite diviser les données en une Vue de formation (1700 lignes) et une Vue de test (225 lignes). Nous ne sommes pas obligés de faire cela dans Integrated ML ; c'est juste à des fins de comparaison avec l'article précédent. curs.execute("CREATE VIEW %s AS SELECT * FROM %s WHERE ID<=1700" % (dataTableViewTrain, dataTable)) df62 = pd.read_sql("SELECT * from %s" % dataTableViewTrain, conn) display(df62) print(dataTableViewTrain, modelName, dataColumn) CovidPPP62Train1 ICUP621 ICU   ### **Formation du modèle à l'aide de l'AutoML par défaut d'IntegratedML** curs.execute("CREATE MODEL %s PREDICTING (%s) FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODÈLE_INFO 9 USIP621 USIP6212 AutoML 2020-07-22 19:28:16.174000 classification ModelType:Random Forest, Paquet:sklearn, Prob... Ainsi, nous pouvons voir que le résultat montre qu'IntegratedML a automatiquement choisi "ModelType" comme étant "Random Forest" (forêt aléatoire), et traite le problème comme une tâche de "Classification".  C'est exactement ce que nous avons obtenu après les longues comparaisons de modèles et les sélections par boîte à moustaches, ainsi que le long réglage des paramètres du modèle par quadrillage, etc. dans l'article précédent, n'est-ce pas ? **Remarque**: le SQL ci-dessus est le strict minimum selon la syntaxe d'IntegratedML. Je n'ai pas spécifié d'approche de formation ou de sélection de modèle, et je n'ai pas défini de plateforme de ML. Tout a été laissé à la décision de l'IML, qui a réussi à mettre en œuvre sa stratégie de formation interne, avant de se contenter d'un modèle raisonnable avec des résultats finaux corrects. Je dirais que cela a dépassé mes attentes.    Effectuons un rapide test de comparaison du modèle actuellement entraîné sur notre ensemble de test réservé.   ### **Prédiction des résultats sur la base de données de test** Nous avons utilisé 1700 lignes pour la formation. Ci-dessous, nous créons une vue des données de test avec les 225 lignes restantes, et nous exécutons SELECT PREDICT sur ces enregistrements. Nous sauvegarderons le résultat prédit dans '`dataTablePredict`', et le chargerons dans 'df62' en tant que data frame. dataTableViewTest = "SQLUSER.DTT621" curs.execute("CREATE VIEW %s AS SELECT * FROM %s WHERE ID > 1700" % (dataTableViewTest, dataTable)) curs.execute("DROP TABLE %s" % dataTablePredict ) curs.execute("Create Table %s (%s VARCHAR(100), %s VARCHAR(100))" % (dataTablePredict, dataColumnPredict, dataColumn)) curs.execute("INSERT INTO %s SELECT PREDICT(%s) AS %s, %s FROM %s" % (dataTablePredict, modelName, dataColumnPredict, dataColumn, dataTableViewTest)) df62 = pd.read_sql("SELECT * from %s ORDER BY ID" % dataTablePredict, conn) display(df62) Nous n'avons pas besoin de calculer manuellement sa matrice de confusion. Il s'agit simplement d'une comparaison : TP = df62[(df62['ICUPredicted'] == '1') & (df62['ICU']=='1')].count()['ICU'] TN = df62[(df62['ICUPredicted'] == '0') & (df62['ICU']=='0')].count()["ICU"] FN = df62[(df62['ICU'] == '1') & (df62['ICUPredicted']=='0')].count()["ICU"] FP = df62[(df62['ICUPredicted'] == '1') & (df62['ICU']=='0')].count()["ICU"] print(TP, FN, '\n', FP, TN) precision = (TP)/(TP+FP) recall = (TP)/(TP+FN) f1 = ((precision*recall)/(precision+recall))*2 accuracy = (TP+TN) / (TP+TN+FP+FN) print("Precision: ", precision, " Recall: ", recall, " F1: ", f1, " Accuracy: ", accuracy) 34 20 8 163 Précision: 0.8095238095238095 rappel: 0.6296296296296297 F1: 0.7083333333333334 Exactitude: 0.8755555555555555 Nous pouvons également utiliser la syntaxe IntegratedML pour obtenir sa matrice de confusion intégrée : # valider les données de test curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 ... ... ... ... ... Si l'on compare avec le "Résultat original" de la section " Exécuter une formation de base en LR " dans la partie I, le résultat ci-dessus présente un rappel de 63 % contre 57 %, et une exactitude de 88 % contre 85 %. Il s'agit donc d'un meilleur résultat avec IntegratedML.   ### **Former à nouveau IntegratedML sur des données de formation rééquilibrées via SMOTE** Le test ci-dessus a été effectué sur des données déséquilibrées, dans lesquelles le rapport entre les patients admis en USI et les patients non admis est de 1:3. Donc, comme dans l'article précédent, nous allons simplement effectuer un SMOTE pour que les données soient équilibrées, puis nous allons réexécuter le pipeline IML ci-dessus. 'X\_train\_res' and 'y\_train\_res' sont des dataframes après SMOTE de la Partie I précédente dans sa section " Exécuter une formation de base en LR ".  df_x_train = pd.DataFrame(X_train_res) df_y_train = pd.DataFrame(y_train_res) df_y_train.columns=['ICU'] df_smote = pd.concat([df_x_train, df_y_train], 1) display(df_smote) iris_schema = 'SQLUser' iris_table = 'CovidSmote' to_sql_iris(curs, df_smote, iris_table, iris_schema, drop_table=True) # sauvegarder ceci dans un nouveau tableau IRIS portant le nom spécifié df2 = pd.read_sql("SELECT COUNT(*) from %s.%s" %(iris_schema, iris_table),conn) display(df2)   Aggregate_1 2490 Le jeu de données comporte désormais 2490 lignes au lieu de 1700, car SMOTE a enrichi davantage d'enregistrements avec USI = 1. dataTable = iris_table dataTableViewTrain = dataTable + 'TrainSmote' dataTablePredict = dataTable + 'PredictSmote' dataColumn = 'ICU' dataColumnPredict = 'ICUPredictedSmote' modelName = "ICUSmote1" #choisir un nom - doit être unique du côté serveur curs.execute("CREATE VIEW %s AS SELECT * FROM %s" % (dataTableViewTrain, dataTable)) df_smote = pd.read_sql("SELECT * from %s" % dataTableViewTrain, conn) display(df_smote) print(dataTableViewTrain, modelName, dataColumn) CovidSmoteTrainSmote ICUSmote1 ICU curs.execute("CREATE MODEL %s PREDICTING (%s)  FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODEL_INFO 9 USIP621 USIP6212 AutoML 2020-07-22 19:28:16.174000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 12 USISmote1 USISmote12 AutoML 2020-07-22 20:49:13.980000 classification ModelType:Random Forest, Paquet:sklearn, Prob... Ensuite, nous préparons à nouveau un ensemble réservé de 225 lignes de données de test et nous exécutons le modèle reformé de SMOTE sur ces lignes : df_x_test = pd.DataFrame(X3_test) df_y_test = pd.DataFrame(y3_test) df_y_test.columns=['ICU'] df_test_smote = pd.concat([df_x_test, df_y_test], 1) display(df_test_smote) iris_schema = 'SQLUser' iris_table = 'CovidTestSmote' to_sql_iris(curs, df_test_smote, iris_table, iris_schema, drop_table=True) dataTableViewTest = "SQLUSER.DTestSmote225" curs.execute("CREATE VIEW %s AS SELECT * FROM %s" % (dataTableViewTest, iris_table)) curs.execute("Create Table %s (%s VARCHAR(100), %s VARCHAR(100))" % (dataTablePredict, dataColumnPredict, dataColumn)) curs.execute("INSERT INTO %s SELECT PREDICT(%s) AS %s, %s FROM %s" % (dataTablePredict, modelName, dataColumnPredict, dataColumn, dataTableViewTest)) df62 = pd.read_sql("SELECT * from %s ORDER BY ID" % dataTablePredict, conn) display(df62) TP = df62[(df62['ICUPredictedSmote'] == '1') & (df62['ICU']=='1')].count()['ICU'] TN = df62[(df62['ICUPredictedSmote'] == '0') & (df62['ICU']=='0')].count()["ICU"] FN = df62[(df62['ICU'] == '1') & (df62['ICUPredictedSmote']=='0')].count()["ICU"] FP = df62[(df62['ICUPredictedSmote'] == '1') & (df62['ICU']=='0')].count()["ICU"] print(TP, FN, '\n', FP, TN) precision = (TP)/(TP+FP) recall = (TP)/(TP+FN) f1 = ((precision*recall)/(precision+recall))*2 accuracy = (TP+TN) / (TP+TN+FP+FN) print("Precision: ", precision, " Recall: ", recall, " F1: ", f1, " Accuracy: ", accuracy) 45 15 9 156 Précision: 0.8333333333333334 Rappel: 0.75 F1: 0.7894736842105262 Exactitude: 0.8933333333333333 # valider les données d'essai à l'aide du modèle reformé de SMOTE curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) #Covid19aTest500, Covid19aTrain1000 df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 USISmote122 0.89 0.79 0.83 0.75 Le résultat indique une amélioration significative du rappel de 75 % par rapport aux 63 % précédents, ainsi qu'une légère amélioration de l'exactitude et du score F1.   Plus notablement, ce résultat est conforme à notre "approche ML traditionnelle" dans l'article précédent, après une "sélection de modèle" intensive et un "réglage des paramètres par quadrillage", comme indiqué dans la section "Exécuter le modèle sélectionné en poursuivant "Ajustement des paramètres via la recherche par quadrillage" supplémentaire". Le résultat de l'IML n'est donc pas mauvais du tout.   ### **Changement de fournisseur H2O d'IntegratedML ** Nous pouvons modifier le fournisseur AutoML de l'IML d'une seule ligne, puis former à nouveau le modèle comme nous l'avons fait à l'étape précédente :    curs.execute("SET ML CONFIGURATION %H2O; ") modelName = 'ICUSmoteH2O' print(dataTableViewTrain) curs.execute("CREATE MODEL %s PREDICTING (%s) FROM %s" % (modelName, dataColumn, dataTableViewTrain)) curs.execute("TRAIN MODEL %s FROM %s" % (modelName, dataTableViewTrain)) df3 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_TRAINED_MODELS", conn) display(df3)   NOM_DU_MODÈLE NOM_DU_MODÈLE_FORMÉ FOURNISSEUR HORODATAGE_FORMÉ TYPE_DU_MODÈLE MODÈLE_INFO 12 USISmote1 USISmote12 AutoML 2020-07-22 20:49:13.980000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 13 USIPPP62 USIPPP622 AutoML 2020-07-22 17:48:10.964000 classification ModelType:Random Forest, Paquet:sklearn, Prob... 14 USISmoteH2O USISmoteH2O2 H2O 2020-07-22 21:17:06.990000 classification Aucun # valider les données de test curs.execute("VALIDATE MODEL %s FROM %s" % (modelName, dataTableViewTest) ) #Covid19aTest500, Covid19aTrain1000 df5 = pd.read_sql("SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS", conn) df6 = df5.pivot(index='VALIDATION_RUN_NAME', columns='METRIC_NAME', values='METRIC_VALUE') display(df6) NOM_MÉTRIQUE Exactitude Mesure F Précision Rappel NOM_DE_L'EXÉCUTION_DE_LA_VALIDATION         USIP62121 0.88 0.71 0.81 0.63 USISmote122 0.89 0.79 0.83 0.75 USISmoteH2O21 0.90 0.79 0.86 0.73 Les résultats semblent montrer que H2O AutoML a une précision légèrement supérieure, le même F1, mais un rappel légèrement inférieur. Cependant, notre objectif principal dans cette tâche de Covid19 USI est de minimiser les faux négatifs si nous le pouvons. Il semble donc que le changement de fournisseur pour H2O n'ait pas encore permis d'augmenter notre performance cible. J'aimerais certainement tester également le fournisseur DataRobot d'IntegratedML, mais je n'ai malheureusement pas encore de clé API de DataRobot, alors je vais la mettre de côté ici.   ## Récapitulatif: 1. **Performance** : Pour cette tâche spécifique de l'unité de soins intensifs de Covid-19, nos comparaisons de tests indiquent que les performances de l'IntegratedML d'IRIS sont au moins équivalentes ou similaires aux résultats de l'approche ML traditionnelle. Dans ce cas précis, IntegratedML a été capable de choisir automatiquement et correctement la stratégie d'entraînement interne, et a semblé établir le bon modèle, fournissant le résultat escompté. 2. **Simplicité** : IntegratedML a un processus beaucoup plus simplifié que les pipelines ML traditionnels. Comme indiqué ci-dessus, je n'ai plus besoin de me préoccuper de la sélection des modèles et l'ajustement des paramètres, etc. Je n'ai pas non plus besoin de la sélection des caractéristiques, si ce n'est à des fins de comparaison. De plus, je n'ai utilisé que la syntaxe minimale d'IntegratedML, comme indiqué dans le cahier de démonstration d'Integrated-demo-template. Bien sûr, le désavantage est que nous sacrifions les capacités de personnalisation et d'ajustement des outils courants de science des données via leurs pipelines traditionnels, mais c'est aussi plus ou moins vrai pour d'autres plateformes AutoML. 3. **Le prétraitement des données reste important** : Il n'y a malheureusement pas de solution miracle ; ou plutôt, cette solution miracle prendrait du temps. Spécifiquement pour cette tâche de Covid19 USI, les tests ci-dessus montrent que les données ont encore beaucoup d'importance pour l'approche actuelle d'IntegratedML : données brutes, caractéristiques sélectionnées avec données manquantes imputées, et données rééquilibrées avec suréchantillonnage SMOTE de base, elles ont toutes abouti à des performances significativement différentes. C'est vrai pour l'AutoML par défaut d'IML et son fournisseur H2O. J'imagine que DataRobot pourrait revendiquer une performance légèrement supérieure, mais cela doit être testé plus avant avec l'enveloppe SQL d'IntegratedML. **En bref, la normalisation des données est toujours importante dans IntegratedML.** 4. **Déployabilité** : Je n'ai pas encore comparé la déployabilité, la gestion de l'API, la surveillance et la facilité d'utilisation non fonctionnelle, etc.   ## Suivant 1. **Déploiements de modèles** : Jusqu'à présent, nous avons fait des démonstrations d'IA sur les radiographies pour le Covid-19 et des prédictions pour l'unité de soins intensifs pour le Covid-19 sur les signes vitaux et les observations. Pouvons-nous les déployer dans les piles de services Flask/FastAPI et IRIS, et exposer leurs capacités ML/DL de démonstration via des API REST/JSON ? Bien sûr, nous pouvons essayer de le faire dans le prochain article. Ensuite, nous pourrons ajouter d'autres capacités d'IA de démonstration au fil du temps, y compris des API NLP, etc. 2. **Interopérabilité de l'API enveloppée dans FHIR** : Nous disposons également d'un modèle FHIR, ainsi que d'une API native IRIS, etc. dans cette communauté de développeurs. Pourrions-nous transformer notre service d'IA de démonstration en SMART on FHIR apps, ou en services d'IA enveloppés dans FHIR selon les normes correspondantes - pourrions-nous essayer cela ? Et n'oubliez pas que dans la gamme de produits IRIS, nous avons également API Gateway, ICM avec support Kubernetes, et SAM etc. que nous pourrions également exploiter avec nos piles de démonstrations d'IA. 3. **Démonstration d'intégration avec HealthShare Clinical Viewer et/ou Trak etc** ? J'ai brièvement montré [une démonstration d'intégration du PACS Viewer d'un fournisseur d'IA tiers (pour les CT Covid-19) avec HealthShare Clinical Viewer](https://community.intersystems.com/post/run-some-covid-19-lung-x-ray-classification-and-ct-detection-demos), et nous pourrions peut-être terminer cette randonnée avec nos propres services de démonstration d'IA, dans divers domaines de spécialité au fil du temps.
Article
Guillaume Rongier · Sept 28, 2022

HL7v2 vers FHIR, c'est facile !

# Service Iris Healthtoolkit [![Vidéo](https://raw.githubusercontent.com/grongierisc/iris-healthtoolkit-service/main/misc/images/Cover.png)](https://youtu.be/lr2B7zSFkds "Video") Utilisation facile de HL7v2 vers FHIR, CDA vers FHIR, FHIR vers HL7v2 en tant que service. L'objectif de ce projet est de fournir une API REST capable de convertir facilement divers formats de santé. Publiez le format souhaité dans le corps REST, obtenez la réponse dans le nouveau format. * Version officielle : https://aws.amazon.com/marketplace/pp/prodview-q7ryewpz75cq2 :fire: * Vidéo : https://youtu.be/lr2B7zSFkds :tv: ## Installation Clonez ce référentiel ``` git clone https://github.com/grongierisc/iris-healthtoolkit-service.git ``` Docker ``` docker-compose up --build -d ``` ## Utilisation * Atteignez : http://localhost:32783/swagger-ui/index.html ## Détails de l'Api ### HL7 vers FHIR ``` POST http://localhost:32783/api/hl7/fhir ``` #### Exemple Saisie ```text MSH|^~\&||^^NPI|||20211105165829+0000||ADT^A01|66053,61109.396628|P|2.5.1|||AL|AL|||||PH_SS-Ack^SS Sender^2.16.840.1.114222.4.10.3^ISO EVN||202111051658|||||^^NPI PID|1||060a6bd5-5146-4b08-a916-009858997bd3^^^https://github.com/synthetichealth/synthea^~060a6bd5-5146-4b08-a916-009858997bd3^^^http://hospital.smarthealthit.org^MR~999-97-4582^^^&^SS~S99986284^^^&^DL~X84330364X^^^&^PPN||Masson^Livia^^^Mrs.^^||19920820|F|Simon^Livia^^||615 Avenue Lemaire^^Lyon^Auvergne-Rhone-Alpes^63000||^PRN^PH^^^555^286||||||||||||||||||||| PV1|1|O||424441002|||||||||||||||1^^^&&^VN|||||||||||||||||||||||||200812312325|20090101044004 PV2|||72892002^Grossesse normale^SCT OBX|1||8302-2^Taille du corps^LN||20101014002504^^|cm^^UCUM|||||F|||20101014 OBX|2||72514-3^Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee^LN||20101014002504^^|{score}^^UCUM|||||F|||20101014 OBX|3||29463-7^Poids corporel^LN||20101014002504^^|kg^^UCUM|||||F|||20101014 OBX|4||39156-5^Indice de masse corporelle^LN||20101014002504^^|kg/m2^^UCUM|||||F|||20101014 OBX|5||72166-2^Statut du tabagisme^LN||20171026002504^Ancien fumeur^SCT^^^^^^Ancien fumeur||||||F|||20171026 ``` Sortie ```json { "typeDeRessource": "Paquet", "type": "transaction", "saisie": [ { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "https://github.com/synthetichealth/synthea" } ] } }, { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "http://hospital.smarthealthit.org" } ] } }, { "demande": { "méthode": "POST", "url": "Patient" }, "UrlComplète": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Patient", "adresse": [ { "ville": "Lyon", "ligne": [ "615 Avenue Lemaire" ], "codePostal": "63000", "région": "Auvergne-Rhone-Alpes" } ], "dateDeNaissance": "1992-08-20", "sex": "femme", "identifiant": [ { "assigner": { "référence": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002" }, "système": "https://github.com/synthetichealth/synthea", "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "assigner": { "référence": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002" }, "système": "http://hospital.smarthealthit.org", "type": { "codage": [ { "code": "MR", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "MRN" }, "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "SS" } ], "texte": "SS" }, "valeur": "999-97-4582" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "DL", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "DL" }, "valeur": "S99986284" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "PPN", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "PPN" }, "valeur": "X84330364X" } ], "nom": [ { "famille": "Simon", "prénom": [ "Livia" ], "texte": "Livia Simon" }, { "famille": "Masson", "prénom": [ "Livia" ], "préfixe": [ "Mrs." ], "texte": "Mrs. Livia Masson", "utilisation": "officiel" } ], "telecom": [ { "système": "téléphone", "utilisation": "domicile", "valeur": "(555) 286" } ] } }, { "demande": { "méthode": "POST", "url": "Visite" }, "UrlComplète": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Visite", "class": { "code": "AMB", "système": "http://terminology.hl7.org/CodeSystem/v3-ActCode" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-encounter-type", "valeurDeLigne": "O" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-entered-on", "valeurDateHeur": "2008-12-31T23:25:00+00:00" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-to-time", "valeurDateHeur": "2009-01-01T04:40:04+00:00" } ], "identifiant": [ { "type": { "texte": "NuméroDeVisite" }, "utilisation": "officiel", "valeur": "1" } ], "période": { "lancement": "2008-12-31T23:25:00+00:00" }, "reasonCode": [ { "codage": [ { "code": "72892002", "affichage": "Grossesse normale", "système": "http://snomed.info/sct" } ] } ], "état": "inconnu", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "type": [ { "codage": [ { "code": "424441002" } ] } ] } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3a13745e-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72166-2", "affichage": "Statut du tabagisme", "système": "http://loinc.org" } ] }, "effectiveDateHeure": "2017-10-26T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-observation-coded-value", "valeurConceptCodifiable": { "codage": [ { "code": "20171026002504", "affichage": "Ancien fumeur", "système": "http://snomed.info/sct" } ], "texte": "Ancien fumeur" } } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurDeLigne": "Ancien fumeur" } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3b6212fc-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "Signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "8302-2", "affichage": "Taille du corps", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "cm", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3c8aba30-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72514-3", "affichage": "Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "{score}", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3de455d0-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes -vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "29463-7", "affichage": "Poids corporel", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3f501418-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "39156-5", "affichage": "Indice de masse corporelle", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg/m2", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } } ] } ``` ### FHIR vers HL7 ADT ``` POST http://localhost:32783/api/fhir/hl7/adt ``` #### Exemple ```json { "typeDeRessource": "Paquet", "type": "transaction", "saisie": [ { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "https://github.com/synthetichealth/synthea" } ] } }, { "demande": { "méthode": "POST", "url": "Organisation" }, "UrlComplète": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Organisation", "identifiant": [ { "valeur": "http://hospital.smarthealthit.org" } ] } }, { "demande": { "méthode": "POST", "url": "Patient" }, "UrlComplète": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Patient", "adresse": [ { "ville": "Lyon", "ligne": [ "615 Avenue Lemaire" ], "codePostal": "63000", "région": "Auvergne-Rhone-Alpes" } ], "dateDeNaissance": "1992-08-20", "sex": "femme", "identifiant": [ { "assigner": { "référence": "urn:uuid:347a0c88-e7fa-11ec-9601-0242ac1a0002" }, "système": "https://github.com/synthetichealth/synthea", "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "assigner": { "référence": "urn:uuid:34d03d1a-e7fa-11ec-9601-0242ac1a0002" }, "système": "http://hospital.smarthealthit.org", "type": { "codage": [ { "code": "MR", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "MRN" }, "valeur": "060a6bd5-5146-4b08-a916-009858997bd3" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "SS" } ], "texte": "SS" }, "valeur": "999-97-4582" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "DL", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "DL" }, "valeur": "S99986284" }, { "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/patient-number-i-s-o-assigning-authority", "valeurDeLigne": "&" } ], "type": { "codage": [ { "code": "PPN", "système": "http://terminology.hl7.org/CodeSystem/v2-0203" } ], "texte": "PPN" }, "valeur": "X84330364X" } ], "nom": [ { "famille": "Simon", "prénom": [ "Livia" ], "texte": "Livia Simon" }, { "famille": "Masson", "prénom": [ "Livia" ], "préfixe": [ "Mrs." ], "texte": "Mrs. Livia Masson", "utilisation": "officiel" } ], "telecom": [ { "système": "téléphone", "utilisation": "domicile", "valeur": "(555) 286" } ] } }, { "demande": { "méthode": "POST", "url": "visite" }, "UrlComplète": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "visite", "class": { "code": "AMB", "système": "http://terminology.hl7.org/CodeSystem/v3-ActCode" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-encounter-type", "valeurDeLigne": "O" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-entered-on", "valeurDateHeur": "2008-12-31T23:25:00+00:00" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/encounter-to-time", "valeurDateHeur": "2009-01-01T04:40:04+00:00" } ], "identifiant": [ { "type": { "texte": "EncounterNumber" }, "utilisation": "officiel", "valeur": "1" } ], "période": { "lancement": "2008-12-31T23:25:00+00:00" }, "reasonCode": [ { "codage": [ { "code": "72892002", "affichage": "Grossesse normale", "système": "http://snomed.info/sct" } ] } ], "état": "inconnu", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "type": [ { "codage": [ { "code": "424441002" } ] } ] } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3a13745e-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72166-2", "affichage": "Statut du tabagisme", "système": "http://loinc.org" } ] }, "effectiveDateHeure": "2017-10-26T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" }, { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-observation-coded-value", "valeurConceptCodifiable": { "codage": [ { "code": "20171026002504", "affichage": "Ancien fumeur", "système": "http://snomed.info/sct" } ], "texte": "Ancien fumeur" } } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurDeLigne": "Ancien fumeur" } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3b6212fc-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "8302-2", "affichage": "Taille du corps", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "cm", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3c8aba30-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "72514-3", "affichage": "Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "{score}", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3de455d0-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "category": [ { "codage": [ { "code": "signes-vitaux", "affichage": "Signes vitaux", "système": "http://terminology.hl7.org/CodeSystem/observation-category" } ], "texte": "Signes vitaux" } ], "code": { "codage": [ { "code": "29463-7", "affichage": "Poids corporel", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } }, { "demande": { "méthode": "POST", "url": "Observation" }, "UrlComplète": "urn:uuid:3f501418-e7fa-11ec-9601-0242ac1a0002", "ressource": { "typeDeRessource": "Observation", "code": { "codage": [ { "code": "39156-5", "affichage": "Indice de masse corporelle", "système": "http://loinc.org" } ], "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/code-table-detail-observation-observation-value-units", "valeurConceptCodifiable": { "codage": [ { "code": "kg/m2", "système": "http://unitsofmeasure.org" } ] } } ] }, "effectiveDateHeure": "2010-10-14T00:00:00+00:00", "visite": { "référence": "urn:uuid:38cf2d40-e7fa-11ec-9601-0242ac1a0002" }, "extension": [ { "url": "http://intersystems.com/fhir/extn/sda3/lib/observation-encounter-number", "valeurDeLigne": "1" } ], "état": "final", "sujet": { "référence": "urn:uuid:36dd6e2a-e7fa-11ec-9601-0242ac1a0002" }, "valeurConceptCodifiable": { "codage": [ { "code": "20101014002504" } ] } } } ] } ``` Sortie ```texte MSH|^~\&||^^NPI|||20220609134903+0000||^|66269,49743.388133779|P|2.5.1|||AL|AL|||||PH_SS-Ack^SS Sender^2.16.840.1.114222.4.10.3^ISO EVN||202206091349|||||^^NPI PID|1||060a6bd5-5146-4b08-a916-009858997bd3^^^https://github.com/s&&ISO^~060a6bd5-5146-4b08-a916-009858997bd3^^^http://hospital.smar&&ISO^MR~999-97-4582^^^&^SS~S99986284^^^&^DL~X84330364X^^^&^PPN||Masson^Livia^^^Mrs.^^||19920820|F|Simon^Livia^^||615 Avenue Lemaire^^^^||^PRN^PH^^^555^286||||||||||||||||||||| PV1|1|O||424441002|||||||||||||||1^^^&&^VN|||||||||||||||||||||||||200812312325| PV2|||72892002^Grossesse normale^SCT OBX|1||72166-2^Statut du tabagisme^LN||^^||||||F|||20171026 OBX|2||8302-2^Taille du corps^LN||^^||||||F|||20101014 OBX|3||72514-3^Gravite de la douleur - 0-10 evaluation numerique verbale [Score] - Signaleee^LN||^^||||||F|||20101014 OBX|4||29463-7^Poids corporel^LN||^^||||||F|||20101014 OBX|5||39156-5^Indice de masse corporelle^LN||^^||||||F|||20101014 ``` ### FHIR vers HL7 ORU ``` POST http://localhost:32783/api/fhir/hl7/oru ``` ### FHIR vers HL7 vxu ``` POST http://localhost:32783/api/fhir/hl7/vxu ``` ### CDA vers FHIR ``` POST http://localhost:32783/api/cda/fhir ``` #### Exemple ### Dépôt FHIR ``` GET http://localhost:32783/api/fhir/metadata ``` ## Format d'entrée HL7 pris en charge : * ADT_A01, ADT_A02, ADT_A03, ADT_A04, ADT_A05, ADT_A06, ADT_A07, ADT_A08, ADT_A09, ADT_A10, ADT_A11, ADT_A12, ADT_A13, ADT_A17, ADT_A18, ADT_A23, ADT_A25, ADT_A27, ADT_A28, ADT_A29, ADT_A30, ADT_A31, ADT_A34, ADT_A36, ADT_A39, ADT_A40, ADT_A41, ADT_A45, ADT_A47, ADT_A49, ADT_A50, ADT_A51, ADT_A60 * BAR_P12 * MDM_T02, MDM_T04, MDM_T08, MDM_T11 * OMP_O09 * ORM_O01 * ORU_R01 * PPR_PC1, PPR_PC2, PPR_PC3 * RDE_O11 * SIU_S12, SIU_S13, SIU_S14, SIU_S15, SIU_S16, SIU_S17, SIU_S26 * VXU_V04 ## Comment ça marche Ce projet fonctionne avec le diagramme pivot : SDA. Le SDA (Summary Document Architecture, Architecture du document de synthèse) est le format de données cliniques d'InterSystems. Les correspondances SDA FHIR peuvent être consultées [ici](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/Doc.View.cls?KEY=HXFHIR_transforms), et celles de la CDA -> SDA [ici](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=HXCDA). ![gif sda pivot](https://raw.githubusercontent.com/grongierisc/iris-healthtoolkit-service/main/misc/images/Gif_SDA_Pivot.gif)
Article
Lorenzo Scalese · Fév 27, 2023

OpenAPI Suite - Partie 1

Salut la communauté, J'aimerais vous présenter ma dernière application OpenAPI-Suite, c'est un ensemble d'outils permettant de générer du code ObjectScript à partir d'une specification OpenAPI version 3.0. L'application permet de: Générer les classes serveur REST. C'est assez similaire au code généré par ^%REST, la valeur ajoutée est le support de la version 3.0. Générer les classes pour un client HTTP. Générer une production cliente (business services, business operation, business process, Ens.Request, Ens.Response). Disposer d'une interface web pour générer et télécharger le code ou générer et compiler directement sur le serveur. Convertir les spécifications de version 1.x, 2.x en version 3.0. Aperçu OpenAPI-Suite est divisée en plusieurs packages et utilise différentes bibliothèques de la communauté des développeurs ainsi que des services REST publics. Vous pouvez voir sur le schéma ci-dessous, tous les packages développés et les bibliothèques et services web utilisés: Note: En cas de problème d'utilisation des services REST publics, il est possible de démarrer une instance docker du service de convertisseur et du validateur (nous verrons cela dans la partie 2). Quel est le rôle de chaque package? La suite OpenAPI a été conçue en différents pacakges pour faciliter la maintenance, les améliorations et extensions futures. Chaque package a un rôle. Jetons-y un coup d'oeil ! openapi-common-lib Il contient tout le code commun aux autres packages. Par exemple, openapi-client-gen et openapi-server-gen acceptent comme donnée en entrée: URL Chemin d'accès d'un fichier %Stream.Object %DynamicObject Format YAML Format JSON OpenAPI version 1.x, 2.x, 3.0.x. Cependant, seule la specification 3.0.x au format %DynamicObject peut être traité. Le code de transformation se trouve dans ce package. Il contient également divers utilitaires. swagger-converter-cli C'est une dépendance de openapi-common-lib et un client HTTP du service public REST converter.swagger.io afin de convertir les spécifications OpenAPI version 1.x ou 2.x en version 3.0. swagger-validator-cli C'est aussi une dépendance de openapi-common-lib, même si son nom est "validator", il n'est pas utilisé pour valider la spécification. converter.swagger.io fournit le service "parse" permettant de simplifier la structure d'une spécification OpenAPI. Par exemple: Il permet de créer une définition lorsqu'un objet est imbriqué et génère un "$ref" à la place. Cela réduit le nombre de cas à traiter dans l'algorithme de génération de code. openapi-client-gen Ce package est dédié à la génération de code côté client afin d'aider les développeurs à consommer les services REST. Il inclut la génération d'un simple HTTP client ou d'une production (business services, processes, ...). Il a été conçu intialement pour supporter swagger version 2.0, mais il a vient d'être complètement remanié pour supporter OpenAPI version 3.0. openapi-server-gen A l'opposé openapi-client-gen, ce package est dédié à la génération de code côté serveur. Il n'a aucun intérêt si vous avez besoin de générer un services REST à partir d'une spécification swagger 2.0. L'objectif de ce package est le support de la version 3.0. openapi-suite Il rassemble tous les packages mentionnés ci-dessus et fournit un API REST afin de: Générer le code et le compiler sur l'instance IRIS. Générer le code sans le compiler dans le but de le télécharger uniquement. Une interface web est également fournie pour consommer cette API REST et ainsi exploiter les fonctionnalités de la suite OpenAPI. Et les bibliothèques? Voici quelques bibliothèques existantes sur OpenExchange qui ont été utilisées dans ce développement : objectscript-openapi-definition Une bibliothèque utile pour générer les modèles à partir d'une spécification OpenAPI. C'est un élément très important de ce projet et je suis aussi un contributeur de ce projet. ssl-client Principalement utilisée pour créer une configuration "DefaultSSL" utilisées pour les requêtes HTTPS. yaml-utils Dans le cas de la spécification du format YAML, cette bibliothèque est utilisée pour la convertir au format JSON. Un must-have dans ce projet. D'ailleurs, elle a été initialement développée pour tester la spécification YAML avec openapi-client-gen version 1. io-redirect C'est une de mes bibliothèques, elle permet de rediriger les écritures vers un fichier, une variable, globale ou une chaîne de caractères. Elle est utilisée par le service REST pour garder un log des actions effectuées. Elle est inspirée par ce post de la communauté. Installation IPM La meilleure solution pour installation OpenAPI-Suite est d'utiliser IPM (zpm). Il y a beaucoup de pacakges et de dépendances. L'utilisation de IPM est très pratique et recommandée. zpm "install openapi-suite" ; optional ; zpm "install swagger-ui" Installation Docker ll n'y a rien de spécial, ce projet utilise le template intersystems-iris-dev-template git clone git@github.com:lscalese/openapi-suite.git cd openapi-suite docker-compose up -d Si vous obtenez une erreur lors du démarrage d'Iris, il peut s'agir d'un problème de permissions avec le fichier iris-main.log. Essayez ceci: touch iris-main.log && chmod 777 iris-main.log Note: ajouter les permissions RW pour l'utilisateur irisowner devrait être suffisant. Comment l'utiliser OpenAPI-Suite fournit une interface web pour "Générer et télécharger" ou "Générer et installer" le code. L'interface est disponible à l'adresse http://localhost:52796/openapisuite/ui/index.csp (*adapter avec votre numéro de port si nécessaire). C'est très simple, il suffit de remplir le formulaire : Nom du paquetage de l'application : c'est le package utilisé pour les classes générées. Il doit s'agir d'un nom de paquet inexistant. Sélectionnez ce que vous voulez générer : client HTTP, une Production ou serveur REST. Sélectionnez l'espace de nom où le code sera généré. Cela n'a d'importance si vous cliquez sur "Installer sur le serveur", sinon ce champs sera ignoré. Le nom de l'application Web est facultatif et n'est disponible que si vous sélectionnez "Server REST" pour la génération. Laissez ce champs vide si vous ne souhaitez pas créer d'application web liée à la classe de distribution REST générée. Le champ "OpenAPI specification" peut être une URL pointant vers la spécification ou un copier-coller de la spécification elle-même (dans ce cas, la spécification doit être au format JSON). Si vous cliquez sur le bouton "Télécharger uniquement", le code sera généré et téléchargé au formatXML, les classes seront ensuite supprimées du serveur. L'espace de nom utilisé pour stocker temporairement les classes générées est celui où OpenAPI-Suite est installé (par défaut IRISAPP si vous utilisez une installation docker). Cependant, si vous cliquez sur le bouton "Installer sur le serveur", le code sera généré et compilé et le serveur renverra un message JSON avec l'état de la génération et compilation du code. Par défaut, cette fonctionnalité est désactivée. Pour l'activer, il suffit d'ouvrir un terminal IRIS et de faire : Set ^openapisuite.config("web","enable-install-onserver") = 1 Explore the OpenAPI-suite REST API Le formulaire CSP utilise les services REST disponibles sur http://localhost:52796/openapisuite. Ouvrez swagger-ui http://localhost:52796/swagger-ui/index.html et explorez http://localhost:52796/openapisuite/_spec Il s'agit de la première étape vers la création d'une application front-end plus avancée avec le framework Angular plus tard. Generate code programmatically Bien sûr, il n'est pas obligatoire d'utiliser l'interface utilisateur, dans cette section nous verrons comment générer du code de manière programmatique et comment utiliser les services générés. Tous les extraits de code sont également disponibles dans la classe dc.openapi.suite.samples.PetStore. HTTP client Set features("simpleHttpClientOnly") = 1 Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreclient", "https://petstore3.swagger.io/api/v3/openapi.json", .features) Le premier argument est le package dans lequel les classes seront générées, assurez-vous donc de passer un nom de package valide. Le second argument peut être une URL pointant vers la spécification, un nom de fichier, un stream ou un %DynamicObject. "Features" est un array, seuls les clés suivantes sont disponibles: simpleHttpClientOnly: si égal à 1, seul un client HTTP simple sera généré sinon une production sera également générée (comportement par défaut). compile: Si égal à 0, le code généré ne sera pas compilé. Cela peut être utile si vous voulez générer du code uniquement pour l'exportation. Par défaut, compile est égal à 1. Vous trouverez ci-dessous un exemple d'utilisation du service "addPet" avec le client HTTP qui vient d'être généré: Set messageRequest = ##class(petstoreclient.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":456,"name":"Mittens","photoUrls":["https://static.wikia.nocookie.net/disney/images/c/cb/Profile_-_Mittens.jpg/revision/latest?cb=20200709180903"],"status":"available"}) Set httpClient = ##class(petstoreclient.HttpClient).%New("https://petstore3.swagger.io/api/v3","DefaultSSL") ; MessageResponse will be an instance of petstoreclient.responses.addPet Set sc = httpClient.addPet(messageRequest, .messageResponse) If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Quit sc Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Click to show generated classes. Class petstoreclient.HttpClient Extends %RegisteredObject [ ProcedureBlock ] { Parameter SERVER = "https://petstore3.swagger.io/api/v3"; Parameter SSLCONFIGURATION = "DefaultSSL"; Property HttpRequest [ InitialExpression = {##class(%Net.HttpRequest).%New()} ]; Property SSLConfiguration As %String [ InitialExpression = {..#SSLCONFIGURATION} ]; Property Server As %String [ InitialExpression = {..#SERVER} ]; Property URLComponents [ MultiDimensional ]; Method %OnNew(Server As %String, SSLConfiguration As %String) As %Status { Set:$Data(Server) ..Server = Server Set:$Data(SSLConfiguration) ..SSLConfiguration = SSLConfiguration Quit ..InitializeHttpRequestObject() } Method InitializeHttpRequestObject() As %Status { Set ..HttpRequest = ##class(%Net.HttpRequest).%New() Do ##class(%Net.URLParser).Decompose(..Server, .components) Set:$Data(components("host"), host) ..HttpRequest.Server = host Set:$Data(components("port"), port) ..HttpRequest.Port = port Set:$$$LOWER($Get(components("scheme")))="https" ..HttpRequest.Https = $$$YES, ..HttpRequest.SSLConfiguration = ..SSLConfiguration Merge:$Data(components) ..URLComponents = components Quit $$$OK } /// Implement operationId : addPet /// post /pet Method addPet(requestMessage As petstoreclient.requests.addPet, Output responseMessage As petstoreclient.responses.addPet = {##class(petstoreclient.responses.addPet).%New()}) As %Status { Set sc = $$$OK $$$QuitOnError(requestMessage.LoadHttpRequestObject(..HttpRequest)) $$$QuitOnError(..HttpRequest.Send("POST", $Get(..URLComponents("path")) _ requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(..HttpRequest.HttpResponse, "addPet")) Quit sc } ... } Class petstoreclient.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use this property for body content with content-type = application/json.<br/> /// Use this property for body content with content-type = application/xml.<br/> /// Use this property for body content with content-type = application/x-www-form-urlencoded. Property Pet As petstoreclient.model.Pet; /// Load %Net.HttpRequest with this property object. Method LoadHttpRequestObject(ByRef httpRequest As %Net.HttpRequest) As %Status { Set sc = $$$OK Set httpRequest.ContentType = ..%ContentType Do httpRequest.SetHeader("accept", ..%Consume) If $Piece($$$LOWER(..%ContentType),";",1) = "application/json" Do ..Pet.%JSONExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/xml" Do ..Pet.XMLExportToStream(httpRequest.EntityBody) If $Piece($$$LOWER(..%ContentType),";",1) = "application/x-www-form-urlencoded" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } } Class petstoreclient.responses.addPet Extends petstoreclient.responses.GenericResponse [ ProcedureBlock ] { /// http status code = 200 content-type = application/xml /// http status code = 200 content-type = application/json /// Property Pet As petstoreclient.model.Pet; /// Implement operationId : addPet /// post /pet Method LoadFromResponse(httpResponse As %Net.HttpResponse, caller As %String = "") As %Status { Set sc = $$$OK Do ##super(httpResponse, caller) If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/xml",httpResponse.StatusCode = "200" { $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If $$$LOWER($Piece(httpResponse.ContentType,";",1))="application/json",httpResponse.StatusCode = "200" { Set ..Pet = ##class(petstoreclient.model.Pet).%New() Do ..Pet.%JSONImport(httpResponse.Data) Return sc } Quit sc } } Production Client Set sc = ##class(dc.openapi.client.Spec).generateApp("petstoreproduction", "https://petstore3.swagger.io/api/v3/openapi.json") Le premier argument est le nom du paquet, si vous testez la génération de code du client HTTP simple et la production du client, assurez-vous d'utiliser un nom de paquet différent. Le deuxième et le troisième suivent les mêmes règles que le client HTTP. Avant de tester, veuillez démarrer la production via le portail d'administration ou en utilisant cette commande dans un terminal IRIS: Do ##class(Ens.Director).StartProduction("petstoreproduction.Production") Vous trouverez ci-dessous un exemple d'utilisation du service "addPet", mais cette fois avec la production générée Set messageRequest = ##class(petstoreproduction.requests.addPet).%New() Set messageRequest.%ContentType = "application/json" Do messageRequest.PetNewObject().%JSONImport({"id":123,"name":"Kitty Galore","photoUrls":["https://www.tippett.com/wp-content/uploads/2017/01/ca2DC049.130.1264.jpg"],"status":"pending"}) ; MessageResponse will be an instance of petstoreclient.responses.addPet Set sc = ##class(petstoreproduction.Utils).invokeHostSync("petstoreproduction.bp.SyncProcess", messageRequest, "petstoreproduction.bs.ProxyService", , .messageResponse) Write !, "Take a look in visual trace (management portal)" If $$$ISERR(sc) Do $SYSTEM.Status.DisplayError(sc) Write !,"Http Status code : ", messageResponse.httpStatusCode,! Do messageResponse.Pet.%JSONExport() Maintenant, vous pouvez ouvrir la trace visuelle et voir les détails: Les classes générées dans les packages "model", "requests" et "responses" sont assez similaires au code généré pour un simple client HTTP. Les classes du package requests héritent de "Ens.Request" et les classes du package "responses" héritent de "Ens.Response". L'implémentation par défaut du "Business Operation" est très simple : Class petstoreproduction.bo.Operation Extends Ens.BusinessOperation [ ProcedureBlock ] { Parameter ADAPTER = "EnsLib.HTTP.OutboundAdapter"; Property Adapter As EnsLib.HTTP.OutboundAdapter; /// Implement operationId : addPet /// post /pet Method addPet(requestMessage As petstoreproduction.requests.addPet, Output responseMessage As petstoreproduction.responses.addPet) As %Status { Set sc = $$$OK, pHttpRequestIn = ##class(%Net.HttpRequest).%New(), responseMessage = ##class(petstoreproduction.responses.addPet).%New() $$$QuitOnError(requestMessage.LoadHttpRequestObject(pHttpRequestIn)) $$$QuitOnError(..Adapter.SendFormDataArray(.pHttpResponse, "post", pHttpRequestIn, , , ..Adapter.URL_requestMessage.%URL)) $$$QuitOnError(responseMessage.LoadFromResponse(pHttpResponse, "addPet")) Quit sc } ... } } Génération de serveur REST Set sc = ##class(dc.openapi.server.ServerAppGenerator).Generate("petstoreserver", "https://petstore3.swagger.io/api/v3/openapi.json", "/petstore/api") Le premier argument est le nom du package pour générer les classes. Le second suit les mêmes règles que le client HTTP. Le troisième argument n'est pas obligatoire, mais s'il est présent, une application web sera créée avec le nom donné (attention à bien donner un nom d'application web valide). La classe "petstoreserver.disp" (classe de dispatch %CSP.REST) ressemble à un code généré par ^%REST, effectue de nombreuses vérifications pour accepter ou rejeter la requête et appelle l'implémentation "ClassMethod" du service correspondant dans la classe "petstoreserver.impl". Pour les développeurs déjà familiarisé avec le code généré par ^%REST, la principale différence est l'argument passé à la méthode d'implémentation, il s'agit de l'objet "petstoreserver.requests". Exemple : Class petstoreserver.disp Extends %CSP.REST [ ProcedureBlock ] { Parameter CHARSET = "utf-8"; Parameter CONVERTINPUTSTREAM = 1; Parameter IgnoreWrites = 1; Parameter SpecificationClass = "petstoreserver.Spec"; /// Process request post /pet ClassMethod addPet() As %Status { Set sc = $$$OK Try{ Set acceptedMedia = $ListFromString("application/json,application/xml,application/x-www-form-urlencoded") If '$ListFind(acceptedMedia,$$$LOWER(%request.ContentType)) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP415UNSUPPORTEDMEDIATYPE,$$$ERROR($$$RESTContentType,%request.ContentType)) Quit } Do ##class(%REST.Impl).%SetContentType($Get(%request.CgiEnvs("HTTP_ACCEPT"))) If '##class(%REST.Impl).%CheckAccepts("application/xml,application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit If '$isobject(%request.Content) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR($$$RESTRequired,"body")) Quit Set requestMessage = ##class(petstoreserver.requests.addPet).%New() Do requestMessage.LoadFromRequest(%request) Set scValidateRequest = requestMessage.RequestValidate() If $$$ISERR(scValidateRequest) Do ##class(%REST.Impl).%ReportRESTError(..#HTTP400BADREQUEST,$$$ERROR(5001,"Invalid requestMessage object.")) Quit Set response = ##class(petstoreserver.impl).addPet(requestMessage) Do ##class(petstoreserver.impl).%WriteResponse(response) } Catch(ex) { Do ##class(%REST.Impl).%ReportRESTError(..#HTTP500INTERNALSERVERERROR,ex.AsStatus(),$parameter("petstoreserver.impl","ExposeServerExceptions")) } Quit sc } ... } Comme vous pouvez le voir, la classe de répartition appelle "LoadFromRequest" et "RequestValidate" avant d'appeler la méthode d'implémentation. Ces méthodes ont une implémentation par défaut, mais le générateur de code ne peut pas couvrir tous les cas. Actuellement, les cas les plus courants sont automatiquement traités comme les paramètres "query", "headers", "path" et body avec le type de contenu "application/json", "application/octet-stream", "application/xml" ou "multipart/form-data". Le développeur doit vérifier l'implémentation (par défaut, le générateur de code définit $$$ThrowStatus($$$ERROR($$$NotImplemented)) pour les cas non pris en charge). Example of request class : Class petstoreserver.requests.addPet Extends %RegisteredObject [ ProcedureBlock ] { Parameter METHOD = "post"; Parameter URL = "/pet"; Property %Consume As %String; Property %ContentType As %String; Property %URL As %String [ InitialExpression = {..#URL} ]; /// Use this property for body content with content-type = application/json.<br/> /// Use this property for body content with content-type = application/xml.<br/> /// Use this property for body content with content-type = application/x-www-form-urlencoded. Property Pet As petstoreserver.model.Pet; /// Load object properties from %CSP.Request object. Method LoadFromRequest(request As %CSP.Request = {%request}) As %Status { Set sc = $$$OK Set ..%ContentType = $Piece(request.ContentType, ";", 1) If ..%ContentType = "application/json"{ Do ..PetNewObject().%JSONImport(request.Content) } If ..%ContentType = "application/xml" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } If ..%ContentType = "application/x-www-form-urlencoded" { ; To implement. There is no code generation yet for this case. $$$ThrowStatus($$$ERROR($$$NotImplemented)) } Quit sc } /// Load object properties from %CSP.Request object. Method RequestValidate() As %Status { Set sc = $$$OK $$$QuitOnError(..%ValidateObject()) If ''$ListFind($ListFromString("application/json,application/xml,application/x-www-form-urlencoded"), ..%ContentType) { Quit:..Pet="" $$$ERROR(5659, "Pet") } If $IsObject(..Pet) $$$QuitOnError(..Pet.%ValidateObject()) Quit sc } } Comme pour l'utilisation de ^%REST, la classe "petstoreserver.impl" contient toutes les méthodes liées aux services que le développeur doit implémenter. Class petstoreserver.impl Extends %REST.Impl [ ProcedureBlock ] { Parameter ExposeServerExceptions = 1; /// Service implemntation for post /pet ClassMethod addPet(messageRequest As petstoreserver.requests.addPet) As %Status { ; Implement your service here. ; Return {} $$$ThrowStatus($$$ERROR($$$NotImplemented)) } ... } Short description of the generated packages Package name \ Class Name Type Description petstoreclient.model petstoreproduction.model Client-side and server-side Il contient tous les modèles. Ces classes étendent %JSON.Adaptor pour faciliter le chargement des objets à partir de JSON. Si une production est générée, ces classes étendent également %Persistent. petstoreclient.requests petstoreproduction.requests Client-side and server-side Objet utilisé pour initialiser facilement %Net.HttpRequest. Il existe une classe par opération définie dans la spécification.Dans le cas d'une production, ces classes étendent Ens.Request. Note: L'implémentation de cette classe est différente si elle est générée pour le côté serveur ou le côté client. Dans le cas du côté client, toutes les classes contienent la méthode "LoadHttpRequestObject" permettant de charger un "%Net.HttpRequest" à partir des propriétés de cette classe. Si les classes sont générées à des fins côté serveur, chaque classe contient une méthode "LoadFromRequest" permettant de charger l'instance à partir de l'objet "%request". petstoreclient.responses petstoreproduction.responses Client-side and server-side C'est l'opposé de "petstoreclient.requests". Elle permet de manipuler la réponse d'une requête %Net.HttpRequest. S'il s'agit d'une production, ces classes étendent Ens.Response. petstoreclient.HttpClient Client-side Contient toutes les méthodes pour exécuter les requêtes HTTP, il y a une méthode par opération définie dans la spécification OpenAPI. petstoreproduction. bo.Operation Client-side La classe Operation possède une méthode par opération définie dans la spécification OpenAPI. petstoreproduction.bp Client-side Deux business process par défaut sont définis : sync et async. petstoreproduction.bs Client-side Contient tous les services vides donc à implémenter par le développeur. petstoreproduction.Production Client-side Contient les paramètres de la production. petstoreserver.disp Server-side Classe de dispatch %CSP.REST. petstoreserver.Spec Server-side Cette classe contient la spécification OpenAPI dans un block XData. petstoreserver.impl Server-side It contains all empty methods related to operations defined in the OpenAPI specification. This is the class (extend %REST.Impl) where developers have to implement services. Elle contient toutes les méthodes vides liées aux opérations définies dans la spécification OpenAPI. Le développeur devra implémenter les méthodes. Statut du développement OpenAPI-Suite est encore un produit très jeune et a besoin d'être plus testé et amélioré. Le support d'OpenAPI 3 est partiel, l'implémentation pourrait être plus complète. Les tests ont été effectués avec la spécification publique https://petstore3.swagger.io/api/v3/openapi.json et deux autres relativement simples. Bien sûr, ce n'est pas suffisant pour couvrir tous les cas. Si vous avez des spécifications à partager, je serais heureux de les utiliser pour mes tests. Je pense que la base du projet est bonne et qu'il peut facilement évoluer, par exemple, être étendu pour supporter AsyncAPI. J'espère que vous apprécierez cette application et n'hésitez pas à laisser des commentaires. Merci pour votre attention.