Escrito por

Sales Engineer at InterSystems
Artigo Danusa Calixto · Nov. 28, 2023 8m read

Procedimento armazenado (stored procedure) em Python embutido

Visão geral

A documentação online contém o tópico Defining and Using Class Queries (Definir e usar consultas de classe) para referência-

A personalização direta de procedimentos armazenados com ObjectScript tem sido útil para acessar o armazenamento NoSQL e as mensagens externas pela integração, para apresentar a saída em um formato tabular.

Por exemplo: um aplicativo que já usa 90% da interação SQL de um front-end também pode estender esse acesso aos outros 10% da funcionalidade de plataforma necessária, pelo mesmo acesso SQL.

A finalidade deste artigo é explorar como alcançar o mesmo efeito usando os métodos do Embedded Python.

Figura 1: procedimento armazenado como um SQL gateway para outra funcionalidade de plataforma

Demonstração

Para esse exemplo, foi definido o seguinte armazenamento NoSQL:

^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"

Para teste, o procedimento armazenado pode ser executado em um 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
---------------------------------------------------------------------------

Como foi fornecida a aba ( primeira chave ) "A", os dados dos sub-nós são expandidos e retornados como registros.

Os outros nós que não foram selecionados ou não contêm dados são retornados como registros "0".

Conceitos de código

Quando uma consulta ( GetNotes ) é implementada em uma classe, o conteúdo da consulta pode ser obtido apenas com SQL.

Após a compilação, são gerados três class methods:

  • GetNotesExecute
  • GetNotesFetch
  • GetNotesClose

Há vários cenários em que os dados não são SQL:

  • Globais NoSQL
  • Interação parametrizada com um sistema externo para recuperar e consolidar dados

Ao implementar esses três métodos diretamente, é possível controlar o acesso e dar respostas tabulares para uma ampla variedade de recursos de plataforma.

Estas são algumas das variações de interação:

Armazenar em cache todos os dados de resposta antecipadamente

1. O método GetNotesExecute acessaria os recursos para criar uma reposta em um global temporário.

Isso seria útil para uma visão consistente sobre os dados, o que pode envolver o bloqueio do acesso a atualizações por um breve período.

2. O método GetNotesFetch seria chamado repetidamente, retornando registros dos dados temporários

3. The GetNotesClose limparia e excluiria os dados temporários

Dados de respostas dinâmicas

1. O método GetNotesExecute é chamado. Isso não faz muito coisa além de iniciar um contexto qHandle disponível para o método Fetch

2. O método GetNotesFetch é chamado. Sempre que um novo registro é recuperado dinamicamente

3. O método GetNotesClose exige pouca ou nenhuma limpeza

Essa é a abordagem usada no código de exemplo.

Oportunidades de paginação etc.

Dependendo do cenário, o preenchimento dinâmico de lotes de registros de retorno pode ser usado para reduzir a necessidade de executar uma "consulta completa" quando só é necessária uma área da fatia de registros de retorno.

O código

O método execute tem uma expressão $C(0). Isso serve apenas para corresponder a uma string Null, que é diferente de uma string vazia.

Uma string nula pode ser passada quando um procedimento armazenado é invocado com um argumento de string vazio.

O método GetNotesFetch atua como um wrapper objectscript para GetNotesFetchPy, onde ocorre o trabalho de verdade. A lógica é a expectativa de o framework de chamada usar os argumentos ByRef e o wrapper unir isso.

O código é um exemplo de como navegar e recuperar dados NoSQL por código Python.

A implementação em Python usa um bloco try-except para interromper problemas de ambiente de execução do código Python e propagar esses detalhes das informações de erros da maneira normal de volta ao aplicativo do cliente. Isso pode ser ativado ao descomentar a linha que começa com "#x=10/0".

Por exemplo, o erro interrompido retornado ao cliente:

[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
///https://docs.intersystems.com/iris20232/csp/docbook/DocBook.UI.Page.cls?KEY=GOBJ_queries
Classalwo.PyProcTest [ Abstract ]
{
 
///<example>
///do $SYSTEM.SQL.Shell()
///call alwo.PyProcTest_GetNotes('D')
///</example>
QueryGetNotes(tabNameAs%String) As%Query(ROWSPEC = "Tab:%String,NoteId:%Integer,NoteText:%String") [ SqlName = PyProcTest_GetNotes, SqlProc ]
{
}
 
///ObjectScript due to ByRef signature
ClassMethodGetNotesExecute(ByRefqHandleAs%Binary, tabNameAs%String = "") As%Status
{
  setqHandle=##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
ClassMethodGetNotesFetch(ByRefqHandleAs%Binary, ByRefRowAs%List, ByRefAtEndAs%Integer = 0) As%Status [ PlaceAfter = GetNotesExecute ]
{
  setrefRow=##class(alwo.PyNote.GetNotes.Row).%New()
 
  setstatus=..GetNotesFetchPy(.qHandle,.refRow)
  ifqHandle.atEnd {
    setAtEnd=1
  } else {
    // repack output row to $List format
    setRow=$ListBuild(refRow.Tab,+refRow.NoteId,refRow.NoteText)
  }
  Quitstatus
}
 
///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>
ClassMethodGetNotesFetchPy(qHandleAsalwo.PyNote.GetNotes.qHandle, pRowAsalwo.PyNote.GetNotes.Row) As%String [ Language = python ]
{
importiris
importtraceback
ret=iris.cls('%SYSTEM.Status').OK()
try:
  # basedontheexistanceofdefinednodestheniterate
  gname="^alwo.IndexBook"
  gIterator=iris.gref(gname)
  # IterateonKey1"Tab name"whenKey2"NoteId"waspreviouslysettoempty
  if (None==qHandle.currentPage) or (""==qHandle.currentPage):
    qHandle.currentTab=gIterator.order([qHandle.currentTab])
    # changeoftabcontext
    if (None==qHandle.currentTab) or (qHandle.currentTab==""):  # norecords
      qHandle.atEnd=True
      returnret
    # defaultopenfirsttabifhasvalues
    ifqHandle.selectedTab==NoneorqHandle.selectedTab=="":
      qHandle.selectedTab=qHandle.currentTab
  pRow.Tab=qHandle.currentTab
  #x=10/0 # uncommenttodemonstrateZeroDivisionErrorhandling
  # IterateonKey2"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])
      # checksifcurrentrecordwasthelastone
      next=gIterator.order([qHandle.currentTab,qHandle.currentPage])
      if (None==next) or (""==next):
        qHandle.currentPage=None  # causesiterateonKey1onnextmethodinvocation
exceptException:
 pErrorMessage='alwo.PyProcTest::GetNotesFetchPy:'+(traceback.format_exc())
 returniris.cls('%SYSTEM.Status').Error(2603,pErrorMessage)
returnret
}
 
///ObjectScript due to ByRef signature
ClassMethodGetNotesClose(ByRefqHandleAs%Binary) As%Status
{
  setqHandle=""
  Quit$$$OK
}
 
}

Classe helper qHandle. É inicializada em Execute e atualizada durante a recuperação de registro.

Classalwo.PyNote.GetNotes.qHandleExtends%RegisteredObject
{
 
PropertycurrentTabAs%String;
 
PropertycurrentPageAs%String;
 
PropertyselectedTabAs%String;
 
PropertyatEndAs%Integer [ InitialExpression = 0 ];
 
}

Classe helper para preencher linhas do Python com nomes legíveis:

Classalwo.PyNote.GetNotes.RowExtends%RegisteredObject
{
 
PropertyTabAs%String;
 
PropertyNoteIdAs%String;
 
PropertyNoteTextAs%String;
 
}

Espero que esse exemplo seja útil para explorar algumas novas ideias e possibilidades com o Embedded.