Article
· Sept 14, 2023 8m de lecture

Procédure stockée en Embedded Python

Aperçu

La documentation en ligne contient une référence expliquant comment définir et utiliser les requêtes de classes.

La personnalisation des procédures stockées en ObjectScript s'est avérée utile pour accéder au stockage NoSQL et à la messagerie externe via l'intégration, afin de présenter la sortie sous forme de tableau.

Par exemple : une application qui utilise déjà 90 % d'interaction SQL depuis un frontal peut alors également étendre cet accès aux 10 % restants des fonctionnalités requises de la plate-forme, via le même accès SQL.

Le but de cet article est d'explorer comment obtenir le même effet via les méthodes Embedded Python.

Figure 1 : procédure stockée en tant que passerelle SQL vers d'autres fonctionnalités de la plateforme

Démonstration

Pour cet exemple, le stockage NoSQL suivant a été défini :

^alwo.IndexBook("A",1)="abc"
^alwo.IndexBook("A",2)="def"
^alwo.IndexBook("B")=""
^alwo.IndexBook("C")=""
^alwo.IndexBook("D",1)="gef"
^alwo.IndexBook("E",1)="ijk"
^alwo.IndexBook("E",2)="lmn"

Pour tester, la procédure stockée peut être exécutée à partir d'un terminal :

GBI>Do $SYSTEM.SQL.Shell()
SQL Command Line Shell
----------------------------------------------------
 
The command prefix is currently set to: <<nothing>>.
Enter <command>, 'q' to quit, '?' for help.

[SQL]GBI>>call alwo.PyProcTest_GetNotes('A')

Dumping result #1
Tab     NoteId  NoteText
A       1       abc
A       2       def
B       0
C       0
D       0
E       0
 
6 Rows(s) Affected
statement prepare time(s)/globals/cmds/disk: 0.0003s/4/100/0ms
          execute time(s)/globals/cmds/disk: 0.0009s/12/1,096/0ms
                                query class: %sqlcq.GBI.cls27, %Library.ProcedureContext
---------------------------------------------------------------------------

Lorsque l'onglet (première clé) "A" a été fourni, les données des sous-nœuds sont développées et renvoyées sous forme d'enregistrements.

Les autres nœuds qui ne sont pas sélectionnés ou qui ne contiennent pas de données sont renvoyés sous forme d'enregistrements « 0 ».

Concepts de Code

Lorsqu'une requête ( GetNotes ) est implémentée dans une classe, le contenu de la requête peut être obtenu uniquement avec SQL.

Une fois compilées, trois méthodes de classe sont générées :

  • GetNotesExecute
  • GetNotesFetch
  • GetNotesClose

Il existe de nombreux scénarios dans lesquels les données ne sont pas SQL :

  • globales NoSQL
  • interaction paramétrée avec un système externe pour récupérer et consolider les données

En implémentant directement ces trois méthodes, il est possible de contrôler l’accès et de donner des réponses tabulaires pour un large éventail de fonctionnalités de la plateforme.

Il existe quelques variantes d'interaction :

Mettre en cache toutes les données de réponse à l'avance

1. La méthode GetNotesExecute accéderait aux ressources pour créer une réponse dans un global temporaire.

Cela pourrait être utile pour une vue cohérente des données, ce qui peut impliquer le verrouillage de l'accès aux mises à jour pendant une brève période.

2. La méthode GetNotesFetch serait appelée à plusieurs reprises pour renvoyer des enregistrements à partir des données temporaires.

3. La méthode GetNotesClose rangerait et supprimerait les données temporaires

Données de réponse dynamiques

1. La méthode GetNotesExecute est appelée. Cela ne fait pas grand-chose à part lancer un contexte qHandle disponible pour la méthode Fetch

2. La méthode GetNotesFetch est appelée. Chaque fois qu'un nouvel enregistrement est récupéré dynamiquement

3. La méthode GetNotesClose nécessite peu ou pas de retouche.

C'est l'approche utilisée dans l'exemple de code donné.

Opportunités de pagination et autres

En fonction du scénario, le remplissage dynamique des enregistrements peut être utilisé pour réduire le besoin d'exécuter une « requête complète » renvoyant toutes les lignes, quand une seule page d'enregistrements est suffisante.

Le code

La méthode d'exécution a une expression $C(0). Il s'agit simplement de faire correspondre une chaîne Null, qui est différente d'une chaîne vide.

Une chaîne nulle peut être transmise lorsqu'une procédure stockée a été invoquée avec un argument de chaîne vide.

La méthode GetNotesFetch agit comme un wrapper objectscript pour GetNotesFetchPy où le vrai travail se déroule. La justification est que le framework appelant s'attend à exploiter les arguments par référence (ByRef), et le wrapper comble cela.

Le code est un exemple de navigation et de récupération de données NoSQL via du code Python.

L'implémentation Python utilise un bloc try-except pour piéger les problèmes d'exécution du code Python et transmet ces informations d'erreurs de la manière normale à l'application client. Ceci peut être activé en décommentant la ligne commençant par "#x=10/0" pour provoquer une erreur de division par zéro.

Exemple  d'une erreur ZeroDivisionError est renvoyée au client :

[SQLCODE: <-400>:<Fatal error occurred>]
[%msg: <Python general error 'alwo.PyProcTest::GetNotesFetchPy:Traceback (most recent call last):
                   File "PyProcTest", line 21, in GetNotesFetchPy
                                                                 ZeroDivisionError: division by zero
                    '>]

 

/// Ref: Defining and Using Class Queries
Class alwo.PyProcTest [ Abstract ]
{
 
/// <example>
/// do $SYSTEM.SQL.Shell()
/// call alwo.PyProcTest_GetNotes('D')
/// </example>
Query GetNotes(tabName As %String) As %Query(ROWSPEC = "Tab:%String,NoteId:%Integer,NoteText:%String") [ SqlName = PyProcTest_GetNotes, SqlProc ]
{
}
 
/// ObjectScript due to ByRef signature
ClassMethod GetNotesExecute(ByRef qHandle As %Binary, tabName As %String = "") As %Status
{
  set qHandle=##class(alwo.PyNote.GetNotes.qHandle).%New()
  // Note that an empty string passed from SQL statement may appear as the null character $C(0) instead of empty string ""
  set:tabName'=$C(0) qHandle.selectedTab=tabName // may be empty string
  Quit $$$OK
}
 
/// ObjectScript due to ByRef signature
ClassMethod GetNotesFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = GetNotesExecute ]
{
  set refRow=##class(alwo.PyNote.GetNotes.Row).%New()
 
  set status=..GetNotesFetchPy(.qHandle,.refRow)
  if qHandle.atEnd {
    set AtEnd=1
  } else {
    // repack output row to $List format
    set Row=$ListBuild(refRow.Tab,+refRow.NoteId,refRow.NoteText)
  }
  Quit status
}
 
/// Access to tabular view of global 2 keys deep with data at level 2 nodes
/// <example>
/// zwrite ^alwo.IndexBook
///
/// ^alwo.IndexBook("A",1)="abc"
/// ^alwo.IndexBook("A",2)="def"
/// ^alwo.IndexBook("B")=""
/// ^alwo.IndexBook("C")=""
/// ^alwo.IndexBook("D",1)="gef"
/// ^alwo.IndexBook("E",1)="ijk"
/// ^alwo.IndexBook("E",2)="lmn"
/// <example>
///
/// Required output
/// <example>
/// | Tab | NoteId | NoteText
/// --------------------------
/// | A   | 1      | abc
/// | A   | 2      | def
/// | B   | 0      |
/// | C   | 0      |
/// | D   | 1      | gef
/// | E   | 1      | ijk
/// | E   | 2      | lmn
/// --------------------------
/// </example>
ClassMethod GetNotesFetchPy(qHandle As alwo.PyNote.GetNotes.qHandle, pRow As alwo.PyNote.GetNotes.Row) As %String [ Language = python ]
{
import iris
import traceback
ret=iris.cls('%SYSTEM.Status').OK()
try:
  # based on the existance of defined nodes then iterate
  gname="^alwo.IndexBook"
  gIterator=iris.gref(gname)
  # Iterate on Key1 "Tab name" when Key2 "NoteId" was previously set to empty
  if (None==qHandle.currentPage) or (""==qHandle.currentPage):
    qHandle.currentTab=gIterator.order([qHandle.currentTab])
    # change of tab context
    if (None==qHandle.currentTab) or (qHandle.currentTab==""):  # no records
      qHandle.atEnd=True
      return ret
    # default open first tab if has values
    if qHandle.selectedTab==None or qHandle.selectedTab=="":
      qHandle.selectedTab=qHandle.currentTab
  pRow.Tab=qHandle.currentTab
  #x=10/0 # uncomment to demonstrate ZeroDivisionError handling
  # Iterate on Key2 "NoteId"
  if (qHandle.selectedTab==qHandle.currentTab):
    qHandle.currentPage=gIterator.order([qHandle.currentTab, qHandle.currentPage])
    if (qHandle.currentPage!=None) and (qHandle.currentPage!=""):
      pRow.NoteId=qHandle.currentPage
      pRow.NoteText=gIterator.get([qHandle.currentTab, qHandle.currentPage])
      # checks if current record was the last one
      next=gIterator.order([qHandle.currentTab, qHandle.currentPage])
      if (None==next) or (""==next):
        qHandle.currentPage=None  # causes iterate on Key1 on next method invocation
except Exception:
 pErrorMessage='alwo.PyProcTest::GetNotesFetchPy:'+(traceback.format_exc())
 return iris.cls('%SYSTEM.Status').Error(2603,pErrorMessage)
return ret
}
 
/// ObjectScript due to ByRef signature
ClassMethod GetNotesClose(ByRef qHandle As %Binary) As %Status
{
  set qHandle=""
  Quit $$$OK
}
 
}

Classe d'assistance qHandle. Celle-ci est initialisée lors de l'exécution et mise à jour lors de la récupération des enregistrements : 

Class alwo.PyNote.GetNotes.qHandle Extends %RegisteredObject
{
 
Property currentTab As %String;
 
Property currentPage As %String;
 
Property selectedTab As %String;
 
Property atEnd As %Integer [ InitialExpression = 0 ];
 
}

Classe d'assistance pour remplir les lignes depuis Python avec des noms lisibles :

Class alwo.PyNote.GetNotes.Row Extends %RegisteredObject
{
 
Property Tab As %String;
 
Property NoteId As %String;
 
Property NoteText As %String;
 
}

 

J'espère que cet exemple sera utile pour explorer de nouvelles idées et possibilités avec Embedded Python.

Discussion (0)1
Connectez-vous ou inscrivez-vous pour continuer