新しい投稿

Encontrar

記事
· 2024年10月14日 9m read

What's the Catch?

Many programming languages use the try-and-catch construct to handle runtime errors gracefully. If the code within the try block encounters an error, it will throw an exception to the catch block, where the error handling occurs. Today we will dive into the ObjectScript implementation of this construct and discuss some ways to clean things up.

ObjectScript Implementation Basics

To get started, we will first look at the basic structure of the try/catch block:

try{
    //some code here
}
catch ex{
    //some error handling here
}

In this case, ex is a variable name for the exception object that has been thrown. You may utilize any name you wish for this variable.

If you are familiar with catch handling from other programming languages, remember that some features you may have used before are unavailable in ObjectScript. 

FirstFrst of all, there is no “finally” block there. In other languages, the "finally" block defines a piece of code that will run whether or not there is an exception in the "try" portion of the block. Still, you can easily work around it simply by following the "catch" block with more code. As long as there is no direct return from the try/catch, that code will be executed.

Secondly, you can not define multiple catch blocks for one try block to detect different kinds of exceptions. A common usage of this in Java, for example, is to have diverse catches for a few specific types of exceptions, and one more for all other exceptions later on. Instead, you can operate the exception object’s %IsA method to handle various other methods differently. That method takes a string as an argument, and if the exception is the one you are looking for, it returns a 1. Therefore we could handle certain exceptions diversely as follows:

catch ex{
    If ex.%IsA(“%Exception.StatusException”){
        //status exception handling here
    }
    elseif ex.%IsA(“%Exception.SQL”){
        //SQL exception handling here 
    }
    Else{
        //all other exception handling here
    }
}

Try/catch blocks can be nested inside one another. If an exception is thrown from the inner catch block, it will be handled by the outer catch block. If an exception is thrown from the last catch block, it will be managed by the next appropriate error handler if one exists. Issuing an argumentless quit command will exit the current try/catch block and send you back to the previous stack levelleve.

The Exception Object

Speaking of the exception object, let's take a closer look at what it is. There are several kinds of exceptions available on the market right now, but you can also extend those classes to create your own. All of them extend with %Exception.AbstractException besides the %Exception.CPPException which is meant for InterSystems internal use only. If you decide to make your own exception class, it will probably extend with %Exception.AbstractException as well. This class contains several useful properties and functions common for all exceptions.

The Name property typically has such exception names as <DIVIDE>, <NOLINE>, <UNDEFINED>, etc. The code contains the error code associated with the error. Both Name and Code can be helpful in catch blocks when trying to handle specific kinds of errors differently. For instance, if the Name is “<DIVIDE>”, you have divided by zero somewhere in the try block. If you know where that happened because you anticipated it, and you want to set the variable to zero, your catch block might look like the following:

catch ex{
    If ex.Name = “<DIVIDE>”{
        set myvar = 0
    }
}

The Location property might contain the spot where the error occurred. I have emphasized the word might since exceptions thrown manually via the throw command may not include this information. Exceptions thrown due to errors during code execution will have Location set to a routine, and they will offset letting you know where the error occurred. This information is practical for debugging. 

The InnerException property is an exception itself. It is usually set when you are already in a catch block and throw another exception. In that case, the InnerException of the second exception should be set to the original exception that got you to the first catch block.

The DisplayString method returns a string describing the error that caused the exception to be thrown in the first place. If you need to notify the user via your UI that an error has occurred, this is a worthwhile string to show him or her. The AsStatus method creates a %Library.Status object based on the exception which can come in handy if you are using a method that needs to return a status. The Log method adds the exception to the application error log found in the System Management Portal under System Operation, System Logs, and Application Error Log. Some methods also return the SQL code and SQL message, which is mostly advantageous if the exception is an SQL one.

Throw

If you want to catch something, it needs to be thrown first. Many exceptions are caught and thrown automatically during code execution. However, if you want to throw one manually, you can use the THROW command. The object you throw must be of a class inherited from %Exception.AbstractException though. It is beneficial for familiarizing yourself with the constructors of various exception classes and will help you later generate them when needed. Take a look at some of the most useful ones below:

THROW ##class(%Exception.GeneralException).%New(name,errnumber,location,data)
// Where name is the error name, e.g., <DIVIDE> or <UNDEFINED>; errnumber is the error code; location is the location in the code where the error occurred, and data is any additional data you want to send along.
THROW ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE)
// Where SQLCODE has been set by a query.
THROW ##class(%Exception.StatusException).CreateFromStatus(sc)
// Where sc is a %Library.Status object.

Like many other ObjectScript commands, it can take a postconditional expression and only throw the exception if that expression is true. For example, if you have a %Status object called sc, the following lines would throw a status exception based on that status only if the status is an error, or it would throw an SQL exception only if SQLCODE indicates an error:

THROW:($$$ISERR(sc)) ##class(%Exception.StatusException).CreateFromStatus(sc)

THROW:(SQLCODE<0) ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE)

THROW will send the exception to the successive error handler to be processed. When it is inside a try block, it will go to the catch block. If it is outside a try block, it may be a $ZTRAP. In that case, the exception object will be stored in the special variable $THROWOBJ for your use.

Issuing a THROW without an argument will re-throw the current system error stored in $ZERROR, passing it to the following error handler. However, it is generally not recommended because it can cause unexpected behavior if the current error handler changes $ZERROR directly or by causing another unintentional error.

Catch and Clean

You have caught your first exception. Now what?

In many cases, you will select to undo whatever you were testing in your try block when there is an error. It is a perfect case for using transactions. They start with a TSTART command. Once it has been issued, changes to your data will not be committed until you issue a TCOMMIT command. If you decide not to commit your changes, use the TROLLBACK command to undo them. You do not wish to leave a transaction hanging open, so if you use TSTART in your try block, you will need to ensure you employ TROLLBACK in your catch. If the TSTART is not the first thing in your try block, you can always check $TLEVEL to see if you have any active transactions. If you are concerned that there may be other transactions started outside of your try block, you will want to provide the number of transactions to roll back as well. The following might be a good basic form for your try/catch:

try{
    TSTART
    //do things here
    TCOMMIT
}
catch ex{
    If $TLEVEL > 0{
        TROLLBACK
    }
}

If your process uses any locks, you may also need to manage them. Locks are usually released when a process ends. However, if you create any locks that interfere with the current process, it will be better to unlock them. The easiest way to release all locks is to operate the LOCK command without arguments. Yet, it is typically recommended to unlock specific locks individually as needed.

Depending on your requirements, you might wish to inform the user that an error has occurred in a human-readable manner. As previously mentioned, the exception’s DisplayString() method gives a somewhat useful message to demonstrate to the user. If shown to a human, that string can be displayed on their screen through something similar to a pop-up. If it is an API, you may decide to include the exception’s Code property as well. It will allow the software calling the API to handle errors more easily since it will be consistent. However, the display string may change due to localization.

If your try/catch block is a method that must return a value, you need to ensure you return an appropriate value in order not to cause any errors further up the stack. If your method returns a status, your perfect choice would be the exception object’s AsStatus method. You can simply use “return ex.AsStatus()” at the end of your catch block to return an appropriate status. Otherwise, you will want to return an appropriate value of the type that the method is defined to return. Be aware that you can not use the QUIT command with an argument in a try/catch block. You must utilize return instead.

As a programmer, you might also wish to log the error for further inspection in case of a programming error that needs to be addressed. The previously mentioned Log method is a great place to start. It includes all information that is a part of the exception comment and allows you to add a comment to the error. You can see the code line (unless the error was thrown using the THROW command), the namespace, the error name, the device, the stack, the routine name, the security roles the process had, and many other things from that line alone.

If you want to dig a bit deeper, you may find a few useful things in the %SYS.ProcessQuery class. It includes such practical details as the client executable name and the user’s operating system user name. To get the information about the current process, you may use the following:

set myproc = ##class(%SYS.ProcessQuery).%OpenId($J)

Some properties, e.g., MemoryAllocated, MemoryPeak, and MemoryUsed might help you diagnose an issue caused by hardware limitations. Several other properties, however, turn out to be less helpful than they seem at first sight. For instance, items like CurrentLineAndRoutine would give you the line where you are referring to this property in your catch block, but not the one where the error occurred. 

On the other hand, the GetVariableList method can come in very handy. This method can obtain a list of variables defined in the process right now. Once you know which variable has been defined, you can log this information, and that can be very helpful indeed! This method requires you to pass a variable by reference that contains a list of lists. For its part, it returns an integer telling you how many sublists that list has. Each sublist includes two items: a variable name and the value of $D(variable) which tells you whether or not that variable holds any data. Using the MyProc mentioned above as a starting point, consider the following code:

Set x = myproc.GetVariableList(,.mylist)
For i=1:1:x{
    Set mysublist = $LG(mylist,i)
    If $LG(mysublist,2){
        Set myvar = $LG(mysublist,i)
        W myvar,!,@myvar
    }
}

The above-mentioned code will write out the variable names and values for every defined variable in the current process. Of course, instead of writing them down, you may choose to store them somewhere in a table or global for your further examination. It is a treasure trove of useful information. It can show you what variables may have been set to 0, which you did not account for, or if a variable that had to be defined never got defined. You can also see if something was supposed to be a number but ended up as a string. From my experience, you might find out that you misspelled a variable name somewhere, leading you to unanticipated results because the variable “tax” was initialized to 0, but the variable “txa” was the one where the math actually happened.

I hope this information will help you improve your clean-up process, inform users about an issue, and diagnose the root cause of your caught exceptions.

1 Comment
ディスカッション (1)2
続けるにはログインするか新規登録を行ってください
記事
· 2024年10月14日 4m read

Desenvolvendo Integrações com o InterSystems IRIS - Aplicação REST

Projeto 3 – Requisição REST

Vamos montar nossa próxima integração utilizando uma aplicação REST. Para tal vamos utilizar um BS que chamará o BP do nosso serviço demo (ver Primeira Integração). Vamos reaproveitar o serviço que então poderá ser chamado via SOAP ou REST. Teremos então dois BS que irão chamar o mesmo BP. A imagem abaixo ilustra essa arquitetura:

O primeiro passo vai ser criar um novo BS que não terá nenhum adaptador. Esse BS será chamado pela nossa aplicação REST que veremos mais a frente:

Class ws.rest.bs.Service Extends Ens.BusinessService
{

Parameter SERVICENAME = "entrada";

Method entrada(pInput As ws.demo.msg.Request) As ws.demo.msg.Response [ WebMethod ]
{
              Set tSC=..SendRequestSync("bpEntrada",pInput,.tResponse)
              Quit tResponse
}

}

 

Note que utilizamos o mesmo Request e Response do serviço que vimos na primeira integração, e chamamos o mesmo BP.


Agora vamos montar nossa aplicação REST e depois publica-la no IRIS:

Class ws.rest.api.Entrada Extends %CSP.REST
{

XData UrlMap
{
<Routes>
        <Route Url="/entrada" Method="GET" Call="Entrada"/>
</Routes>
}

ClassMethod Entrada() As %Status
{
              Set obj=##Class(ws.demo.msg.Request).%New()
    Set obj.string1=%request.Get("string1")
    Set obj.string2=%request.Get("string2")
    Set tSC=##Class(Ens.Director).CreateBusinessService("ws.rest.bs.Service",.tService)
    If tSC
    {
                  Set resp=tService.entrada(obj)
    } Else {
                  Set resp=##Class(ws.demo.msg.Response).%New()
                  Set resp.status=0
                  Set resp.mensagem=$SYSTEM.Status.GetErrorText(tSC)
    }
    Set saida = {}
    Set saida.saida=resp.saida
    Set saida.status=resp.status
    Set saida.mensagem=resp.mensagem
    Set saida.sessionId=resp.sessionId
    Write saida.%ToJSON()
    Quit $$$OK
}

}

 

Veja que usamos a chamada ENS.Director para acessar o BS ws.rest.bs.Service que colocamos na production.

Para maiores informações sobre a criação de aplicações REST veja o documento em https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GREST_csprest e quanto a Ens.Director veja em https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?...

Agora que temos nossa aplicação REST desenvolvida vamos publica-la. Para isso vá no Portal de Administração no caminho Administração do Sistema->Segurança-Aplicações-Aplicações Web. Vamos criar uma nova aplicação Web de nome /api/entrada conforme a tela a seguir:

Salve a aplicação web que criamos. Note que você precisa apontar para o namespace onde você está montando seu código. No nosso exemplo é o namespace INTEGRA. Atente também para a marcação na caixa Habilitar REST e o preenchimento da caixa Expedir Classe com o nome da nossa classe REST.

Após salvar a aplicação web criada, vamos voltar a nossa production e incluir nela o nosso BS. Para isso clique no botão de incluir novo BS (sinal de mais ao lado do termo Services) e preencha a tela:

 

Pronto. Temos nosso BS configurado na nossa production, apontando para o BP da nossa primeira integração conforme vimos na tela no inicio deste texto que reproduzimos novamente abaixo:

Agora podemos utilizar uma ferramenta de teste para consumir o serviço. No nosso exemplo vamos usar o POSTMAN que foi baixado da internet e pode ser encontrado em https://www.postman.com/downloads/

Crie uma nova requisição REST e preencha conforme a tela a seguir:

O endereço do serviço é http://127.0.0.1/api/service/entrada onde /api/service é o nome da aplicação web que criamos anteriormente, e /entrada é a rota que queremos chamar. Passamos os parâmetros string1 e string2, enviamos a requisição e temos nossa resposta. Veja que recebemos o sessionID 2204, e com ele podemos consultar o que aconteceu:

Assim como na primeira integração podemos colocar o TCPTRACE para monitorar o tráfego da integração e verificar o que foi enviado e recebido em detalhes. Para isso ative o TCPTRACE e faça a configuração do mesmo para escutar na porta 8080 e repassar os dados recebidos para a porta 80 do nosso servidor:

Mude a configuração no POSTMAN para agora chamar o serviço na porta 8080, que é onde o TCPTRACE está esperando as chamadas. Reenvie a requisição e veja o resultado:

Agora, volte ao TCPTRACE e veja o que trafegou:

E temos o trace do integrador paea o sessionID 2208:

Neste artigo usamos o REST para receber uma requisição e reaproveitamos um serviço já desenvolvido para responder nossa requisição.

Assim fechamos nossa terceira  integração. Utilizamos em nosso teste o IRIS 2024.1 que disponível para download na sua versão Community na internet.

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください
記事
· 2024年10月14日 2m read

Funções do lado esquerdo em ObjectScript

Em ObjectScript, você tem uma ampla coleção de funções que retornam algum valor tipicamente:

set variable = $somefunction(param1,param2, ...)

Não há nada de especial nisso.
Mas há um conjunto de funções que classifico como Funções de Lado Esquerdo
A especialidade delas é que você também pode usá-las à esquerda do operador igual como um alvo no comando SET:

set $somefunction(param1,param2, ...) = value

O motivo para levantar esse assunto é que com o IRIS 2024.1 há depois de muitos anos um "novo garoto nessa vizinhança"

$VECTOR()

Atribui, retorna e exclui dados vetoriais em posições especificadas, especialmente
set $VECTOR(MyVector , position , type) = value

Eu não quero entrar em detalhes. A documentação é realmente completa.
Há também outras 3 novas funções lado direito relacionadas a vetores: $VECTORDEFINED (),  $VECTOROP(), $ISVECTOR()  

Se você seguir os exemplos na documentação, verá que a nova função relacionada SQL TO_VECTOR ()
faz praticamente o mesmo em notação SQL (na verdade, não é visível na InterSystems SQL Reference)

Como lembrete/visão geral, uma lista das funções tradicionais do lado esquerdo:

  • $BIT – Retorna ou define o valor bit de uma posição específica numa bitstring.
  • $EXTRACT – Extrai uma substring de uma string de caractere por posição, ou substitui uma substring por posição.
  • $LIST – Retorna ou substitui elementos em uma lista.
  • $PIECE – Retorna ou substitui uma substring, usando um delimitador.
  • $WEXTRACT – Extrai uma substring de uma string de caractere por posição, ou substitui uma substring por posição, reconhecendo pares de substituição.

É uma lista curta, mas pode ser altamente eficiente em alguns casos para evitar conteúdo duplicado e manipulações confusas.

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください
記事
· 2024年10月14日 9m read

FHIRValidation - Valide o seu próprio FHIR IG usando IRIS

Este programa de demonstração é usado para mostrar como um perfil FHIR personalizado pode ser empregado para validar a conformidade dos dados. O guia de implementação FHIR personalizado foi desenvolvido com base na Versão R4 do FHIR, e, neste exemplo, implementa a extensão do recurso Organização para validar a conformidade dos dados.

ディスカッション (0)1
続けるにはログインするか新規登録を行ってください
質問
· 2024年10月14日

Using Cache, Python and pypyodbc, struggling dates

New to Python.  Attempting to use pypyodbc to select data from a table in one Cache database, and inserting into a similarly configured table in another.  Process works fine except for tables containing Date types.  NULL values in date columns are handled without issue, but when data is present, insert fails with:

An error occurred: argument 7: TypeError: 'NoneType' object cannot be interpreted as an integer.

Source table:

CREATE TABLE "SAT"."AuditAttribute" (
    "ID"                 INTEGER NOT NULL PRIMARY KEY DEFAULT $i(^SAT.AuditAttributeD),
    "AddDelete"          VARCHAR(50),
    "ConstituentId"      VARCHAR(50),
    "CreatedDate"        DATE,
    "CreatedTime"        TIME,
    "DeleteDate"         DATE,
    "DeleteTime"         TIME,
    "FinderId"           VARCHAR(50),
    "Tag"                VARCHAR(50),
    "UserName"           VARCHAR(50)
);

Target table:

CREATE TABLE "SAT_D3"."AuditAttribute_DWN" (
    "DBase"              VARCHAR(6),
    "ID_OLD"             INTEGER,
    "AddDelete"          VARCHAR(50),
    "ConstituentId"      VARCHAR(50),
    "CreatedDate"        DATE,
    "CreatedTime"        TIME,
    "DeleteDate"         DATE,
    "DeleteTime"         TIME,
    "FinderId"           VARCHAR(50),
    "Tag"                VARCHAR(50),
    "UserName"           VARCHAR(50)
)
;

select query:

 

select_query = 'select \'DWN\' as DBase, ID as "ID_OLD", AddDelete, ConstituentId, Tag, UserName, "CreatedDate" from SAT.AuditAttribute'

insert_query = 'insert into SAT_D3.AuditAttribute_DWN (DBase, "ID_OLD",  AddDelete, ConstituentId, Tag, UserName, "CreatedDate") values (?,?,?,?,?,?,?)'

Displaying row[6[ for first failing row shows this: "datetime.date(2018, 6, 28)"

Have tried various methods, datetime.strftime(),  datetime.strptime(), but haven't hit on the magic strategy.  I assume it's something simple.

Thanks for your help!

2 Comments
ディスカッション (2)2
続けるにはログインするか新規登録を行ってください