0 Abonnés · 123 Publications

  

InterSystems Caché est un SGBD multi-modèles et un serveur d'applications. Consultez plus de détails ici.

Documentation.

Article Lorenzo Scalese · Oct 10, 2022 3m read

L'interopérabilité des soins de santé permet d'améliorer les soins aux patients, de réduire les coûts des prestataires de soins et de fournir une image plus précise aux prestataires. Cependant, avec un si grand nombre de systèmes différents, les données sont formatées de nombreuses manières différentes. De nombreuses normes ont été créées pour tenter de résoudre ce problème, notamment HL7v2, HL7v3 et CDA, mais toutes présentent des inconvénients.

FHIR (Fast Healthcare Interoperability Resources), ou Ressources rapides d'interopérabilité des soins de santé, est un nouveau format pour les échanges des informations médicales qui vise à résoudre ces problèmes. Il est développé par Health Level Seven International (HL7), une organisation qui a également développé HL7v2, HL7v3 et CDA.

Aujourd'hui nous allons explorer comment créer et valider une ressource FHIR en utilisant le schéma FHIR à l'aide d'IntelliSense et de la fonctionnalité de complétion automatique dans VS Code.

Etape 1 : Téléchargement du fichier de schéma JSON pour la validation des ressources sur le site officiel de FHIR https://www.hl7.org/fhir/.

Étape 2: Création d'un dossier (dans cet exemple, j'utilise le dossier Patient et la ressource Patient) et copiage du fichier fhir.schema.json extrait dans le même dossier, puis ouverture du dossier à partir du code VS. 

 

Étape 3: Configurez le code VS pour reconnaître le schéma FHIR en modifiant le fichier setting.json.
Appuyez sur CTRL+SHIFT+P et tapez les paramètres de l'espace de travail JSON
 

Étape 4: Création d'un nouveau fichier patient.fhir.json dans le même dossier.
Appuyez sur Ctrl+Espace et vous obtiendrez tous les attributs des ressources FHIR à travers IntelliSense

#

Ajoutez le type de ressource Patient et tous les attributs liés à la ressource Patient vont apparaître dans l'IntelliSense.

VS Code validera automatiquement la structure et la syntaxe de la ressource.


 

Avec l'aide d'IntelliSense et de la fonction de complétion automatique, nous avons créé et validé notre ressource patient.

Step 5: Affichez la ressource créée dans le serveur FHIR d'InterSystems en utilisant l'API Rest à partir de postman

Récupérer la ressource patient créée en utilisant la méthode "Get"

Félicitations, nous avons créé, validé notre ressource patient et réussi à l'envoyer et la récupérer sur le serveur FHIR d'InterSystems en utilisant postman.
De cette façon, nous pouvons facilement créer et valider n'importe quelle ressource FHIR.

0
0 456
Article Danny Wijnschenk · Juil 19, 2022 4m read

Utiliser des méthodes avec syntax objet et SQL est l'une des caractéristiques les plus intéressantes dans Object Script. Mais dans un cas précis, ça m'a donné des résultats inattendus, donc j'ai essayé d'isoler le cas et le décrire ici.

Disons que vous devez écrire une méthode de classe qui met à jour une seule propriété. Habituellement, j'écrirais cela en utilisant SQL comme ceci :

0
0 60
Annonce Irène Mykhailova · Juin 15, 2022

Nous avons hâte de voir tout le monde au InterSystems Global Summit la semaine prochaine !
George James Software sera présent pour proposer des démonstrations de notre contrôle de source Deltanji. Il a été prouvé qu'il améliore l'efficacité des développeurs individuels, des grandes organisations et de tous les autres en apportant de la clarté au développement de votre système. Il encourage la gestion de la configuration, la gestion des versions et le contrôle des processus pour améliorer la qualité de votre code.
Si vous souhaitez en savoir plus, rendez-vous dans le pavillon des partenaires ou réservez pour notre session de groupe d'utilisateurs pendant le déjeuner du mercredi 22 juin. Envoyez un e-mail laurelj@georgejames.com pour nous informer de votre présence.

 

0
0 78
Article Irène Mykhailova · Juin 9, 2022 1m read

Les champs peuvent être obtenu à l'aide du schéma INFORMATION_SCHEMA.

INFORMATION_SCHEMA est un schéma système et n'apparaît pas dans le menu SQL du Management Portal par défaut.

La méthode d'affichage est la suivante.

  1. Ouvrez le Management Portal → System Explorer → SQL
  2. Cochez "System" sur le côté gauche du menu déroulant du schéma.
  3. Sélectionnez INFORMATION_SCHEMA dans le menu déroulant du schéma.

Le SQL pour obtenir l'ID, le nom du champ (COLUMN_NAME), le type de données (DATA_TYPE) et la description (DESCRIPTION) pour la table spécifiée (Test.Person) est le suivant.

0
0 1517
Annonce Irène Mykhailova · Juin 8, 2022

Venez nous dire bonjour dans le pavillon des partenaires du Global Summit !Nous présenterons notre débogueur sur place Serenji qui a subi de grands changements au cours de la dernière année. Les utilisateurs peuvent désormais profiter d'une expérience de débogage transparente sans aucune configuration, tout en profitant des dernières fonctionnalités de VS Code lui-même, permettant aux utilisateurs d'identifier en douceur et de corriger rapidement les erreurs dans votre code, contribuant ainsi à la production d'un code de qualité et maintenable.Si vous voulez voir Serenji en action, rendez-vous

0
0 84
Article Lorenzo Scalese · Juin 8, 2022 11m read

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é. 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 <METHOD DOES NOT EXIST> ; 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 <classname>.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 <classname>.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 {
<BREAK>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 :

0
0 129
Article Irène Mykhailova · Juin 7, 2022 4m read

Pour chaque propriété, requête ou index défini, plusieurs méthodes correspondantes seraient automatiquement générées lors de la compilation d'une classe. Ces méthodes peuvent être très utiles. Dans cet article, je décrirai certaines d'entre elles.

Properties

Disons que vous avez défini une propriété nommée "Property". Les méthodes suivantes seraient automatiquement disponibles (la propriété indiquée en gras est une partie variable, égale au nom de la propriété) :

ClassMethod PropertyGetStored(id)

Pour les propriétés de type de données, cette méthode renvoie leur valeur logique, pour les propriétés d'objet, elle renvoie l'id. C'est une référence globale wrappée à la globale de données de la classe et le moyen le plus rapide de récupérer la valeur de la propriété singulière. Cette méthode n'est disponible que pour les propriétés stockées.

Method PropertyGet()

C'est un getter de propriété. Peut être redéfini.

Method PropertySet(val) As %Status

C'est un définisseur de propriété. Peut être redéfini.


Propriétés de l'objet

S'il s'agit d'une propriété objet, certaines méthodes supplémentaires, liées à l'accès aux ID et OID, deviennent disponibles :

Method PropertySetObjectId(id)

Cette méthode définit la valeur de la propriété par ID, il n'est donc pas nécessaire d'ouvrir un objet pour le définir comme valeur de la propriété.

Method PropertyGetObjectId()

Cette méthode renvoie l'ID de la valeur de la propriété.

Method PropertySetObject(oid)

Cette méthode définit la valeur de la propriété par OID.

Method PropertyGetObject()

Cette méthode renvoie l'OID de la valeur de la propriété.

Propriétés du type de données

Pour une propriété de type de données, plusieurs autres méthodes de conversion entre différents formats sont disponibles :

ClassMethod PropertyDisplayToLogical(val)
ClassMethod PropertyLogicalToDisplay(val)
ClassMethod PropertyOdbcToLogical(val)
ClassMethod PropertyLogicalToOdbc(val)
ClassMethod PropertyXSDToLogical(val)
ClassMethod PropertyLogicalToXSD(val)
ClassMethod PropertyIsValid(val) As %Status

Vérification de la validité de la valeur de la propriété

ClassMethod PropertyNormalize(val)

Renvoi de la valeur logique normalisée

Remarques

  • Les relations constituent des propriétés et peuvent être obtenues ou définies à l'aide des méthodes suivantes
  • L'entrée val est toujours une valeur logique, sauf pour les méthodes de conversion de format.

  • Index

    Pour un index nommé "Index", les méthodes suivantes seraient automatiquement disponibles

    ClassMethod IndexExists(val) As %Boolean

    Renvoi de 1 ou 0 selon l'existence d'un objet avec ce val, où val est une valeur logique de la propriété indexée.


    Index uniques

    Pour les index uniques, des méthodes supplémentaires sont disponibles :

    ClassMethod IndexExists(val, Output id) As %Boolean

    Renvoi de 1 ou 0 en fonction de l'existence d'un objet avec ce val, où val est une valeur logique de la propriété indexée. Renvoi également de l'identifiant de l'objet (s'il a été trouvé) comme second argument.

    ClassMethod IndexDelete(val, concurrency = -1) As %Status

    Suppression de l'entrée dont la valeur d'index est égale à val.

    ClassMethod IndexOpen(val, concurrency, sc As %Status)  

    Renvoi de l'objet existant dont l'indice est égal à val.

    Remarques:

    a) Étant donné qu'un index peut être basé sur plusieurs propriétés, la signature de la méthode serait modifiée pour avoir plusieurs valeurs en entrée, par exemple, considérez cet index :

    Index MyIndex On (Prop1, Prop2);

    La méthode IndexExists aurait alors la signature suivante :

    ClassMethod IndexExists(val1, val2) As %Boolean

    Où val1 correspond à la valeur de Prop1 et val2 correspond à la valeur de Prop2. Les autres méthodes suivent la même logique.

    b) Caché génère un index IDKEY qui indexe le champ ID (RowID). Il peut être redéfini par l'utilisateur et peut également contenir plusieurs propriétés. Par exemple, pour vérifier si une classe a une propriété définie, exécutez :

    Write ##class(%Dictionary.PropertyDefinition).IDKEYExists(class, property)

    c) Toutes les méthodes d'indexation vérifient la présence d'une valeur logique

    d) Documentation

    Requêtes

    En ce qui concerne une requête (qui peut être une simple requête SQL ou une requête de classe personnalisée, voici mon post à ce sujet) nommée "Query", la méthode Func est générée :

    ClassMethod QueryFunc(Arg1, Arg2) As %SQL.StatementResult

    qui renvoie un %SQL.StatementResult utilisé pour itérer sur la requête. Par exemple, la classe Sample.Person de l'espace de noms Samples possède une requête ByName acceptant un paramètre. Elle peut être appelée depuis le contexte objet au moyen de ce code :

    Set ResultSet=##class(Sample.Person).ByNameFunc("A")
    While ResultSet.%Next() { Write ResultSet.Name,! }

    En outre, une classe de démonstration sur GitHub illustre ces méthodes.

    0
    0 79
    Article Guillaume Rongier · Juin 3, 2022 13m read

    Class Query dans InterSystems IRIS (et Cache, Ensemble, HealthShare) est un outil utile qui sépare les requêtes SQL du code Object Script. En principe, cela fonctionne comme suit : supposons que vous souhaitiez utiliser la même requête SQL avec différents arguments à plusieurs endroits différents. Dans ce cas, vous pouvez éviter la duplication du code en déclarant le corps de la requête comme une Class Query, puis en appelant cette requête par son nom. Cette approche est également pratique pour les requêtes personnalisées, dans lesquelles la tâche consistant à obtenir la ligne suivante est définie par un développeur. Cela vous intéresse ? Alors lisez la suite !

    Class queries de base

    Plus simplement, les Class Queries de base vous permettent de représenter des requêtes SQL SELECT. L'optimiseur et le compilateur SQL les traitent comme des requêtes SQL standards, mais elles sont plus pratiques lorsqu'il s'agit de les exécuter à partir du contexte Caché Object Script. Ils sont déclarés en tant qu'éléments de requête Query dans les définitions de classe (similaires aux méthodes ou aux propriétés) de la manière suivante :

    • Type: %SQLQuery
    • Tous les arguments de votre requête SQL doivent être énumérés dans la liste des arguments
    • Type de requête: SELECT
    • Utiliser les deux-points pour accéder à chaque argument (similaire au SQL statique)
    • Définissez le paramètre ROWSPEC qui contient des informations sur les noms et les types de données des résultats de sortie ainsi que l'ordre des champs
    • (Facultatif) Définissez le paramètre CONTAINID qui correspond à l'ordre numérique si le champ contient l'ID. Si vous n'avez pas besoin de renvoyer l'ID, n'attribuez pas de valeur à CONTAINID
    • (Facultatif) Définissez le paramètre COMPILEMODE qui correspond au paramètre similaire en SQL statique et spécifie quand l'expression SQL doit être compilée. Lorsque ce paramètre est défini sur IMMEDIATE (par défaut), la requête sera compilée en même temps que la classe. Lorsque ce paramètre a la valeur DYNAMIC, la requête sera compilée avant sa première exécution (similaire au SQL dynamique)
    • (Facultatif) Définissez le paramètre SELECTMODE qui spécifie le format des résultats de la requête
    • Ajoutez la propriété SqlProc, si vous voulez appeler cette requête comme une procédure SQL.
    • Définissez la propriété SqlName, si vous souhaitez renommer la requête. Le nom par défaut d'une requête dans le contexte SQL est le suivant : PackageName.ClassName_QueryName
    • Caché Studio fournit l'assistant intégré pour la création de Class Query


    Exemple de définition de la classe Sample.Person avec la requête ByName qui renvoie tous les noms d'utilisateur qui commencent par une lettre spécifiée

    Class Sample.Person Extends %Persistent
    {
    Property Name As %String;
    Property DOB As %Date;
    Property SSN As %String;
    Query ByName(name As %String = "") As %SQLQuery
        (ROWSPEC="ID:%Integer,Name:%String,DOB:%Date,SSN:%String",
         CONTAINID = 1, SELECTMODE = "RUNTIME",
         COMPILEMODE = "IMMEDIATE") [ SqlName = SP_Sample_By_Name, SqlProc ]
    {
    SELECT ID, Name, DOB, SSN
    FROM Sample.Person
    WHERE (Name %STARTSWITH :name)
    ORDER BY Name
    }
    }

    Vous pouvez appeler cette requête depuis Caché Object Script de la manière suivante : 

    Set statement=##class(%SQL.Statement).%New()   
    Set status=statement.%PrepareClassQuery("Sample.Person","ByName")   
    If $$$ISERR(status) {
        Do $system.OBJ.DisplayError(status)
    }   
    Set resultset=statement.%Execute("A")   
    While resultset.%Next() {
        Write !, resultset.%Get("Name")   
    }

    Vous pouvez également obtenir un ensemble de résultats en utilisant la méthode générée automatiquement queryNameFunc :

    Set resultset = ##class(Sample.Person).ByNameFunc("A")    
    While resultset.%Next() {
        Write !, resultset.%Get("Name")   
    }
    

    Cette requête peut également être appelée à partir du SQLcontext de ces deux manières :

    Call Sample.SP_Sample_By_Name('A')
    Select * from Sample.SP_Sample_By_Name('A')

    Cette classe peut être trouvée dans l'espace de nom par défaut SAMPLES Caché. Et c'est tout pour les requêtes simples. Passons maintenant aux requêtes personnalisées

    Class queries personnalisées

    Bien que les Class Queries de base fonctionnent parfaitement dans la plupart des cas, il est parfois nécessaire d'exécuter un contrôle total sur le comportement des requêtes dans les applications, par exemple :

    • Des critères de sélection sophistiqués. Puisque dans les requêtes personnalisées vous implémentez une méthode Caché Object Script qui renvoie la ligne suivante de façon autonome, ces critères peuvent être aussi sophistiqués que vous le souhaitez.
    • Si les données sont accessibles uniquement via l'API dans un format que vous ne souhaitez pas utiliser
    • Si les données sont stockées dans des globales (sans classes)
    • Si vous avez besoin d'élever les droits afin d'accéder aux données
    • Si vous devez appeler une API externe afin d'accéder à des données
    • Si vous devez accéder au système de fichiers afin d'accéder aux données
    • Vous devez effectuer des opérations supplémentaires avant d'exécuter la requête (par exemple, établir une connexion, vérifier les autorisations, etc.)

    Alors, comment créer des requêtes de classes personnalisées ? Tout d'abord, vous devez définir 4 méthodes qui mettent en œuvre l'ensemble du flux de travail de votre requête, de l'initialisation à la destruction :

    • queryName — fournit des informations sur une requête (similaire aux requêtes de classe de base)
    • queryNameExecute — construit une requête
    • queryNameFetch — obtient le résultat de la ligne suivante d'une requête
    • queryNameClose — détruit une requête

    Analysons maintenant ces méthodes plus en détail.

    La méthode queryName

    La méthode queryName représente des informations sur une requête

    • Type: %Query
    • Laissez le corps vide
    • Définissez le paramètre ROWSPEC qui contient les informations sur les noms et les types de données des résultats de sortie ainsi que l'ordre des champs
    • (Facultatif) Définissez le paramètre CONTAINID qui correspond à l'ordre numérique si le champ contient l'ID. Si vous ne renvoyez pas d'ID, n'attribuez pas de valeur à CONTAINID

    Par exemple, créons la requête AllRecords (queryName = AllRecords, et la méthode est simplement appelée AllRecords) qui produira toutes les instances de la nouvelle classe persistante Utils.CustomQuery, une par une. Tout d'abord, créons une nouvelle classe persistante Utils.CustomQuery :

    Class Utils.CustomQuery Extends (%Persistent, %Populate){
    Property Prop1 As %String;
    Property Prop2 As %Integer;
    }

    Maintenant, écrivons la requête AllRecords :

    Query AllRecords() As %Query(CONTAINID = 1, ROWSPEC = "Id:%String,Prop1:%String,Prop2:%Integer") [ SqlName = AllRecords, SqlProc ]
    {
    }

    La méthode queryNameExecute
    La méthode queryNameExecute initialise complètement une requête. La signature de cette méthode est la suivante :

    ClassMethod queryNameExecute(ByRef qHandle As %Binary, args) As %Status

    où:

    • qHandle est utilisé pour la communication avec les autres méthodes de l'implémentation de la requête
    • Cette méthode doit mettre qHandle dans l'état qui sera ensuite transmis à la méthode queryNameFetch
    • qHandle peut être défini comme OREF, une variable ou une variable multidimensionnelle
    • Les args sont des paramètres supplémentaires transmis à la requête. Vous pouvez ajouter autant d'args que vous le souhaitez (ou ne pas les utiliser du tout)
    • La méthode doit retourner le statut d'initialisation de la requête

    Revenons à notre exemple. Vous pouvez itérer dans l'étendue de plusieurs façons (je décrirai plus loin les approches de travail de base pour les requêtes personnalisées), mais pour cet exemple, itérons dans la globale en utilisant la fonction $Order. Dans ce cas, qHandle stockera l'ID actuel, et puisque nous n'avons pas besoin d'arguments supplémentaires, l'argument arg n'est pas nécessaire. Le résultat est le suivant :

    ClassMethod AllRecordsExecute(ByRef qHandle As %Binary) As %Status {  
        Set qHandle = ""    Quit $$$OK
    }

    La méthode queryNameFetch
    La méthode queryNameFetch renvoie un seul résultat sous la forme $List. La signature de cette méthode est la suivante :

    ClassMethod queryNameFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = queryNameExecute ]

    where:

    • qHandle est utilisé pour la communication avec les autres méthodes de l'implémentation de la requête
    • Lorsque la requête est exécutée, les valeurs spécifiées par queryNameExecute ou par un appel précédent de queryNameFetch sont attribuées à qHandle.
    • Le rang sera défini soit par une valeur de %List, soit par une chaîne vide, si toutes les données ont été traitées
    • AtEnd doit être mis à 1, une fois que la fin des données est atteinte.
    • La méthode "Fetch" doit être positionnée après la méthode "Execute", mais cela n'est important que pour SQL statique, c'est-à-dire les curseurs à l'intérieur des requêtes.

    En général, les opérations suivantes sont effectuées dans le cadre de cette méthode :

    1. Vérifier si nous avons atteint la fin des données
    2. S'il reste encore des données : Créez une nouvelle %List et attribuez une valeur à la variable Row
    3. Sinon, mettez AtEnd à 1
    4. Préparer qHandle pour la prochaine récupération de résultat
    5. Retourner l'état

    Voici comment cela se présente dans notre exemple :

    ClassMethod AllRecordsFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status {
        #; itérer dans ^Utils.CustomQueryD    
        #; ecrire le prochain id dans qHandle et écriture de la valeur de la globale avec le nouvel id dans val
        Set qHandle = $Order(^Utils.CustomQueryD(qHandle),1,val)
        #; Vérifier s'il reste des données
           If qHandle = "" {
            Set AtEnd = 1
            Set Row = ""
            Quit $$$OK    
        }
        #; Si ce n'est pas le cas, créer %List
        #; val = $Lb("", Prop1, Prop2) voir définition de Storage
        #; Row =$lb(Id,Prop1, Prop2)  voir ROWSPEC pour la demande AllRecords
        Set Row = $Lb(qHandle, $Lg(val,2), $Lg(val,3))
        Quit $$$OK
    }

    La méthode queryNameClose
    La méthode queryNameClose met fin à la requête, une fois toutes les données obtenues. La signature de cette méthode est la suivante :

    ClassMethod queryNameClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = queryNameFetch ]

    où :

    • Caché exécute cette méthode après le dernier appel à la méthode queryNameFetch
    • En d'autres termes, il s'agit d'un destructeur de requête
    • Par conséquent, vous devez disposer de tous les curseurs SQL, des requêtes et des variables locales dans sa mise en œuvre
    • Les méthodes renvoient l'état actuel

    Dans notre exemple, nous devons supprimer la variable locale qHandle :

    ClassMethod AllRecordsClose(ByRef qHandle As %Binary) As %Status {
        Kill qHandle
        Quit $$$OK
      }

    Et voilà ! Une fois que vous aurez compilé la classe, vous serez en mesure d'utiliser la requête AllRecords à partir de %SQL.Statement - tout comme les requêtes de la classe de base.

    Approches de la logique d'itération pour les requêtes personnalisées

    Alors, quelles approches peuvent être utilisées pour les requêtes personnalisées ? En général, il existe 3 approches de base :

    Itération à travers une globale
    Cette approche est basée sur l'utilisation de $Order et de fonctions similaires pour l'itération à travers une globale. Elle peut être utilisée dans les cas suivants :

    • Les données sont stockées dans des globales (sans classes)
    • Vous voulez réduire le nombre de glorefs dans le code
    • Les résultats doivent/peuvent être triés par l'indice de la globale


    SQL statique
    L'approche est basée sur les curseurs et le SQL statique. Elle est utilisée pour :

    • Rendre le code int plus lisible
    • Faciliter le travail avec les curseurs
    • Accélération du processus de compilation (le SQL statique est inclus dans la requête de la classe et n'est donc compilé qu'une seule fois).

    Remarque:

    • Les curseurs générés à partir de requêtes du type %SQLQuery sont nommés automatiquement, par exemple Q14.
    • Tous les curseurs utilisés dans une classe doivent avoir des noms différents
    • Les messages d'erreur sont liés aux noms internes des curseurs qui comportent des caractères supplémentaires à la fin de leur nom. Par exemple, une erreur dans le curseur Q140 est en fait causée par le curseur Q14.
    • Utilisez PlaceAfter et assurez-vous que les curseurs sont utilisés dans la même routine int où ils ont été déclarés.
    • INTO doit être utilisé en conjonction avec FETCH, mais pas DECLARE.


    Exemple de SQL statique pour Utils.CustomQuery :

    Query AllStatic() As %Query(CONTAINID = 1, ROWSPEC = "Id:%String,Prop1:%String,Prop2:%Integer") [ SqlName = AllStatic, SqlProc ]
    {
    }
    
    ClassMethod AllStaticExecute(ByRef qHandle As %Binary) As %Status
    {
        &sql(DECLARE C CURSOR FOR
            SELECT Id, Prop1, Prop2
            FROM Utils.CustomQuery
         )
         &sql(OPEN C)
        Quit $$$OK
    }
    
    ClassMethod AllStaticFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = AllStaticExecute ]
    {
        #; INTO doit être associé à FETCH
        &sql(FETCH C INTO :Id, :Prop1, :Prop2)
        #; Vérifier si la fin des données est atteinte
        If (SQLCODE'=0) {
            Set AtEnd = 1
            Set Row = ""
            Quit $$$OK
        }
        Set Row = $Lb(Id, Prop1, Prop2)
        Quit $$$OK
    }
    
    ClassMethod AllStaticClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = AllStaticFetch ]
    {
        &sql(CLOSE C)
        Quit $$$OK
    }

    SQL dynamique
    L'approche est basée sur les requêtes d'autres classes et le SQL dynamique. Cette approche est raisonnable lorsqu'en plus d'une requête SQL proprement dite, vous devez également effectuer certaines opérations supplémentaires, par exemple exécuter une requête SQL dans plusieurs espaces de noms ou escalader les permissions avant d'exécuter la requête.

    Exemple de SQL dynamique pour Utils.CustomQuery :

    Query AllDynamic() As %Query(CONTAINID = 1, ROWSPEC = "Id:%String,Prop1:%String,Prop2:%Integer") [ SqlName = AllDynamic, SqlProc ]
    {
    }
    
    ClassMethod AllDynamicExecute(ByRef qHandle As %Binary) As %Status
    {
        Set qHandle = ##class(%SQL.Statement).%ExecDirect(,"SELECT * FROM Utils.CustomQuery")
        Quit $$$OK
    }
    
    ClassMethod AllDynamicFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status
    {
        If qHandle.%Next()=0 {
            Set AtEnd = 1
            Set Row = ""
            Quit $$$OK
        }
        Set Row = $Lb(qHandle.%Get("Id"), qHandle.%Get("Prop1"), qHandle.%Get("Prop2"))
        Quit $$$OK
    }
    
    ClassMethod AllDynamicClose(ByRef qHandle As %Binary) As %Status
    {
        Kill qHandle
        Quit $$$OK
    }

    Approche alternative : %SQL.CustomResultSet

    Vous pouvez également créer une requête en sous-classant la classe %SQL.CustomResultSet. Les avantages de cette approche sont les suivants :

    • Une légère augmentation de la vitesse
    • ROWSPEC est inutile, puisque toutes les métadonnées sont obtenues à partir de la définition de la classe
    • Respect des principes de conception orientée objet

    Pour créer une requête à partir de la sous-classe de la classe %SQL.CustomResultSet, assurez-vous d'effectuer les étapes suivantes :

    1. Définir les propriétés correspondant aux champs résultants
    2. Définir les propriétés privées où le contexte de la requête sera stocké
    3. Remplacer la méthode %OpenCursor (similaire à queryNameExecute) qui initie le contexte. En cas d'erreur, définissez également %SQLCODE et %Message
    4. Remplacer la méthode %Next (similaire à queryNameFetch) qui obtient le résultat suivant. Remplacer les propriétés. La méthode renvoie 0 si toutes les données ont été traitées et 1 s'il reste des données
    5. Remplacer la méthode %CloseCursor (similaire à queryNameClose) si nécessaire


    Exemple de %SQL.CustomResultSet pour Utils.CustomQuery :

    Class Utils.CustomQueryRS Extends %SQL.CustomResultSet
    {
    Property Id As %String;
    Property Prop1 As %String;
    Property Prop2 As %Integer;
    Method %OpenCursor() As %Library.Status
    {
        Set ..Id = ""
        Quit $$$OK
    }
    
    Method %Next(ByRef sc As %Library.Status) As %Library.Integer [ PlaceAfter = %Execute ]
    {
        Set sc = $$$OK
        Set ..Id = $Order(^Utils.CustomQueryD(..Id),1,val)
        Quit:..Id="" 0
        Set ..Prop1 = $Lg(val,2)
        Set ..Prop2 = $Lg(val,3)
        Quit $$$OK
    }
    }

    Vous pouvez l'appeler à partir de Caché Object Script code de la manière suivante :

    Set resultset= ##class(Utils.CustomQueryRS).%New()
           While resultset.%Next() {
            Write resultset.Id,!
     }

    Un autre exemple est disponible dans l'espace de noms SAMPLES - il s'agit de la classe Sample.CustomResultSet qui implémente une requête pour Samples.Person.

    Résumé

    Les requêtes personnalisées vous aideront à séparer les expressions SQL du code Caché Object Script et à mettre en œuvre un comportement sophistiqué qui peut être trop difficile pour le SQL pur.

    Références

    Class Queries

    Itération à travers une globale

    SQL statique

    Dynamic SQL

    %SQL.CustomResultSet

    Classe Utils.CustomQuery

    Classe Utils.CustomQueryRS

    L'auteur tient à remercier [Alexander Koblov] (https://community.intersystems.com/user/alexander-koblov) pour son aide à la composition de cet article.

    0
    0 150
    Article Irène Mykhailova · Juin 2, 2022 1m read

    Étant donné que SELECT ... FOR UPDATE est implémenté dans de nombreux RDBMS en tant que méthode d'acquisition de lock de ligne explicite, vous utilisez probablement cette fonctionnalité dans de nombreux cas.

    Cette syntaxe n'entraîne pas d'erreur dans les produits InterSystems, mais elle n'acquiert pas les locks de ligne attendus.

    Cet article vous montrera comment obtenir la même fonctionnalité.

    DECLARE CURSOR C1 IS
    SELECT Name FROM Person WHERE Name LIKE 'A%' FOR UPDATE
    OPEN C1
    LOOP FETCH C1 INTO name 
    ...afficher le nom...
    ...sortir de loop lorsque vous avez terminé...
    END LOOP
    CLOSE C1

     

    L'instruction SQL ci-dessus peut être remplacée par l'instruction SQL suivante.

     &SQL(START TRANSACTION ISOLATION LEVEL READ COMMITTED)
     &SQL(UPDATE Person SET ID=ID Where Name like 'A%')
     &SQL(DECLARE C1 CURSOR FOR SELECT ID,Name into :id,:name FROM Person Where Name like 'A%')
     &SQL(OPEN C1)
     &SQL(FETCH C1)
     While (SQLCODE = 0) {
       Write id, ":  ", name,!  &SQL(FETCH C1)
     }
     &SQL(CLOSE C1)&SQL(COMMIT) 

     

    Remarque : &SQL() est appelé Embedded SQL et est une méthode de description qui peut être utilisée lorsque vous souhaitez incorporer des instructions SQL dans la logique côté serveur. Veuillez vous référer au document pour plus de détails.

    0
    0 211
    Article Lorenzo Scalese · Juin 1, 2022 9m read

    Un système de stockage global d'aspect plus industriel

    Dans le premier article de cette série, nous avons étudié le modèle entité-attribut-valeur (EAV) dans les bases de données relationnelles, et nous avons examiné les avantages et les inconvénients du stockage de ces entités, attributs et valeurs dans des tables. Nous avons appris que, malgré les avantages de cette approche en termes de flexibilité, elle présente de réels inconvénients, notamment une inadéquation fondamentale entre la structure logique des données et leur stockage physique, qui entraîne diverses difficultés.

    Pour résoudre ces problèmes, nous avons décidé de voir si l'utilisation de globales - qui sont optimisées pour le stockage d'informations hiérarchiques - serait efficace pour les tâches que l'approche EAV traite habituellement.

    Dans la Partie 1, nous avons créé un catalogue pour une boutique en ligne, d'abord en utilisant des tables, puis en utilisant une seule globale. Maintenant, essayons d'implémenter la même structure pour quelques globales.

    Dans la première globale, ^catalog, nous allons stocker la structure du répertoire. Dans la deuxième globale, ^good, nous allons stocker les marchandises. Et dans la globale ^index, nous allons stocker les index. Puisque nos propriétés sont liées à un catalogue hiérarchique, nous ne créerons pas de globale séparée pour elles.

    Avec cette approche, pour chaque entité (à l'exception des propriétés), nous avons une globale séparée, ce qui est bon du point de vue de la logique. Voici la structure du catalogue global :

     
    Set ^сatalog(root_id, "Properties", "capacity", "name") = "Capacity, GB"
    Set ^сatalog(root_id, "Properties", "capacity", "sort") = 1
    
    Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "name") = "Endurance, TBW"
    Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "sort") = 2
    
    Set ^сatalog(root_id, sub1_id, "goods", id_good1) = 1
    Set ^сatalog(root_id, sub1_id, "goods", id_good2) = 1
    
    Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "name") = "Rotate speed, ms"
    Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "sort") = 3
    
    Set ^сatalog(root_id, sub2_id, "goods", id_good3) = 1
    Set ^сatalog(root_id, sub2_id, "goods", id_good4) = 1
    

     

    Une globale avec des marchandises ressemblera à quelque chose comme ceci :

    Set ^good(id_good, property1) = value1
    Set ^good(id_good, property2) = value2
    Set ^good(id_good, property3) = value3
    Set ^good(id_good, "catalog") = catalog_id
    

     

    Bien sûr, nous avons besoin d'index afin que pour toute section du catalogue contenant des marchandises, nous puissions trier par les propriétés dont nous avons besoin. Une globale d'index aura une structure semblable à quelque chose comme ceci :

    Configurer ^index(id_catalog, property1, id_good) = 1
    ; Pour obtenir rapidement le chemin complet du sous-catalogue concret
    Configurer ^index("path", id_catalog) = "^catalog(root_id, sub1_id)"
    

     

    Ainsi, dans n'importe quelle section du catalogue, on peut obtenir une liste triée. Une globale d'index est facultative. Il n'est utile que si le nombre de produits dans cette section du catalogue est important.

    Code ObjectScript pour travailler avec des données de démonstration Demo Data

    Maintenant, nous allons utiliser ObjectScript pour travailler avec nos données. Pour commencer, nous allons obtenir les propriétés d'une marchandise spécifique. Nous avons l'ID d'une marchandise particulière et nous devons afficher ses propriétés dans l'ordre donné par la valeur de tri. Voici le code pour cela :

    get_sorted_properties(path, boolTable)
    {
      ; mémoriser toutes les propriétés dans la globale temporaire
      While $QLENGTH(@path) > 0 {
        if ($DATA(@path("Properties"))) {
          set ln=""
          for {
    	    Set ln = $order(@path("Properties", ln))
    	    Quit: ln = ""
    
            IF boolTable & @path("Properties", ln, "table_view") = 1 {
      	      Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
    	    }
    	  ELSE {
      	    Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
    	  }
        }
      }
    }
    
    print_sorted_properties_of_good(id_good)
    {
      Set id_catalog = ^good(id_good, "catalog")
      Set path = ^index("path", id_catalog)
    
      Do get_sorted_properties(path, 0)
    
      set ln =""
      for {
       Set ln = $order(^tmp(ln))
       Quit: ln = ""
       Set fn = ""
       for {
     	Set fn = $order(^tmp(ln, fn))
     	Quit: fn = ""
     	Write ^tmp(ln, fn), " ", ^good(id_good, fn),!
       }
      }
    }
    

     

    Ensuite, nous voulons récupérer les produits de la section catalogue sous la forme de la table, basé sur id_catalog :

    print_goods_table_of_catalog(id_catalog)
    {
      Set path = ^index("path", id_catalog)
      Do get_sorted_properties(path, 1)
    
      set id=""
      for {
        Set id = $order(@path("goods"), id)
        Quit: id = ""
    
        Write id," ", ^good(id, "price"), " "
    
        set ln =""
        for {
          Set ln = $order(^tmp(ln))
          Quit: ln = ""
          Set fn = ""
          for {
     	    Set fn = $order(^tmp(ln, fn))
     	    Quit: fn = ""
     	    Write ^tmp(ln, fn), " ", ^good(id, fn)
          }
          Write !
        }
      }
    }
    

     

    Lisibilité : EAV SQL contre les globales

    Comparons maintenant l'utilisation d'EAV et de SQL par rapport à l'utilisation de globales. En ce qui concerne la clarté du code, il est évident qu'il s'agit d'un paramètre subjectif. Mais regardons, par exemple, la création d'un nouveau produit.

    Nous allons commencer par l'approche EAV, en utilisant SQL. Tout d'abord, nous devons obtenir une liste des propriétés de l'objet. Il s'agit d'une tâche distincte qui prend beaucoup de temps. Supposons que nous connaissions déjà les IDs de ces trois propriétés : capacité, poids, et endurance.

    START TRANSACTION
    INSERT INTO good (name, price, item_count, catalog_id) VALUES ('F320 3.2TB AIC SSD', 700, 10, 15);
    
    SET @last_id = LAST_INSERT_ID ();
    
    INSERT INTO NumberValues ​​Values​​(@last_id, @id_capacity, 3200);
    INSERT INTO NumberValues ​​Values​​(@last_id, @id_weight, 0.4);
    INSERT INTO NumberValues ​​Values​​(@last_id, @id_endurance, 29000);
    COMMIT
    

     

    Dans cet exemple, nous n'avons que trois propriétés, et l'exemple ne semble donc pas si inquiétant. Dans le cas général, nous aurions toujours quelques insertions dans la table de texte à l'intérieur de la transaction :

    INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop1, 'Text value of property 1');
    INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop2, 'Text value of property 2');
    ...
    INSERT INTO TextValues Values (@last_id, @id_text_propN, 'Text value of property N');
    

     

    Bien sûr, nous pourrions simplifier un peu la version SQL si nous utilisions la notation textuelle à la place des propriétés ID, par exemple "capacité" au lieu d'un nombre. Mais dans le monde SQL, ce n'est pas acceptable. Il est plutôt d'usage d'utiliser un ID numérique pour énumérer les instances d'entités. Cela permet d'obtenir des index plus rapides (il faut indexer moins d'octets), il est plus facile de suivre l'unicité et il est plus facile de créer automatiquement un nouvel ID. Dans ce cas, le fragment d'insertion aurait l'apparence suivante :

    INSERT INTO NumberValues ​​Values​​(@last_id, 'capacity', 3200);
    INSERT INTO NumberValues ​​Values​​(@last_id, 'weight', 0.4);
    INSERT INTO NumberValues ​​Values​​(@last_id, 'endurance', 29000);
    

     

    Voici le même exemple en utilisant des globales :

    TSTART
    Set ^good(id, "name") = "F320 3.2TB AIC SSD"
    Set ^("price") = 700, ^("item_count") = 10, ^("reserved_count") = 0, ^("catalog") = id_catalog
    Set ^("capacity") = 3200, ^("weight") = 0.4, ^("endurance") = 29000
    TCOMMIT
    

     

    Supprimons maintenant une marchandise en utilisant l'approche EAV :

    START TRANSACTION
    DELETE FROM good WHERE id = @ good_id;
    DELETE FROM NumberValues ​​WHERE good_id = @ good_id;
    DELETE FROM TextValues ​​WHERE good_id = @ good_id;
    COMMIT
    

     

    Et ensuite, faisons la même chose avec les globales :

    Kill ^good(id_good)
    

    Nous pouvons également comparer les deux approches en termes de longueur de code. Comme vous pouvez le constater dans les exemples précédents, lorsque vous utilisez des globales, le code est plus court. C'est une bonne chose. Plus le code est court, moins il y a d'erreurs et plus il est facile à comprendre et à gérer.

    En général, un code plus court est aussi plus rapide. Et, dans ce cas, c'est certainement vrai, puisque les globales constituent une structure de données de niveau inférieur aux tables relationnelles.

    Mise à l'échelle des données avec EAV et Globales

    Ensuite, examinons la mise à l'échelle horizontale. Avec l'approche EAV, nous devons au moins distribuer les trois plus grandes tables sur les serveurs : Good, NumberValues, et TextValues. Les tables contenant des entités et des attributs peuvent simplement être entièrement copiés sur tous les serveurs, car ils contiennent peu d'informations.

    Dans chaque serveur, avec une mise à l'échelle horizontale, des produits différents seraient stockés dans les tables Good, NumberValues et TextValues. Nous devrions allouer certains blocs d'identification pour les produits sur chaque serveur afin d'éviter la duplication des identifiants pour des produits différents.

    Pour une mise à l'échelle horizontale avec des globales, il faudrait configurer des plages d'ID dans la globale et attribuer une plage de globale à chaque serveur.

    La complexité est à peu près la même pour EAV et pour les globales, sauf que pour l'approche EAV, nous devrions configurer des plages d'ID pour trois tables. Avec les globales, nous configurons les ID pour une seule globale. C'est-à-dire qu'il est plus facile d'organiser la mise à l'échelle horizontale pour les globales.

    Perte de données avec EAV et avec Globales

    Enfin, considérons le risque de perte de données dû à des fichiers de base de données corrompus. Où est-il plus facile de sauvegarder toutes les données : dans cinq tables ou dans trois globales ( y compris une globale d'index ) ?

    Je pense que c'est plus facile dans trois globales. Avec l'approche EAV, les données des marchandises différentes sont mélangées dans des tables, alors que pour les globales, les informations sont stockées de manière plus holistique. Les branches sous-jacentes sont stockées et triées séquentiellement. Par conséquent, la corruption d'une partie de la globale est moins susceptible d'entraîner des dommages que la corruption de l'une des tables dans l'approche EAV, où les données sont stockées comme des pâtes entremêlées.

    Un autre casse-tête dans la récupération des données est l'affichage des informations. Avec l'approche EAV, les informations sont réparties entre plusieures tables et des scripts spéciaux sont nécessaires pour les assembler en un seul ensemble. Dans le cas des globales, vous pouvez simplement utiliser la commande ZWRITE pour afficher toutes les valeurs et les branches sous-jacentes du nœud.

    Les Globales d'InterSystems IRIS : Une meilleure approche ?

    L'approche EAV est apparue comme une astuce pour stocker des données hiérarchiques. Les tables n'ont pas été conçus à l'origine pour stocker des données imbriquées. L'approche EAV de facto est l'émulation des globales dans les tables. Étant donné que les tables représentent une structure de stockage de données de plus haut niveau et plus lente que les globales, l'approche EAV échoue par rapport aux globales.

    À mon avis, pour les structures de données hiérarchiques, les globales sont plus pratiques et plus compréhensibles en termes de programmation, tout en étant plus rapides.

    Si vous avez prévu une approche EAV pour votre projet, je vous suggère d'envisager d'utiliser les globales d'InterSystems IRIS pour stocker les données hiérarchiques.

    0
    0 407
    Article Irène Mykhailova · Mai 31, 2022 1m read

    La cause de cette erreur est que la ressource locked est déjà locked par un autre processus dans l'application et que le lock n'est pas libéré pour une raison quelconque.

    S'il n'y a aucun signe que d'autres processus avec le lock, il est possible que la table de locks manque d'espace libre. Dans ce cas, le message LOCK TABLE FULL est envoyé au Message Log

    Si vous effectuez un traitement transactionnel, il est possible que le report du lock ait un effet.
    Veuillez vous référer aux documents suivants pour la transaction et le report de lock.

    Using LOCK in Transactions【IRIS】

    Using LOCK in Transactions

    De plus, s'il existe un grand nombre d'enregistrements mis à jour par des instructions SQL dans la même table au cours d'une transaction, le seuil de lock (la valeur par défaut est 1000) est atteint et une escalade de lock se produit, entraînant un état de lock de table.

    Comme vous pouvez le voir, il existe plusieurs causes possibles pour l'erreur de délai d'attente de lock. Tout d'abord, vérifiez l'état actuel du lock dans le menu de locks de Management Portal.

    【Version 2011.1 ou ultérieure】
    Management Portal : [System Operations]> [Lock]

    【Version 2010.2 ou antérieure】
    Management Portal :[Operations]> [Lock]

    0
    0 154
    Article Irène Mykhailova · Mai 27, 2022 3m read

    Il est possible de construire (reconstruire) l'index pendant que des données sont enregistrées/supprimées, mais si vous construisez l'index pendant ce processus, il sera référencé pendant sa mise à jour, utilisez donc l'utilitaire dédié et procédez à la construction de l'index.

    La procédure est la suivante.

    1. Masquez le nom d'index que vous prévoyez d'ajouter l'optimiseur de requête.
    2. Ajoutez la définition de l'index et effectuez la construction de l'index.
    3. Une fois la construction de l'index est terminée, publiez l'index ajouté dans l'optimiseur.

    L'exemple d'exécution est le suivant.

    * Dans l'exemple, l'index standard HomeStateIdx est défini pour la colonne Home_State (informations d'état de l'adresse de contact) de Sample.Person.

    1. Masquez le nom d'index que vous prévoyez d'ajouter l'optimiseur de requête.
    SAMPLES>write $system.SQL.SetMapSelectability("Sample.Person","HomeStateIdx",0)
    1


    2.Après avoir ajouté la définition d'index, reconstruisez-la.
      Exemple de définition: Index HomeStateIdx On Home.State;

    SAMPLES>do ##class(Sample.Person).%BuildIndices($LB("HomeStateIdx"))


    3. Une fois la construction de l'index est terminée, publiez l'index ajouté dans l'optimiseur.

    SAMPLES>write $system.SQL.SetMapSelectability("Sample.Person","HomeStateIdx",1)
    1

    Reportez-vous au plan de requête pour voir si l'index a été utilisé/non utilisé.
    Dans l'exemple suivant, le résultat de la confirmation du plan avec le terminal basculé sur l'environnement d'exécution SQL avec $system.SQL.Shell() s'affiche (lors du référencement dans le Management Portal, après avoir exécuté SQL sur l'écran d'exécution de la requête, cliquez sur le bouton "Affichage du plan").

    SAMPLES>do $system.SQL.Shell()
    SQL Command Line Shell
    ----------------------------------------------------
    The command prefix is currently set to: <>.
    Enter q to quit, ? for help.
    SAMPLES>>select ID,Name from Sample.Person where Home_State='NY'
    1.      select ID,Name from Sample.Person where Home_State='NY'
    ID      Name
    61      Alton,Debby O.
    138     Isaksen,Charlotte L.
    175     Walker,Emily O.
    3 Rows(s) Affected
    statement prepare time(s)/globals/lines/disk: 0.0026s/35/974/0ms
              execute time(s)/globals/lines/disk: 0.0017s/216/2447/0ms
                              cached query class: %sqlcq.SAMPLES.cls1
    ---------------------------------------------------------------------------
    SAMPLES>>show plan    // ★ Affichage du plan lorsque l'index n'est pas utilisé DECLARE QRS CURSOR FOR SELECT ID , Name FROM Sample . Person WHERE Home_State = ?
    Read master map Sample.Person.IDKEY, looping on ID.
    For each row:
        Output the row.
    SAMPLES>>show plan    // ★ Affichage du plan lors de l'utilisation de l'index DECLARE QRS CURSOR FOR SELECT ID , Name FROM Sample . Person WHERE Home_State = ?
    Read index map Sample.Person.HomeStateIdx, using the given %SQLUPPER(Home_State), and looping on ID.
    For each row:
        Read master map Sample.Person.IDKEY, using the given idkey value.
        Output the row.
    SAMPLES>>

    Pour plus de détails, veuillez consulter les documents suivants.
    Building Indices on a READ and WRITE Active System【IRIS】

    Building Indices on a READ and WRITE Active System

    0
    0 81
    Article Irène Mykhailova · Mai 25, 2022 9m read

    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 $H64146,54027USER>

    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

    $PIECE

    SQL Functions reference

    %SYS.NLS.Format

    %SYSTEM.Process.TimeZone()

    %SYSTEM.SQL

    %SYSTEM.SYS.Horolog

    %SYSTEM.Util.IsDST()

    $ZDATE

    $ZDATEH

    $ZDATETIME

    $ZDATETIMEH

    1
    0 254
    Article Irène Mykhailova · Mai 23, 2022 6m read

    Le type DATE correspond au type de données du produit InterSystems %Date et le type TIME correspond à %Time.

    %Date enregistre une date interne (premier élément séparé par une virgule de la variable spéciale $Horolog), et %Time enregistre l'heure interne (deuxième élément séparé par une virgule de la variable spéciale $Horolog). La logique côté serveur utilise donc la valeur au format (logique) interne, sauf si vous changez le mode d'affichage.
    La méthode permettant de modifier le format d'affichage de la date et de l'heure internes dans la logique côté serveur dépend de la méthode d'exploitation.

    Dans les exemples suivants, nous utiliserons le tableau Sample.Person.
    (L'exemple d'exécution de la commande est présenté pour une instruction SELECT, mais il peut également être écrit pour une instruction de mise à jour.)

    Pour essayer IRIS/IRIS for Health, téléchargez la documentation à partir de (Télécharger des échantillons à utiliser avec InterSystems IRIS),
    ou à partir de Articles connexes (téléchargement de la définition de classe de l'échantillon (Sample.Person) et création de données d'échantillon), veuillez commencer par importer la classe Sample.Person et créer les données d'exemple.

    Si vous essayez Caché/Ensemble, utilisez Sample.Person dans l'espace de noms SAMPLES.


    (1) Si vous utilisez l'Embedded SQL

    Pour changer le format d'affichage à l'aide d'Embedded SQL, utilisez #sqlcomple select.
    Les valeurs suivantes peuvent être spécifiées.

    • Logical (par défaut)
    • Display
    • ODBC
    • Runtime

    Documentation (IRIS) : Compilation du SQL intégré et du préprocesseur de macros【IRIS】
    Documentation : Compilation du SQL intégré et du préprocesseur de macros

    #sqlcompile select=ODBC
    &sql(declare C1 Cursor for select ID,Name,DOB into :pid,:name,:dob from Sample.Person where ID<=5)
    &sql(open C1)
    for {
        &sql(fetch C1)
        if SQLCODE'=0 { quit }
       //Exemple d'affichage)1-Mastrolito,Susan T.-2013-01-01
        write pid,"-",name,"-",dob,!
    }
    &sql(close C1)

    (2) Si vous utilisez le Dynamic SQL 

    Pour changer le format d'affichage en SQL dynamique à l'aide de %SQL.Statement, utilisez la propriété %SelectMode.
    Cette propriété doit être définie avant l'exécution de %Execute().

    Les valeurs qui peuvent être définies sont les suivantes.

    • 0: mode logique
    • 1: mode ODBC
    • 2: mode d'affichage
    SAMPLES>set sql="select ID,Name,DOB from Sample.Person where ID <= 5" SAMPLES>set stmt=##class(%SQL.Statement).%New() SAMPLES>set st=stmt.%Prepare(sql) SAMPLES>set rset=stmt.%Execute() SAMPLES>do rset.%Display()
    ID      Name    DOB
    1       Gallant,Yan N.  42146
    2       Waal,Umberto G. 45359
    3       Jenkins,Sam A.  37404
    4       Marks,Milhouse B.       52043
    5       Hernandez,Phyllis W.    64590 5 Rows(s) Affected
    SAMPLES>

    (3) Lorsque vous utilisez une requête de classe 

    Pour changer le format d'affichage dans une requête de classe, utilisez le paramètre de définition de la requête : SELECTMODE.
    Les valeurs qui peuvent être spécifiées sont les suivantes

    • RUNTIME (par défaut)
    • LOGICAL
    • DISPLAY
    • ODBC

    Voici un exemple de définition.

    Query NewQuery1() As %SQLQuery(SELECTMODE = "ODBC")&lt;br>{&lt;br>select ID,Name,DOB from Sample.Person where ID&lt;=5&lt;br>}

    (4) Comment changer le format d'affichage des processus en cours

    L'objet système $SYSTEM.SQL.SetSelectMode() peut être utilisé pour modifier le format d'affichage du processus en cours.
    Les arguments et les valeurs de retour sont les suivants.

    • Spécifiez 0 (logique), 1 (ODBC) ou 2 (affichage) comme premier argument.
    • Le second argument est un argument de type pass-by-reference dont le résultat d'exécution est défini par %Status.
    • La valeur de retour est le numéro du mode d'affichage en cours.

    Veuillez vous référer à la page du document ci-dessous pour plus de détails.

    Les bases d'InterSystems SQL - Options d'affichage des données【IRIS】
    Options d'affichage de CachéSQL Basics_Data

    // Changement du format par défaut au format ODBC
    SAMPLES>set cm=$system.SQL.SetSelectMode(1,.st)
    SAMPLES>set sql="select ID,Name,DOB from Sample.Person where ID <= 5"
    SAMPLES>set stmt=##class(%SQL.Statement).%New()
    SAMPLES>set st=stmt.%Prepare(sql)
    SAMPLES>set rset=stmt.%Execute()
    SAMPLES>do rset.%Display()
    ID      Name    DOB
    1       Gallant,Yan N.  1956-05-23
    2       Waal,Umberto G. 1965-03-10
    3       Jenkins,Sam A.  1943-05-30
    4       Marks,Milhouse B.       1983-06-28
    5       Hernandez,Phyllis W.    2017-11-03 5 Rows(s) Affected
    SAMPLES>

    ※Après avoir changé le format d'affichage d'un processus, si le format d'affichage est modifié pour chaque méthode d'exécution SQL, le dernier format d'affichage spécifié sera utilisé.

    (5) Comment convertir le format d'affichage à l'aide de fonctions ObjectScript

    Une autre méthode consiste à utiliser les fonctions de conversion d'affichage d'ObjectScript pour convertir le format interne en format d'affichage.

    Pour les fonctions de datation,
     Affichage -> Format interne $ZDATEH(yyyymmdd,8) ou $ZDATE(yyyy-mm-dd,3)
     Interne -> Pour obtenir le résultat du format d'affichage YYYYYMMDD : $ZDATE(+$Horolog,8) Si vous voulez obtenir le résultat de YYYYY-MM-DD : $ZDATEH(+$H,3), dans la fonction horaire
     Affichage -> format interne $ZTIMEH("HH:MM:SS")
     Interne -> Si vous voulez obtenir le résultat au format d'affichage HH:MM:SS : $ZTIMEH($piece($Horolog,"",2)), il existe également les fonctions $ZDATETIME() et $ZDATETIMEH() pour manipuler la date et l'heure.

    Vous trouverez plus de détails sur les fonctions de date dans des documents suivants.
    ObjectScript fonction【IRIS】
    ObjectScript fonction

    SAMPLES>write $horolog
    63895,34979
    SAMPLES>write $ZDATE(+$horolog,8)  /Conversion au format yyyymmdd>63895
    SAMPLES>write $ZDATEH("2015-12-09",3)  // Conversion du format yyyy-mm-dd en format interne
    63895
    SAMPLES>write $ZTIME($piece($horolog,",",2))  // Conversion du format interne en format horaire
    09:44:16
    SAMPLES>write $ZTIMEH("10:01:11")  // Conversion de l'heure d'affichage au format interne
    36071
    SAMPLES>write $ZDATETIME($horolog,8)  // Conversion date/heure avec $horolog
    20151209 09:45:15
    SAMPLES>write $ZDATETIME($horolog,3)
    2015-12-09 09:45:16
    SAMPLES>
    0
    0 88
    Article Guillaume Rongier · Mai 21, 2022 16m read

    Comme nous le savons tous, Caché est une excellente base de données qui accomplit de nombreuses tâches en son sein. Cependant, que faites-vous lorsque vous avez besoin d'accéder à une base de données externe ? Une façon de le faire est d'utiliser la passerelle Caché SQL Gateway via JDBC. Dans cet article, mon objectif est de répondre aux questions suivantes pour vous aider à vous familiariser avec cette technologie et à déboguer certains problèmes courants.

    Plan de travail

    Avant de se plonger dans ces questions, discutons rapidement de l'architecture de la passerelle JDBC SQL Gateway. Pour simplifier, vous pouvez considérer que l'architecture est la suivante : Cache établit une connexion TCP avec un processus Java, appelé processus de passerelle. Le processus de passerelle se connecte ensuite à une base de données distante, telle que Caché, Oracle ou SQL Server, en utilisant le pilote spécifié pour cette base de données. Pour plus d'informations sur l'architecture de la passerelle SQL Gateway, veuillez consulter la documentation sur Utilisation de la passerelle Caché SQL Gateway.

    Paramètres de connexion

    Lorsque vous vous connectez à une base de données distante, vous devez fournir les paramètres suivants :

    • nom d'utilisateur
    • mot de passe
    • nom du pilote
    • URL
    • chemin de classe

    Connexion à la base de données Caché

    Par exemple, si vous avez besoin de vous connecter à une instance de Caché en utilisant la passerelle SQL Gateway via JDBC, vous devez naviguer vers [System Administration] -> [Configuration] -> [Connectivity] -> [SQL Gateway Connections] dans le portail de gestion du système (SMP). Cliquez ensuite sur "Créer une nouvelle connexion" et spécifiez "JDBC" comme type de connexion.

    Lors de la connexion à un système Caché, le nom du pilote doit toujours être com.intersys.jdbc.CacheDriver, comme indiqué dans la capture d'écran. Si vous vous connectez à une base de données tierce, vous devrez utiliser un nom de pilote différent (voir Connexion à des bases de données tierces ci-dessous).

    Lorsque vous vous connectez aux bases de données Caché, vous n'avez pas besoin de spécifier un chemin de classe car le fichier JAR est téléchargé automatiquement.

    Le paramètre URL varie également en fonction de la base de données à laquelle vous vous connectez. Pour les bases de données Caché, vous devez utiliser une URL de la forme suivante

    jdbc:Cache://[server_address]:[superserver_port]/[namespace]
    

    Connexion à des bases de données tierces

    Une base de données tierce courante est Oracle. Un exemple de configuration est présenté ci-dessous.

    Comme vous pouvez le constater, le nom du pilote et l'URL ont des caractéristiques différentes de celles que nous avons utilisées pour la connexion précédente. En outre, j'ai spécifié un chemin de classe dans cet exemple, car je dois utiliser le pilote d'Oracle pour me connecter à leur base de données.

    Comme vous pouvez l'imaginer, SQL Server utilise différents modèles d'URL et de noms de pilotes.

    Vous pouvez tester si les valeurs sont valides en cliquant sur le bouton " Testez la connexion ". Pour créer la connexion, cliquez sur "Enregistrer".

    JDBC Gateway vs le service Java Gateway Business Service

    Tout d'abord, la passerelle JDBC et le service de passerelle Java sont complètement indépendants l'un de l'autre. La passerelle JDBC peut être utilisée sur tous les systèmes basés sur Caché, alors que le service de passerelle Java n'existe que dans le cadre d'Ensemble. En outre, le service de passerelle Java utilise un processus différent de celui utilisé par la passerelle JDBC. Pour plus de détails sur le service commercial de passerelle Java, veuillez consulter Le service commercial de passerelle Java.

    Méthodes et outils

    Vous trouverez ci-dessous 5 outils et méthodes couramment utilisés pour résoudre des problèmes avec la passerelle JDBC SQL Gateway. Je vais d'abord parler de ces outils et vous montrer quelques exemples de leur utilisation dans la section suivante.

    1. Journaux

    A. Journal du pilote et journal de la passerelle

    Lorsque vous utilisez la passerelle JDBC, le journal correspondant est le journal de la passerelle JDBC SQL. Comme nous l'avons vu précédemment, la passerelle JDBC est utilisée lorsque Caché doit accéder à des bases de données externes, ce qui signifie que Caché est le client. Le journal du pilote, par contre, correspond à l'utilisation du pilote JDBC d'InterSystems pour accéder à une base de données Caché à partir d'une application externe, ce qui signifie que Caché est le serveur. Si vous avez une connexion d'une base de données Caché à une autre base de données Caché, les deux types de journaux peuvent être utiles.

    Dans notre documentation la section relative à l'activation du journal du pilote est intitulée "Activation de la journalisation pour JDBC", et la section relative à l'activation du journal de la passerelle est intitulée "Activation de la journalisation pour la passerelle SQL JDBC".

    Même si les deux journaux comportent le mot "JDBC", ils sont totalement indépendants. L'objet de cet article est la passerelle JDBC, c'est pourquoi j'aborderai plus en détail le journal de la passerelle. Pour plus d'informations sur le journal du pilote, veuillez vous reporter à la section Activation du journal du pilote.

    B. Activation du journal du pilote

    Si vous utilisez la passerelle Caché JDBC SQL Gateway, vous devez effectuer les opérations suivantes pour activer la journalisation : dans le portail de gestion, allez dans [System Administration] > [Configuration] > [Connectivity] > [JDBC Gateway Settings]. Indiquez une valeur pour le journal de la passerelle JDBC. Ce doit être le chemin complet et le nom d'un fichier journal (par exemple, /tmp/jdbcGateway.log). Le fichier sera automatiquement créé s'il n'existe pas, mais le répertoire ne le sera pas. Caché va démarrer la passerelle JDBC SQL Gateway avec journalisation pour vous.

    Si vous utilisez le service commercial Java Gateway dans Ensemble, veuillez consulter Activation de la journalisation de la passerelle Java Gateway dans Ensemble pour savoir comment activer la journalisation.

    C. Analyse du journal d'une passerelle

    Maintenant que vous avez collecté un journal de passerelle, vous vous posez peut-être la question suivante : quelle est la structure du journal et comment le lire ? Bonne question ! Je vais vous fournir ici quelques informations de base pour vous aider à démarrer. Malheureusement, il n'est pas toujours possible d'interpréter complètement le journal sans avoir accès au code source. Pour les situations complexes, n'hésitez pas à contacter le WRC (Centre de réponse global d'InterSystems) !

    Pour démystifier la structure du journal, rappelez-vous qu'il s'agit toujours d'un morceau de données suivi d'une description de ce qu'il fait. Par exemple, voyez cette image avec une coloration syntaxique de base :

    Afin de comprendre ce que Received signifie ici, vous devez vous rappeler que le journal de la passerelle enregistre les interactions entre la passerelle et la base de données descendante. Ainsi, Received signifie que la passerelle a reçu l'information de Caché/Ensemble. Dans l'exemple ci-dessus, la passerelle a reçu le texte d'une requête SELECT. Les significations des différentes valeurs de msgId peuvent être trouvées dans le code interne. Le 33 que nous voyons ici signifie " Preparer l'instruction ".

    Le journal lui-même fournit également des informations sur le pilote, ce qui est intéressant à vérifier lors du débogage des problèmes. Voici un exemple,

    Comme nous pouvons le voir, le Driver Name est com.intersys.jdbc.CacheDriver, ce qui est le nom du pilote utilisé pour se connecter au processus de passerelle. Le Jar File Name est cachejdbc.jar, ce qui est le nom du fichier jar situé dans <cache_install_directory>\lib\.

    2. Trouver le processus de passerelle

    Pour trouver le processus de passerelle, vous pouvez exécuter la commande ps. Par exemple,

    ps -ef | grep java
    

    Cette commande ps affiche des informations sur le processus Java, notamment le numéro de port, le fichier jar, le fichier journal, l'ID du processus Java et la commande qui a lancé le processus Java.

    Voici un exemple du résultat de la commande :

    mlimbpr15:~ mli$ ps -ef | grep java
    17182 45402 26852   0 12:12PM ??         0:00.00 sh -c java -Xrs -classpath /Applications/Cache20151/lib/cachegateway.jar:/Applications/Cache20151/lib/cachejdbc.jar com.intersys.gateway.JavaGateway 62972 /Applications/Cache20151/mgr/JDBC.log 2>&1
    17182 45403 45402   0 12:12PM ??         0:00.22 /usr/bin/java -Xrs -classpath /Applications/Cache20151/lib/cachegateway.jar:/Applications/Cache20151/lib/cachejdbc.jar com.intersys.gateway.JavaGateway 62972 /Applications/Cache20151/mgr/JDBC.log
    502 45412 45365   0 12:12PM ttys000    0:00.00 grep java
    

    Dans Windows, vous pouvez consulter le gestionnaire des tâches pour trouver des informations sur le processus de passerelle.

    3. Lancement et arrêt de la passerelle

    Il y a deux façons de lancer et d'arrêter la passerelle :

    1. Par le biais du SMP
    2. Utilisation du terminal

    A. Par le biais du SMP

    Vous pouvez lancer et arrêter la passerelle dans le SMP en accédant à [System Administration] -> [Configuration] -> [Connectivity] -> [JDBC Gateway Server].

    B. Utilisation du terminal

    Sur les machines Unix, vous pouvez également démarrer la passerelle depuis le terminal. Comme nous l'avons vu dans la section précédente, le résultat de ps -ef | grep java contient la commande qui a démarré le processus Java, qui dans l'exemple ci-dessus est le suivant:

    java -Xrs -classpath /Applications/Cache20151/lib/cachegateway.jar:/Applications/Cache20151/lib/cachejdbc.jar com.intersys.gateway.JavaGateway 62972 /Applications/Cache20151/mgr/JDBC.log
    

    Pour arrêter la passerelle depuis le terminal, vous pouvez tuer le processus. L'ID du processus Java est le deuxième chiffre de la ligne qui contient la commande ci-dessus, dans l'exemple ci-dessus c'est 45402. Ainsi, pour arrêter la passerelle, vous pouvez exécuter :

    kill 45402
    

    4. Écrire un programme Java

    Exécuter un programme Java pour se connecter à une base de données descendante est un excellent moyen de tester la connexion, de vérifier la requête et d'aider à isoler la cause d'un problème donné. Je joins un exemple de programme Java qui établit une connexion avec SQL Server et imprime une liste de tous les tableaux. J'expliquerai pourquoi cela peut être utile dans la section suivante.

    import java.sql.*;
    import java.sql.Date;
    import java.util.*;
    import java.lang.reflect.Method;
    import java.io.InputStream;
    import java.io.ByteArrayInputStream;
    import java.math.BigDecimal;
    import javax.sql.*;
    
    // Auteur : Vicky Li
    // Ce programme établit une connexion avec le serveur SQL et récupère tous les tableaux. Le résultat est une liste de tableaux.
    
    public class TestConnection {
        public static void main(String[] args) {
            try {
                Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");
                //please replace url, username, and password with the correct parameters
                Connection conn = DriverManager.getConnection(url,username,password);
    
                System.out.println("connected");
    
                DatabaseMetaData meta = conn.getMetaData();
                ResultSet res = meta.getTables(null, null, null, new String[] {"TABLE"});
                System.out.println("List of tables: ");
                while (res.next()) {
                    System.out.println(
                        "   " + res.getString("TABLE_CAT") +
                        ", " + res.getString("TABLE_SCHEM") +
                        ", " + res.getString("TABLE_NAME") +
                        ", " + res.getString("TABLE_TYPE")
                    );
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    Pour exécuter ce programme Java (ou tout autre programme Java), vous devez d'abord compiler le fichier .java, qui dans notre cas s'appelle TestConnection.java. Ensuite, un nouveau fichier sera généré au même endroit, que vous pourrez ensuite exécuter avec la commande suivante sur un système UNIX :

    java -cp "<path to driver>/sqljdbc4.jar:lib/*:." TestConnection
    

    Dans Windows, vous pouvez exécuter la commande suivante :

    java -cp "<path to driver>/sqljdbc4.jar;lib/*;." TestConnection
    

    5. Suivi d'une trace de jstack

    Comme son nom l'indique, jstack imprime l'arborescence des appels de procédure Java. Cet outil peut devenir pratique lorsque vous avez besoin de mieux comprendre ce que fait le processus Java. Par exemple, si vous voyez le processus de la passerelle s'accrocher à un certain message dans le journal des passerelles, vous pourriez vouloir recueillir une trace jstack. Je tiens à souligner que jstack est un outil de bas niveau qui ne devrait être utilisé que lorsque d'autres méthodes, comme l'analyse du journal des passerelles, ne résolvent pas le problème.

    Avant de collecter une trace jstack, vous devez vous assurer que le JDK est installé. Voici la commande pour collecter une trace jstack :

    jstack -F <pid> > /<path to file>/jstack.txt
    

    où le pid est l'ID du processus de la passerelle, qui peut être obtenu en exécutant la commande ps, telle que ps -ef | grep java. Pour plus d'informations sur la façon de trouver le pid, veuillez consulter Lancement et arrêt de la passerelle.

    Maintenant, voici quelques considérations spéciales pour les machines Red Hat. Dans le passé, il y a eu des problèmes pour attacher jstack au processus de la passerelle JDBC (ainsi qu'au processus du service métier de la passerelle Java lancé par Ensemble) sur certaines versions de Red Hat, donc la meilleure façon de collecter une trace jstack sur Red Hat est de lancer le processus de la passerelle manuellement. Pour les instructions, veuillez consulter Collecter une trace jstack sur Red Hat.

    Types courants de problèmes et approches pour les résoudre

    1. Problème : Java n'est pas installé correctement

    Dans cette situation, vérifiez la version de Java et les variables d'environnement.

    Pour vérifier la version de Java, vous pouvez exécuter la commande suivante à partir d'un terminal :

    java -version
    

    Si vous obtenez l'erreur java : Command not found, cela signifie que le processus Cache ne peut pas trouver l'emplacement des exécutables Java. Cela peut généralement être résolu en plaçant les exécutables Java dans le PATH. Si vous rencontrez des problèmes, n'hésitez pas à contacter le WRC (Centre de réponse global).

    2. Problème : échec de la connexion

    Un bon diagnostic des échecs de connexion est la vérification du lancement du processus de la passerelle. Vous pouvez le faire en vérifiant le journal de la passerelle ou le processus de la passerelle. Sur les versions modernes, vous pouvez également aller sur le SMP et visiter [System Administration] -> [Configuration] -> [Connectivity] -> [JDBC Gateway Server], et vérifier si la page affiche "JDBC Gateway is running".

    Si le processus de passerelle ne s'exécute pas, il est probable que Java n'est pas installé correctement ou que vous utilisez le mauvais port ; si le processus de passerelle s'exécute, il est probable que les paramètres de connexion sont incorrects.

    Dans le premier cas, veuillez vous reporter à la section précédente et vérifiez le numéro de port. Je discuterai plus en détail de la deuxième situation ici.

    Il est de la responsabilité du client d'utiliser les paramètres de connexion corrects :

    • nom d'utilisateur
    • mot de passe
    • nom du pilote
    • URL
    • chemin de classe

    Vous pouvez vérifier si vous avez les bons paramètres de l'une des trois façons suivantes :

    • Utilisez le bouton "Test Connection" après avoir sélectionné un nom de connexion dans [System Administration] -> [Configuration] -> [Connectivity] -> [SQL Gateway Connections]. Note : pour les systèmes modernes, "Test Connection" donne des messages d'erreur utiles ; pour les systèmes plus anciens, le JDBC gateway log est nécessaire pour trouver plus d'informations sur l'échec.

    • Exécutez la ligne de commande suivante depuis un terminal Caché pour tester la connexion :

        d $SYSTEM.SQLGateway.TestConnection(<connection name>)
      
    • Exécutez un programme Java pour établir une connexion. Le programme que vous écrivez peut être similaire à l' example dont nous avons parlé précédemment.

    3. Problème : décalage entre la façon dont Caché comprend JDBC et la façon dont la base de données distante comprend JDBC, par exemple :

    • problèmes de type de données
    • procédure stockée avec des paramètres de sortie
    • flux

    Pour cette catégorie, il est souvent plus utile de travailler avec le WRC (Centre de réponse global). Voici ce que nous faisons souvent pour déterminer si le problème se situe dans notre code interne ou dans la base de données distante (ou dans le pilote) :

    Remarque

    Le service commercial de la passerelle Java

    Le nom de la classe du Service Métier d' Ensemble est EnsLib.JavaGateway.Service, et la classe de l'adaptateur est EnsLib.JavaGateway.ServiceAdapter. La session Ensemble crée d'abord une connexion avec le serveur Java Gateway, qui est un processus Java. L'architecture est similaire à celle de la passerelle JDBC SQL, sauf que le processus Java est géré par l'opération commerciale. Pour plus de détails, veuillez consulter la documentation.

    Activation du journal du pilote

    Pour activer le journal du pilote, vous devez ajouter un nom de fichier journal à la fin de la chaîne de connexion JDBC. Par exemple, si la chaîne de connexion originale ressemble à czci :

    jdbc:Cache://127.0.0.1:1972/USER
    

    Pour activer la journalisation, ajoutez un fichier (jdbc.log) à la fin de la chaîne de connexion, de sorte qu'elle ressemble à ceci :

    jdbc:Cache://127.0.0.1:1972/USER/jdbc.log
    

    Le fichier journal sera enregistré dans le répertoire de travail de l'application Java.

    Activation de la journalisation de la passerelle Java dans Ensemble

    Si vous utilisez le service métier de la passerelle Java dans Ensemble pour accéder à une autre base de données, vous devez, pour activer la journalisation, spécifier le chemin et le nom d'un fichier journal (par exemple, /tmp/javaGateway.log) dans le champ "Log File" du service de la passerelle Java. Veuillez noter que le chemin d'accès doit exister.

    N'oubliez pas que la connexion de la passerelle Java utilisée par la production Ensemble est distincte des connexions utilisées par les tableaux liés ou d'autres productions. Ainsi, si vous utilisez Ensemble, vous devez collecter le journal dans le service de passerelle Java. Le code qui démarre le service de passerelle Java utilise le paramètre "Log File" dans Ensemble, et n'utilise pas le paramètre dans la passerelle Caché SQL dans le SMP comme décrit précédemment.

    Récupération d'une trace jstack sur Red Hat

    La clé ici est de lancer le processus de la passerelle manuellement, et la commande pour lancer la passerelle peut être obtenue en exécutant ps -ef | grep java. Vous trouverez ci-dessous les étapes complètes à suivre pour collecter une trace jstack sur Red Hat lors de l'exécution de la passerelle JDBC ou du service métier de la passerelle Java.

    1. Assurez-vous que le JDK est installé.

    2. Dans un terminal, exécutez ps -ef | grep java. Obtenez les deux informations suivantes à partir du résultat :

      • a. Copiez la commande qui a lancé la passerelle. Cela devrait ressembler à quelque chose comme ça : java -Xrs -classpath /Applications/Cache20151/lib/cachegateway.jar:/Applications/Cache20151/lib/cachejdbc.jar com.intersys.gateway.JavaGateway 62972 /Applications/Cache20151/mgr/JDBC2.log

      • b. Obtenez l'ID du processus Java (pid), qui est le deuxième chiffre de la ligne qui contient la commande ci-dessus.

    3. Arrêtez le processus avec kill <pid>.

    4. Exécutez la commande que vous avez copiée à l'étape 2.a. pour lancer manuellement un processus de passerelle.

    5. Jetez un coup d'oeil au journal de la passerelle (dans notre exemple, il est situé dans /Applications/Cache20151/mgr/JDBC2.log) et assurez-vous que vous voyez des entrées comme >> LOAD_JAVA_CLASS: com.intersys.jdbc.CacheDriver. Cette étape est juste pour vérifier qu'un appel à la passerelle est effectué avec succès.

    6. Dans un nouveau terminal, exécutez ps -ef | grep java pour obtenir le pid du processus de la passerelle.

    7. Rassemblez une trace jstack : jstack -F <pid> > /tmp/jstack.txt

    0
    0 286
    Article Irène Mykhailova · Mai 19, 2022 1m read

    Pour SQL, null et la chaîne vide ('') sont distinguées. Chaque méthode de définition/réception est la suivante.

    (1) NULL

    【SQL】

    insert into test (a) values (NULL)
    select * from test where a IS NULL

    【InterSystems ObjectScript】

    set x=##class(User.test).%New()
    set x.a=""

    (2) Chaîne vide ('')

    【SQL】

    insert into test (a) values ('')
    select * from test where a = ''

    【InterSystems ObjectScript】

    set x=##class(User.test).%New()
    set x.a=$C(0)

    Pour plus de détails, veuillez consulter les documents suivants.

    NULL and the Empty String (IRIS)
    NULL and the Empty String (Caché)

    0
    0 100
    Article Lorenzo Scalese · Mai 16, 2022 11m read

    Les modèles de données objet et relationnel de la base de données Caché supportent trois types d'index, à savoir standard, bitmap et 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 par notre équipe : Andrey Rechitsky, Aleksander Pogrebnikov et moi-même. (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.)

    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 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 : https://github.com/intersystems-ru/spatialindex/tree/no-web-interface. 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. 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))
    

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

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

    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:

    0
    0 115
    Article Irène Mykhailova · Mai 9, 2022 14m read

    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.

    0
    0 119
    Article Lorenzo Scalese · Avr 28, 2022 4m read

    Alors je sais que ça fait un peu longtemps, et je déteste laisser tomber mes fans adorateurs... mais pas assez pour recommencer à écrire.  Mais l'attente est terminée et je suis de retour !  Maintenant, profitez de mes mots vraiment magnifiques !

    Pour cette série, je vais examiner certains problèmes courants que nous rencontrons au WRC et discuter de certaines solutions communes.  Bien sûr, même si vous trouvez une solution ici, vous êtes toujours le bienvenu pour me contacter et exprimer votre gratitude, ou simplement entendre ma voix !

    Le problème courant de cette semaine : "Ma requête ne renvoie aucune donnée."

    Maintenant, je suppose que vous avez vérifié que votre requête DEVRAIT retourner des données.  En d'autres termes, si vous effectuez un "SELECT * FROM MyTable" et que vous n'obtenez aucune donnée, je ne pense pas que votre requête plus compliquée avec des JOINs, une clause WHERE et un GROUP BY le fera également.  Donc, si vous avez déterminé que les données se trouvent dans votre tableau, qu'est-ce qui peut bien se passer d'autre ?

    1) Vous êtes dans un mauvais espace de noms.

    Vous pouvez vous moquer si vous le souhaitez, mais c'est le problème le plus courant.  En général, les gens ne signalent pas ce problème, mais il arrive qu'il se présente.  Si vous n'obtenez pas de données, la première chose à faire est de vérifier votre espace de noms et, tant que vous y êtes, de vous assurer que vous vous connectez à la bonne instance.  Débarrassez-vous d'abord du problème le plus simple.

    2) Vous devez construire des indices.

    C'est le problème qui perturbe la plupart des gens.  Si vous ouvrez votre définition de classe et ajoutez un index, il n'est pas automatiquement construit pour vous.  Par conséquent, lorsque vous ajoutez un index, il est disponible pour être utilisé par le compilateur SQL Compiler, mais il ne contient pas de données.  Donc quand on regarde, pas de données, boom, requête faite, pas de résultats !  Vous devez appeler ##class().%BuildIndices($LB("<New Index>")) afin que ce nouvel index soit rempli avec les données appropriées.  Attention toutefois à ne pas faire cela sur un système actif !  Pour obtenir des conseils sur la construction d'un index sur un système actif, veuillez contacter le WRC et nous faire part de votre version !

    3) Vous êtes dans un mauvais SELECTMODE.

    Lorsque vous exécutez une requête, vous pouvez utiliser plusieurs modes : Logical, Display et ODBC.  La façon typique de montrer la différence est avec les propriétés %Date.  Par exemple, la date d'aujourd'hui est 64295, 01/12/2017, 2017-01-12 dans les modes Logical, Display, et ODBC respectivement.  Si vous utilisez le mauvais SELECTMODE, vos requêtes de date ne renverront parfois aucune donnée, même si elles le devraient.  Une bonne façon de tester cela est d'exécuter une requête dans le Shell SQL et de définir votre mode de sélection, comme suit :

    SAMPLES>d $SYSTEM.SQL.Shell()

    SAMPLES>>selectmode = odbc

    De cette façon, vous avez un contrôle absolu (et simple) sur le selectmode de votre requête.  Vous pouvez également exécuter des requêtes xDBC (ODBC/JDBC) en sachant que nous sommes en mode ODBC.  

    4) Problèmes de collation

    Si vous avez suivi l'un des excellents articles de Brendan concernant la création de votre propre stockage, sachez qu'il existe un problème supplémentaire qui peut vous rattraper.  Vous devez vous assurer que la collation que vous avez défini pour votre champ est correctement placé dans votre index.  Par conséquent, si votre champ %String a été classé en SQLUPPER, l'index doit être classé en SQLUPPER.  Si ce n'est pas le cas, vous pouvez constater que vous n'avez pas de données. 

    5) NLS sur CacheTemp

    C'est plutôt rare, mais si vous utilisez une collation NLS différent, vous devez vous assurer que la collation NLS de CACHTEMP correspond à la collation NLS de votre base de données.  Le texte officiel est disponible dans les documents ici:

    http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_basics#GSQL_basics_collation_sqlnls

    6) Bug d'InterSystems

    Hé, je ne suis pas trop fier de l'admettre.  De temps en temps, nous avons des bugs et ça pourrait être le cas.  Si vous le pensez, contactez le WRC !

    7) Votre requête comporte un bug.

    Quoi, vous pensiez que vous aviez décroché ! ?  Si notre code peut avoir un bug, votre requête peut aussi en avoir un !  Vérifiez encore une fois votre requête !

    Ce sont les plus importants.  Essayez d'examiner certains des éléments mentionnés ici, et si vous avez des difficultés à les résoudre, n'hésitez pas à contacter le service d'assistance et nous serons heureux de les analyser avec vous !

    -----------------------------------------------------------------------------

    Vous avez besoin d'un TL;DR pour ça !?  C'était si court !

    7 raisons expliquant pourquoi votre requête peut ne pas renvoyer de données :

    1. Vous êtes dans un mauvais espace de noms.

    2. Vous devez construire des indices.

    3. Vous êtes dans un mauvais SELECTMODE.

    4. Problèmes de collation

    5. NLS sur CacheTemp

    6. Bug d'InterSystems

    7. Votre requête comporte un bug.

    Correction : Dernière remarque : avez-vous rencontré un problème où une requête ne renvoyant aucune donnée ?  Ajoutez votre expérience dans les commentaires !

    0
    0 47
    Article Irène Mykhailova · Avr 22, 2022 2m read

    Comment rechercher une globale contenant un string spécifique ?

    Vous pouvez afficher les globales dans le Management Portal et effectuer une recherche avec Ctrl + F, mais les grandes globales peuvent prendre du temps et être difficiles à afficher.

    Bien sûr, vous pouvez effectuer une boucle en utilisant les fonctions $ORDER et $QUERY pour trouver la chaîne.

    Mais il existe un moyen plus simple et plus pratique.

    Il s'agit d'une méthode qui utilise la fonction de recherche globale de chaîne de caractères qui peut être effectuée dans le Management Portal.

    Cela peut être facilement fait dans [System Explorer]> [Globals] : Rechercher dans le Management Portal.

    Par exemple, pour rechercher dans le global ^%ISCLOG une variable contenant l'erreur "Erreur CSP de nettoyage après la page" :

     ↓


    Si vous voulez vraiment le faire par programmation en utilisant les fonctions $ORDER et $QUERY :

    * S'il y a plusieurs indices, utilisez la fonction $QUERY pour boucler dans plusieurs couches.

    set glb="^%ISCLOG"
    set glb=$query(@glb@(""))
    for {
      if glb="" quit
      
      if @glb [ "CSP error cleaning up after page" {
         write glb,"=",@glb,! ;; <= you can take values with @glb
      }
      set glb=$query(@glb)
    }


    Le résultat de l'exécution est le suivant (la routine ci-dessus est enregistrée et exécutée par test.mac).

    %SYS>Do ^test
    ^%ISCLOG("Data",5960)=
                            CSPServerUError cleaning up after page, HALTing ...
    ^%ISCLOG("Data",5960,0,"$ZE")=CSP error cleaning up after page, HALTing
    ^%ISCLOG("Data",10046)=
                             CSPServerUError cleaning up after page, HALTing...
    ^%ISCLOG("Data",10046,0,"$ZE")=CSP error cleaning up after page, HALTing
    ^%ISCLOG("Data",13398)=
                             CSPServerUError cleaning up after page, HALTing ...
    ^%ISCLOG("Data",13398,0,"$ZE")=CSP error cleaning up after page, HALTing
     
    %SYS>
    0
    0 102
    Article Sylvain Guilbaud · Avr 20, 2022 4m read

    Lors d'une montée de version majeure il est conseillé de recompiler les classes et les routines de tous vos espaces de noms (cf. Major Version Post-Installation Tasks).

    do $system.OBJ.CompileAllNamespaces("u")
    do ##Class(%Routine).CompileAllNamespaces()

    Pour automatiser cette tâche d'administration et conserver un journal des erreurs éventuelles, vous trouverez ci-dessous un exemple d'une classe à importer et compiler dans l'espace de noms USER que vous pourrez utiliser après chaque montée de version : admin.utils.cls

    0
    1 175
    Article Lorenzo Scalese · Avr 15, 2022 26m read

    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.

    0
    0 59
    Article Guillaume Rongier · Avr 13, 2022 7m read

    Ce texte est la suite de mon article où j'ai expliqué la structure d'une base de données Caché. Dans cet article, j'ai décrit les types de blocs, les connexions entre eux et leur relation avec les globales. L'article est purement théorique. J'ai fait un projet qui aide à visualiser l'arbre des blocs - et cet article explique comment il fonctionne en détail.

    Pour les besoins de la démonstration, j'ai créé une nouvelle base de données et l'ai débarrassée des globales que Caché initialise par défaut pour toutes les nouvelles bases de données. Créons une globale simple :
    set ^colors(1)="red"
     set ^colors(2)="blue"
     set ^colors(3)="green"
    ​ set ^colors(4)="yellow"

    Notez l'image illustrant les blocs du global créé. Celui-ci est simple, c'est pourquoi nous voyons sa description dans le bloc de type 9 (bloc catalogue des globales). Il est suivi par le bloc "pointeur supérieur et inférieur" (type 70), car l'arbre des globales n'est pas encore profond, et vous pouvez utiliser un pointeur vers un bloc de données qui tient encore dans un seul bloc de 8 Ko.

    Maintenant, écrivons tant de valeurs dans une autre globale qu'elles ne peuvent pas être placées dans un seul bloc - et nous verrons de nouveaux nœuds dans le bloc de pointeurs pointant vers de nouveaux blocs de données qui ne pouvaient pas être placés dans le premier.

    Écrivons 50 valeurs, de 1000 caractères chacune. Rappelez-vous que la taille du bloc dans notre base de données est de 8192 octets.

       set str=""
       for i=1:1:1000 {
           set str=str_"1"
       }
       for i=1:1:50 {
           set ^test(i)=str
       }
    ​   quit

    Regardez l'image suivante :

    Nous avons plusieurs nœuds au niveau du bloc de pointeurs pointant vers des blocs de données. Chaque bloc de données contient des pointeurs vers le bloc suivant ("lien correct"). Offset - pointe vers le nombre d'octets occupés dans ce bloc de données.

    Essayons de simuler une division de bloc. Ajoutons tellement de valeurs au bloc que la taille totale du bloc dépasse 8 Ko, ce qui provoquera la division du bloc en deux.

    Exemple de code

       set str=""
       for i=1:1:1000 {
           set str=str_"1"
       }
       set ^test(3,1)=str
       set ^test(3,2)=str
    ​   set ^test(3,3)=str

    Le résultat est présenté ci-dessous :

    Le bloc 50 est divisé et rempli de nouvelles données. Les valeurs remplacées se trouvent maintenant dans le bloc 58 et un pointeur vers ce bloc apparaît maintenant dans le bloc des pointeurs. Les autres blocs sont restés inchangés.

    Un exemple avec de longues chaînes de caractères

    Si nous utilisons des chaînes plus longues que 8 Ko (la taille du bloc de données), nous obtiendrons des blocs de "données longues". Nous pouvons simuler une telle situation en écrivant des chaînes de caractères de 10000 octets, par exemple.

    Exemple de code

       set str=""
       for i=1:1:10000 {
           set str=str_"1"
       }
       for i=1:1:50 {
           set ^test(i)=str
    ​   }

    Voyons le résultat :

    En conséquence, la structure des blocs de l'image est restée la même, puisque nous n'avons pas ajouté de nouveaux nœuds globaux, mais seulement modifié les valeurs. Cependant, la valeur Offset (nombre d'octets occupés) a changé pour tous les blocs. Par exemple, la valeur Offset du bloc #51 est maintenant 172 au lieu de 7088. Il est clair que maintenant, lorsque la nouvelle valeur ne peut pas être insérée dans le bloc, le pointeur vers le dernier octet de données devrait être différent, mais où sont nos données ? Pour le moment, mon projet ne supporte pas la possibilité d'afficher des informations sur les "grands blocs". Utilisons l'outil ^REPAIR pour obtenir des informations sur le nouveau contenu du bloc #51.

    Laissez-moi vous expliquer le fonctionnement de cet outil. Nous voyons un pointeur sur le bloc correct #52, et le même numéro est spécifié dans le bloc du pointeur parent dans le noeud suivant. Le collatéral de la globale est défini sur le type 5. Le nombre de noeuds avec des chaînes longues est de 7. Dans certains cas, le bloc peut contenir à la fois des valeurs de données pour certains noeuds et des chaînes longues pour d'autres, le tout dans un seul bloc. Nous voyons également quelle référence de pointeur suivante doit être attendue au début du bloc suivant.

    Concernant les blocs de longues chaînes de caractères : nous voyons que le mot clé "BIG" est spécifié comme valeur du global. Cela nous indique que les données sont en fait stockées dans des "gros blocs". La même ligne contient la longueur totale de la chaîne contenue, et la liste des blocs stockant cette valeur. Jetons un coup d'oeil au "bloc de chaînes longues", le bloc #73.

    Malheureusement, ce bloc est montré encodé. Cependant, nous pouvons remarquer que les informations de service de l'en-tête du bloc (qui font toujours 28 octets) sont suivies de nos données. Connaître le type de données rend le décodage du contenu de l'en-tête assez facile :

    <td>
      Value
    </td>
    
    <td>
      Description
    </td>
    
    <td>
      Comment
    </td>
    
    <td>
      E4 1F 00 00
    </td>
    
    <td>
      Offset pointant vers la fin des données
    </td>
    
    <td>
      Nous avons 8164 octets, plus 28 octets d'en-tête pour un total de 8192 octets, le bloc est plein.
    </td>
    
    <td>
      18
    </td>
    
    <td>
      Type de bloc
    </td>
    
    <td>
      Comme on s'en souvient, 24 est l'identifiant de type pour les longues chaînes de caractères.
    </td>
    
    <td>
      05
    </td>
    
    <td>
      Collate
    </td>
    
    <td>
      Collate 5 signifie "Caché standard"
    </td>
    
    <td>
      4A 00 00 00
    </td>
    
    <td>
      Lien correct
    </td>
    
    <td>
      Nous obtenons 74 ici, car nous nous souvenons que notre valeur est stockée dans les blocs 73 et 74
    </td>
    
    Position
    0-3
    4
    5
    8-11

    Je vous rappelle que les données du bloc 51 n'occupent que 172 octets. Cela s'est produit lorsque nous avons enregistré de grandes valeurs. Il semble donc que le bloc soit devenu presque vide avec seulement 172 octets de données utiles, et pourtant il occupe 8ko ! Il est clair que dans une telle situation, l'espace libre sera rempli de nouvelles valeurs, mais Caché nous permet également de compresser une telle globale. Pour cela, la classe %Library.GlobalEdit dispose de la méthode CompactGlobal. Pour vérifier l'efficacité de cette méthode, utilisons notre exemple avec un grand volume de données - par exemple, en créant 500 noeuds.

    Voici ce que nous avons obtenu.

       kill ^test
       for l=1000,10000 {
           set str=""
           for i=1:1:l {
               set str=str_"1"
           }
           for i=1:1:500 {
               set ^test(i)=str
           }
       }
       quit

    Nous n'avons pas montré tous les blocs ci-dessous, mais le résultat devrait être clair. Nous avons beaucoup de blocs de données, mais avec un petit nombre de noeuds.

    Exécution de la méthode CompactGlobal :

    write ##class(%GlobalEdit).CompactGlobal("test","c:\intersystems\ensemble\mgr\test")

    Jetons un coup d'oeil au résultat. Le bloc des pointeurs ne compte plus que 2 nœuds, ce qui signifie que toutes nos valeurs sont allées à deux nœuds, alors que nous avions initialement 72 nœuds dans le bloc des pointeurs. Nous nous sommes donc débarrassés de 70 nœuds et avons ainsi réduit le temps d'accès aux données lors du passage par la globale, puisque cela nécessite moins d'opérations de lecture de bloc.

    CompactGlobal accepte plusieurs paramètres, comme le nom du global, la base de données et la valeur de remplissage cible, 90% par défaut. Et maintenant nous voyons que Offset (le nombre d'octets occupés) est égal à 7360, ce qui est autour de ces 90%. Quelques paramètres de sortie de la fonction : le nombre de mégaoctets traités et le nombre de mégaoctets après compression. Auparavant, les globales étaient compressés à l'aide de l'outil ^GCOMPACT qui est maintenant considéré comme obsolète.

    Il convient de noter qu'une situation où les blocs ne sont que partiellement remplis est tout à fait normale. De plus, la compression des globales peut parfois être indésirable. Par exemple, si votre globale est principalement lue et rarement modifiée, la compression peut s'avérer utile. Mais si la globale change tout le temps, une certaine sparsité dans les blocs de données permet d'éviter de diviser les blocs trop souvent, et l'enregistrement de nouvelles données sera plus rapide.

    0
    0 103
    Article Guillaume Rongier · Avr 12, 2022 7m read

    Les globales d'InterSystems Caché offrent des fonctionnalités très pratiques pour les développeurs. Mais pourquoi les globales sont-elles si rapides et efficaces ?

    Théorie

    Fondamentalement, la base de données Caché est un catalogue portant le même nom que la base de données et contenant le fichier CACHE.DAT. Sur les systèmes Unix, la base de données peut également être une partition de disque ordinaire.

    Toutes les données dans Caché sont stockées dans des blocs qui, à leur tour, sont organisés sous forme d'un arbre B* équilibré. En tenant compte du fait que tous les globales sont fondamentalement stockées dans un arbre, les indices des globales seront représentés comme des branches, tandis que les valeurs des indices des globales seront stockées comme des feuilles. La différence entre un arbre B* équilibré et un arbre B ordinaire est que ses branches ont également des liens corrects qui peuvent aider à itérer à travers les souscripts (c'est-à-dire les globales dans notre cas) en utilisant rapidement les fonctions $Order et $Query sans revenir au tronc de l'arbre. 

    Par défaut, chaque bloc du fichier de la base de données a une taille fixe de 8 192 octets. Vous ne pouvez pas modifier la taille du bloc pour une base de données déjà existante. Lorsque vous créez une nouvelle base de données, vous pouvez choisir des blocs de 16 Ko, 32 Ko ou même 64 Ko, en fonction du type de données que vous allez stocker. Cependant, gardez toujours à l'esprit que toutes les données sont lues bloc par bloc - en d'autres termes, même si vous demandez une valeur unique d'un octet, le système lira plusieurs blocs parmi lesquels le bloc de données demandé sera le dernier. Vous devez également vous rappeler que Caché utilise des buffers globaux pour stocker les blocs de base de données en mémoire pour une seconde utilisation, et que les buffers ont la même taille que les blocs. Vous ne pouvez pas monter une base de données existante ou en créer une nouvelle si un buffer global avec la taille de bloc correspondante est absent du système. Vous devez définir la taille de la mémoire que vous souhaitez allouer pour la taille spécifique des blocs. Il est possible d'utiliser des blocs de buffer plus grands que les blocs de base de données, mais dans ce cas, chaque bloc de buffer ne stockera qu'un seul bloc de base de données, voire plus petit.


    Dans cette image, une mémoire pour le buffer global d'une taille de 8 ko est allouée pour l'utilisation avec des bases de données constituées de blocs de 8 ko. Les blocs qui ne sont pas vides dans cette base de données sont définis dans des cartes, de sorte qu'une des cartes définit 62 464 blocs (pour des blocs de 8 ko). 

    Types de blocs

    Le système prend en charge plusieurs types de blocs. À chaque niveau, les liens corrects d'un bloc doivent pointer vers un bloc du même type ou vers un bloc nul qui définit la fin des données.

    • Type 9: Le système prend en charge plusieurs types de blocs. À chaque niveau, les liens corrects d'un bloc doivent pointer vers un bloc du même type ou vers un bloc nul qui définit la fin des données.
    • Type 66: Bloc de pointeurs de haut niveau. Seul un bloc d'un catalogue global peut se trouver au-dessus de ces blocs.
    • Type 6: Bloc de pointeurs de bas niveau. Seuls les blocs de pointeurs de haut niveau peuvent se trouver au-dessus de ces blocs et seuls les blocs de données peuvent être placés plus bas.
    • Type 70: Bloc de pointeurs de haut niveau et de bas niveau. Ces blocs sont utilisés lorsque la globale correspondante stocke un petit nombre de valeurs et que plusieurs niveaux de blocs ne sont donc pas nécessaires. Ces blocs pointent généralement vers des blocs de données, tout comme le font les blocs d'un catalogue global.
    • Type 2: Bloc de pointeurs pour le stockage de globales relativement grandes. Afin de répartir uniformément les valeurs entre les blocs de données, vous pouvez créer des niveaux supplémentaires de blocs de pointeurs. Ces blocs sont généralement placés entre les blocs de pointeurs.
    • Type 8: Bloc de données. Ces blocs stockent généralement les valeurs de plusieurs nœuds globaux plutôt que celles d'un seul nœud.
    • Type 24: Bloc pour les grandes chaînes de caractères. Lorsque la valeur d'une seule globale est plus grande qu'un bloc, cette valeur est enregistrée dans un bloc spécial pour les grandes chaînes de caractères, tandis que le nœud de bloc de données stocke les liens vers la liste des blocs pour les grandes chaînes de caractères ainsi que la longueur totale de cette valeur.
    • Type 16: Bloc de carte. Ces blocs sont conçus pour stocker des informations sur les blocs non alloués.

    Ainsi, le premier bloc d'une base de données Caché typique contient des informations de service sur le fichier de base de données lui-même, tandis que le deuxième bloc fournit une carte des blocs. Le premier bloc de catalogue va sur la troisième place (bloc #3), et une seule base de données peut avoir plusieurs blocs de catalogue. Les blocs suivants sont des blocs de pointeurs (branches), des blocs de données (feuilles) et des blocs de grandes chaînes de caractères. Comme je l'ai mentionné ci-dessus, les blocs de catalogues globaux stockent des informations sur tous les globales existants dans la base de données ou les paramètres globaux (si aucune donnée n'est disponible dans une telle globale). Dans ce cas, un nœud qui décrit une telle globale aura un pointeur inférieur nul. Vous pouvez consulter la liste des globales existantes à partir du catalogue de globales sur le portail de gestion. Ce portail vous permet également de sauvegarder une globale dans le catalogue après sa suppression (par exemple, sauvegarder sa séquence de collationnement) ainsi que de créer une nouvelle globale avec un collationnement par défaut ou personnalisé.

    En général, l'arbre des blocs peut être représenté comme dans l'image ci-dessous. Notez que les liens vers les blocs sont représentés en rouge.

    Intégrité des bases de données

    Dans la version actuelle de Caché, nous avons résolu les questions et problèmes les plus importants concernant les bases de données, de sorte que les risques de dégradation des bases de données sont extrêmement faibles. Cependant, nous vous recommandons toujours d'exécuter régulièrement des contrôles d'intégrité automatiques à l'aide de notre outil ^Integrity - vous pouvez le lancer dans le terminal à partir de l'espace de noms %SYS, via notre portail de gestion, sur la page Database ou via le gestionnaire de tâches. Par défaut, le contrôle d'intégrité automatique est déjà configuré et prédéfini, de sorte que la seule chose que vous devez faire est de l'activer :

     ​​​​​​​

    Le contrôle d'intégrité comprend la vérification des liens aux niveaux inférieurs, la validation des types de blocs, l'analyse des bons liens et la mise en correspondance des nœuds globaux avec la séquence de collationnement appliquée. Si des erreurs sont détectées lors du contrôle d'intégrité, vous pouvez exécuter notre outil ^REPAIR à partir de l'espace de noms %SYS. Grâce à cet outil, vous pouvez visualiser n'importe quel bloc et le modifier si nécessaire, c'est-à-dire réparer votre base de données. 

    Pratique

    Cependant, ce n'était que de la théorie. Il est encore difficile de savoir à quoi ressemblent réellement une globale et ses blocs. Actuellement, la seule façon de visualiser les blocs est d'utiliser notre outil ^REPAIR mentionné ci-dessus. La sortie typique de ce programme est présentée ci-dessous :

     

    Il y a un an, j'ai lancé un nouveau projet visant à développer un outil qui itère à travers un arbre de blocs sans risque d'endommager la base de données, qui visualise ces blocs dans une interface utilisateur Web et qui offre des options pour sauvegarder leur visualisation en SVG ou PNG. Le projet s'appelle CacheBlocksExplorer, et vous pouvez télécharger son code source sur Github.

    Les fonctionnalités mises en œuvre comprennent :

    • Visualisation de toute base de données configurée ou simplement installée dans le système ;
    • Affichage des informations par bloc, type de bloc, pointeur droit, liste des nœuds avec liens ;
    • Afficher des informations détaillées sur tout nœud pointant vers un bloc inférieur ;
    • Masquer des blocs en supprimant les liens vers ceux-ci (sans aucun dommage pour les données stockées dans ces blocs).

    La liste des choses à faire :

  • Affichage des liens droits : dans la version actuelle, les liens droits sont affichés dans les informations sur les blocs, mais il serait préférable de les afficher sous forme de flèches ;
  • Affichage des blocs de grandes chaînes de caractères : ils ne sont tout simplement pas affichés dans la version actuelle ;
  • Affichage de tous les blocs du catalogue global plutôt que du troisième seulement.
  • Je voudrais également afficher l'intégralité de l'arbre, mais je ne trouve toujours pas de bibliothèque capable de rendre rapidement des centaines de milliers de blocs avec leurs liens - la bibliothèque actuelle les rend dans les navigateurs web bien plus lentement que Caché ne lit la structure entière.

    Dans mon prochain article, j'essaierai de décrire plus en détail son fonctionnement, de fournir quelques exemples d'utilisation et de montrer comment récupérer de nombreuses données exploitables sur les globales et les blocs à l'aide de mon Cache Block Explorer.

    0
    0 152
    Article Guillaume Rongier · Avr 6, 2022 10m read

    Dans les parties précédentes (1 et 2) nous avons parlé des globales en tant qu'arbres. Dans cet article, nous allons les considérer comme des listes éparses.

    Une liste éparse - est un type de liste où la plupart des valeurs ont une valeur identique.

    En pratique, vous verrez souvent des listes éparses si volumineuses qu'il est inutile d'occuper la mémoire avec des éléments identiques. Il est donc judicieux d'organiser les listes éparses de telle sorte que la mémoire ne soit pas gaspillée pour stocker des valeurs en double.

    Dans certains langages de programmation, les listes éparses font partie intégrante du langage - par exemple, in J, MATLAB. Dans d'autres langages, il existe des bibliothèques spéciales qui vous permettent de les utiliser. Pour le C++, il s'agit de Eigen et d'autres bibliothèques de ce type.

    Les globales sont de bons candidats pour la mise en œuvre de listes éparses pour les raisons suivantes :

    1. Ils stockent uniquement les valeurs de nœuds particuliers et ne stockent pas les valeurs indéfinies ;

    2. L'interface d'accès à une valeur de nœud est extrêmement similaire à ce que de nombreux langages de programmation proposent pour accéder à un élément d'une liste multidimensionnelle.   Set ^a(1, 2, 3)=5 Write ^a(1, 2, 3)

    3. Une structure globale est une structure de niveau assez bas pour le stockage des données, ce qui explique pourquoi les globales possèdent des caractéristiques de performance exceptionnelles (des centaines de milliers à des dizaines de millions de transactions par seconde selon le matériel, voir 1)

    Puisqu'une globale est une structure persistante, il n'est logique de créer des listes éparses sur leur base que dans les situations où vous savez à l'avance que vous disposerez de suffisamment de mémoire pour elles.   L'une des nuances de la mise en œuvre des listes éparses est le retour d'une certaine valeur par défaut si vous vous adressez à un élément indéfini.

    Ceci peut être mis en œuvre en utilisant la fonction $GET dans COS. Prenons l'exemple d'une liste tridimensionnelle.

    SET a = $GET(^a(x,y,z), defValue)

    Quel type de tâches nécessite des listes éparses et comment les globales peuvent-elles vous aider ?

    Matrice d'adjacence

    Ces matrices sont utilisées pour la représentation des graphiques :

    Il est évident que plus un graphe est grand, plus il y aura de zéros dans la matrice. Si nous regardons le graphe d'un réseau social, par exemple, et que nous le représentons sous la forme d'une matrice de ce type, il sera principalement constitué de zéros, c'est-à-dire qu'il s'agira d'une liste éparse.

    Set ^m(id1, id2) = 1
    Set ^m(id1, id3) = 1
    Set ^m(id1, id4) = 1
    Set ^m(id1) = 3
    Set ^m(id2, id4) = 1
    Set ^m(id2, id5) = 1
    Set ^m(id2) = 2
    ....
    

    Dans cet exemple, nous allons sauvegarder la matrice d'adjacence dans le ^m globale, ainsi que le nombre d'arêtes de chaque nœud (qui est ami et avec qui et le nombre d'amis).

    Si le nombre d'éléments du graphique ne dépasse pas 29 millions (ce nombre est calculé comme 8 * longueur maximale de la chaîne), il existe même une méthode plus économique pour stocker de telles matrices - les chaines binaires, car elles optimisent les grands espaces d'une manière spéciale.

    Les manipulations de chaînes binaires sont effectuées à l'aide de la fonction $BIT.

    ; setting a bit
    SET $BIT(rowID, positionID) = 1
    ; getting a bit
    Write $BIT(rowID, positionID)
    

    Tableau des commutateurs FSM

    Le graphe des commutateurs FSM étant un graphe régulier, le tableau des commutateurs FSM est essentiellement la même matrice d'adjacence dont nous avons parlé précédemment.

    Automates cellulaires

    L'automate cellulaire le plus célèbre est le jeu "Life", dont les règles (lorsqu'une cellule a de nombreux voisins, elle meurt) en font essentiellement une liste éparse.

    Stephen Wolfram estime que les automates cellulaires représentent un nouveau domaine de la science. En 2002, il a publié un livre de 1280 pages intitulé "A New Kind of Science", dans lequel il affirme que les réalisations dans le domaine des automates cellulaires ne sont pas isolées, mais sont plutôt stables et importantes pour tous les domaines de la science.

    Il a été prouvé que tout algorithme qui peut être traité par un ordinateur peut également être mis en œuvre à l'aide d'un automate cellulaire. Les automates cellulaires sont utilisés pour simuler des environnements et des systèmes dynamiques, pour résoudre des problèmes algorithmiques et à d'autres fins.

    Si nous avons un champ considérable et que nous devons enregistrer tous les états intermédiaires d'un automate cellulaire, il est logique d'utiliser les globales.

    Cartographie

    La première chose qui me vient à l'esprit lorsqu'il s'agit d'utiliser des listes éparses est la cartographie.

    En règle générale, les cartes comportent beaucoup d'espace vide. Si nous imaginons que la carte du monde est composée de grands pixels, nous verrons que 71 % de tous les pixels de la Terre seront occupés par le réseau creux de l'océan. Et si nous ajoutons uniquement des structures artificielles à la carte, il y aura plus de 95 % d'espace vide.

    Bien sûr, personne ne stocke les cartes sous forme de tableaux bitmap, tout le monde utilise plutôt la représentation vectorielle.
    Mais en quoi consistent les cartes vectorielles ? C'est une sorte de cadre avec des polylignes et des polygones.
    En fait, il s'agit d'une base de données de points et de relations entre eux.

    L'une des tâches les plus difficiles en cartographie est la création d'une carte de notre galaxie réalisée par le télescope Gaia. Au sens figuré, notre galaxie est un gigantesque réseau creux : d'immenses espaces vides avec quelques points lumineux occasionnels - des étoiles. C'est 99,999999.......% d'espace absolument vide. Caché, une base de données basée sur des globales, a été choisie pour stocker la carte de notre galaxie.

    Je ne connais pas la structure exacte des globales dans ce projet, mais je peux supposer que c'est quelque chose comme ça :

    Set ^galaxy(b, l, d) = 1; le numéro de catalogue de l'étoile, s'il existe
    Set ^galaxy(b, l, d, "name") = "Sun"
    Set ^galaxy(b, l, d, "type") = "normal" ; les autres options peuvent inclure un trou noir, quazar, red_dwarf et autres.
    Set ^galaxy(b, l, d, "weight") = 14E50
    Set ^galaxy(b, l, d, "planetes") = 7
    Set ^galaxy(b, l, d, "planetes", 1) = "Mercure"
    Set ^galaxy(b, l, d, "planetes", 1, weight) = 1E20
    ...
    

    Où b, l, d représententcoordonnées galactiques: la latitude, la longitude et la distance par rapport au Soleil.

    La structure flexible des globales vous permet de stocker toutes les caractéristiques des étoiles et des planètes, puisque les bases de données basées sur les globales sont exemptes de schéma.

    Caché a été choisi pour stocker la carte de notre univers non seulement en raison de sa flexibilité, mais aussi grâce à sa capacité à sauvegarder rapidement un fil de données tout en créant simultanément des globales d'index pour une recherche rapide.

    Si nous revenons à la Terre, les globales ont été utilisées dans des projets axés sur les cartes comme OpenStreetMap XAPI et FOSM, un branchement d'OpenStreetMap.

    Récemment, lors d'un hackathon Caché, un groupe de développeurs a mis en œuvre des index géospatiaux en utilisant cette technologie. Pour plus de détails, consultez l'article.

    Mise en œuvre d'index géospatiaux à l'aide de globales dans OpenStreetMap XAPI

    Les illustrations sont tirées de cette présentation.

    Le globe entier est divisé en carrés, puis en sous-carrés, puis en encore plus de sous-carrés, et ainsi de suite. Au final, nous obtenons une structure hiérarchique pour laquelle les globales ont été créées.

    À tout moment, nous pouvons instantanément demander n'importe quelle case ou la vider, et toutes les sous-carrés seront également retournées ou vidées.

    Un schéma détaillé basé sur les globales peut être mis en œuvre de plusieurs façons.

    Variante 1:

    Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 1) = idPointOne
    Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 2) = idPointTwo
    ...

    Variante 2:

    Set ^m('abacdabcdabacdabcda', 1) = idPointOne
    Set ^m('abacdabcdabacdabcda', 2) = idPointTwo
    ...

    Dans les deux cas, il ne sera pas très difficile dans COS/M de demander des points situés dans un carré de n'importe quel niveau. Il sera un peu plus facile de dégager des segments d'espace carrés de n'importe quel niveau dans la première variante, mais cela est rarement nécessaire.

    Un exemple de carré de bas niveau :

    Et voici quelques globales du projet XAPI : représentation d'un index basé sur des globales :

    La globale ^voie est utilisé pour stocker les sommets des polylines (routes, petites rivières, etc.) et des polygones (zones fermées : bâtiments, bois, etc.).

    Une classification approximative de l'utilisation des listes éparses dans les globales.

    1. Nous stockons les coordonnées de certains objets et leur état (cartographie, automates cellulaires).
    2. Nous stockons des matrices creuses.

    Dans la variante 2), lorsqu'une certaine coordonnée est demandée et qu'aucune valeur n'est attribuée à un élément, nous devons obtenir la valeur par défaut de l'élément de la liste éparse.

    Les avantages que nous obtenons en stockant des matrices multidimensionnelles dans les globales

    Suppression et/ou sélection rapide de segments d'espace qui sont des multiples de chaînes, de surfaces, de cubes, etc. Pour les cas avec des index intégraux, il peut être pratique de pouvoir supprimer et/ou sélectionner rapidement des segments d'espace qui sont des multiples de chaînes, de surfaces, de cubes, etc.

    La commande Kill permet de supprimer un élément autonome, une chaîne de caractères et même une surface entière. Grâce aux propriétés de la globale, elle se produit très rapidement, mille fois plus vite que la suppression élément par élément.

    L'illustration montre un tableau tridimensionnel dans la globale ^a et différents types d'enlèvements.

    Pour sélectionner des segments d'espace par des indices connus, vous pouvez utiliser la commande Merge.

    Sélection d'une colonne de la matrice dans la colonne Variable :

    ; Définissons un tableau tridimensionnel creux 3x3x3
    Set ^a(0,0,0)=1,^a(2,2,0)=1,^a(2,0,1)=1,^a(0,2,1)=1,^a(2,2,2)=1,^a(2,1,2)=1
    Colonne de fusion = ^a(2,2)
    ; Produisons la colonne Variable
    Zwrite colonne
    

    Produit :

    Column(0)=1
    Colonne(2)=1
    

    Ce qui est intéressant, c'est que nous avons obtenu un tableau épars dans la colonne Variable que vous pouvez adresser via $GET puisque les valeurs par défaut ne sont pas stockées ici.

    La sélection de segments d'espace peut également se faire à l'aide d'un petit programme utilisant la fonction $Order. Ceci est particulièrement utile pour les espaces dont les indices ne sont pas quantifiés (cartographie).

    Conclusion

    Les réalités d'aujourd'hui posent de nouveaux défis. Les graphes peuvent comporter des milliards de sommets, les cartes peuvent avoir des milliards de points, certains peuvent même vouloir lancer leur propre univers basé sur des automates cellulaires (1, 2).

    Lorsque le volume de données dans les listes éparses ne peut pas être comprimé dans la RAM, mais que vous devez quand même travailler avec elles, vous devriez envisager de mettre en œuvre de tels projets en utilisant des globales et des COS.

    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é d'InterSystems.
    0
    0 116
    Article Guillaume Rongier · Avr 4, 2022 13m read

    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.

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

    2. Une fonction spéciale $LB (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.

    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.

    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.

    <th>
      Pros
    </th>
    
    <td>
      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.
    </td>
    
    Tables dans un arborescence deux niveaux vs. Tables dans un arborescence mono niveau.
    Cons
    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.  

    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

    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, 1,2,3,4,5,6, 7,8), 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 :
    1. 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.
    2. 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.
    3. 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 :

    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.

    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.

    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.

    <th>
      Commodité du traitement/de la présentation des données
    </th>
    
    <td>
    
    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  
      
    Vitesse
    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
    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.
    0
    0 107
    Article Lorenzo Scalese · Mars 30, 2022 3m read

    Comme vous le savez, dans Caché / IRIS, vous avez la possibilité de définir une propriété comme Multidimensionnelle, comme documenté ici et l'explication de la façon de l'utiliser est ici.

    Bien que l'accès soit assez confortable (au sens traditionnel du COS), il y a 2 restrictions principales qui font mal :

    1. Il n'est pas sauvegardé sur le disque, sauf si votre application inclut du code pour le sauvegarder spécifiquement.
    2. Il ne peut pas être stocké dans des tableaux SQL ou exposé à travers ceux-ci.

    il y en a d'autres Je vais vous montrer comment surmonter ces limites.

    1. Prenons cette classe simple comme exemple :
    Class DC.Multi Extends (%Persistent, %Populate) [ Final ]
    {
        Property Name As %String;
        Property DOB As %Date;
        Property Multi As %String [ MultiDimensional 
    ];

    La carte de stockage montre déjà le problème n°1 : pas de place pour "Multi"

    Storage Default
    {
      <Data name="MultiDefaultData">
        <Value name="1">
          <Value>Name</Value>
        </Value>
        <Value name="2">
          <Value>DOB</Value>
        </Value>
      </Data>
      <DataLocation>^DC.MultiD</DataLocation>
      <DefaultData>MultiDefaultData</DefaultData>
      <IdLocation>^DC.MultiD</IdLocation>
      <IndexLocation>^DC.MultiI</IndexLocation>
      <StreamLocation>^DC.MultiS</StreamLocation>
      <Type>%Storage.Persistent</Type>
    }

    donc nous ajoutons 2 méthodes :

    /// save Multidimensional property
    Method %OnAfterSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ]
    {    Merge ^(..%Id(),"Multi")=i%Multi quit $$$OK   }
    /// load Multidimensional property
    Method %OnOpen() As %Status [ Private, ServerOnly = 1 ]
    {   Merge i%Multi=^(..%Id(),"Multi") quit $$$OK  }

    nous attachons juste la structure orpheline à notre objet réel. Pour être franc, ce n'est pas mon invention, mais l'approche (simplifiée) qui était utilisée dans la classe %CSP.Session lorsqu'elle a été écrite vers le début du millénaire. Avec le simple ajout suivant, votre structure multidimensionnelle devient persistante.

    L'objet en mémoire ressemble à ceci :

    CACHE>zw o2
    o2=3@DC.Multi  ; <OREF>
    +----------------- general information ---------------
    |      oref value: 3
    |      class name: DC.Multi
    |           %%OID: $lb("2","DC.Multi")
    | reference count: 2
    +----------------- attribute values ------------------
    |       %Concurrency = 1  <Set>
    |                DOB = 62459
    |         Multi("a") = 1
    |     Multi("rob",1) = "rcc"
    |     Multi("rob",2) = 2222
    |               Name = "Klingman,Uma C."
    +-----------------------------------------------------

    et c'est le stockage associé, une jolie globale multidimensionnelle :

    CACHE>zw ^DC.MultiD(2)
    ^DC.MultiD(2)=$lb("Klingman,Uma C.",62459)
    ^DC.MultiD(2,"Multi","a")=1
    ^DC.MultiD(2,"Multi","rob",1)="rcc"
    ^DC.MultiD(2,"Multi","rob",2)=2222

    Jusqu'à présent, tout va bien.

    1. SELECT * from DC.Multi n'a aucune idée de ce que peut être une colonne "Multi".

    donc nous ajoutons une propriété calculée SQL et un style approprié.

    Property SqlMulti As %String(MAXLEN = "") [ Calculated, SqlComputed,
    SqlComputeCode 
    = { set {*}= ##class(DC.Multi).ShowMulti({ID}) }];
    ClassMethod ShowMulti(id) As %String(MAXLEN="")
    { set res="{"
          ,obj=..%OpenId(id)
       if $isobject(obj) {
          set qry=$query(obj.Multi(""),1,value)
          while qry'="" {
             set res=res_$piece(qry,".",2,99)_"="_value_","
                ,qry=$query(@qry,1,value)
             }
         set $extract(res,*)=""
       }
       quit res_"}"   
    }

    Et cela ressemble à ceci

    Inutile de dire que la conception est totalement entre vos mains.

    Comme il y a un progrès évident par rapport au passé, le prochain article vous montrera une solution plus appropriée au nouveau siècle.

    0
    0 94