#Object Data Model

0 Abonnés · 11 Publications

Dans un modèle de bases de données orientées objet, les données ou le code sont composés de modules qui combinent les données et les procédures qui travaillent sur les données.

En savoir plus.

Article Iryna Mykhailova · Juin 5, 2024 3m read

Le stockage en colonnes est l'une des offres les plus récentes proposées par InterSystems IRIS.Contrairement au stockage traditionnel basé sur les lignes, il optimise le traitement des requêtes en stockant les données dans des colonnes plutôt que dans des lignes, permettant ainsi un accès et une récupération plus rapides des informations pertinentes.

Quelques articles ont été rédigés sur les moments il doit être utilisé pour donner le plus grand coup de pouce à un système et sur la façon de créer des tables comme celle-ci à l'aide de SQL.

CREATETABLEtable (column1 type1, column2 type2, column3 type3) WITH STORAGETYPE = COLUMNAR  -- ex 1CREATETABLEtable (column1 type1, column2 type2, column3 type3 WITH STORAGETYPE = COLUMNAR)  -- ex 2

et même les tests de performances.

Comme nous le savons tous, InterSystems IRIS est un SGBD multimodèle et il donne un accès transparent aux mêmes données en utilisant un accès relationnel et objet. Le premier est donc couvert dans d’autres articles, mais qu’en est-il du dernier?

0
0 59
Article Pierre LaFay · Jan 13, 2024 3m read

InterSystems FAQ rubric

Les définitions de classe créées par les utilisateurs sont stockées dans des classes de définition de classe. Ils peuvent être utilisés pour obtenir une liste de définitions de classe à partir d'un programme.

Remarque : Les classes de définition de classe font référence à toutes les classes contenues dans le package %Dictionary.

Dans l'exemple de code ci-dessous, une liste de définitions de classe est obtenue à l'aide de la commande query Summary de la  classe %Dictionary.ClassDefinitionQuery.

0
0 61
Article Iryna Mykhailova · Juil 19, 2022 9m read

Pour parler des différentes bases de données et des différents modèles de données qui existent, on doit premièrement comprendre ce qui est une base de données et comment les utiliser.

Une base de données est une collection organisée de données stockées et accessibles par voie électronique. Elle permet de stocker et de retrouver des données structurées, semi-structurées ou des données brutes souvent en rapport avec un thème ou une activité.

Au cœur de chaque base de données se trouve au moins un modèle utilisé pour décrire ses données. Et selon le modèle sur lequel elle est basée, elle peut avoir des caractéristiques un peu différentes et stocker différents types de données.

Pour inscrire, retrouver, modifier, trier, transformer ou imprimer les informations de la base de données on utilise un logiciel qui s’appelle système de gestion de base de données (SGBD, en anglais DBMS pour Database management system).

La taille, les capacités et les performances des bases de données et de leurs SGBD respectifs ont augmenté de plusieurs ordres de grandeur. Ces augmentations de performances ont été rendues possibles par les progrès technologiques dans différents domaines, tels que les domaines des processeurs, de la mémoire informatique, du stockage informatique et des réseaux informatiques. Le développement ultérieur de la technologie des bases de données peut être divisé en quatre générations basées sur le modèle ou la structure des données : navigation, relationnel, objet et post-relationnel.

4
0 642
Article Lorenzo Scalese · Juin 13, 2022 19m read

Une session concurrente dans IRIS : SQL, Objects, REST, et GraphQL  

Kazimir Malevitch, "Athlètes" (1932) 

"Mais bien sûr, vous ne comprenez pas ! Comment celui qui a toujours voyagé en calèche peut-il comprendre les sentiments et les impressions du voyageur en express ou du pilote dans les airs ?"

Kazimir Malevich (1916)

Introduction

Nous avons déjà abordé le sujet des raisons pour lesquelles la représentation objet/type est préférable à SQL pour la mise en œuvre des modèles de domaine. Et ces conclusions et ces faits sont aussi vrais aujourd'hui qu'ils l'ont toujours été. Alors pourquoi devrions-nous faire un pas en arrière et discuter des technologies qui ramènent les abstractions au niveau global, où elles se trouvaient à l'ère pré-objet et pré-type ? Et pourquoi devrions-nous encourager l'utilisation d'un code spaghetti, qui donne lieu à des bogues difficiles à repérer et qui ne repose que sur les compétences virtuoses des développeurs ?

Plusieurs arguments sont favorables à la transmission de données via des API basées sur SQL/REST/GraphQL plutôt qu'à leur représentation sous forme de types/objects:

0
0 499
Article Guillaume Rongier · Juin 10, 2022 8m read

Cette publication est le résultat direct d'une collaboration avec un client d'InterSystems qui est venu me consulter pour le problème suivant :

SELECT COUNT(*) FROM MyCustomTable

Cela prend 0,005 secondes, pour un total de 2300 lignes.  Cependant :

SELECT * FROM MyCustomTable

Prenait des minutes.  La raison en est subtile et suffisamment intéressante pour que j'écrive un article à ce sujet.  Cet article est long, mais si vous faites défiler la page jusqu'en bas, je vous donnerai un résumé rapide. Si vous êtes arrivé jusqu'ici et que vous pensez en avoir lu assez, faites défiler la page jusqu'à la fin pour connaître l'essentiel.  Vérifiez la phrase en gras.


Lors de la création de vos classes, il faut tenir compte de la question du stockage.  Comme beaucoup d'entre vous le savent, toutes les données dans Caché sont stockées dans des Globales.  

<Digression> 

Si vous ne le savez pas, je pense que cet article sera un peu trop long.  Je vous recommande de consulter un excellent tutoriel dans notre documentation :

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

Si vous n'avez jamais utilisé Caché/Ensemble/HealthShare, le tutoriel ci-dessus est très utile, et même si vous l'avez fait, il vaut la peine de le consulter !  

Maintenant, comme toutes les données sont stockées dans des globales, il est important de comprendre comment les définitions de vos classes correspondent aux globales.  Construisons une application ensemble !  Nous allons examiner certains pièges courants et discuter de la façon dont le développement de vos classes affecte vos stratégies de stockage, avec un regard particulier sur les performances SQL.  

Imaginons que nous soyons le Bureau du recensement des États-Unis et que nous voulions disposer d'une base de données pour stocker les informations concernant tous les habitants des États-Unis.  Nous construisons donc une classe de la manière suivante :

Class USA.Person extends %Persistent
{
 Property Name as %String;
 Property SSN as %String;
 Property Address as %String;
 Property DateOfBirth as %Date;
}

SSN est l'abréviation de "Social Security Number" (numéro de sécurité sociale) qui, bien qu'il n'ait pas été conçu à l'origine pour être un numéro d'identification des personnes, est leur numéro d'identification de facto.  Cependant, comme nous sommes traditionalistes, nous ne l'utiliserons pas pour l'identification.  Cela dit, nous tenons à ce que cet élément soit indexé, car c'est un excellent moyen de rechercher une personne.  Nous savons que nous devrons parfois rechercher des personnes par le nom, c'est pourquoi nous voulons également un index des noms.  Et parce que notre patron aime ses rapports basés sur des tranches d'âge, nous pensons qu'un index des dates de naissance pourrait également être utile.  Ajoutons-les donc à notre classe

Class USA.Person extends %Persistent
{
 Property Name as %String;
 Property SSN as %String;
 Property Address as %String;
 Property DateOfBirth as %Date;

 Index NameIDX On Name;
 Index SSNIDX On SSN [Unique];
 Index DOBIDX on DateOfBirth;

}

Très bien.  Alors ajoutons une ligne et voyons à quoi ressemblent nos globales.  Notre instruction INSERT est la suivante :

INSERT INTO USA.Person (Name,SSN,Address,DateOfBirth) VALUES
   ('Baxter, Kyle','111-11-1111','1 Memorial Drive, Cambridge, MA 02142','1985-07-20')

Et la globale:

USER>zw ^USA.PersonD
^USA.PersonD=1
^USA.PersonD(1)=$lb("","Baxter, Kyle","111-11-1111","1 Memorial Drive, Cambridge, MA 02142",52796)

Le stockage par défaut d'une classe stocke vos données dans ^Package.ClassD.  Si le nom de la classe est trop long, il peut être haché, et vous pouvez le trouver dans la définition de stockage au bas de votre définition de classe.  Les index, à quoi ressemblent-ils ?

USER>zw ^USA.PersonI                      
^USA.PersonI("DOBIDX",52796,1)=""
^USA.PersonI("NameIDX"," BAXTER, KYLE",1)=""
^USA.PersonI("SSNIDX"," 111-11-1111",1)=""

Excellent, notre stockage est plutôt bon pour l'instant.  Donc on ajoute nos 320 millions de personnes et on peut trouver des gens assez rapidement.  Mais maintenant nous avons un problème, car nous voulons traiter le président et tous les ex-présidents avec une considération spéciale.  Nous ajoutons donc une classe spéciale pour le président :

Class USA.President extends USA.Person
{
Property PresNumber as %Integer;

Index PresNumberIDX on PresNumber;
}

Bien.  En raison de l'héritage, nous récupérons toutes les propriétés de USA.Person, et nous en ajoutons une pour nous permettre de savoir quel numéro de président il était.  Puisque je veux faire un peu de politique, je vais INSÉRER notre PROCHAIN président.  Voici l'instruction :

INSERT INTO USA.President (Name,SSN,DateOfBirth,Address,PresNumber) VALUES ('McDonald,Ronald','221-18-7518','01-01-1963','1600 Pennsylvania Ave NW, Washington, DC 20006',45)

Note : Son numéro de sécurité sociale s'écrit 'Burger'.  Désolé si c'est le vôtre.

Alors c'est génial !  Regardons votre Globale du Président :

USER>zw ^USA.PresidentD

Pas de données !  Et c'est là que nous arrivons à l'essentiel de cet article.  Parce que nous avons décidé d'hériter de USA.Person FIRST, nous avons hérité non seulement de ses propriétés et index, mais aussi de son stockage !  Donc pour localiser le président McDonald, nous devons regarder dans ^USA.PersonD.  Et nous pouvons voir ce qui suit :

^USA.PersonD(2)=$lb("~USA.President~","McDonald,Ronald","221-18-7518","1600 Pennsylvania Ave NW, Washington, DC 20006",44560)
^USA.PersonD(2,"President")=$lb(45&)

Deux choses à noter ici.  La première est que nous pouvons voir que le nœud (2) possède toutes les informations déjà stockées dans USA.Person.  Alors que le noeud (2, "President") ne contient que les informations spécifiques à la classe USA.President.  

Qu'est-ce que cela signifie en pratique ?  Eh bien, si nous voulons faire une opération de type : SELECT * FROM USA.President, nous aurons BESOIN de parcourir l'ensemble du tableau des personnes.  Si nous pensons que le tableau des personnes contient 320 000 000 lignes et que le tableau des présidents en contient 45, alors nous devons faire plus de 320 000 045 références globales pour extraire 45 lignes !  En effet, si l'on regarde le plan de requête :

  • Lire la carte maîtresse USA.President.IDKEY, en bouclant sur ID.
  • Pour chaque ligne:
  •  Résultat de la ligne.

Nous observons ce que nous attendons.  Cependant, nous avons déjà vu que cela signifie qu'il faut nécessairement regarder dans la globale ^USA.PersonD.  Donc, cela va être une référence globale de 320 000 000+ car nous devons tester CHAQUE ^USA.PersonD pour vérifier s'il y a des données dans ^USA.PersonD(i, "Président") puisque nous ne savons pas quelles personnes seront présidents.  Eh bien, c'est mauvais ! Ce n'est pas du tout ce que nous voulions !  Que pouvons-nous faire ?  Eh bien, nous avons deux options :

Option 1

Ajouter un index d'éxtent.  Si nous faisons cela, nous obtenons une liste d'identifiants qui nous permet de savoir quelles personnes sont des présidents et nous pouvons utiliser cette information pour lire des nœuds spécifiques de la globale ^USA.Person.  Comme je dispose d'un stockage par défaut, je peux utiliser un index bitmap, ce qui rendra l'opération encore plus rapide.  Nous ajoutons l'index comme suit :

Index Extent [Type=Bitmap, Extent];

Et quand nous regardons notre plan de requête pour SELECT * FROM USA.President nous pouvons voir :

  • Lecture de l'extent du bitmap USA.President.Extent, en bouclant sur l'ID.

  • Pour chaque ligne :

  •  Lecture de la carte maîtresse USA.President.IDKEY, en utilisant la valeur idkey donnée.
     Résultat de la ligne.

Ah, maintenant ça va être sympa et rapide.  Une référence globale pour lire l'Extent et ensuite 45 autres pour les présidents.  C'est plutôt efficace.  

Les inconvénients ?  La connexion à ce tableau devient un peu plus compliquée et peut impliquer un plus grand nombre de tableaux temporaires que vous ne le souhaiteriez.  

Option 2

Changement de la définition de la classe en ::

Class USA.President extends (%Persistent, USA.Person)

En faisant de %Persistent la première classe étendue, USA.President aura sa propre définition de stockage.  Ainsi, les présidents seront stockés de la manière suivante :

USER>zw ^USA.PresidentD
^USA.PresidentD=1
^USA.PresidentD(1)=$lb("","McDonald,Ronald","221-18-7518","1600 Pennsylvania Ave NW, Washington, DC 20006",44560,45)

C'est donc une bonne chose, car choisir USA.President signifie simplement lire les 45 membres de cette globale.  C'est facile et agréable, et le design est clair.

Les inconvénients ?  Eh bien maintenant, les présidents ne sont PAS dans le tableau des personnes Person.  Donc si vous voulez des informations sur les présidents ET les non-présidents, vous devez faire SELECT ... FROM USA.Person UNION ALL SELECT ... FROM USA.President


Si vous avez arrêté de lire au début, recommencez ici !

Lors de la création d'un héritage, nous avons deux options

Option 1: L'héritage de la superclasse est le premier.  Cela permet de stocker les données dans la même globale que la superclasse.  Utile si vous voulez avoir toutes les informations ensemble, et vous pouvez atténuer les problèmes de performance dans la sous-classe en ayant un index extent.

Option 2: Héritage de %Persistent first.  Cela permet de stocker les données dans une nouvelle globale.  C'est utile si vous interrogez beaucoup la sous-classe, mais si vous voulez voir les données de la super-classe et de la sous-classe, vous devez utiliser une requête UNION.

Laquelle de ces solutions est la meilleure ?  Cela dépend de la façon dont vous allez utiliser votre application.  Si vous souhaitez effectuer un grand nombre de requêtes sur l'ensemble des données, vous opterez probablement pour la première approche.  En revanche, si vous ne pensez pas interroger les données dans leur ensemble, vous opterez probablement pour la seconde approche.  Les deux approches sont tout à fait acceptables, à condition de ne pas oublier l'index extent de l'option 1.

Des questions ? Des commentaires ? De longues pensées contradictoires ?  Laissez-les ci-dessous !

0
1 100
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 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 · Mars 28, 2022 5m read

    Lorsque je décris InterSystems IRIS à des personnes plus orientées vers la technique, je commence toujours par dire qu'il s'agit d'un DBMS (système de gestion de base de données) multi-modèle.

    À mon avis, c'est son principal avantage (du côté du DBMS). Et les données ne sont stockées qu'une seule fois. Vous choisissez simplement l'API d'accès que vous voulez utiliser.

    • Voulez-vous une sorte de résumé pour vos données ? Utilisez SQL !
    • Souhaitez-vous travailler en profondeur avec un seul enregistrement ? Utilisez des objets !
    • Voulez-vous accéder ou définir une valeur et vous connaissez la clé ? Utilisez les globales !

    À première vue, c'est une belle histoire - courte et concrète, elle fait passer le message, mais lorsque les gens commencent vraiment à travailler avec InterSystems IRIS, les questions apparaissent. Comment les classes, les tables et les globales sont-ils liés ? Que sont-ils les uns pour les autres ? Comment les données sont-elles réellement stockées ?

    Dans cet article, je vais essayer de répondre à ces questions et d'expliquer ce qui se passe réellement.

    Première partie. Le biais des modèles.

    Les personnes qui travaillent avec des données ont souvent un biais en faveur du modèle avec lequel elles travaillent.

    Les développeurs pensent en objets. Pour eux, les bases de données et les tableaux sont des boîtes avec lesquelles vous interagissez via CRUD (Créer-Lire-Mettre à jour-Supprimer, de préférence via ORM), mais le modèle conceptuel sous-jacent est constitué d'objets (bien sûr, c'est surtout vrai pour les développeurs utilisant des langages orientés objet - donc la plupart d'entre nous).

    D'autre part, pour avoir passé beaucoup de temps dans des DBMS relationnels, les DBA considèrent souvent les données comme des tables. Dans ce cas, les objets ne sont que des enveloppes sur les lignes.

    Et avec InterSystems IRIS, une classe persistante est aussi un table, qui stocke les données en globale, donc une clarification est nécessaire.

    Deuxième partie. Un exemple.

    Disons que vous avez créé une classe Point :

    Class try.Point Extends %Persistent [DDLAllowed]
    {
        Property X;
        Property Y;
    }
    

    Vous pouvez également créer la même classe avec DDL/SQL :

    CREATE Table try.Point (
        X VARCHAR(50),
        Y VARCHAR(50))
    

    Après la compilation, notre nouvelle classe aurait généré automatiquement une structure de stockage qui fait correspondre les données qui sont nativement stockées dans les globaux aux colonnes (ou aux propriétés si vous êtes un penseur orienté objet) :

    Storage Default
    {
    <Data name="PointDefaultData">
        <Value name="1">
            <Value>%%CLASSNAME</Value>
        </Value>
        <Value name="2">
            <Value>X</Value>
        </Value>
        <Value name="3">
            <Value>Y</Value>
        </Value>
    </Data>
    <DataLocation>^try.PointD</DataLocation>
    <DefaultData>PointDefaultData</DefaultData>
    <IdLocation>^try.PointD</IdLocation>
    <IndexLocation>^try.PointI</IndexLocation>
    <StreamLocation>^try.PointS</StreamLocation>
    <Type>%Library.CacheStorage</Type>
    }
    

    Qu'est-ce qui se passe ici ?

    De bas en haut (les mots en gras sont importants, ignorez le reste) :

    • Type : le type de stockage généré, dans notre cas le stockage par défaut pour les objets persistants
    • StreamLocation - l'endroit où nous stockons les flux de données séquencé
    • IndexLocation - la globale pour les indices
    • IdLocation - la globale où nous stockons l'ID compteur autoincrémental
    • DefaultData - l'élément XML de stockage qui fait correspondre la valeur globale aux colonnes/propriétés
    • DataLocation - la globale dans lequel les données sont stockées

    Maintenant notre "DefaultData" est PointDefaultData alors regardons de plus près sa structure. Essentiellement, cela dit que le noeud global a cette structure :

    • 1 - %%CLASSNAME
    • 2 - X
    • 3 - Y

    On peut donc s'attendre à ce que notre globale ressemble à ceci :

    ^try.PointD(id) = %%CLASSNAME, X, Y
    

    Mais si nous imprimons notre globale, il sera vide car nous n'avons pas ajouté de données :

    zw ^try.PointD
    

    Ajoutons un objet :

    set p = ##class(try.Point).%New()
    set p.X = 1
    set p.Y = 2
    write p.%Save()
    

    Et voici notre globale

    zw ^try.PointD
    ^try.PointD=1
    ^try.PointD(1)=$lb("",1,2)
    

    Comme vous le voyez, notre structure attendue %%CLASSNAME, X, Y est définie avec $lb("",1,2) qui correspond aux propriétés X et Y de notre objet (%%CLASSNAME est une propriété du système, ignorez-la).

    Nous pouvons également ajouter une ligne via SQL :

    INSERT INTO try.Point (X, Y) VALUES (3,4)
    

    Maintenant, notre globale ressemble à ceci :

    zw ^try.PointD
    ^try.PointD=2
    ^try.PointD(1)=$lb("",1,2)
    ^try.PointD(2)=$lb("",3,4)
    

    Ainsi, les données que nous ajoutons par le biais d'objets ou de SQL sont stockées dans des globales en fonction des définitions de stockage (remarque : vous pouvez modifier manuellement la définition de stockage en remplaçant X et Y dans PointDefaultData - vérifiez ce qu'il arrive aux nouvelles données !)

    Maintenant, que se passe-t-il lorsque nous voulons exécuter une requête SQL ?

    SELECT * FROM try.Point
    

    Elle est traduite en code ObjectScript qui itère sur la globale ^try.PointD et remplit les colonnes en fonction de la définition du stockage - la partie PointDefaultData précisément.

    Maintenant pour les modifications. Supprimons toutes les données du table :

    DELETE FROM try.Point
    

    Et voyons notre globale à ce stade :

    zw ^try.PointD
    ^try.PointD=2
    

    Notez que seul le compteur d'ID est laissé, donc le nouvel objet/ligne aura un ID=3. De même, notre classe et notre table continuent d'exister. Mais que se passe-t-il quand on lance :

    DROP TABLE try.Point
    

    Il détruirait le table, la classe et supprimerait la globale.

    zw ^try.PointD
    

    Si vous avez suivi cet exemple, j'espère que vous comprenez maintenant mieux comment les globales, les classes et les tables s'intègrent et se complètent. L'utilisation de la bonne API pour le travail à effectuer permet un développement plus rapide, plus agile et moins bogué.

    0
    0 213