#Compilateur

0 Abonnés · 4 Publications

L'outil qui compile le code source vers une autre forme de code exécutable.

InterSystems officiel Adeline Icard · Déc 12, 2024

Les premiers aperçus pour les développeurs de la plateforme de données InterSystems IRIS®, InterSystems IRIS® for Health et HealthShare® Health Connect 2025.1 ont été publiés sur le site d'aperçus pour les développeurs sur WRC. Les conteneurs sont disponibles dans notre registre de conteneurs et sont étiquetés latest-preview.

0
0 45
Question Jules Pontois · Oct 19, 2023

Cette question est apparue à l'origine dans les commentaires du post : Surcharge les méthodes SendRequestAsync et SendRequestSync
 

Bonjour,

Effectivement, retirer ForceGenerate des mots clés résout le problème des doublons. Merci !

Une dernière question : Une fois le code généré, il n'est pas directement visible dans les classes crées. Et notamment en utilisant VS Code. Du coup, actuellement, je lance la compilation des sous classe avec $system.OBJ.Compile et le flag subclasses.

1
0 85
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 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