Limpiar filtro
Artículo
Estevan Martinez · 27 nov, 2019
++ Update: August 1, 2018
El uso de la dirección IP virtual (VIP) de InterSystems incorporada en Mirroring de la base de datos de Caché tiene ciertas limitaciones. En particular, solo puede utilizarse cuando los miembros Mirror se encuentran en la misma subred. Cuando se utilizan varios centros de datos, las subredes normalmente no se “extienden” más allá del centro de datos físico debido a la complejidad añadida de la red (puede obtener más información aquí). Por las mismas razones, la IP virtual con frecuencia no puede utilizarse cuando la base de datos se aloja en la nube.
Los dispositivos para la administración del tráfico de red, como los balanceadores de carga (físicos o virtuales), pueden utilizarse para lograr el mismo nivel de transparencia, presentando una dirección única para las aplicaciones o dispositivos del cliente. El administrador para el tráfico de red redirige automáticamente a los clientes hacia la dirección IP real de la Mirror principal actual. La automatización tiene por objeto satisfacer las necesidades tanto de la tolerancia contra fallos de HA como para la promoción de la DR después de un desastre.
Integración de un Administrador de Tráfico de la Red
Hoy en día existen numerosas opciones en el mercado que son compatibles con la redirección del tráfico de red. Cada una de ellas admite metodologías similares e incluso varias para controlar el flujo de la red basada en los requisitos de la aplicación. Para simplificar estas metodologías, consideramos tres categorías: API llamada por el servidor de base de datos, el Sondeo de la Red de Aplicaciones, o una combinación de ambos.
En la siguiente sección se describirá cada una de estas metodologías y se proporcionará orientación sobre la forma en que cada una de ellas puede integrarse con los productos de InterSystems. En todos los escenarios, el árbitro se utiliza para proporcionar decisiones seguras de la tolerancia contra fallos cuando los miembros Mirror no pueden comunicarse directamente. Puede encontrar más información sobre el árbitro aquí .
Para cumplir con los objetivos de este artículo, en los diagramas de ejemplo se mostrarán 3 miembros Mirror: principal, copia de seguridad y DR asíncronos. Sin embargo, reconocemos que su configuración puede ser más grande o menor a esto.
Opción 1: Sondeo de la Red de Aplicaciones (Recomendado)
En este método, el dispositivo de red con carga equilibrada utiliza el mecanismo de sondeo incorporado para comunicarse con ambos miembros Mirror con el fin de determinar al miembro Mirror principal.
El método de sondeo que utiliza la página mirror_status.cxw de CSP Gateway está disponible en la versión 2017.1, puede utilizarse como método de sondeo en el supervisor de estado del ELB para cada miembro Mirror que se añadió al grupo de servidores del ELB. Únicamente la miembro Mirror principal responderá ‘SUCCESS’, dirigiendo así el tráfico de red solo al miembro Mirror principal que esté activo.
En este método no es necesario agregar cualquier lógica a ^ZMIRROR. Tenga en cuenta que la mayoría de los dispositivos de red con carga equilibrada tienen un límite en la frecuencia de ejecución de la comprobación de estado. Normalmente, la frecuencia más alta no es menor de 5 segundos, lo cual es aceptable para admitir la mayoría de los acuerdos en el nivel de servicio para el tiempo de actividad.
Una solicitud HTTP para el siguiente recurso probará cuál es el estado del miembro espejo para la configuración LOCAL de Caché.
/csp/bin/mirror_status.cxw
Para todos los demás casos, la ruta hacia estas solicitudes de estado de réplica deben resolverse en el servidor de Caché y en el Namespace apropiados, mediante el mismo mecanismo jerárquico, como el que se utiliza para solicitar páginas reales en el CSP.
Por ejemplo, para probar el estado de configuración de la Mirror que presenta a las aplicaciones en la ruta /csp/user/, se utiliza:
/csp/user/mirror_status.cxw
Note: Una licencia CSP no se consume cuando se llama a la comprobación de estado de la Mirror.
Dependiendo de si la instancia de destino es o no un miembro principal activo, la puerta de enlace devolverá alguna de las siguientes respuestas del CSP:
** Éxito (Es el Miembro Principal)
===============================
HTTP/1.1 200 OK
Content-Type: text/plain
Connection: close
Content-Length: 7
SUCCESS
** Se produjo un error (no es el Miembro Principal)
===============================
HTTP/1.1 503 Service Unavailable
Content-Type: text/plain
Connection: close
Content-Length: 6
FAILED
** Se produjo un error (El servidor de Caché no es compatible con la solicitud Mirror_Status.cxw)
===============================
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Connection: close
Content-Length: 6
FAILED
Considere los siguientes diagramas como un ejemplo de sondeo.
La tolerancia contra fallos se produce automáticamente entre los miembros Mirror síncronos de una tolerancia contra fallos:
En el siguiente diagrama se muestra la promoción de los miembros Mirror asíncronos en la DR dentro del grupo con cargas equilibradas, esto normalmente asume que el mismo dispositivo de red con carga equilibrada brinda servicio a todos los miembros Mirror (los escenarios que están divididos geográficamente se analizan más adelante en este artículo). Según el procedimiento estándar de la DR, la promoción del miembro de recuperación en caso de desastres implica una decisión humana y luego una simple acción administrativa a nivel de la base de datos. Sin embargo, una vez tomada esta acción, no es necesaria ninguna opción administrativa en el dispositivo de red: descubra automáticamente el nuevo principal.
Opción 2: API llamada por el servidor de base de datos
En este método se utiliza el dispositivo para administrar el tráfico de red y tiene un grupo de servidores definido tanto con miembros Mirror de una tolerancia contra fallos como miembros Mirror de una DR asíncrona.
Cuando un miembro Mirror se convierte en el miembro Mirror principal se realiza una llamada de API al dispositivo de red para ajustar la prioridad o ponderación e indicar inmediatamente al dispositivo de red que dirija el tráfico al nuevo miembro Mirror principal.
El mismo modelo se aplica a la promoción de un miembro Mirror de una DR asíncrona en el caso de que los miembros Mirror principal y copia de seguridad dejen de estar disponibles.
Esta API se define en la rutina ^ZMIRROR, específicamente como parte de la llamada al procedimiento: $$CheckBecomePrimaryOK^ZMIRROR()
Dentro de esta llamada de procedimiento, inserte cualquier lógica de API y métodos disponibles para el dispositivo de red correspondiente, como API REST, interfaz de línea de comandos, etc. Al igual que con la IP virtual, este es un cambio abrupto en la configuración de la red y no implica ninguna lógica en las aplicaciones para informar a los clientes que ya existen y están conectados al miembro Mirror principal que el error está sucediendo en la tolerancia contra fallos. Dependiendo de la naturaleza del error, esas conexiones pueden terminar como resultado del error en sí mismo, debido al tiempo de espera o error de la aplicación, debido a que el principal nuevo obliga al principal antiguo a detenerse, o debido al vencimiento del temporizador de mantenimiento TCP utilizado por el cliente.
Como resultado, es posible que los usuarios tengan que volver a conectarse e iniciar sesión. El comportamiento de su aplicación determinaría este comportamiento.
Opción 3: Implementaciones Geográficamente Dispersas
En configuraciones con varios centros de datos y posiblemente implementaciones geográficamente dispersas, tales como implementaciones en nube con varias zonas de disponibilidad y zonas geográficas, surge la necesidad de tener en cuenta las prácticas de redireccionamiento geográfico en un modelo simple y fácilmente compatible que utiliza tanto el balanceo de cargas basado en DNS como el balanceo de cargas local.
Con este modelo de combinación, se introduce un dispositivo de red adicional que funciona con servicios DNS como Amazon Route 53, F5 Global Traffic Manager, Balanceador de carga global del servidor Citrix NetScaler o Cisco Global Site Selector en combinación con balanceadores de carga de red en cada centro de datos, zona de disponibilidad o georregión de nube.
En este modelo, el sondeo (recomendado ) o los métodos de API descritos anteriormente se utilizan de forma local para ubicar la operación de cualquiera de los miembros Mirror (tolerancia contra fallos o DR asíncrono). Se utiliza para informar al dispositivo de red geográfica/global si puede dirigir el tráfico a cualquiera de los centros de datos. También, en esta configuración, el dispositivo de administración para el tráfico de la red local presenta su propio VIP al dispositivo de red geográfica/global.
En un estado normal de equilibrio, el miembro Mirror principal activo informa al dispositivo de red local que es principal y proporciona un estado “Up”. Este estado “Up” se transmite al dispositivo geográfico/global para ajustar y mantener el registro DNS con el fin de reenviar todas las solicitudes a este miembro Mirror principal activo.
En un escenario de tolerancia contra fallos dentro del mismo centro de datos (el miembro Mirror síncrono de la copia de seguridad se convierte en principal), se utiliza una API o un método de sondeo con el balanceador de cargas local para ahora redirigirlo al nuevo miembro Mirror principal dentro del mismo centro de datos. No se realizan cambios en el dispositivo geográfico/global ya que el balanceador de carga local sigue respondiendo con el estado “Up” porque el nuevo miembro espejo principal está activo.
Con el fin de cumplir los objetivos de este ejemplo, el método API se utiliza en el siguiente diagrama para la integración local en el dispositivo de red.
En un escenario de tolerancia contra fallos a un centro de datos diferente (ya sea una miembro Mirror síncrona o un miembro Mirror de una DR asíncrona en un centro de datos alternativo) que utiliza la API o los métodos de sondeo, el miembro Mirror principal recién promovido comienza a informar como principal al dispositivo de red local.
Durante la tolerancia contra fallos, el centro de datos que antes contenía al principal ya no reporta “Up” desde el balanceador de carga local al geográfico/global. El dispositivo geográfico/global no dirigirá el tráfico a ese dispositivo local. El dispositivo local del centro de datos alternativo reportará “Up” al dispositivo geográfico/global e llamará la actualización del registro DNS ahora directamente a la IP virtual presentada por el balanceador de carga local del centro de datos alternativo.
Opción 4: Implementaciones geográficamente dispersas y en varios niveles
Para llevar la solución un paso más allá, la introducción de un nivel de servidor web separado se realiza como una WAN interna a privada o accesible mediante Internet. Esta opción puede ser un modelo de implementación normal para aplicaciones de grandes empresas.
En el siguiente ejemplo se visualiza una muestra de configuración que utiliza varios dispositivos de red para aislar y dar soporte de forma segura a la web y los niveles de las base de datos. En este modelo se utilizan dos ubicaciones geográficamente dispersas, una de las cuales se considera la ubicación “principal” y la otra es puramente de “recuperación en caso de desastres” para el nivel de la base de datos. La ubicación de la recuperación en caso de desastres del nivel de la base de datos debe utilizarse en caso de que la ubicación principal esté fuera de servicio por cualquier motivo. Además, el nivel web de este ejemplo se ilustrará como activo-activo, lo cual significa que los usuarios son dirigidos a cualquiera de las dos ubicaciones basándose en varias reglas como la menor latencia, las conexiones menores, los rangos de direcciones IP u otras reglas de enrutamiento que usted considere apropiadas.
Como se muestra en el ejemplo anterior, en el caso de una tolerancia contra fallos dentro de la misma ubicación, se produce una tolerancia contra fallos automática y el dispositivo de red local apunta ahora al nuevo principal. Los usuarios aún se conectan a los servidores web de cualquiera de las dos ubicaciones y los servidores web con su CSP Gateway asociada continúan apuntando a la Ubicación A.
En el siguiente ejemplo, considere una tolerancia contra fallos o una interrupción completa en la Ubicación A en la que tanto el principal o los miembros Mirror de la tolerancia contra fallos en la copia de seguridad estén fuera de servicio. Los miembros Mirror de la DR asíncrona entonces serían promovidos manualmente a principal y miembros Mirror de la tolerancia contra fallos de la copia de seguridad. A partir de esa promoción, el nuevo miembro Mirror principal designado permitirá que el dispositivo de balanceo de carga que se encuentra en la Ubicación B informe “Up” mediante el método API discutido anteriormente (el método de sondeo también es una opción). Como resultado del balanceador de carga local que ahora informa “Up”, el dispositivo basado en DNS reconocerá y redirigirá el tráfico de la ubicación A a la ubicación B para los servicios en el servidor de base de datos.
Conclusión
Existen muchas permutaciones posibles para diseñar Mirror de una tolerancia contra fallos sin una IP virtual. Estas opciones pueden aplicarse tanto a los escenarios de alta disponibilidad más simples como a las implementaciones en regiones multigeográficas con varios niveles, incluidos los miembros espejo de la DR asíncrona para obtener una solución altamente disponible y tolerante a los desastres que tenga el objetivo de mantener los más altos niveles de resiliencia operativa en sus aplicaciones.
Esperamos que este artículo haya proporcionado algo de conocimiento acerca de las diferentes combinaciones y casos de uso posibles para implementar con éxito Mirror de bases de datos con tolerancia contra fallos que sean adecuados para sus necesidades de aplicación y disponibilidad.
Artículo
Luis Angel Pérez Ramos · 27 jun, 2023
Como sabréis, si leeis habitualmente los artículos que se publican en la Comunidad, el pasado mes de mayo InterSystems organizó el Hackaton del JOnTheBeach2023 celebrado en Málaga. El tema que se propuso fue el del uso de las herramientas de análisis predictivo que InterSystems IRIS pone a disposición de todos los desarrolladores con IntegratedML. Debemos agradecer tanto a @Thomas.Dyar como a @Dmitry.Maslennikov todo el trabajo y el empeño que pusieron para que fuese un rotundo éxito.
Introduzcamos brevemente IntegratedML:
IntegratedML
IntegratedML es una herramienta de análisis predictivo que permite a cualquier desarrollador simplificar las tareas necesarias para el diseño, elaboración y prueba de modelos predictivos.
Nos permite pasar de un modelo de diseño así:
A uno mucho más rapido y sencillo como este:
Y lo hace utilizando comandos SQL, de tal forma que todo sea mucho más sencillo y cómodo de utilizar. IntegratedML también nos permite elegir qué motor vamos a usar en la creación de nuestro modelo, pudiendo así elegir el que más adecuado nos resulte.
¿Cómo verlo en acción?
Siempre que he visto presentaciones de IntegratedML me ha encantado su sencillez, pero me quedaba la duda de cómo traspasar esa sencillez de su uso a un caso real. Pensando un poco en nuestros clientes habituales recordé lo común que es el uso de IRIS para integrar los datos de las aplicaciones departamentales de los hospitales con un HIS y la gran cantidad de información de episodios clínicos disponible en todos ellos, así que me puse manos a la obra para montar un ejemplo completo.
Tenéis el código fuente en Open Exchange. El proyecto se arranca con Docker y sólo deberéis alimentar la producción desplegada con los archivos adjuntos que iremos mostrando.
Como podéis ver el proyecto contiene clases de ObjectScript que se van a cargar automáticamente en el momento de construir la imagen. Para ello sólo necesitáis abrir el terminal de VS Code y ejecutar los siguientes comandos (con Docker arrancado).
docker-compose build
docker-compose up -d
Al arrancar el contenedor se va a crear un namespace llamado MLTEST y se arrancará una producción en el que encontraremos todos los componentes del negocio necesarios para la ingesta de los datos en crudo, la creación del modelo, su entrenamiento y su posterior puesta en práctica mediante la recepción de mensajería HL7.
Pero no nos adelantemos aún y sigamos el gráfico del análisis predictivo.
Adquisición de los datos
Muy bien, acotemos el objetivo de nuestra predicción. Rebuscando por páginas de la Administración Pública de España encontré unos cuantos CSV que encajaban perfectamente con el universo de integraciones con origen y destino en un HIS. En este caso el fichero que elegí fue el relativo a los datos de ingresos y altas hospitalarias por rotura de cadera en Castilla y León entre los años 2020 y 2022.
Como véis tenemos datos como la edad y sexo del paciente, fechas de ingreso y alta y centro hospitalario. Perfecto, con estos datos podríamos intentar predecir la estancia hospitalaria de cada paciente, es decir, el número de días entre ingreso y alta.
Tenemos un CSV pero necesitamos almacenarlo en nuestro IRIS y nada más sencillo que usar el Record Mapper de IRIS. El resultado del uso de Record Mapper lo podéis ver en la columna de Business Services de la producción de MLTEST:
CSVToEpisodeTrain es el BS encargado de leer el CSV y enviar los datos al BP MLTEST.BP.RecordToEpisodeTrain que explicaremos más adelante. Los datos obtenidos por este BS serán los usados para entrenar nuestro modelo.
CSVToEpisode es el BS que leera los datos del CSV que usaremos posteriormente para lanzar predicciones de prueba antes de poner en marcha nuestras predicciones obtenidas a partir de mensajes HL7.
Ambos BS van a crear por cada línea del CSV un objeto de la clase User.IctusMap.Record.cls que se lo enviarán a sus respectivos BP donde se realizarán las transformaciones necesarias para finalmente obtener registros de nuestras tablas MLTEST_Data.Episode y MLTEST_Data.EpisodeTrain, esta última será la tabla que usaremos para generar el modelo de predicción, mientras que la anterior es donde almacenaremos nuestros episodios.
Preparación de los datos
Antes de crear nuestro modelo deberemos transformar la lectura del CSV en objetos que sean fácilmente utilizables por el motor de predicciones y para ello usaremos los siguientes BP:
MLTEST.BP.RecordToEpisode: que nos realizará la transformación del registro de CSV a nuestra tabla de episodios MLTEST_Data.Episode
MLTEST.BP.RecordToEpisodeTrain: que realiza la misma transformación que en el caso anterior pero almacenando el episodio en MLTEST_Data.EpisodeTrain.
Podríamos haber usado un sólo BP para el registro en ambas tablas, pero para que sea más claro el proceso lo dejaremos así. En la transformación realizada por los BP hemos reemplazado todos los campos de texto por valores numéricos para agilizar el entrenamiento del modelo.
Muy bien, tenemos nuestros BS y nuestros BP funcionando, alimentemoslos copiando en el proyecto el archivo /shared/train-data.csv a la ruta /shared/csv/trainIn:
Aquí tenemos todos los registros de nuestro archivo consumidos, transformados y registrados en nuestra tabla de entrenamiento. Repitamos la operación con los registros que vamos a usar para una primera prueba de predicciones. Copiando /shared/test-data.csv a la ruta /shared/csv/newIn ya tendríamos todo preparado para crear nuestro modelo.
En este proyecto no sería necesario que ejecutáseis las instrucciones de creación y entrenamiento, ya que se encuentran incluidas en el BO que gestiona el registro de los datos recibidos por mensajería HL7, pero para que lo podáis ver con más detalle vamos a hacerlo antes de probar la integración con los mensajes HL7.
AutoML
Tenemos nuestros datos de entrenamiento y nuestros datos de prueba, creemos nuestro modelo. Para ello accederemos desde el namespace MLTEST a la pantalla SQL de nuestro IRIS (System Explorer --> SQL) y ejecutaremos los siguientes comandos:
CREATE MODEL StayModel PREDICTING (Stay) FROM MLTEST_Data.EpisodeTrain
En esta query estamos creando un modelo de predicción llamado StayModel que va a predecir el valor de la columna Stay (estancia) de nuestra tabla con episodios de entrenamiento. La columna de estancia no venía en nuestro CSV pero la hemos calculado en el BP encargado de la transformación del registro de CSV.
A continuación procedemos a entrenar el modelo:
TRAIN MODEL StayModel
Esta instrucción como el lógico le llevará un tiempo pero una vez que concluya el entrenamiento podremos validar el modelo con nuestros datos de prueba ejecutando la siguiente instrucción:
VALIDATE MODEL StayModel FROM MLTEST_Data.Episode
Esta instrucción nos calculará como de aproximadas son nuestras estimaciones. Como podréis imaginar con los datos que tenemos, estas no serán precisamente para tirar cohetes. Podéis visualizar el resultado de la validación con la siguiente consulta:
SELECT * FROM INFORMATION_SCHEMA.ML_VALIDATION_METRICS
A partir de las metricas que obtenemos con esa consulta podemos inferir que el el modelo elegido automáticamente por AutoML es de clasificación en lugar de un modelo de regresión. Expliquemos que significan los resultados obtenidos (¡gracias @Yuri.Gomes por tu artículo!):
Precision: se calcula dividiendo la cantidad de verdaderos positivos por la cantidad de positivos previstos (suma de verdaderos positivos y falsos positivos).
Recall: se calcula dividiendo el número de verdaderos positivos por el número de positivos reales (suma de verdaderos positivos y falsos negativos).
F-Measure: calculado por la siguiente expresión: F = 2 * (Precision * Recall) / (Precision + Recall)
Accuracy: calculado por la división del número de positivos verdaderos y negativos verdaderos por el numero total de filas (suma de positivos verdaderos, falsos positivos, negativos verdaderos y falsos negativos) de todo el conjunto de datos.
Con esta explicación ya podemos entender como de bueno es el modelo generado:
Como véis, en números generales nuestro modelo es bastante malo, apenas alcanzamos un 35% de aciertos, si entramos en más detalle vemos que para estancias cortas la precisión anda entre el 35% y el 50%, por lo que seguramente necesitaríamos ampliar los datos que tenemos con información a cerca de posibles patologías que pueda tener el paciente y el triaje respecto a la fractura.
Como no disponemos de esos datos que afinarían mucho más nuestro modelo vamos a imaginar que con lo que tenemos es más que suficiente para nuestro objetivo, así que ya podemos empezar a alimentar nuestra producción con mensajes ADT_A01 de admisión de pacientes y veremos las predicciones que obtenemos.
Puesta en producción
Con el modelo ya entrenado sólo nos resta preparar la producción para crear un registro en nuestra tabla MLTEST_Data.Episode por cada mensaje recibido. Veamos los componentes de nuestra producción:
HL7ToEpisode: es el BS que capturará el archivo con mensajes HL7. Este BS redirigirá los mensajes al BP MLTEST.BP.RecordToEpisodeBPL
MLTEST.BP.RecordToEpisodeBPL: este BPL tendrá los siguientes pasos.
Transformación del HL7 en un objeto MLTEST.Data.Episode
Registro en la base de datos del objeto Episodio.
Consulta al BO MLTEST.BO.PredictStayEpisode para obtener la predicción de días de hospitalización.
Escritura de traza con la predicción obtenida.
MLTEST.BO.PredictStayEpisode: BO encargado de lanzar de forma automática las consultas necesarias al modelo de predicción. Si este no existe se encargará de crearlo y entrenarlo automáticamente, de tal forma que no será necesario ejecutar los comandos de sql. Echemos un vistazo al código.
Class MLTEST.BO.PredictStayEpisode Extends Ens.BusinessOperation
{
Property ModelName As %String(MAXLEN = 100);
/// Description
Parameter SETTINGS = "ModelName";
Parameter INVOCATION = "Queue";
/// Description
Method PredictStay(pRequest As MLTEST.Data.PredictionRequest, pResponse As MLTEST.Data.PredictionResponse) As %Status
{
set predictionRequest = pRequest
set pResponse = ##class("MLTEST.Data.PredictionResponse").%New()
set pResponse.EpisodeId = predictionRequest.EpisodeId
set tSC = $$$OK
// CHECK IF MODEL EXISTS
set sql = "SELECT MODEL_NAME FROM INFORMATION_SCHEMA.ML_MODELS WHERE MODEL_NAME = '"_..ModelName_"'"
set statement = ##class(%SQL.Statement).%New()
set status = statement.%Prepare(sql)
if ($$$ISOK(status)) {
set resultSet = statement.%Execute()
if (resultSet.%SQLCODE = 0) {
set modelExists = 0
while (resultSet.%Next() '= 0) {
if (resultSet.%GetData(1) '= "") {
set modelExists = 1
// GET STAY PREDICTION WITH THE LAST EPISODE PERSISTED
set sqlPredict = "SELECT PREDICT("_..ModelName_") AS PredictedStay FROM MLTEST_Data.Episode WHERE %ID = ?"
set statementPredict = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1
set statusPredict = statementPredict.%Prepare(sqlPredict)
if ($$$ISOK(statusPredict)) {
set resultSetPredict = statementPredict.%Execute(predictionRequest.EpisodeId)
if (resultSetPredict.%SQLCODE = 0) {
while (resultSetPredict.%Next() '= 0) {
set pResponse.PredictedStay = resultSetPredict.%GetData(1)
}
}
}
else {
set tSC = statusPredict
}
}
}
if (modelExists = 0) {
// CREATION OF THE PREDICTION MODEL
set sqlCreate = "CREATE MODEL "_..ModelName_" PREDICTING (Stay) FROM MLTEST_Data.EpisodeTrain"
set statementCreate = ##class(%SQL.Statement).%New()
set statusCreate = statementCreate.%Prepare(sqlCreate)
if ($$$ISOK(status)) {
set resultSetCreate = statementCreate.%Execute()
if (resultSetCreate.%SQLCODE = 0) {
// MODEL IS TRAINED WITH THE CSV DATA PRE-LOADED
set sqlTrain = "TRAIN MODEL "_..ModelName
set statementTrain = ##class(%SQL.Statement).%New()
set statusTrain = statementTrain.%Prepare(sqlTrain)
if ($$$ISOK(statusTrain)) {
set resultSetTrain = statementTrain.%Execute()
if (resultSetTrain.%SQLCODE = 0) {
// VALIDATION OF THE MODEL WITH THE PRE-LOADED EPISODES
set sqlValidate = "VALIDATE MODEL "_..ModelName_" FROM MLTEST_Data.Episode"
set statementValidate = ##class(%SQL.Statement).%New()
set statusValidate = statementValidate.%Prepare(sqlValidate)
if ($$$ISOK(statusValidate)) {
set resultSetValidate = statementValidate.%Execute()
if (resultSetValidate.%SQLCODE = 0) {
// GET STAY PREDICTION WITH THE LAST EPISODE PERSISTED
set sqlPredict = "SELECT PREDICT("_..ModelName_") AS PredictedStay FROM MLTEST_Data.Episode WHERE %ID = ?"
set statementPredict = ##class(%SQL.Statement).%New(), statement.%ObjectSelectMode = 1
set statusPredict = statementPredict.%Prepare(sqlPredict)
if ($$$ISOK(statusPredict)) {
set resultSetPredict = statementPredict.%Execute(predictionRequest.EpisodeId)
if (resultSetPredict.%SQLCODE = 0) {
while (resultSetPredict.%Next() '= 0) {
set pResponse.PredictedStay = resultSetPredict.%GetData(1)
}
}
}
else {
set tSC = statusPredict
}
}
}
else {
set tSC = statusValidate
}
}
}
else {
set tSC = statusTrain
}
}
}
else {
set tSC = status
}
}
}
}
else {
set tSC = status
}
quit tSC
}
XData MessageMap
{
<MapItems>
<MapItem MessageType="MLTEST.Data.PredictionRequest">
<Method>PredictStay</Method>
</MapItem>
</MapItems>
}
}
Como podéis observar, tenemos una propiedad que nos servirá para definir el nombre que queremos para nuestro modelo de predicción e inicialmente lanzaremos una consulta a la tabla ML_MODELS para asegurarnos de que el modelo exista.
Pues bien, ya estamos listos para lanza nuestros mensajes, para ello copiaremos el fichero del proyecto /shared/messagesa01.hl7 a la carpeta /shared/hl7/in esta acción nos enviará 50 mensajes de datos generados a nuestra producción. Veamos algunas de las predicciones.
Para nuestra paciente Sonia Martínez con 2 meses de edad tendremos una estancia de...
¡8 días! Le deseamos una pronta recuperación.
Veamos a otro paciente:
Ana Torres Fernández, de 50 años de edad...
9 días de estancia para ella.
Pues por hoy es todo. Lo menos importante de este ejemplo es el valor numérico de la predicción, ya véis que es bastante pobre por las estadísticas que hemos obtenido, pero os podrá resultar muy útil para casos en los que tengáis un buen conjunto de datos sobre el que aplicar esta funcionalidad tan genial de IntegratedML.
Si tenéis ganas de trastear con ella podéis descargaros la versión Community o usar la configurada en el proyecto de OpenExchange asociada a este artículo.
Si tenéis cualquier duda o necesitais alguna aclaración no dudéis en preguntar en los comentarios.
Artículo
Luis Angel Pérez Ramos · 17 jul, 2023
Hola de nuevo a todos.
En nuestro artículo anterior vimos como configurar nuestro EMPI para recibir mensajería FHIR. Para ello instalábamos el Adaptador FHIR que InterSystems pone a nuestra disposición que configuraba un endpoint REST al que podíamos enviar nuestro mensaje FHIR. A continuación obteníamos el mensaje y lo transformábamos a un %String que enviábamos vía TCP a la producción de nuestro EMPI configurada en nuestro namespace HSPIDATA.
Muy bien, es el momento de ver como recuperamos el mensaje, lo transformamos nuevamente a un %DynamicObject y lo parseamos a la clase usada por el EMPI para almacenar la información.
Recepción de mensaje por TCP
Como hemos indicado, desde la producción que tiene configurada la recepción de recursos FHIR hemos enviado un mensaje a un puerto específico TCP en el que tenemos escuchando un Business Service, en nuestro caso este Business Service será un simple EnsLib.TCP.PassthroughService cuyo objetivo es capturar el mensaje y reenviarlo a un Business Process donde realizaremos la transformación de datos precectiva.
Aquí tenemos nuestro Business Service:
Y aquí la configuración básica de la misma:
Transformación de nuestro mensaje FHIR
Como podéis ver sólo hemos configurado el puerto en el que se va a recibir nuestro mensaje vía TCP y el componente al que vamos a enviar nuestro mensaje, en nuestro caso lo hemos llamado Local.BP.FHIRProcess, echemos un vistazo a dicha clase para ver como recuperamos la información de nuestro recurso FHIR:
Class Local.BP.FHIRProcess Extends Ens.BusinessProcess [ ClassType = persistent ]
{
Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status
{
set tDynObj = {}.%FromJSON(pRequest.Stream)
If (tDynObj '= "") {
set hubRequest = ##class(HS.Message.AddUpdateHubRequest).%New()
// Create AddUpdateHub Message
// Name, sex, DOB
set givenIter = tDynObj.name.%Get(0).given.%GetIterator()
while givenIter.%GetNext(, .givenName){
if (hubRequest.FirstName '= "")
{
Set hubRequest.FirstName=givenName
}
else {
Set hubRequest.FirstName=hubRequest.FirstName_" "_givenName
}
}
Set hubRequest.FirstName=tDynObj.name.%Get(0).given.%Get(0)
Set hubRequest.LastName=tDynObj.name.%Get(0).family
Set hubRequest.Sex=tDynObj.gender
Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate)
// Inserts full birth name information for the patient
set nameIter = tDynObj.name.%GetIterator()
while nameIter.%GetNext(, .name){
Set tName = ##class(HS.Types.PersonName).%New()
if (name.prefix '= "")
{
Set tName.Prefix = name.prefix.%Get(0)
}
Set tName.Given = name.given.%Get(0)
Set tName.Middle = ""
Set tName.Family = name.family
Set tName.Suffix = ""
Set tName.Type=^Ens.LookupTable("TypeOfName",name.use)
Do hubRequest.Names.Insert(tName)
}
set identIter = tDynObj.identifier.%GetIterator()
while identIter.%GetNext(, .identifier){
if (identifier.type'=""){
if (identifier.type.coding.%Get(0).code = "MR") {
Set hubRequest.MRN = identifier.value
Set hubRequest.AssigningAuthority = ^Ens.LookupTable("hospital",identifier.system)
Set hubRequest.Facility = ^Ens.LookupTable("hospital",identifier.system)
}
elseif (identifier.type.coding.%Get(0).code = "SS") {
Set hubRequest.SSN = identifier.value
}
else {
Set tIdent=##class(HS.Types.Identifier).%New()
Set tIdent.Root = identifier.system // refers to an Assigning Authority entry in the OID Registry
Set tIdent.Extension = identifier.value
Set tIdent.AssigningAuthorityName = identifier.system
Set tIdent.Use = identifier.type.coding.%Get(0).code
Do hubRequest.Identifiers.Insert(tIdent)
}
}
}
// Address
set addressIter = tDynObj.address.%GetIterator()
while addressIter.%GetNext(, .address){
Set addr=##class(HS.Types.Address).%New()
Set addr.City=address.city
Set addr.State=address.state
Set addr.Country=address.country
Set addr.StreetLine=address.line.%Get(0)
Do hubRequest.Addresses.Insert(addr)
}
//Telephone
set identTel = tDynObj.telecom.%GetIterator()
while identTel.%GetNext(, .telecom){
if (telecom.system = "phone") {
Set tel=##class(HS.Types.Telecom).%New()
Set tel.PhoneNumber=telecom.value
Do hubRequest.Telecoms.Insert(tel)
}
}
}
Set tSC = ..SendRequestSync("HS.Hub.MPI.Manager", hubRequest, .pResponse)
Quit tSC
}
Storage Default
{
<Type>%Storage.Persistent</Type>
}
}
Veamos un poco más en detalle qué estamos haciendo:
Primeramente recibimos el mensaje enviado desde el Business Service:
Method OnRequest(pRequest As Ens.StreamContainer, Output pResponse As Ens.Response) As %Status
{
set tDynObj = {}.%FromJSON(pRequest.Stream)
Como podemos ver en la firma del método OnRequest el mensaje de entrada corresponde a una clase del tipo Ens.StreamContainer, esta transformación de nuestro mensaje de tipo %String se ha realizado en el Business Service. En la primera línea del método lo que haremos será recuperar el mensaje que se encuentra como un Stream dentro de la variable pRequest. A continuación lo transformamos a un %DynamicObject mediante la instrucción %FromJSON.
Con nuestro mensaje mapeado a un objeto dinámico podremos acceder a cada uno de los campos del recurso FHIR que hemos enviado:
set tDynObj = {}.%FromJSON(pRequest.Stream)
If (tDynObj '= "") {
set hubRequest = ##class(HS.Message.AddUpdateHubRequest).%New()
// Create AddUpdateHub Message
// Name, sex, DOB
set givenIter = tDynObj.name.%Get(0).given.%GetIterator()
while givenIter.%GetNext(, .givenName){
if (hubRequest.FirstName '= "")
{
Set hubRequest.FirstName=givenName
}
else {
Set hubRequest.FirstName=hubRequest.FirstName_" "_givenName
}
}
Set hubRequest.FirstName=tDynObj.name.%Get(0).given.%Get(0)
Set hubRequest.LastName=tDynObj.name.%Get(0).family
Set hubRequest.Sex=tDynObj.gender
Set hubRequest.DOB=hubRequest.DOBDisplayToLogical(tDynObj.birthDate)
En este fragmento vemos como creamos un objecto de la clase HS.Message.AddUpdateHubRequest que es el que enviaremos al Business Operation HS.Hub.MPI.Manager encargado de realizar las operaciones correspondientes dentro del EMPI, ya sea la creación del nuevo paciente o la actualización del mismo, así como vincularlo con las posibles coincidencias que pueda haber con otros pacientes ya dentro del EMPI.
El siguiente paso es poblar el nuevo objeto con los datos recibidos desde el Business Service, como podéis observar lo único que hacemos es recuperar los datos de los diferentes campos del objecto dinámico que acabamos de crear. El formato del objeto dinámico corresponde exáctamente con el definido por HL7 FHIR para el recurso paciente, podéis ver ejemplos directamente en la página de HL7 FHIR
Para nuestro ejemplo hemos elegido este paciente de la lista que nos proporciona la propia página de HL7 FHIR:
{
"resourceType": "Patient",
"id": "example",
"text": {
"status": "generated",
"div": "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n\t\t\t<table>\n\t\t\t\t<tbody>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Name</td>\n\t\t\t\t\t\t<td>Peter James \n <b>Chalmers</b> ("Jim")\n </td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Address</td>\n\t\t\t\t\t\t<td>534 Erewhon, Pleasantville, Vic, 3999</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Contacts</td>\n\t\t\t\t\t\t<td>Home: unknown. Work: (03) 5555 6473</td>\n\t\t\t\t\t</tr>\n\t\t\t\t\t<tr>\n\t\t\t\t\t\t<td>Id</td>\n\t\t\t\t\t\t<td>MRN: 12345 (Acme Healthcare)</td>\n\t\t\t\t\t</tr>\n\t\t\t\t</tbody>\n\t\t\t</table>\n\t\t</div>"
},
"identifier": [
{
"use": "usual",
"type": {
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0203",
"code": "MR"
}
]
},
"system": "urn:oid:1.2.36.146.595.217.0.1",
"value": "12345",
"period": {
"start": "2001-05-06"
},
"assigner": {
"display": "Acme Healthcare"
}
}
],
"active": true,
"name": [
{
"use": "official",
"family": "Chalmers",
"given": [
"Peter",
"James"
]
},
{
"use": "usual",
"given": [
"Jim"
]
},
{
"use": "maiden",
"family": "Windsor",
"given": [
"Peter",
"James"
],
"period": {
"end": "2002"
}
}
],
"telecom": [
{
"use": "home"
},
{
"system": "phone",
"value": "(03) 5555 6473",
"use": "work",
"rank": 1
},
{
"system": "phone",
"value": "(03) 3410 5613",
"use": "mobile",
"rank": 2
},
{
"system": "phone",
"value": "(03) 5555 8834",
"use": "old",
"period": {
"end": "2014"
}
}
],
"gender": "male",
"birthDate": "1974-12-25",
"_birthDate": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/patient-birthTime",
"valueDateTime": "1974-12-25T14:35:45-05:00"
}
]
},
"deceasedBoolean": false,
"address": [
{
"use": "home",
"type": "both",
"text": "534 Erewhon St PeasantVille, Rainbow, Vic 3999",
"line": [
"534 Erewhon St"
],
"city": "PleasantVille",
"district": "Rainbow",
"state": "Vic",
"postalCode": "3999",
"period": {
"start": "1974-12-25"
}
}
],
"contact": [
{
"relationship": [
{
"coding": [
{
"system": "http://terminology.hl7.org/CodeSystem/v2-0131",
"code": "N"
}
]
}
],
"name": {
"family": "du Marché",
"_family": {
"extension": [
{
"url": "http://hl7.org/fhir/StructureDefinition/humanname-own-prefix",
"valueString": "VV"
}
]
},
"given": [
"Bénédicte"
]
},
"telecom": [
{
"system": "phone",
"value": "+33 (237) 998327"
}
],
"address": {
"use": "home",
"type": "both",
"line": [
"534 Erewhon St"
],
"city": "PleasantVille",
"district": "Rainbow",
"state": "Vic",
"postalCode": "3999",
"period": {
"start": "1974-12-25"
}
},
"gender": "female",
"period": {
"start": "2012"
}
}
],
"managingOrganization": {
"reference": "Organization/1"
}
}
Antes de nada hemos creado 2 tablas Lookup para el mapeo de los tipos de nombre y de la autoridad de asignación del número de historia clínica (MR), la primera para que sea compatible con los tipos manejados por el EMPI y el segundo para que se reconozca la autoridad de asignación que generó el identificador:
Set tName.Type=^Ens.LookupTable("TypeOfName",name.use)
Set hubRequest.AssigningAuthority = ^Ens.LookupTable("hospital",identifier.system)
Set hubRequest.Facility = ^Ens.LookupTable("hospital",identifier.system)
Lanzando un mensaje de pruebas
Perfecto, lancemos nuestro mensaje FHIR contra nuestro endpoint que definimos en el artículo anterior:
Como véis hemos recibido un 200 de respuesta, esto sólo significa que el EMPI ha recibido correctamente el mensaje, veamos ahora la traza que se ha generado en nuestra producción:
Aquí tenemos a nuestro paciente, como podéis ver se ha realizado la transformación con éxito y se han asignado correctamente todos los campos que venían informados en el mensaje FHIR. Como podemos observar se ha generado un mensaje de notificación IDUpdateNotificationRequest, este tipo de notificaciones se generan cuando se realizar alguna operación de creación o actualización de pacientes en el sistema.
Muy bien, comprobemos que el paciente está correctamente registrado en nuestro sistema realizando una búsqueda del mismo por su nombre y apellidos:
¡BINGO! Veamos más en detalle los datos de nuestro querido Peter:
Perfecto, ya tenemos en nuestro EMPI toda la información necesaria del paciente. Como véis el mecanismo es bastante sencillo, repasemos los pasos que hemos realizado:
Hemos instalado la herramienta de FHIR Adapter que nos proporciona InterSystems en un NAMESPACE configurado para trabajar con interoperabilidad (un namespace diferente el generado por el EMPI que en mi caso he llamado WEBINAR).
Hemos creado en este namespace un Business Operation que transforma el mensaje recibido de tipo HS.FHIRServer.Interop.Request a un %String que enviará a un Business Service configurado en la producción del namespace del EMPI (HSPIDATA).
A continuación hemos añadido el Business Service de la clase EnsLib.TCP.PassthroughService que recibe el mensaje enviado desde la producción del namespace WEBINAR y redirige al Business Process Local.BP.FHIRProcess.
En el BP Local.BP.FHIRProcess hemos transformado el Stream recibido a un objeto del tipo HS.Message.AddUpdateHubRequest y lo hemos enviado al Business Operation HS.Hub.MPI.Manager que se encargará de registrarlo en nuestro EMPI.
Como véis, la unión de las funcionalidades del EMPI con las proporcionadas por motor de integración de IRIS nos permite trabajar prácticamente con cualquier tipo de tecnología.
Espero que os haya resultado útil este artículo. Si tenéis alguna pregunta o sugerencia ya sabéis, dejad un comentario y estaré encantado de contestaros.
Artículo
Ricardo Paiva · 12 sep, 2022

En primer lugar, ¿qué es la anonimización de datos?
Según la [Wikipedia](https://en.wikipedia.org/wiki/Data_anonymization):
> **La anonimización** es un tipo de [sanitización de información](https://en.wikipedia.org/wiki/Sanitization_(classified_information)) cuya intención es la protección de la privacidad. Es el proceso de eliminar [información personal](https://es.wikipedia.org/wiki/Informaci%C3%B3n_personal) de los conjuntos de datos, de modo que las personas que son descritas por los datos permanecen en el anonimato.
>
En otras palabras, la anonimización de datos es un proceso que conserva los datos pero mantiene la fuente anónima.
Según la técnica de anonimización que se adopte, los datos son editados, ocultados o sustituidos.
Y ese es el propósito de iris-Disguise, ofrecer un conjunto de herramientas de anonimización.
Lo puedes usar de dos formas diferentes, por método de ejecución o especificar tu estrategia de anonimización dentro de la definición de la clase persistente en sí misma.
La actual versión de iris-Disguise ofrece 6 estrategias para anonimizar datos:
- ***Destruction* (Destrucción)**
- ***Scramble* (Codificación)**
- ***Shuffling* (Reorganización)**
- ***Partial masking* (Ocultación parcial)**
- ***Randomization* (Distribución aleatoria)**
- ***Faking* (Falsificación)**
Vamos a explicar cada estrategia. Mostraré un método de ejecución con un ejemplo como he mecionado, y también mostraré cómo aplicarlo dentro de la definición de la clase persistente.
Para usar **iris-Disguise** de esta forma, necesitaréis "*llevar unas gafas de disfraz"*.
En la clase persistente, se puede extender la clase **dc.Disguise.Glasses** y cambiar cualquier propiedad con los tipos de datos con la estrategia que prefieras.
Tras ello, en cualquier momento, simplemente llama al método **DisguiseProcess** en la clase. Todos los valores serán sustituidos usando la estrategia del tipo de datos.
Así que... abrochaos el cinturón que empezamos!
### *Destruction* (Destrucción)
Esta estrategia reemplazará una columna entera con una palabra ('CONFIDENTIAL' (CONFIDENCIAL) por defecto).
```
Do ##class(dc.Disguise.Strategy).Destruction("classname", "propertyname", "Word to replace")
```
El tercer parámetro es opcional. Si no se aporta, se usará la palabra 'CONFIDENTIAL' (CONFIDENCIAL).
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.String(FieldStrategy = "DESTRUCTION");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Scramble* (Codificación)
Esta estrategia codificará todos los caracteres de una propiedad.
```
Do ##class(dc.Disguise.Strategy).Scramble("classname", "propertyname")
```
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.String(FieldStrategy = "SCRAMBLE");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Shuffling* (Reorganización)
*Shuffling* reorganizará todos los valores de una propiedad dada. No es una estrategia de ocultación porque trabaja "verticalmente".
Esta estrategia es útil para relaciones, porque se mantendrá la integridad referencial.
Hasta esta versión, este método solo funciona en **relaciones de uno a muchos**.
```
Do ##class(dc.Disguise.Strategy).Shuffling("classname", "propertyname")
```
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property Weapon As dc.Disguise.DataTypes.String(FieldStrategy = "SHUFFLING");
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### Partial Masking (Ocultación parcial)
Esta estrategia ocultará parte de los datos. Por ejemplo, el número de una tarjeta de crédito puede ser sustituido por 456X XXXX XXXX X783
```
Do ##class(dc.Disguise.Strategy).PartialMasking("classname", "propertyname", prefixLength, suffixLength, "mask")
```
PrefixLength, suffixLength y mask son opcionales. Si no se aporta, se usarán los valores por defecto.
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property SSN As dc.Disguise.DataTypes.PartialMaskString(prefixLength = 2, suffixLength = 2);
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### *Randomization* (Distribución aleatoria)
Esta estrategia generará sencillamente datos aleatorios. Hay tres tipos de distribución aleatoria: integer, numeric y date.
```
Do ##class(dc.Disguise.Strategy).Randomization("classname", "propertyname", "type", from, to)
```
**type**: "integer", "numeric" o "date". "integer" es el predeterminado.
"from" y "to" son opcionales. Es para definir el rango de distribución aleatoria.
Para el tipo "integer" el rango por defecto es de 1 a 100. Para el tipo "numeric" el rango por defecto es de 1.00 a 100.00.
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As %String;
Property Age As dc.Disguise.DataTypes.RandomInteger(MINVAL = 10, MAXVAL = 25);
Property SSN As %String;
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### Faking (Falsificación)
La idea de la falsificación es reemplazar datos con valores aleatorios pero plausibles.
**iris-Disguise** proporciona una pequeño conjunto de métodos para generar datos falsos.
```
Do ##class(dc.Disguise.Strategy).Fake("classname", "propertyname", "type")
```
**type**: "firstname", "lastname", "fullname", "company", "country", "city" y "email"
```
Class packageSample.FictionalCharacter Extends (%Persistent, dc.Disguise.Glasses)
{
Property Name As dc.Disguise.DataTypes.FakeString(FieldStrategy = "FIRSTNAME");
Property Age As %Integer;
Property SSN As %String;
Property Weapon As %String;
}
```
```
Do ##class(packageSample.FictionalCharacter).DisguiseProcess()
```
.png)
### ¿Qué os parece?
Me gustaría conocer vuestra opinión y leer vuestros comentarios - qué os parece, si se ajusta a vuestras necesidades y qué funcionalidades echais de menos.
Y quería agradecer de forma especial a @Henrique.GonçalvesDias, @Oliver.Wilms, @Robert.Cemper1003, @Yuri.Gomes y @Evgeny.Shvarov sus comentarios, revisiones, sugerencias y valiosos aportes, que me han inspirado tanto y ayudado a crear y mejorar iris-Disguise.
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Yaron Munz · 23 sep, 2022
Resumen
Empezamos a usar Azure Service Bus (ASB) como solución de mensajería empresarial hace tres años. La hemos usado para publicar y consumir datos entre muchas aplicaciones de la organización. Como el flujo de datos es complejo, y normalmente se necesitan los datos de una aplicación en muchas otras aplicaciones, el modelo publicador -> múltiples subscriptores resultó muy adecuado. El uso de ASB en la organización es de docenas de millones de mensajes por día, mientras que la plataforma IRIS tiene unos 2-3 millones de mensajes/día.
El problema con ASB
Cuando empezamos con la integración de ASB, encontramos que el protocolo AMQP no tiene la configuración predeterminada para la implementación de IRIS, por lo que estuvimos buscando una solución alternativa para poder comunicar con ASB.
La solución
Desarrollamos un servicio local en Windows (.NET y Swagger) que se encargaba de la comunicación con ASB. Se instaló en la misma máquina que IRIS. Enviamos los mensajes a ASB dirigidos a este servicio local en Windows (usando: localhost y haciendo una API REST) y el propio servicio hacía el resto. Esta solución funcionó bien durante años, pero era muy difícil monitorizar el tráfico, depurar los mensajes "perdidos" y medir el rendimiento general. El hecho de tener este servicio local en Windows como un agente intermedio no resultaba la mejor arquitectura.
Programa de Acceso Preferente a Python Embebido (EAP)
Me pidieron (o me presenté voluntario, no lo recuerdo bien) participar en el Programa de Acceso Preferente a Python Embebido. Fue muy interesante para mí, porque pude poder probar nuevas funcionalidades y dar feedback al equipo de desarrollo. También me gustó mucho participar de forma activa e influir en el desarrollo de este producto. En esa fase, empezamos a probar Python Embebido para el ERP y decidimos comprobar si podíamos utilizar Python Embebido para resolver problemas “reales”. Nos plantearnos entonces intentar resolver la conectividad directa de IRIS a ASB.
El POC (prueba de concepto)
Empezamos a programar la solución y vimos que usar la librería ASB de Microsoft para Python nos haría nuestra vida (y nuestro trabajo) mucho más fácil. El primer paso fue desarrollar una función que pueda conectar con un topic específico en ASB y recibir mensajes. Esto se hizo bastante rápido (1 día de desarrollo) y avanzamos a la fase publicar (enviar mensajes a ASB).
Nos llevó varios días desarrollar toda la cubierta para esas funciones enviar y recibir. Preparamos lo siguiente:
Un “área de preparación” entrante y saliente adecuada, para controlar el flujo de mensajes entrantes/salientes.
Un mecanismo central para almacenar el tráfico ASB entrante y saliente para poder tener estadísticas adecuadas y medidas del rendimiento.
Una página de monitorización CSP para activar/desactivar servicios, mostrar estadísticas y dar alertas sobre cualquier incidencia.
La implementación
Configuración Preliminar
Antes de usar las librerías ASB de Microsoft para Python, hay que instalarlas en un directorio ..\python dedicado.Nosotros elegimos usar ../mge/python/ pero se puede usar cualquier carpeta que se quiera (inicialmente era una carpeta vacía)Los siguientes comandos hay que ejecutarlos bien con una sesión CMD con privilegios (Windows) o con un usuario con los privilegios suficientes (Linux):
..\bin\irispip install --target ..\mgr\python asyncio
..\bin\irispip install --target ..\mgr\python azure.servicebus
Recibir mensajes desde ASB (consumo)
Tenemos una ClassMethod con varios parámetros:
The topiId - El ASB se divide en topics de los cuales se consumen los mensajes
subscriptionName - Nombre de la subscripción Azure
connectionString - Para poder conectar con el topic al que estás suscrito
Ten en cuenta que se está usando [ Language = python ] para indicar que este ClassMethod está escrito en Python (!)
ClassMethod retrieveASB(topicId As %Integer = 1, topicName As %String = "", subscriptionName As %String = "", connectionString As %String = "", debug As %Integer = 1, deadLetter As %Integer = 0) As %Integer [ Language = python ]
Para usar la librería ASB, primero necesitamos importar las librerías ServiceBusClient y ServiceBusSubQueue:
from azure.servicebus import ServiceBusClient,ServiceBusSubQueue
para poder interactuar con IRIS (por ejemplo, ejecutar código) también necesitamos:
import iris
En este punto, podemos usar las librerías ASB:
with ServiceBusClient.from_connection_string(connectionString) as client:
with client.get_subscription_receiver(topicName, subscriptionName, max_wait_time=1, sub_queue=subQueue) as receiver:
for msg in receiver:
En este punto, tenemos un objeto Python "msg" (stream) donde podemos pasarlo (como un stream, por supuesto) a IRIS y almacenarlo directamente en la base de datos:
result = iris.cls("anyIRIS.Class").StoreFrom Python(stream)
Enviar mensajes a ASB (publicación)
Tenemos un ClassMethod con varios parámetros:
topicName - El Topic en el que queremos publicar (aquí tenemos que pasar el nombre, no el id)
connectionString - Para poder conectar con el topic al que se está suscrito
JSONmessage - el mensaje que queremos enviar (publicar)
Ten en cuenta que se está usando [ Language = python ] para indicar que este ClassMethod está escrito en Python (!)
ClassMethod publishASB(topicName As %String = "", connectionString As %String = "", JSONmessage As %String = "") As %Status [ Language = python ]
Para usar la librería ASB, primero necesitamos importar las librerías ServiceBusClient y ServiceBusMessage:
from azure.servicebus import ServiceBusClient, ServiceBusMessage
Usar las librerías ASB es muy sencillo:
try:
result=1
with ServiceBusClient.from_connection_string(connectionString) as client:
with client.get_queue_sender(topicName) as sender:
single_message = ServiceBusMessage(JSONmessage)
sender.send_messages(single_message)
except Exception as e:
print(e)
result=0
return result
Beneficios de usar la conectividad ASB directa
Mucho más rápida que usar la antigua alternativa del servicio local en Windows
Fácil de monitorizar, recoger estadísticas, resolver incidencias
Retirar el agente intermedio (servicio local en Windows) reduce un potencial punto de fallo
Posibilidad de gestionar mensajes perdidos para cualquier topic automáticamente y hacer que ASB re-envíe esos mensajes a los subscriptores del topic
Agradecimientos
Me gustaría dar las gracias a @David.Satorres5186 (Senior Developer para IRIS) por su enorme contribución en el diseño, programación y pruebas. Sin su ayuda este proyecto no hubiera sido posible.
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Bernardo Linarez · 30 mar, 2020
¡Hola Comunidad!
En este artículo hablaré sobre las pruebas y la depuración de las aplicaciones web de Caché (principalmente REST) con herramientas externas. La segunda parte trata sobre las herramientas de Caché.
Usted escribió el código del lado del servidor y quiere probarlo con un cliente, o ya tiene una aplicación web pero no funciona. Aquí es donde entra la depuración. En este artículo abarcaré desde las herramientas más fáciles de utilizar (el navegador), hasta las más completas (el analizador de paquetes), pero primero conversemos un poco sobre los errores más comunes y cómo pueden resolverse.
Los errores
Error 401 No autorizado
Creo que este error es el que encontramos con más frecuencia durante la implementación de la producción. El servidor de desarrollo local normalmente tiene una configuración de seguridad mínima o normal pero muy convencional. Sin embargo, el servidor de producción puede tener un esquema con más restricciones. Entonces:
Compruebe que ha iniciado sesión
Compruebe que el usuario tiene acceso a la base de datos/tabla/procedimiento/fila/columna a la que desea acceder
Compruebe que la solicitud para las OPCIONES puede realizarse por un usuario no autorizado
Error 404 No encontrado
Compruebe:
Que la URL es correcta
Si es una nueva aplicación y está utilizando un servidor web externo, cargar nuevamente el servidor web puede ser útil
Errores en la aplicación
En cierto modo estos son los más fáciles de encontrar - realizar un seguimiento del stack será muy útil. La resolución es totalmente específica para la aplicación.
Herramientas de depuración
Navegador web
La primera herramienta de depuración que siempre está disponible es un navegador web, es preferible que utilice Chrome, pero Firefox también sería suficiente. Las solicitudes GET pueden probarse al ingresar en la URL que se encuentra en la barra de direcciones, todas las demás solicitudes necesitan de una aplicación web o que estén escritas en un código js. El método general es el siguiente:
Presione F12 para abrir las herramientas del desarrollador
Vaya a la pestaña Network
Marque la casilla Preserve Log, si no está marcada
Despliegue únicamente las solicitudes XHR
Realice una acción para depurar errores en la aplicación web
Desde aquí, puede examinar las solicitudes y enviarlas nuevamente. En Firefox también pueden editarse las solicitudes antes de repetirlas.
Ventajas:
Siempre está disponible
Es fácil de utilizar (los usuarios finales pueden enviar capturas de pantalla de la red y las pestañas de la consola)
Este es el entorno del usuario final
Desventajas:
No muestra respuestas parciales como send/broken/ etc.
Es lento con las respuestas que son grandes
Es lento cuando hay una gran cantidad de respuestas
Todo se hace manualmente
Cliente REST
El cliente REST es una aplicación web independiente o un complemento del navegador, que se diseñó específicamente para probar aplicaciones web. Aquí utilicé Postman, pero existen muchos otros. Así es como se ven las depuraciones en Postman:
Postman funciona mediante solicitudes agrupadas en colecciones. La solicitud puede enviarse a un entorno. El entorno es una colección de variables. Por ejemplo, en mi entorno CACHE@localhost la variable del host está configurada como localhost y el usuario como _SYSTEM. Cuando se envía una solicitud, las variables se reemplazan por sus valores en el entorno seleccionado y entonces la solicitud se envía.
Aquí puede consultar una recopilación de ejemplos y del entorno para el proyecto MDX2JSON.
Ventajas:
Solo es necesario escribirlo una vez y podrá utilizarlo en todas partes
Mejor control de las solicitudes
Optimización de respuestas
Desventajas:
La depuración de solicitudes encadenadas sigue siendo manual (la respuesta a la solicitud 1 puede forzar una respuesta, ya sea, en la solicitud 2 o en la solicitud 2B)
Algunas veces tiene errores en las respuestas parciales como send/broken/etc.
Proxy de depuraciones HTTP
Es una aplicación independiente que registra el tráfico en los HTTP. Las solicitudes registradas pueden modificarse y reenviarse. Yo utilizo Charles y Fiddler.
Ventajas:
Procesa respuestas parciales como send/broken/ etc.
Optimización de respuestas
Mejor soporte técnico para el tráfico HTTPS (que con el analizador de paquetes)
Puede guardar sesiones de captura
Desventajas:
Algunas cosas son necesarias para enviar la solicitud (por ejemplo, aplicaciones web/cliente REST/código JS)
Analizador de paquetes
Es un programa computacional que puede interceptar y registrar el tráfico que pasa por una red. Conforme el flujo de datos fluye a través de la red, el rastreador captura cada paquete y, si es necesario, decodifica los datos sin procesar del paquete. Esta es la opción más completa, pero también requiere de ciertas habilidades para funcionar correctamente. Yo utilizo WireShark. Aquí tiene una pequeña guía sobre cómo instalarlo y utilizarlo:
Si va a capturar paquetes locales, lea acerca del loopback e instale los requisitos previos del software (npcap para Windows)
Instale WireShark
Configure los filtros de captura (por ejemplo, un filtro para capturar únicamente el tráfico de la dirección 57772: con el puerto 57772
Inicie la captura
Configure los filtros de visualización (por ejemplo, un filtro para mostrar únicamente el tráfico http para una IP específica: ip.addr == 1.2.3.4 && http
A continuación, se muestra un ejemplo de captura para el tráfico http (filtro de visualización) en el puerto 57772 (filtro de captura):
Ventajas:
Procesa respuestas parciales como send/broken/ etc.
Puede capturar grandes cantidades de tráfico
Puede capturar cualquier cosa
Puede guardar sesiones de captura
Desventajas:
Algunas cosas son necesarias para enviar la solicitud (por ejemplo, aplicaciones web/cliente REST/código JS)
Qué utilizar
Bueno, eso depende de cuál sea el objetivo. En primer lugar, podemos registrar (depurar el proxy, analizar paquetes) o generar solicitudes (navegadores, clientes REST).
Si está desarrollando una API Web para REST, el cliente REST es la forma más rápida de probar que funciona.
Sin embargo, si las solicitudes del cliente REST funcionan, pero la aplicación web del cliente no, posiblemente necesite un navegador, un proxy de depuraciones http y un analizador de paquetes.
Si tiene clientes y necesita desarrollar una API del lado del servidor para trabajar con ellos, necesitará un proxy de depuraciones http o un analizador de paquetes.
Lo mejor es estar familiarizado con los 4 tipos de herramientas y cambiar rápidamente entre una y otra si la que utiliza actualmente no es suficiente para realizar el trabajo.
Algunas veces resulta evidente cuál es la herramienta correcta.
Por ejemplo, recientemente desarrollé una API en el lado del servidor para un protocolo de extensión http muy popular, los requisitos fueron:
No podemos cambiar el código que ya habían escrito los clientes
Clientes diferentes se comportan de distintas maneras
El comportamiento entre http y https es diferente
El comportamiento con distintos tipos de autenticación es diferente
Es posible procesar hasta cien solicitudes por segundo, por cliente
Todo el mundo ignora el RFC
Aquí solamente existe una solución, el analizador de paquetes.
O si estoy desarrollando una API REST para el consumo de JS, el cliente REST es la herramienta perfecta para realizar pruebas.
Cuando depure la aplicación web, siempre comience con el navegador.
En la parte 2 discutiremos todas las cosas (muchas) que puede hacer para depurar la web en el lado de Caché.
¿Qué métodos utiliza para depurar las comunicaciones entre el cliente y el servidor?
Este artículo ha sido etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Kurro Lopez · 11 feb, 2022
El tiempo dirá, siempre lo hace.
El Doctor.
No es una tarea facil dominar fechas y horas, siempre es un problema y a veces confuso en cualquier lenguaje de programación, vamos a aclarar y a poner unos cuantos tips para que esta tarea sea lo mas sencilla posible.
Súbete a la TARDIS que te voy a convertir en un Señor del tiempo
Empecemos por lo básico
Si vienes de otros lenguajes, comentar que las fechas en Intersystems Object Script (en adelante IOS, no confundir con los móviles) son un poco peculiares.Cuando ejecutamos el comando $HOROLOG en el terminal, para tener la fecha y hora actual, verás que se divide en dos partes:
WRITE $HOROLOG
> 66149,67164
El primer valor es el día, para ser exactos el número de días desde el 31 de diciembre de 1840, osea, que el valor 1 es el 1 de enero de 1841 y el segundo los segundos desde la 00:00 de ese día.
En este ejemplo, 66149 corresponde con 09/02/2022 y 67164 con las 18:39:24 horas. A este formato, lo vamos a llamar formato interno de fecha y hora.
¿Confuso? pues vamos a empezar a desvelar los grandes secretos del universo (de fechas y horas)
¿Cómo puedo convertir el formato interno en formato mas claro?
Para ello vamos a usar el comando $ZDATETIME
El comando básico sería
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo)
> 02/09/2022 18:39:24
Por defecto, utiliza el formato americano (mm/dd/yyyy). Si quieres utilizar la fecha en otro formato, vamos a utilizar el segundo parámetro, como por ejemplo el europeo (dd/mm/yyyy), en ese caso le vamos a dar el valor 4 (para mas formatos, ver la documentación $ZDATETIME.dformat)
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,4)
> 09/02/2022 18:39:24
Esta opción utiliza como separador y formato de año lo que tengamos definido en las variables locales
Si queremos además poner otro formato la hora, por ejempo en formato 12 horas (AM/PM) en lugar de formato 24 horas, utilizamos el tercer parámetro con el valor 3, si no queremos que muestre los segundos, usaremos el valor 4 (ver la documentación $ZDATETIME.tformat)
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,4,3)
> 09/02/2022 06:39:24PM
WRITE $ZDATETIME(AhoraMismo,4,4)
> 09/02/2022 06:39PM
¿Ahora está mas claro? pues vamos a profundizar mas
Formato ODBC
Este formato es independiente de tu configuración local, siempre se mostrará como yyyy-mm-dd, su valor es 3. Se recomiendo utilizarlo si queremos crear datos que se van a exportar en ficheros, como ficheros CSV, HL7, etc..
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATETIME(AhoraMismo,3)
> 2022-02-09 18:39:24
Día de la semana, nombre del día, día del año
Valor
Descripción
10
El día de la semana será un valor entre 0 y 6, siendo el 0 el domingo y el 6 el sábado.
11
El nombre del día de la semana abreviado, lo devolverá según la configuración local definda, la instalación por defecto de IRIS es enuw (English, United States, Unicode)
12
El nombre del día de la semana en formato largo. Igual que 11
14
El día del año, pues eso, el número de días desde el 1 de enero
Si solo queremos tratar fechas y horas por separado, debes de usar los comandos $ZDATE y $ZTIME respectivamente. El parámetro para los formatos son los mismos que define en $ZDATETIME.dformat y $ZDATETIME.tformat
SET AhoraMismo = $HOROLOG
WRITE AhoraMismo
> 66149,67164
WRITE $ZDATE(AhoraMismo,10)
> 3
WRITE $ZDATE(AhoraMismo,11)
> Wed
WRITE $ZDATE(AhoraMismo,12)
> Wednesday
¿Y cómo convierto una fecha en formato interno?
Pues ahora vamos a ver el paso contrario, es decir, tener un texto con una fecha y lo convertimos en formato IOS. Para ello vamos a usar el comando $ZDATETIMEH
Esta vez, tenemos que indicar en que formato está la fecha y la hora (si usamos $ZDATETIMEH) o la fecha ($ZDATEH) o la hora ($ZTIMEH) por separado.
Los formatos siguen siendo los mismos, es decir si tenemos una cadena con la fecha en formato ODBC (yyyy-mm-dd), pues utilizaremos el valor 3
SET MiFecha = "2022-02-09 18:39:24"
SET FechaInterna1 = $ZDATETIMEH(MiFecha, 3, 1) // Formato ODBC
SET MiFecha = "09/02/2022 18:39:24"
SET FechaInterna2 = $ZDATETIMEH(MiFecha, 4, 1) // Formato europeo
SET MiFecha = "02/09/2022 06:39:24PM"
SET FechaInterna3 = $ZDATETIMEH(MiFecha, 1, 3) // Formato Americano con hora en 12h AM/PM
WRITE FechaInterna1,!,FechaInterna2,!,FechaInterna3
> 66149,67164
66149,67164
66149,67164
Lógicamente, si le decimos que es un formato y le damos el valor erroneo, puede pasar cualquier cosa, como que en lugar de 9 de febrero lo entiende como 2 de septiembre.
No mezclar formatos que luego vienen los problemas.
SET MiFecha = "09/02/2022"
/// Formato Americano
SET FechaInterna = $ZDATEH(MiFecha, 1)
/// Formato Europeo
SET OtraFecha = $ZDATETIME(FechaInterna, 4)
WRITE FechaInterna,!,OtraFecha
> 66354
02/09/2022
Ni que decir, si intentamos poner fecha europea e intentar transformarlo en americano... ¿Que pasaría en San Valentín?
SET MiFecha = "14/02/2022"
SET FechaInterna = $ZDATEH(MiFecha, 1) // Formato Americano. OJO, el mes 14 no existe
^
<ILLEGAL VALUE>
Pues como todos los San Valentín.. corazones rotos, bueno... mas bién código roto.
Pues vamos a hacer algo con esto que ya has aprendido
READ !,"Por favor, indica tu fecha de nacimiento (dd/mm/yyyy): ",fechaNacimiento
SET formatoInterno = $ZDATEH(fechaNacimiento, 4)
SET diaSemana = $ZDATE(formatoInterno, 10)
SET nombreDia = $ZDATE(formatoInterno, 12)
WRITE !,"El día de la semana de tu nacimiento es: ",nombreDia
IF diaSemana = 5 WRITE "Siempre te ha gustado la fiesta!!!" // Nació un viernes
Pero mas adelante veremos otras formas de hacer las cosas, y como controlar los errores.
Próximo capítulo: Cómo viajar en el tiempo
Curiosidad
Si quieres saber porque se toma el valor de 01/01/1841 como el valor 1, viene porque fué elegido esta fecha por ser el año no bisiesto anterior al nacimiento del ciudadano estadounidense de mayor edad vivo, que era un veterano de la guerra civil con 121 años, cuando se diseñó el lenguaje de programación MUMPS, del cual extiende Object Script
Artículo
Ricardo Paiva · 31 mayo, 2022
Hola desarrolladores,
En el artículo anterior, describimos cómo utilizar config-api para configurar IRIS.
Ahora, vamos a intentar combinar la biblioteca con el cliente ZPM.
El objetivo es cargar un documento de configuración durante `zpm install` en la `configure phase`.
Para realizar este ejercicio, hay un repositorio de plantillas disponible [aquí](https://github.com/lscalese/objectscript-docker-template-with-config-api) (está basado en [objectscript-docker-template](https://github.com/intersystems-community/objectscript-docker-template)).
Tratamos de:
* Crear una base de datos `MYAPPDATA`.
* Configurar el mapeo de Globals para `dc.PackageSample.*`.
* Añadir un usuario llamado `SQLUserRO` con acceso a la función SQL de solo lectura.
* Añadir una configuración SSL denominada `SSLAppDefault`.
* Crear una aplicación REST `/rest/myapp`.
El archivo de configuración [`iris-config.json`](https://github.com/lscalese/objectscript-docker-template-with-config-api/blob/master/config-api/iris-config.json) se encuentra en el subdirectorio `config-api`. Es una plantilla vacía que se puede completar con el siguiente contenido:
```json
{
"Security.Roles":{
"MyRoleRO" : {
"Descripion" : "SQL Read Only Role for dc_PackageSample schema.",
"Resources" : "%Service_SQL:U",
"GrantedRoles" : "%SQL"
}
},
"Security.SQLPrivileges": [{
"Grantee": "MyRoleRO",
"PrivList" : "s",
"SQLObject" : "1,dc_PackageSample.*"
}],
"Security.SQLAdminPrivilegeSet" : {
"${namespace}": [
]
},
"Security.Users": {
"${sqlusr}": {
"Name":"${sqlusr}",
"Password":"$usrpwd$",
"AccountNeverExpires":true,
"AutheEnabled":0,
"ChangePassword":false,
"Comment":"Demo SQLUserRO",
"EmailAddress":"",
"Enabled":true,
"ExpirationDate":"",
"FullName":"Demo SQLUserRO",
"NameSpace":"${namespace}",
"PasswordNeverExpires":false,
"PhoneNumber":"",
"PhoneProvider":"",
"Roles":"MyRoleRO",
"Routine":""
}
},
"Security.SSLConfigs": {
"SSLAppDefault":{}
},
"Security.Applications" : {
"/rest/myapp": {
"NameSpace" : "${namespace}",
"Enabled" : 1,
"DispatchClass" : "Your.Dispatch.class",
"CSPZENEnabled" : 1,
"AutheEnabled": 32
}
},
"SYS.Databases":{
"${mgrdir}myappdata" : {
"ExpansionSize":64
}
},
"Databases":{
"MYAPPDATA" : {
"Directory" : "${mgrdir}myappdata"
}
},
"MapGlobals":{
"${namespace}": [{
"Name" : "dc.PackageSample.*",
"Database" : "MYAPPDATA"
}]
},
"Library.SQLConnection": {
}
}
```
Puedes ver el uso de `${namespace}`, `${mgrdir}`. Se trata de variables predefinidas y se evaluarán en el tiempo de ejecución con el *namespace* actual y el directorio de IRIS mgr. Otras variables predefinidas son:
* `${cspdir}`: subdirectorio CSP en el directorio de instalación de IRIS.
* `${bindir}`: directorio bin `$SYSTEM.Util.BinaryDirectory()`
* `${libdir}`: subdirectorio LIB en el directorio de instalación de IRIS.
* `${username}`: nombre de usuario actual `$USERNAME`
* `${roles}`: funciones actuales otorgadas `$ROLES`
Además, utilizamos `${sqlusr}`, no es una variable predefinida y debemos establecer el valor desde `module.xml`. Realmente no lo necesitamos, pero es solo para demostrar cómo los desarrolladores pueden definir y establecer variables desde modules.xml y transmitirlas al archivo de configuración.
Para ayudarte a crear tu propio archivo de configuración, una guía rápida ([cheat sheet `config-api.md`])(https://github.com/lscalese/objectscript-docker-template-with-config-api/blob/master/config-api.md) también está disponible en el repositorio de plantillas.
El archivo de configuración ya está listo, el siguiente paso es el archivo `module.xml`.
Tenemos que:
* Añadir el módulo `config-api` como una dependencia.
* Invocar un método para cargar nuestro archivo de configuración `./config-api/iris-config.json` con el argumento `${sqlusr}`.
```xml
objectscript-template-with-config-api
0.0.1
module
${root}config-api/iris-config.json
sqlusr
SQLUserRO
src
config-api
1.0.0
```
Echa un vistazo a la etiqueta Invoke:
```xml
${root}config-api/iris-config.json
sqlusr
SQLUserRO
```
El primer argumento es la ruta al archivo de configuración, `${root}` contiene el directorio del módulo usado por zpm durante la implementación.
Después de eso, todos los demás argumentos deben emparejarse. En primer lugar, una cadena con el nombre de la variable seguido del valor relacionado.
En nuestro ejemplo `sqlusr` para la variable `${sqlusr}` seguido del valor `SQLUserRO`.
Puedes pasar tantos argumentos emparejados como necesites.
Ten en cuenta que hay una etiqueta FileCopy: `` Esta etiqueta existe solo para forzar al zpm a empaquetar el archivo de configuración. Deberíamos agregar una etiqueta Invoke para eliminar el directorio que se va a limpiar, así:
```xml
${libdir}config-api/
```
Module.xml está listo, es hora de probarlo. Compila e inicia el contenedor `docker-compose up -d --build`.
Deberías ver estos registros durante la fase de configuración:
```
[objectscript-template-with-config-api] Configure START
2021-04-08 19:24:43 Load from file /opt/irisbuild/config-api/iris-config.json ...
2021-04-08 19:24:43 Start load configuration
2021-04-08 19:24:43 {
"Security.Roles":{
"MyRoleRO":{
"Descripion":"SQL Read Only Role for dc_PackageSample schema.",
"Resources":"%Service_SQL:U",
"GrantedRoles":"%SQL"
}
},
"Security.SQLPrivileges":[
{
"Grantee":"MyRoleRO",
"PrivList":"s",
"SQLObject":"1,dc_PackageSample.*"
}
],
"Security.SQLAdminPrivilegeSet":{
"USER":[
]
},
"Security.Users":{
"SQLUserRO":{
"Name":"SQLUserRO",
"Password":"$usrpwd$",
"AccountNeverExpires":true,
"AutheEnabled":0,
"ChangePassword":false,
"Comment":"Demo SQLUserRO",
"EmailAddress":"",
"Enabled":true,
"ExpirationDate":"",
"FullName":"Demo SQLUserRO",
"NameSpace":"USER",
"PasswordNeverExpires":false,
"PhoneNumber":"",
"PhoneProvider":"",
"Roles":"MyRoleRO",
"Routine":""
}
},
"Security.SSLConfigs":{
"SSLAppDefault":{
}
},
"Security.Applications":{
"/rest/myapp":{
"NameSpace":"USER",
"Enabled":1,
"DispatchClass":"Your.Dispatch.class",
"CSPZENEnabled":1,
"AutheEnabled":32
}
},
"SYS.Databases":{
"/usr/irissys/mgr/myappdata":{
"ExpansionSize":64
}
},
"Databases":{
"MYAPPDATA":{
"Directory":"/usr/irissys/mgr/myappdata"
}
},
"MapGlobals":{
"USER":[
{
"Name":"dc.PackageSample.*",
"Database":"MYAPPDATA"
}
]
},
"Library.SQLConnection":{
}
}
2021-04-08 19:24:43 * Security.Roles
2021-04-08 19:24:43 + Create MyRoleRO ... OK
2021-04-08 19:24:43 * Security.SQLPrivileges
2021-04-08 19:24:43 + Create {"Grantee":"MyRoleRO","PrivList":"s","SQLObject":"1,dc_PackageSample.*"} ... OK
2021-04-08 19:24:43 * Security.SQLAdminPrivilegeSet
2021-04-08 19:24:43 * Security.Users
2021-04-08 19:24:43 + Create SQLUserRO ... OK
2021-04-08 19:24:43 * Security.SSLConfigs
2021-04-08 19:24:43 + Create SSLAppDefault ... OK
2021-04-08 19:24:43 * Security.Applications
2021-04-08 19:24:43 + Create /api/config ... OK
2021-04-08 19:24:43 * SYS.Databases
2021-04-08 19:24:43 + Create /usr/irissys/mgr/myappdata ... OK
2021-04-08 19:24:43 * Databases
2021-04-08 19:24:43 + Create MYAPPDATA ... OK
2021-04-08 19:24:44 * MapGlobals
2021-04-08 19:24:44 + Create USER dc.PackageSample.* ... OK
2021-04-08 19:24:44 * Library.SQLConnection
[objectscript-template-with-config-api] Configure SUCCESS
```
Tu configuración se aplicó y el archivo de configuración se empaquetará cuando se publique. Puedes comprobarlo en el Portal de administración.
¡Gracias!
Artículo
Carlos Castro · 28 feb, 2023
Buenas a todos,
una de las herramientas potentes que tiene Intersystems es la posibilidad de implementar en el propio sistema la autenticación OAuth2. Esta herramienta nos da la posibilidad de poder controlar quien accede a nuestros recursos y como accede.
A continuación planteo una solución ante el problema de querer controlar quien accede a mis recursos y la posibilidad de monitorizarlo. Para ello deberemos seguir los siguientes pasos:1.- Definir un Servidor de Autenticacion
1.1.- Crear Servidor
1.2.- Crear Cliente del Servidor
1.3.- Crear Servidor de Recursos
1.4.- Crear cliente de Servidor de Recursos
2.- Definir un servicio SOPA/REST para acceder a los recursos.
2.1.- Crear Servicio
2.2.- Validar Acceso desde el servicio
** Os intentaré ir dejando capturas del portal de gestión para situaros mejor en el proceso. Si lo seguís secuencialmente no deberíais tener problema para completar el proceso.
Teniendo claros los pasos a seguir, ¡COMENCEMOS!
Presentación del Problema:
Supongamos que tenemos un Servicio que accede a unos recursos determinados. La publicación de este servicio es accesible por todo aquel cliente que ataque el servicio, y deseamos controlar quien accede a estos recursos, pues no queremos que todo el mundo acceda a los recursos, ademas que tampoco queremos que los clientes autorizados para acceder a los mismos puedan acceder a los recursos de otros que también tengan permisos, por lo que vamos a ponerle un "portero" al servicio para que nos asegure que este cliente tiene autorizado acceder a nuestros recursos.
Para solucionar este problema de forma ágil, haremos uso del servidor de autenticación OAuth2 que podemos utilizar en IRIS siguiendo los siguientes pasos:
1.1.- Crear Servidor
En la Producción Web seleccionamos Administración>seguridad>oauth2.0>servidor y deberíamos observar:
A continuación, necesitamos escribir en el formulario:
URL: DNS o IP del Servidor donde se va a alojar el Servidor de Autorización
Puerto: Puerto interno del Servidor Web [p.e: 57773]
El puerto del Apache interno es el que se debe utilizar, seguramente sea necesario habilitar el acceso a estos puertos con edición del fichero httpd.conf
Prefijo: Indicar la aplicación donde se va a alojar el Servidor de Autorización. De forma automática se crea la aplicación al guardar el servidor. [p.e: oauth2]
Tipos de concesiones: deja solo credenciales de cliente
Configuración SSL/TLS: Certificado SSL que tengamos
En la segunda pestaña, nombrada como “Ámbitos” define my/scope así:
Ámbito | my/scope | Introducir ámbito compatible: my/scopeDescripción | First Scope | Introducir descripción del ámbito: First Scope
Además, en la 5ª pestaña, “Personalización”; hay que configurar el cuarto valor:
Generar clase de token: %OAuth2.Server.JWT
Guardar y ya deberíamos ver que se ha creado.
En este punto, tenemos el servidor creado.
1.2.- Crear Cliente del Servidor
Estando en la pantalla de: Administración>seguridad>oauth2.0>servidor; pulsar en el botón “Descripciones de cliente”.
Esto nos permitirá indicar qué clientes pudieran autorizarse en nuestro servidor. En concreto nos creamos uno de ejemplo:
Nombre
cliente desde postman
Identificador del Cliente a nivel local.
ID del cliente
HbOAGs6byL-gTlabW8gCDguQnYyHCWT82EToGTjP5fQ
Valor del ID del Cliente.
Tipo de cliente
confidential
URL de redireccionamiento
https://DNS/IP:57773
DNS y Puerto del Servidor de Autorización configurados en el apartado 2.1
Descripción
cliente servidor de autenticación
Descripción del Cliente.
Al guardarlo deberíamos observar:
Llegados aquí, ya disponemos de un servidor de autenticación con un cliente. El proceso ahora consiste en obtener el token de acceso del servidor en base a las credenciales de acceso que acabamos de crear. Una vez obtenemos este token, deberemos validarlo al intentar acceder a los recursos.
Procedimiento para obtener el token:A) Atacar por REST a: https://DNS/IP:57773/oauth2/token?grant_type=client_credentials&scope=my/scope
con datos:
Authorizacion= Basic OAuth
Username= clientID (obtenido de la definición del cliente creada en el paso 1.2)
Password=SecretID (obtenido de la definición del cliente creada en el paso 1.2)
Esta petición nos devolverá un TOKEN de acceso que tendremos que validar a continuación.
1.3.- Crear Servidor de Recursos
En Sistema > Gestión de seguridad > Cliente de OAuth 2.0 > Descripción del servidor - (Configuración de seguridad)
1.4.- Crear cliente de Servidor de Recursos
Con el objetivo de definir a qué información se accede una vez el Sistema Externo se ha validado, se construye el “Servidor de Recursos”.
Para generar el Servidor de Recursos, debemos estar en: Sistema > Gestión de seguridad > Configuración del servidor de autorización OAuth 2.0 > Servidor de OAuth 2.0 y pulsar en el botón “Crear la descripción de cliente” tal y como se muestra en la siguiente captura:
NOMBRE: resserver
Descripción: [Texto descriptivo]
Tipo de cliente: Servidor de recursos
Tipo de concesiones: Código de autorización
Tipo de concesiones compatibles: código
Tipo de autenticación: básico
Llegados a este punto, Ya hemos definido un servidor de autenticación y un servidor de recursos, ¡Vamos a unirlo todo!
A continuación debemos incluir en nuestro servicio SOAP o REST el control para validar el token generado por el servidor de autenticación, para ello incluiremos en nuestro servicio lo siguiente:
Parameter OAUTH2APPNAME = "resserver";
Parameter OAUTH2SCOPES = "openid profile email my/scope";
Dentro del método del servicio al principio:
set authorization = $tr(pInput.GetAttribute("authorization"),"")set tokenSinBearer = $PIECE(authorization,"Bearer ",2)
set tSC= ..ResServer(tokenSinBearer)
Método:
ClassMethod ResServer(accessToken As %String(MAXLEN="")) As %Status{ // validate token set isJWTValid = ##class(%SYS.OAuth2.Validation).ValidateJWT("resserver",accessToken,"","",.jwtPayload ,.securityParameters,.sc) if (('isJWTValid) || ($$$ISERR(sc))) { quit '$$$OK } // introspection set sc = ##class(%SYS.OAuth2.AccessToken).GetIntrospection("resserver", accessToken, .jsonObject) if $$$ISERR(sc) { quit '$$$OK } quit $$$OK}
Con este método lo que vamos a realizar es la validación del token recibido en el servicio contra el servidor de autenticación. La respuesta tSC es la que deberemos validar para dar acceso o no al resto de recursos a los que accede el servicio, de una forma esquematizada sería: Method misRecursos(pInput As %Stream.Object, Output pOutput As %Stream.Object) As %Status
{
set authorization = $tr(pInput.GetAttribute("authorization"),"")set tokenSinBearer = $PIECE(authorization,"Bearer ",2)
set tSC= ..ResServer(tokenSinBearer)
if (tSC= 1){
ACCESO A LOS RECURSOS
quit pOutput
}else{
quit '$$$OK
}
}
En resumidas cuentas, el servidor de autenticación OAUTH2 nos sirve para generar un token en base al clientID y secretID que hemos generado al crear el cliente para el mismo Servidor OUATH2. Este Token seguidamente lo enviaremos al servidor de recursos que previo al acceso de los mismos, validara este token contra el servidor de autenticación, y si es correcto, nos dará acceso a los mismos.
El proceso completo os lo dejo aquí indicado:
Finalmente si quisiéramos aplicar algún control extra, lo podemos realizar dentro del Business Process incluyéndole en el control la información de clientID que queramos monitorizar y/o validar.
Pregunta
Yone Moreno · 17 abr, 2023
Buenas tardes,
Antes que nada, muchísimas gracias por leer esta duda, y sobre todo por dedicar tiempo en entenderla y en responderla. Gracias.
Por favor, necesitaríamos su ayuda. Actualmente estamos desarrollando una Integración REST, y se nos da un caso que nos gustaría comentar con ustedes, a fin de hallar pistas, documentación, ejemplos, o mecanismos para gestionarlo y depurarlo.
En la Operación REST recibimos:
[{"codigo":"5128","descripcion":"LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL","programa":"Probabilidad de contagio ante un accidente hemático.","admitido":1,"desdefecha":"26/10/2022","hastafecha":"26/10/2029","cursohorario":[{"aula":"AULA 1","desdefecha":"2022/12/26","hastafecha":"2022/12/26","desdehora":"16:00:00","hastahora":"20:30:00"},{"aula":"AULA 2","desdefecha":"27/10/2022","hastafecha":"27/12/2022","desdehora":"10:00:00","hastahora":"12:45:00"}],"error":null},{"codigo":"5129","descripcion":"HISTORIA DE SALUD ELECTRONICA DRAGO-AP","programa":"XXXX","admitido":0,"desdefecha":"15/01/2022","hastafecha":"15/01/2029","cursohorario":[{"aula":"AULA MAGNA","desdefecha":"15/01/2022","hastafecha":"15/01/2022","desdehora":"16:00:00","hastahora":"20:30:00"}],"error":null}]
E ingenuamente transformamos el String a objeto Mensaje Response mediante:
// se transforma el objeto JSON a un objeto local
set claseAux = ##class(%ZEN.Auxiliary.jsonProvider).%New()
set tSC= claseAux.%ConvertJSONToObject(.linea,"Mensajes.Response.miFormacion.GetCursosAdmitidosResponse",.pResponse,1)
Lo cual implica Excepción:
Id.:
273670753
Tipo:
Error
Texto:
ERROR #5002: Error de cache: <LIST>%GetSerial+1^%Library.ListOfObjects.1
Registrado:
2023-04-17 14:04:55.913
Origen:
Operaciones.REST.miFormacionv01r00
Sesión:
19206868
Job:
423529
Clase:
Ens.MessageHeader
Método:
NewResponseMessage
Seguimiento paso a paso:
(ninguno)
Pila:
$$^%GetSerial+1^%Library.ListOfObjects.1 +4
$$^%SerializeObject+2^%Library.ListOfObjects.1 +1
$$^%GetSwizzleObject+13^%Library.ListOfObjects.1 +9
$$^zNewResponseMessage+5^Ens.MessageHeader.1 +1
$$^zMessageHeaderHandler+208^Operaciones.REST.miFormacionv01r00.1 +1
$$^zOnTask+42^Ens.Host.1 +1
DO^zStart+62^Ens.Job.1 +2
Para recapitular, el Sistema Destino nos responde con esta String; la cual sí es un JSON válido tal y como se puede comrpobar en https://jsonlint.com/
[
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
"desdefecha": "2022/12/26",
"hastafecha": "2022/12/26",
"desdehora": "16:00:00",
"hastahora": "20:30:00"
},
{
"aula": "AULA 2",
"desdefecha": "27/10/2022",
"hastafecha": "27/12/2022",
"desdehora": "10:00:00",
"hastahora": "12:45:00"
}
],
"error": null
},
{
"codigo": "5129",
"descripcion": "HISTORIA DE SALUD ELECTRONICA DRAGO-AP",
"programa": "XXXX",
"admitido": 0,
"desdefecha": "15/01/2022",
"hastafecha": "15/01/2029",
"cursohorario": [
{
"aula": "AULA MAGNA",
"desdefecha": "15/01/2022",
"hastafecha": "15/01/2022",
"desdehora": "16:00:00",
"hastahora": "20:30:00"
}
],
"error": null
}
]
Siendo nuestros Mensaje Response:
Class Mensajes.Response.miFormacion.GetCursosAdmitidosResponse Extends Ens.Response
{
Property cursos As list Of EsquemasDatos.miFormacion.CursoAdmitido;
Y el EsquemasDatos interno:
Class EsquemasDatos.miFormacion.CursoAdmitido Extends Ens.Response
{
Property codigo As %String(MAXLEN = "");
Property descripcion As %String(MAXLEN = "");
Property programa As %String(MAXLEN = "");
Property admitido As %Boolean;
Property desdefecha As %String(MAXLEN = "");
Property hastafecha As %String(MAXLEN = "");
Property cursohorario As list Of EsquemasDatos.miFormacion.CursoHorario;
Property error As EsquemasDatos.Seguridad.Error;
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
Para tratar de resolverlo por nosotros mismos hemos considerado utilizar alguna herramienta para convertir el objeto JSON en un objeto que podamos manipular en nuestro código. Por ejemplo, en lugar de utilizar la clase %ZEN.Auxiliary.jsonProvider, podríamos utilizar la clase %Object, que es una clase básica de InterSystems IRIS que permite trabajar con objetos de forma dinámica. El código es:
Set pResponse = {} // Creamos un objeto vacío
Set tSC = pResponse.%FromJSON(linea) // Convertimos la respuesta JSON en un objeto
If $$$ISERR(tSC) {
Write "Error al convertir respuesta JSON: ", tSC.GetErrorText(), !
Quit
}
$$$LOGINFO("pResponse: "_pResponse)
Sin embargo de esta forma, sí es verdad que el LOGINFO pinta que disponemos de:
Info
2023-04-17 15:10:46.749
pResponse: 8@%Library.DynamicObject
Ahora bien, en el visor de mensaje se nos ve en azul:
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
¿Pudiera ser que la respuesta JSON de Sistema Destino, no coincida con la definición interna hecha a mano del Mensaje Response y el EsquemasDatos?
Lo pregunto porque: la excepción que se muestra parece indicar que hay un problema al serializar la lista de objetos en Cache ObjectScript. Para depurar este problema, es posible que necesite verificar si la definición de clase de "Mensajes.Response.miFormacion.GetCursosAdmitidosResponse" coincide con el formato del objeto JSON devuelto por el servicio REST. Lo cual en teoría puede hacerse comparando la definición de clase con el objeto JSON.
➕🔎🔍Además hemos indagado:
https://community.intersystems.com/post/convert-string-json
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GJSON_INTRO
https://community.intersystems.com/post/how-convert-xml-string-json-object-script
¿De qué manera nos aconsejan depurar, documentarnos, buscar la causa de la Excepción?
Muchísimas gracias por su ayuda. Y muchas gracias por respondernos con pasos a seguir, documentación y/o ejemplos. Gracias porque es un alivio contar con el apoyo de ustedes que tienen gran cantidad de práctica, experiencia y conocimientos valiosísimos y muy únicos.
Un saludo. Buenas Yone. Sospecho que quizás el problema pueda estar en la definición del objeto GetCursosAdmitidosResponse, según su definición tiene una propiedad cursos:
Property cursos As list Of EsquemasDatos.miFormacion.CursoAdmitido;
Pero lo que enviais para hacer el mapeo de json al objeto comienza así:
[
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
Cuando debiera ser:
"cursos": [
{
"codigo": "5128",
"descripcion": "LAS ENFERMERAS FRENTE A LOS PROBLEMAS DE SALUD MENTAL",
"programa": "Probabilidad de contagio ante un accidente hemático.",
"admitido": 1,
"desdefecha": "26/10/2022",
"hastafecha": "26/10/2029",
"cursohorario": [
{
"aula": "AULA 1",
Yo probaría modificando el JSON que estáis enviado para ver si es ese el problema. ¡Hola Yone! ¿Funcionó la solución propuesta?
Artículo
Alberto Fuentes · 24 mayo, 2019
Al igual que con Pattern Matching, se pueden utilizar Expresiones Regulares para identificar patrones en textos en ObjectScript, sólo que con una potencia mucho mayor.
En este artículo se proporciona una breve introducción sobre las Expresiones Regulares y lo que puede hacerse con ellas en ObjectScript. La información que se proporciona aquí se basa en varias fuentes, entre las que destaca el libro “Mastering Regular Expressions” (Dominando las expresiones regulares) escrito por Jeffrey Friedl y, por supuesto, la documentación online de la plataforma. El procesamiento de textos utiliza patrones que algunas veces pueden ser complejos. Cuando utilizamos expresiones regulares, normalmente tenemos varias estructuras: el texto en el que buscamos los patrones, el patrón en sí mismo (la expresión regular) y las coincidencias (las partes del texto que coinciden con el patrón). Para que sea más sencillo distinguir entre estas estructuras, se utilizan las siguientes convenciones a lo largo de este documento:
Los ejemplos en el texto se muestran en monospace y sin comillas adicionales:
Esta es una "cadena de texto" en la cual deseamos encontrar "algo".
A menos que sean evidentes, las expresiones regulares que estén dentro del texto principal se visualizan en un fondo gris, como en el siguiente ejemplo: \".*?\".
Cuando sea necesario, las coincidencias se resaltarán mediante colores diferentes:
Esta es una "cadena de texto" en la cual deseamos encontrar "algo".
Los ejemplos de códigos que sean más grandes se mostrarán en cuadros:
set t="Esta es una ""cadena de texto"" en la cual queremos encontrar ""algo""."set r="\"".*?\"""w $locate(t,r,,,tMatch)
1. Un poco de historia (y algunas curiosidades)
A principios de los años 40, los neurofisiólogos desarrollaron modelos para representar el sistema nervioso de los humanos. Unos años después, un matemático describió estos modelos mediante expresiones algebraicas que a las que llamó “conjuntos regulares”. La notación que se utilizó en estas expresiones algebraicas se denominó “expresiones regulares”.
En 1965, las expresiones regulares se mencionaron por primera vez en el contexto de la informática. Con qed, un editor que formó parte del sistema operativo UNIX, el uso de las expresiones regulares se extendió. Las siguientes versiones de ese editor proporcionan secuencias de comandos g/expresiones regulares/p (global, expresión regular, imprimir), las cuales realizan búsquedas de coincidencias de las expresiones regulares en todas las líneas del texto y muestran los resultados. Estas secuencias de comandos se convirtieron finalmente en la utilidad grep.
Hoy en día, existen varias implementaciones de expresiones regulares (RegEx) para muchos lenguajes de programación.
2. Regex 101
En esta sección se describen los componentes de las expresiones regulares, su evaluación y algunos de los motores disponibles. Los detalles de cómo utilizarlos se describen en la sección 4.
2.1. Componentes de las expresiones regulares
2.1.1. Metacaracteres
Los siguientes caracteres tienen un significado especial en las expresiones regulares.
. * + ? ( ) [ ] \ ^ $ |
Para utilizarlos como valores literales, deben utilizarse utilizando la contrabarra \. También pueden definirse explícitamente secuencias de valores literales utilizando \Q <literal sequence> \E.
2.1.2. Valores literales
El texto normal y caracteres de escape se tratan como valores literales, por ejemplo:
abc
abc
\f
salto de página
\n
salto de línea
\r
retorno de carro
\r
tabulación
\0+tres dígitos (por ejemplo,\0101)
Número octal.El motor de RegEx que utiliza Caché / IRIS (ICU), es compatible con los números octales hasta el \0377 (255 en el sistema decimal). Al migrar expresiones regulares desde otro motor hay que considerar cómo procesa los octales dicho motor.
\x+dos dígitos (por ejemplo, \x41)
Número hexadecimal.La biblioteca ICU cuenta con varias opciones para procesar los números hexadecimales, consulte los documentos de apoyo sobre ICU (los enlaces están disponibles en la sección 5.8)
2.1.3. Anclas
Las anclas (anchors) permiten encontrar ciertos puntos en un texto/cadena, por ejemplo:
\A Inicio de la cadena
\Z Final de la cadena
^ Inicio del texto o línea
$ Final de un texto o línea
\b Límite de palabras
\B No en un límite de palabras
\< Inicio de una palabra
\> Final de una palabra
Algunos motores de regex se comportan de forma diferente, por ejemplo, al definir lo que constituye exactamente una palabra y cuáles caracteres se consideran delimitadores de palabras.
2.1.4. Cuantificadores
Con los cuantificadores, puede definir qué tan frecuentemente el elemento anterior puede crear una coincidencia:
{x} exactamente x número de veces
{x,y} mínimo x, máximo y número de veces
* 0 ó más, es equivalente a {0,}
+ 1 ó más, es equivalente a {1,}
? 0 ó 1
Codicia
Se dice que los cuantificadores son “codiciosos” (greedy), ya que toman tantos caracteres como sea posible. Supongamos que tenemos la siguiente cadena de texto y queremos encontrar el texto que está entre las comillas:
Este es "un texto" con "cuatro comillas".
Debido a la naturaleza codiciosa de los selectores, la expresión regular \".*\" encontrará demasiado texto:
Este es "un texto" con "cuatro comillas".
En este ejemplo, la expresión regular .* pretende incluir tantos caracteres que se encuentren entre comillas como sea posible. Sin embargo, debido a la presencia del selector punto ( . ) también buscará las comillas, de modo que no obtendremos el resultado que deseamos.
Con algunos motores de regex (incluyendo el que utiliza Caché / IRIS) se puede controlar la codicia de los cuantificadores, al incluir un signo de interrogación en ellos. Entonces, la expresión regular \".*?\" ahora hace que coincidan las dos partes del texto que se encuentran entre las comillas, es decir, exactamente lo que buscábamos:
Este es "un texto" con "cuatro comillas".
2.1.5. Clases de caracteres (rangos)
Los corchetes se utilizan para definir los rangos o conjuntos de caracteres, por ejemplo, [a-zA-Z0-9] o [abcd] . En el lenguaje de las expresiones regulares esta notación se refiere a una clase de caracteres. Un rango coincide con los caracteres individuales, de modo que el orden de los caracteres dentro de la definición del rango no es importante: [dbac] devuelve tantas coincidencias como [abcd].
Para excluir un rango de caracteres, simplemente se coloca el símbolo ^ frente a la definición del rango (¡dentro de los corchetes!): [^abc] buscará todas las coincidencias excepto con a, b o c.
Algunos motores de regex proporcionan clases de caracteres previamente definidas (POSIX), por ejemplo:
[:alnum:] [a-zA-z0-9]
[:alpha:] [a-zA-Z]
[:blank:] [ \t]
…
2.1.6. Grupos
Los componentes de una expresión regular pueden agruparse utilizando un par de paréntesis. Esto resulta útil para aplicar cuantificadores a un grupo de selectores, así como para referirse a los grupos tanto desde las expresiones regulares como desde ObjectScript. Los grupos también pueden anidarse.
Las siguientes coincidencias en las cadenas regex consisten en un número de tres dígitos, seguidos por un guion, a continuación, tres pares de letras mayúsculas y un dígito, seguidas por un guion, y para finalizar el mismo número de tres dígitos como en la primera parte:
([0-9]{3})-([A-Z][[0-9]){3}-\1
El siguiente ejemplo muestra cómo utilizar las referencias previas para que coincidan no solamente la estructura, sino también el contenido: el punto de referencia (en morado) le indica al motor que busque el mismo número de tres dígitos al principio y al final de la cadena (en amarillo). El ejemplo también muestra cómo aplicar un cuantificador a las estructuras más complejas (en verde).
La expresión regular de arriba coincidiría con la siguiente cadena:
123-D1E2F3-123
Pero no coincidiría con las siguientes cadenas:
123-D1E2F3-456 (los últimos tres dígitos son diferentes de los primeros tres)
123-1DE2F3-123 (la parte central no consiste en tres letras/pares de dígitos)
123-D1E2-123 (la parte central incluye únicamente dos letras/pares de dígitos)
Los grupos también puede accederse utilizando los búferes de captura desde ObjectScript ( sección 4.5.1). Esta es una característica muy útil que permite buscar y extraer información al mismo tiempo.
2.1.7. Alternancia
Con el carácter de barra vertical se especifica alternancia entre opciones, por ejemplo, skyfall|done. Esto permite comparar expresiones más complejas, como las clases de caracteres que se describieron en la sección 3.1.5.
2.1.8. Referencias
Los referencias permiten consultar grupos previamente definidos (selectores dentro de los paréntesis). En el siguiente ejemplo se muestra una expresión regular que coincide con tres caracteres consecutivos, los cuales deben ser iguales:
([a-zA-Z])\1\1
Los puntos de referencia se especifican con \x, donde x representa la expresión x-ésima entre paréntesis.
2.1.9. Reglas de precedencia
[] antes de ()
, + y ? antes de una secuencia: ab es equivalente a a(b*), y no a (ab)*
Secuencia antes de una alternancia: ab|c es equivalente a (ab)|c, y no a a(b|c)
2.2. Un poco de teoría
La evaluación de las expresiones regulares, por lo general, se realiza mediante la implementación de alguno de los dos siguientes métodos (las descripciones se han simplificado, en las referencias que se mencionan en el capítulo 5 se pueden encontrar más detalles):
Orientado por el texto (DFA – Autómata finito determinista) El motor de búsqueda avanza a través del texto de entrada, carácter por carácter, e intenta encontrar coincidencias en el texto recorrido hasta el momento. Cuando llega al final del texto que de entrada, declara que el proceso se realizó con éxito.
Orientado por las expresiones regulares (NFA – Autómata finito no determinista) El motor de búsqueda avanza a través de la expresión regular, token por token, e intenta aplicarla al texto. Cuando llega al último token (y encuentra todas las coincidencias), declara que el proceso se realizó con éxito.
El método 1 es determinista, su tiempo de ejecución depende únicamente de la longitud del texto introducido. El orden de los selectores en las expresiones regulares no influye en el tiempo de ejecución.
El método 2 no es determinista, el motor reproduce todas las combinaciones posibles de los selectores en la expresión regular, hasta que encuentre una coincidencia o se produzca algún error. Por ende, este método es particularmente lento cuando no encuentra una coincidencia (debido a que tiene que reproducir todas la combinaciones posibles). El orden de los selectores influye en el tiempo de ejecución. Sin embargo, este método le permite retroceder y capturar los búferes.
2.3. Motores de búsqueda
Existen muchos motores de regex disponibles, algunos ya están incorporados en los lenguajes de programación o en los sistemas operativos, otros son librerías que pueden utilizarse en casi cualquier parte. Aquí se muestran algunos motores de regex, los cuales se agruparon por el tipo de método de evaluación:
DFA: grep, awk, lex
NFA: Perl, Tcl, Python, Emacs, sed, vi, ICU
En la siguiente tabla se realiza una comparación de las características que están disponibles en regex, entre varias bibliotecas y en lenguajes de programación:
Puede encontrar más información aquí: https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines
3. RegEx y Caché
InterSystems Caché/IRIS utiliza la biblioteca ICU para buscar expresiones regulares, en los documentación online se describen muchas de estas características. El objetivo de las siguientes secciones es hacer una introducción rápida sobre cómo utilizarlas.
3.4. $match() y $locate()
En ObjectScript (COS), las funciones $match() y $locate() brindan acceso directo a la mayoría de las características de regex a las que se tiene acceso desde la biblioteca de ICU. $match(String, Regex) busca en la cadena de entrada un patrón de regex. Cuando la función encuentra una coincidencia devuelve 1, en caso contrario devuelve 0.
Ejemplos:
w $match("baaacd",".*(a)\1\1.*") devuelve 1
w $match("bbaacd",".*(a)\1\1.*") devuelve 0
$locate(String,Regex,Start,End,Value) busca en la cadena de entrada un patrón de regex, del mismo modo que lo hace $match(). Sin embargo, $locate() proporciona mayor control del proceso y también devuelve más información. En Start, se puede indicar a $locate en qué punto de la cadena de entrada debe comenzar la búsqueda. Cuando $locate() encuentra una coincidencia, regresa al punto donde se encuentra el primer carácter de la coincidencia y establece End para la siguiente posición del carácter después de la coincidencia. El contenido de la coincidencia es devuelto en Value.
Si $locate() no encuentra alguna coincidencia devuelve 0 y no tocará los contenidos de End y Value (si se especificaron). End y Value se pasan por referencia, por lo que hay que ser cuidadoso si utilizan continuamente (por ejemplo en bucles).
Ejemplo:
w $locate("abcdexyz",".d.",1,e,x) devuelve 3, e se establece en 6, x se establece en "cde"
$locate() realiza la búsqueda de coincidencias en los patrones y puede devolver el contenido de la primera coincidencia, al mismo tiempo. Si necesita extraer el contenido de todas las coincidencias, puede llamar a $locate() continuamente mediante un bucle o puede utilizar los métodos que se proporcionan en %Regex.Matcher (siguiente sección).
3.5.% Regex.Matcher
%Regex.Matcher proporciona acceso a funciones de regex de la librería ICU del mismo modo que lo hacen las funciones $match() y $locate() pero además cuenta con características avanzadas. En las siguientes secciones se retomará el concepto de los búferes de captura, se analizará la posibilidad de sustituir cadenas con expresiones regulares y las formas para controlar el comportamiento del tiempo de ejecución.
3.5.1. Búferes de captura
Como ya hemos visto con los grupos, las referencias y $locate(), las expresiones regulares le permiten buscar simultáneamente los patrones en el texto y devolver el contenido con las coincidencias. Esto funciona al colocar las partes del patrón que desea extraer entre un par de paréntesis (agrupación). Si la búsqueda de coincidencias es exitosa, los búferes de captura tendrán el contenido de todos los grupos en los que se hayan encontrado coincidencias. Hay que tener en cuenta que este procedimiento es un poco diferente de lo que $locate() proporciona con sus parámetros de valores: $locate() devuelve el contenido de todas la coincidencias en sí, mientras que los búferes de captura le dan acceso a algunas partes de las coincidencias (los grupos).
Para utilizarlo, hay crear un objeto a partir de la clase %Regex.Matcher y pasar la expresión regular y la cadena de entrada. Después, puede llamar a uno de los métodos proporcionados por %Regex.Matcher para que realice el trabajo.
Ejemplo 1 (grupos simples):
set m=##class(%Regex.Matcher).%New("(a|b).*(de)", "abcdeabcde")
w m.Locate() returns 1
w m.Group(1) returns a
w m.Group(2) returns de
Ejemplo 2 (grupos anidados y referencias):
set m=##class(%Regex.Matcher).%New("((a|b).*?(de))(\1)", "abcdeabcde")
w m.Match() returns 1
w m.GroupCount returns 4
w m.Group(1) returns abcde
w m.Group(2) returns a
w m.Group(3) returns de
w m.Group(4) returns abcde
(hay que tener en cuenta el orden de los grupos anidados, porque el paréntesis de apertura marca el inicio de un grupo, los grupos internos tienen un índice numérico mayor que los externos)
Como se mencionó anteriormente, los búferes de captura son una característica muy poderosa, ya que permiten buscar coincidencias en los patrones y extraer el contenido de las coincidencias al mismo tiempo. Sin las expresiones regulares, tendría que buscar las coincidencias como se indica en el paso uno (por ejemplo, utilizando el operador para coincidencias de patrones) y extraer el contenido de las coincidencias (o partes de las mismas) basándose en algunos de los criterios que se indican en el paso dos.
Si necesita agrupar alguna parte de su patrón (por ejemplo, para aplicarle un cuantificador a esa parte), pero no desea rellenar un búfer de captura con el contenido de la parte coincidente, puede definir el grupo como "sin captura" o "pasivo" al colocarle un signo de interrogación seguido de dos puntos adelante del grupo, como en el ejemplo 3, el cual se muestra a continuación.
Ejemplo 3 (grupo pasivo):
set m=##class(%Regex.Matcher).%New("((a|b).*?(?:de))(\1)","abcdeabcde")
w m.Match() returns 1
w m.Group(1) returns abcde
w m.Group(2) returns a
w m.Group(3) returns abcde
w m.Group(4) returns <REGULAR EXPRESSION>zGroupGet+3^%Regex.Matcher.1
3.5.2. Reemplazar
%Regex.Matcher también proporciona métodos para reemplazar rápidamente el contenido de las coincidencias: ReplaceAll() y ReplaceFirst():
set m=##class(%Regex.Matcher).%New(".c.","abcdeabcde")
w m.ReplaceAll("xxxx") returns axxxxeaxxxxe
w m.ReplaceFirst("xxxx") returns axxxxeabcde
También puede referirse a los grupos en sus cadenas de reemplazo. Si añadimos un grupo al patrón del ejemplo anterior, podemos referirnos a su contenido al incluir $1 en la cadena de reemplazo:
set m=##class(%Regex.Matcher).%New(".(c).","abcdeabcde")
w m.ReplaceFirst("xx$1xx") returns axxcxxeabcde
Se puede utilizar $0 para incluir todo el contenido de la coincidencia en la cadena de reemplazo:
w m.ReplaceFirst("xx$0xx") returns axxbcdxxeabcde
3.5.3. La propiedad OperationLimit
En la sección 3.2 hablamos sobre los dos métodos para evaluar una expresión regular (DFA y NFA). El motor regex que se utiliza en Caché es un Autómata finito no determinista (NFA, por sus siglas en inglés). Por lo tanto, el tiempo requerido para evaluar varias expresiones regulares en una determinada cadena de entrada puede variar. [1]
Se puede utilizar la propiedad OperationLimit de un objeto %Regex.Matcher para limitar el número de unidades de ejecución (llamados clusters). El tiempo exacto para la ejecución de un cluster depende del entorno. Por lo general, el tiempo requerido para la ejecución de un cluster es de unos cuantos milisegundos. La propiedad OperationLimit se establece de manera predeterminada en 0 (sin límites).
3.6. Un ejemplo de la vida real: la migración de Perl hacia Caché
Esta sección describe la parte relacionada con regex en un caso de migración de Perl a Caché. En este caso, el script en Perl consiste en docenas de expresiones regulares con mayor o menor complejidad, las cuales se utilizaban tanto para buscar coincidencias como para extraer contenido.
Si no existieran funciones de regex disponibles en Caché, el proyecto de migración implicaría un enorme esfuerzo. Sin embargo, las funciones de regex están disponibles en ObjectScript, y las expresiones regulares de los scripts en Perl pueden utilizarse casi sin realizar modificaciones.
Aquí se muestra una parte del script en Perl:
El único cambio necesario en las expresiones regulares para la migración de Perl hacia Caché fue en el modificador /i- (este provoca que regex no distinga entre las mayúsculas y minúsculas), el cual tuvo que moverse desde el final hacia el principio en las expresiones regulares.
En Perl, el contenido de los búferes de captura se copia en variables especiales ($1 y $2 en el código de Perl que vimos anteriormente). Casi todas las expresiones regulares en el proyecto Perl utilizaban este mecanismo. Para emularlo, se escribió un simple método de encapsulamiento en ObjectScript. Este método utiliza a %Regex.Matcher para evaluar una expresión regular contra una cadena de texto y devuelve el contenido del buffer de captura en forma de una lista ($lb()).
El código correspondiente en ObjectScript es:
if ..RegexMatch(
tVCSFullName,
"(?i)[\\\/]([^\\^\/]+)[\\\/]ProjectDB[\\\/](.+)[\\\/]archives[\\\/]",
.tCaptureBufferList)
{
set tDomainPrefix=$zcvt($lg(tCaptureBufferList,1), "U")
set tDomain=$zcvt($lg(tCaptureBufferList,2), "U")
}
…
Classmethod RegexMatch(pString as %String, pRegex as %String, Output pCaptureBuffer="") {
#Dim tRetVal as %Boolean=0
set m=##class(%Regex.Matcher).%New(pRegex,pString)
while m.Locate() {
set tRetVal=1
fori=1:1:m.GroupCount {
set pCaptureBuffer=pCaptureBuffer_$lb(m.Group(i))
}
}
quit tRetVal
}
4. Información de referencia
4.7. Información general
Información general y tutoriales:
http://www.regular-expressions.info/engine.html
Tutoriales y ejemplos:
http://www.sitepoint.com/demystifying-regex-with-practical-examples/
Comparaciones entre varios motores regex:
https://en.wikipedia.org/wiki/Comparison_of_regular_expression_engines
Hoja de referencia:
https://www.cheatography.com/davechild/cheat-sheets/regular-expressions/pdf/
Libros:
Jeffrey E. F. Friedl: “Mastering Regular Expressions (Dominando las expresiones regulares)” (consulte http://regex.info/book.html)
4.8. Documentación online
Resumen sobre "El uso de las expresiones regulares en Caché": http://docs.intersystems.com/latest/csp/docbook/ DocBook.UI.Page.cls?KEY=GCOS_regexp
Documentación sobre $match(): http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fmatch
Documentación sobre $locate(): http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_flocate
Documentación de la clase %Regex.Matcher: http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?APP=1&LIBRARY=%25SYS&CLASSNAME=%25Regex.Matcher
4.9. ICU
Como se mencionó anteriormente, InterSystems Caché/IRIS utiliza los motores de ICU. La información completa está disponible en línea:
http://userguide.icu-project.org/strings/regexp
http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Metacharacters
http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Operators
http://userguide.icu-project.org/strings/regexp#TOC-Replacement-Text
http://userguide.icu-project.org/strings/regexp#TOC-Flag-Options
This is excellent as far as my poor Spanish (inherited from Italian) allows.It would be great to see it also in the English version. It's already available in English! https://community.intersystems.com/post/using-regular-expressions-cach%C3%A9This here is our Spanish version of the great article by Michael Brosdorf
Este artículo está etiquetado como "Mejores prácticas" ("Best practices")
(Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).
Artículo
Javier Lorenzo Mesa · 11 mar, 2020
¡Hola a tod@s!
El desarrollo completo en JavaScript (Full-Stack) permite crear aplicaciones de última generación con Caché. En cualquiera de las aplicaciones (web) que se desarrollan hoy en día, hay que tomar muchas decisiones estructurales y debemos saber cuales son las decisiones correctas. Con el conector Node.js disponible para Caché, se puede crear un potente servidor de aplicaciones, que permitirá utilizar la última tecnología de JavaScript y marcos de aplicaciones (frameworks) tanto del lado del cliente como del servidor.
Con todas estas nuevas tecnologías, lo más importante es integrarlas de la manera más eficiente posible y que permitan generar una experiencia de desarrollo muy productiva. Este artículo pretende introducirte paso a paso en la tecnología Node.js.
Antes de empezar a desarrollar aplicaciones con Node.js para Caché, lo primero que hay que hacer es configurar un entorno.
Para utilizar Node.js con Caché, se necesita un módulo (conector) Node.js para Caché.
En cada kit de distribución de Caché, encontrarás diferentes versiones de este módulo en el directorio bin de Caché, los cuales se llaman, por ejemplo: cache0100.node, cache0120.node... Como las versiones de Node.js se actualizan con frecuencia, recomiendo que primero solicites al Centro de Soporte Internacional (WRC) que te envíen la última versión.
Una vez que recibas del WRC el paquete zip con la última versión, verás que hay diferentes números de versión dentro del directorio bin. No es difícil elegir la versión correcta, pero es muy importante saber que estos módulos son nativos y que la plataforma de Caché, el procesador (x86 o x64) y la versión del módulo Node.js deben coincidir con tu sistema. Este es un paso muy importante a tener en cuenta, pues de lo contrario el conector de Node.js no funcionará en tu sistema.
Las versiones de Node.js están disponibles en dos opciones: las "LTS" y las "actuales". Para nuestras aplicaciones, siempre recomiendo utilizar las LTS, ya que son las más adecuadas para usarse con Caché (proporcionan soporte a largo plazo y estabilidad).
* Si deseas conocer todos los detalles sobre las versiones, puedes encontrar un resumen de todas las versiones en la página web de Node.js. Verás que es una lista larga, pero solo necesitas la última versión de LTS (incluso con los principales números de la versión, que actualmente son v6.x.x y v4.x.x). No utilices las versiones principales que tengan números impares, son versiones que no están diseñadas para usarse en programación y se utilizan para introducir y probar las funciones más recientes de JavaScript la versión 8.
Dos ejemplos:
si utilizas Caché 2016.2 ejecutándose en un sistema Windows x64 y quieres utilizar Node.js v6.9.4, necesitarás que el archivo cache610.node esté en el directorio bin\winx64.
si utilizas Caché 2008.2 ejecutándose en un sistema Windows x86 y quieres utilizar Node.js v4.8.2, necesitarás que el archivo cache421.node esté dentro del directorio bin\winx86.
Ahora describiré paso a paso cómo descargar e instalar Node.js en un sistema Windows x64 y conectarlo con Caché.
Primero, se comienza con la descarga del último Node.js LTS release:
La versión actual es la v6.10.2, solo hay que hacer clic en el botón verde para descargarla. Instala esta versión en tu sistema con la configuración predeterminada, asegurándote de que la opción "add to PATH" esté instalada/verificada:
Ahora que has instalado el entorno de ejecución (runtime) en Node.js, se puede verificar si se instaló correctamente al abrir una línea de comandos y verificar la versión del nodo:
Como se puede ver, actualmente tengo instalada la versión Node.js v6.9.1 en mi sistema.
Por cierto, Node.js ahora está instalado dentro de tu sistema de Windows (en la carpeta "Archivos de programa").
En el portal de gestión de Caché, también deben asegurarse de que el %Service_CallIn se encuentra entre tus servicios de seguridad.
Cuando se inicia una nueva aplicación con Node.js, es una práctica habitual crear primero un nuevo directorio para la aplicación. Lo primero que necesitamos crear es una pequeña aplicación/script en Node.js para probar la conexión entre Node.js y Caché. Este es el paso más importante para comprobar, en primer lugar, si nuestra configuración de Node.js funciona bien antes de comenzar con el desarrollo de otros módulos en Node.js.
Crea un directorio vacío para la prueba, por ejemplo C:\Temp\nodetest
Crea un subdirectorio node_modules (C:\Temp\nodetest\node_modules)
Inserta el cache610.node correcto (que se encuentra dentro del directorio bin\winx64 en el archivo del conector Node.js para Caché) dentro del directorio node_modules y cambia su nombre a cache.node. En todas sus aplicaciones Node.js del lado del servidor, siempre es necesario que el conector para Caché cambie de nombre a cache.node, independientemente de la versión de Node.js, el procesador y sistema operativo en el que se esté ejecutando. ¡Esto hará que tus aplicaciones multiplataforma sean independientes de la versión y sistema operativos instalados!
* Cada vez que desarrolles una nueva aplicación del lado del servidor en Node.js, tendrás que añadir este módulo cache.node al directorio node_modules. Simplemente copia la instancia desde tu aplicación de prueba al directorio node_modules, que se encuentra en la nueva aplicación en sus sistemas. Recuerda que si instalas/implementas tus aplicaciones del lado del servidor en/hacia otro sistema, necesitará revisar cuál de las versiones debes instalar en dicho sistema, ya que la versión de Node.js, el procesador del sistema y el sistema operativo pueden ser diferentes!
Ahora, solo tenemos que crear nuestro script de prueba nodeTest.js dentro de C:\Temp\nodetest mediante este código:
// first, load the Cache connector module inside node_modules
let cacheModule = require('cache');
// instantiate a Cache connector object in JavaScript
let db = new cacheModule.Cache();
console.log('Caché database object instance: ', db);
// Open the connection to the Caché database (adjust parameters for your Cache system):
let ok = db.open({
path: 'C:\\InterSystems\\Cache\\Mgr',
username: '_SYSTEM',
password: 'SYS',
namespace: 'USER'
});
console.log('Open result: ', ok);
console.log('Version: ', db.version());
let d = new Date();
// construct a JavaScript global node object to set a test global in the USER namespace
let node = {
global: 'nodeTest',
subscripts: [1],
data: 'At ' + d.toUTCString() + ': global set from Node.js'
};
// set the global in the database
db.set(node);
// retrieve the global contents back from Cache
let result = db.get(node);
// show it on the console
console.log('Set global ^nodeTest(1) result: ', result);
// close the database connection
db.close();
Si guardas este script e instalas todo correctamente, este código debería funcionar inmediatamente cuando lo llames dentro de una línea de comandos:
¡Enhorabuena! Ya has probado tu servidor para aplicaciones Node.js y lo has conectado con éxito a tu base de datos en Caché.
Con el script de prueba te darás cuenta de que el módulo cache.node contiene toda la funcionalidad que necesitas para acceder a tus datos en Caché (¡no solo los globales, también las funciones, las clases y el SQL!). Encontrarás toda la documentación sobre el módulo Node.js para Caché en los documentos disponibles online en Using Node.js with Caché. Dentro del archivo zip que te proporcione el WRC, también encontrarás un archivo PDF en el directorio de los documentos con todos los detalles sobre las funciones.
También te darás cuenta de que cuando comiences a programar con el módulo Node.js para Caché , este te proporcionará funciones de nivel inferior para acceder a Caché.
En el siguiente artículo, mostraré cómo se pueden utilizar algunos módulos de Node.js para escribir el código fuente de JavaScript, para acceder a sus globales en Caché, las funciones, las clases y las preguntas sobre SQL que utilizan una abstracción funcional de alto nivel.
Artículo
Ricardo Paiva · 14 ene, 2022
Hola desarrolladores,
Escribir un *script* para el despliegue de una aplicación puede ser muy interesante para garantizar un despliegue rápido sin olvidarse de nada.
config-api es una biblioteca para ayudar a los desarrolladores a escribir *scripts* de configuración basados en un documento JSON.
Características implementadas:
* Establecer la configuración del sistema
* Establecer la configuración de seguridad
* Habilitar servicios
* Configurar *namespaces*, bases de datos y mapeos
* Exportar configuración existente
* Todas las funciones están expuestas con una API RESTful
Esta biblioteca se centra en la configuración de IRIS para ayudar a la implementación de aplicaciones. Por lo tanto, config-api no importa/compila la función de código, considerando queesa debería ser la función del módulo de instalación de tu aplicación o el registro del cliente. config-api podría usarse con el cliente ZPM para configurar los ajustes de IRIS en la implementación del módulo; aprenderemos cómo combinar esta biblioteca con ZPM en otro artículo.
## Instalación
```
zpm “install config-api”
```
Si no eres usuario de ZPM, descarga la última versión en formato XML con dependencias [página de publicación](https://github.com/lscalese/iris-config-api/releases/) importación y compilación.
## Primer paso
Vamos a escribir un documento JSON de configuración simple, para establecer algunas configuraciones del sistema.
En este primer documento:
* Habilitamos *journal freeze* en caso de error.
* Establecemos el tamaño límite de *journal* en 256 MB.
* Establecemos SystemMode para desarrollo.
* Aumentamos el `locksiz`.
* Aumentamos el `LockThreshold`.
```
Set config = {
"Journal": { /* Service class Api.Config.Journal */
"FreezeOnError":1,
"FileSizeLimit":256
},
"SQL": { /* Service class Api.Config.SQL */
"LockThreshold" : 2500
},
"config": { /* Service class Api.Config.config */
"locksiz" : 33554432
},
"Startup":{ /* Service class Api.Config.Startup */
"SystemMode" : "DEVELOPMENT"
}
}
Set sc = ##class(Api.Config.Services.Loader).Load(config)
```
### Estructura del documento JSON de configuración
Las claves de primer nivel (`Journal`,`SQL`,`config`,`Startup`) están relacionadas con las clases en el *namespace* %SYS (usando una clase intermedia en el paquete Api.Config.Services). Significa que `Journal` admite todas las propiedades disponibles en [Config.Journal](https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&CLASSNAME=Config.Journal), ` SQL`todas las propiedades en [Config.SQL](https://docs.intersystems.com/irislatest/csp/documatic/%25CSP.Documatic.cls?&LIBRARY=%25SYS&CLASSNAME=Config.SQL), etc...
Salida:
```
2021-03-31 18:31:54 Start load configuration
2021-03-31 18:31:54 {
"Journal":{
"FreezeOnError":1,
"FileSizeLimit":256
},
"SQL":{
"LockThreshold":2500
},
"config":{
"locksiz":33554432
},
"Startup":{
"SystemMode":"DEVELOPMENT"
}
}
2021-03-31 18:31:54 * Journal
2021-03-31 18:31:54 + Update Journal ... OK
2021-03-31 18:31:54 * SQL
2021-03-31 18:31:54 + Update SQL ... OK
2021-03-31 18:31:54 * config
2021-03-31 18:31:54 + Update config ... OK
2021-03-31 18:31:54 * Startup
2021-03-31 18:31:54 + Update Startup ... OK
```
**Truco**: el método `Load` es compatible con un argumento de cadena, en este caso, la cadena debe ser un nombre de archivo para un documento de configuración JSON (también se permite objetos *stream*).
## Crear un entorno de aplicación
En esta sección, escribimos un documento de configuración para crear:
* Un *namespace* "MYAPP"
* 4 bases de datos (MYAPPDATA, MYAPPCODE, MYAPPARCHIVE,MYAPPLOG)
* 1 aplicación web CSP (/csp/zwebapp)
* 1 aplicación web REST (/csp/zrestapp)
* Configuración de mapeo de *globals*
```
Set config = {
"Defaults":{
"DBDIR" : "${MGRDIR}",
"WEBAPPDIR" : "${CSPDIR}",
"DBDATA" : "${DBDIR}myappdata/",
"DBARCHIVE" : "${DBDIR}myapparchive/",
"DBCODE" : "${DBDIR}myappcode/",
"DBLOG" : "${DBDIR}myapplog/"
},
"SYS.Databases":{
"${DBDATA}" : {"ExpansionSize":128},
"${DBARCHIVE}" : {},
"${DBCODE}" : {},
"${DBLOG}" : {}
},
"Databases":{
"MYAPPDATA" : {
"Directory" : "${DBDATA}"
},
"MYAPPCODE" : {
"Directory" : "${DBCODE}"
},
"MYAPPARCHIVE" : {
"Directory" : "${DBARCHIVE}"
},
"MYAPPLOG" : {
"Directory" : "${DBLOG}"
}
},
"Namespaces":{
"MYAPP": {
"Globals":"MYAPPDATA",
"Routines":"MYAPPCODE"
}
},
"Security.Applications": {
"/csp/zrestapp": {
"DispatchClas" : "my.dispatch.class",
"Namespace" : "MYAPP",
"Enabled" : "1",
"AuthEnabled": "64",
"CookiePath" : "/csp/zrestapp/"
},
"/csp/zwebapp": {
"Path": "${WEBAPPDIR}zwebapp/",
"Namespace" : "MYAPP",
"Enabled" : "1",
"AuthEnabled": "64",
"CookiePath" : "/csp/zwebapp/"
}
},
"MapGlobals":{
"MYAPP": [{
"Name" : "Archive.Data",
"Database" : "MYAPPARCHIVE"
},{
"Name" : "App.Log",
"Database" : "MYAPPLOG"
}]
}
}
Set sc = ##class(Api.Config.Services.Loader).Load(config)
```
Salida:
```
2021-03-31 20:20:07 Start load configuration
2021-03-31 20:20:07 {
"SYS.Databases":{
"/usr/irissys/mgr/myappdata/":{
"ExpansionSize":128
},
"/usr/irissys/mgr/myapparchive/":{
},
"/usr/irissys/mgr/myappcode/":{
},
"/usr/irissys/mgr/myapplog/":{
}
},
"Databases":{
"MYAPPDATA":{
"Directory":"/usr/irissys/mgr/myappdata/"
},
"MYAPPCODE":{
"Directory":"/usr/irissys/mgr/myappcode/"
},
"MYAPPARCHIVE":{
"Directory":"/usr/irissys/mgr/myapparchive/"
},
"MYAPPLOG":{
"Directory":"/usr/irissys/mgr/myapplog/"
}
},
"Namespaces":{
"MYAPP":{
"Globals":"MYAPPDATA",
"Routines":"MYAPPCODE"
}
},
"Security.Applications":{
"/csp/zrestapp":{
"DispatchClas":"my.dispatch.class",
"Namespace":"MYAPP",
"Enabled":"1",
"AuthEnabled":"64",
"CookiePath":"/csp/zrestapp/"
},
"/csp/zwebapp":{
"Path":"/usr/irissys/csp/zwebapp/",
"Namespace":"MYAPP",
"Enabled":"1",
"AuthEnabled":"64",
"CookiePath":"/csp/zwebapp/"
}
},
"MapGlobals":{
"MYAPP":[
{
"Name":"Archive.Data",
"Database":"MYAPPARCHIVE"
},
{
"Name":"App.Log",
"Database":"MYAPPLOG"
}
]
}
}
2021-03-31 20:20:07 * SYS.Databases
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myappdata/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myapparchive/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myappcode/ ... OK
2021-03-31 20:20:07 + Create /usr/irissys/mgr/myapplog/ ... OK
2021-03-31 20:20:07 * Databases
2021-03-31 20:20:07 + Create MYAPPDATA ... OK
2021-03-31 20:20:07 + Create MYAPPCODE ... OK
2021-03-31 20:20:07 + Create MYAPPARCHIVE ... OK
2021-03-31 20:20:07 + Create MYAPPLOG ... OK
2021-03-31 20:20:07 * Namespaces
2021-03-31 20:20:07 + Create MYAPP ... OK
2021-03-31 20:20:07 * Security.Applications
2021-03-31 20:20:07 + Create /csp/zrestapp ... OK
2021-03-31 20:20:07 + Create /csp/zwebapp ... OK
2021-03-31 20:20:07 * MapGlobals
2021-03-31 20:20:07 + Create MYAPP Archive.Data ... OK
2021-03-31 20:20:07 + Create MYAPP App.Log ... OK
```
¡Funciona! La configuración se cargó con éxito.
En el próximo artículo, aprenderemos a usar config-api con ZPM para implementar tu aplicación.
Este artículo está etiquetado como "Mejores prácticas" ("Best practices").
Los artículos con la etiqueta "Mejores prácticas" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems.
Artículo
Estevan Martinez · 23 jul, 2019
Los globales de InterSystems Caché proporcionan un conjunto de funciones muy útiles para los desarrolladores. Pero, ¿por qué los globales son tan rápidos y eficientes?TeoríaBásicamente, la base de datos de Caché es un catálogo con el mismo nombre que la base de datos y contiene el archivo CACHE.DAT. En los sistemas Unix, la base de datos también puede ser una partición normal del disco.Todos los datos en Caché se almacenan en bloques que, a su vez, se organizan como un árbol-B* balanceado. Si tenemos en cuenta que todos los globales básicamente se almacenan en un árbol, los subíndices de los globales se representarán como ramas, mientras que los valores de los subíndices globales se almacenarán como hojas. La diferencia entre un árbol-B* balanceado y un árbol-B ordinario es que sus ramas también tienen los enlaces apropiados para realizar iteraciones a través de los subíndices (por ejemplo, en nuestro caso los globales) usando rápidamente las funciones $Order y $Query sin necesidad de regresar al tronco del árbol. De manera predeterminada, cada bloque en el archivo de la base de datos tiene un tamaño fijo de 8,192 bytes. Por lo tanto, no podrá modificar el tamaño de los bloques en una base de datos que ya exista. Mientras crea una nueva base de datos puede elegir entre bloques de 16 KB, 32 KB o incluso bloques de 64 KB, dependiendo del tipo de datos que almacenará. Sin embargo, siempre tenga en cuenta que todos los datos se leen bloque por bloque, en otras palabras, incluso si solicita un solo valor de 1 byte de tamaño el sistema leerá muchos bloques, entre los cuales, el bloque de datos que solicitó será el último. También debe recordar que Caché utiliza búferes globales para almacenar los bloques de las bases de datos en la memoria para utilizarlos después, y que los búferes de bloques tienen el mismo tamaño. No podrá utilizar una base de datos existente o crear una nueva cuando el búfer global, con el tamaño de bloques correspondiente, falte en el sistema. Además, deberá definir la cantidad de memoria que desea asignar para el tamaño específico de los bloques. Es posible utilizar búferes con bloques más grandes que los bloques en las bases de datos, pero en este caso, cada bloque en el búfer solamente almacenará un bloque en dicha base, que será incluso más pequeño.En esta imagen, se asignó memoria para un búfer global con un tamaño de 8 KB, con la finalidad de utilizarla en bases de datos conformadas por bloques de 8 KB. En esta base de datos, los bloques que no están vacíos se definieron en los mapas, de tal modo que alguno de los mapas contenga 62,464 bloques (para bloques con capacidad de 8KB).Tipos de BloquesEl sistema es compatible con varios tipos de bloques. En cada nivel, los enlaces correctos de un bloque deben señalar un bloque del mismo tipo o un bloque nulo que defina el final de los datos.Tipo 9: Bloques de un catálogo de globales. En estos bloques generalmente se describen todos los globales existentes junto con sus parámetros, incluida la compilación de los subíndices globales, el cual es uno de los parámetros más importantes y no puede modificarse una vez que el global se creóTipo 66: Bloques punteros de alto nivel. Solo un bloque de un catálogo de globales puede estar en la parte superior de estos bloques.Tipo 6: Bloques punteros de nivel inferior. Únicamente los bloques punteros de alto nivel pueden estar en la parte superior de estos bloques y solamente los bloques de datos pueden colocarse en niveles inferiores.Tipo 70: Bloques punteros tanto de alto nivel como de nivel inferior. Estos bloques se utilizan cuando el global correspondiente almacena una pequeña cantidad de valores y, por lo tanto, no es necesario tener varios niveles de bloques. Estos bloques generalmente señalan hacia los bloques de datos, al igual que los bloques de un catálogo de globales.Tipo 2: Bloques punteros para almacenar una cantidad relativamente grande de globales. Para asignar valores de manera equitativa entre los bloques de datos, es posible que desee crear niveles adicionales de bloques punteros. Estos bloques generalmente se colocan entre los bloques punteros.Tipo 8: Bloques de datos. En estos bloques generalmente se almacenan varios nodos globales en lugar de un solo nodo.Tipo 24: Bloques para cadenas largas. Cuando el valor de un solo global es mayor que un bloque, dicho valor se registrará en un bloque especial para cadenas largas, mientras que el nodo del bloque de datos almacenará enlaces en la lista de bloques para cadenas largas junto con la longitud total de este valor.Tipo 16: Diagrama de bloques. Estos bloques están diseñados para almacenar información sobre bloques que no han sido asignados.Por lo tanto, el primer bloque en una base de datos típica de Caché contiene la información de servicio acerca del mismo archivo en la base de datos, mientras que los segundos bloques constituyen un diagrama de bloques. El primer bloque del catálogo va en el tercer lugar (bloque #3), y una sola base de datos puede tener varios bloques en el catálogo. Los siguientes son bloques punteros (ramas), bloques de datos (hojas) y bloques de cadenas largas. Como ya mencioné anteriormente, los bloques en los catálogos globales almacenan información sobre todos los globales que existen en la base de datos o en la configuración global (si no hay datos disponibles en este tipo de globales). En este caso, un nodo que describa a dicho global tendrá un puntero nulo de nivel inferior. Puede consultar la lista de los globales que están disponibles en el catálogo de globales que se encuentra en el portal de administración. En este portal también se le permitirá guardar un global en el catálogo después de haberlo eliminado (por ejemplo, guardar su secuencia de compilación) así como crear un nuevo global con un tipo de compilación ya sea predeterminada o personalizada.En general, el árbol de bloques puede representarse como la imagen que se muestra a continuación. Tenga en cuenta que los enlaces a estos bloques se muestran en color rojo.Integridad de la Base de DatosEn la versión actual de Caché, resolvimos los temas y los problemas más importantes con las bases de datos, entonces el riesgo de degradación en las bases de datos es extremadamente bajo. Sin embargo, le recomendamos que realice comprobaciones de la integridad automáticas en las bases de datos de manera regular utilizando nuestra herramienta ^Integrity, la cual puede ejecutarse en el terminal desde el namespace %SYS, mediante nuestro portal de administración, en la página de la Base de datos o desde el administrador de tareas. Por defecto, la comprobación de la integridad automática ya está configurada y predefinida, por lo tanto lo único que debe hacer es activarla: La opción "integrity check" incluye la verificación de los enlaces en los niveles inferiores, la validación de los tipos de bloques, el análisis de los enlaces correctos y la correspondencia de los nodos globales con la secuencia para la aplicación de la compilación. Si el programa encuentra algún error durante la comprobación de la integridad, puede ejecutar nuestra herramienta ^REPAIR desde el namespace %SYS. Al utilizar esta herramienta, podrá visualizar cualquier bloque y modificarlo según sea necesario, por ejemplo, para reparar su base de datos. PrácticaEsto fue solo la teoría. Todavía es difícil comprender qué hace un global y cómo se ven en realidad sus bloques. Actualmente, la única manera en que podemos visualizar los bloques es utilizando nuestra herramienta ^REPAIR, que mencionamos anteriormente. A continuación, se muestra la forma característica en que se visualizan los resultados de este programa: Hace tres años, Dmitry comenzó un nuevo proyecto para desarrollar una herramienta que nos permitiera iterar a través de un árbol de bloques sin ningún riesgo de que la base de datos se dañara, visualizar estos bloques en una IU web y proporcionara opciones para guardar dichas visualizaciones en formatos SVG o PNG. El proyecto se llama CacheBlocksExplorer, y su código fuente está disponible para que lo descargue en GitHub.A partir del momento de la redacción del artículo original en 2016.Las características que se implementaron incluyen:Visualizar cualquier base de datos que se haya configurado o simplemente instalado en el sistema;Visualizar la información por bloque, los tipos de bloques, el puntero correcto o una lista de nodos con sus enlaces;Visualizar a detalle la información sobre cualquier nodo que señale hacia un bloque de nivel inferior;Ocultar bloques al eliminar los enlaces hacia ellos (sin dañar los datos almacenados en estos bloques).Lista de cosas pendientes:Mostrar los enlaces correctos: en la versión actual los enlaces correctos se muestran en la información sobre los bloques, pero lo mejor sería que pudiéramos visualizarlos como flechas,Mostrar los bloques de las cadenas largas: en la versión actual simplemente no se muestran,Mostrar todos los bloques que se encuentran en el catálogo de globales en lugar de que solamente se visualice el terceroAsimismo, me gustaría mostrar el árbol completo, pero todavía no encuentro ninguna biblioteca capaz de renderizar rápidamente los cientos de miles de bloques junto con sus enlaces. Con la biblioteca actual es posible renderizarlos en los navegadores web, pero mucho más lentamente que el tiempo que le toma a Caché leer toda la estructura.En el próximo artículo, intentaré describir con más detalle cómo funciona, proporcionar algunos ejemplos sobre su uso y mostrar cómo se recuperan muchos datos que pueden procesarse sobre los globales y los bloques mediante mi Cache Block Explorer.
Artículo
Nancy Martínez · 13 mar, 2020
Introducción
Un requisito frecuente en muchas aplicaciones es registrar en una base de datos los cambios que se realizan en los datos- qué datos se modificaron, quién los modificó y cuándo (control de cambios). Hay muchos artículos relacionados con el tema y existen diferentes métodos sobre cómo hacer esto en Caché.
Por ello, comparto un procedimiento que puede ayudar con la implementación de una estructura para seguir y registrar los cambios en los datos. Este procedimiento crea un trigger mediante un método "objectgenarator" cuando su clase persistente la hereda de "Audit Abstract Class" (Sample.AuditBase). Debido a que su clase persistente hereda Sample.AuditBase, cuando sea compilada, el activador generará automáticamente el control de las modificaciones.
Audit Class
Esta es la clase en la que se registrarán las modificaciones.
Class Sample.Audit Extends %Persistent{ Property Date As %Date; Property UserName As %String(MAXLEN = ""); Property ClassName As %String(MAXLEN = ""); Property Id As %Integer; Property Field As %String(MAXLEN = ""); Property OldValue As %String(MAXLEN = ""); Property NewValue As %String(MAXLEN = "");}
Audit Abstract Class
Esta es la clase abstracta de la que heredará su clase persistente. Esta clase contiene el método trigger (objectgenerator), que sabe cómo identificar cuáles fueron los campos que se modificaron, quién los modificó, cuáles son los antiguos y los nuevos valores, etc.; además escribirá las modificaciones en la tabla de auditoría (Sample.Audit).
Class Sample.AuditBase [ Abstract ]{Trigger SaveAuditAfter [ CodeMode = objectgenerator, Event = INSERT/UPDATE, Foreach = row/object, Order = 99999, Time = AFTER ]{ #dim %compiledclass As %Dictionary.CompiledClass #dim tProperty As %Dictionary.CompiledProperty #dim tAudit As Sample.Audit Do %code.WriteLine($Char(9)_"; get username and ip adress") Do %code.WriteLine($Char(9)_"Set tSC = $$$OK") Do %code.WriteLine($Char(9)_"Set tUsername = $USERNAME") Set tKey = "" Set tProperty = %compiledclass.Properties.GetNext(.tKey) Set tClassName = %compiledclass.Name Do %code.WriteLine($Char(9)_"Try {") Do %code.WriteLine($Char(9,9)_"; Check if the operation is an update - %oper = UPDATE") Do %code.WriteLine($Char(9,9)_"if %oper = ""UPDATE"" { ") While tKey '= "" { set tColumnNbr = $Get($$$EXTPROPsqlcolumnnumber($$$pEXT,%classname,tProperty.Name)) Set tColumnName = $Get($$$EXTPROPsqlcolumnname($$$pEXT,%classname,tProperty.Name)) If tColumnNbr '= "" { Do %code.WriteLine($Char(9,9,9)_";") Do %code.WriteLine($Char(9,9,9)_";") Do %code.WriteLine($Char(9,9,9)_"; Audit Field: "_tProperty.SqlFieldName) Do %code.WriteLine($Char(9,9,9)_"if {" _ tProperty.SqlFieldName _ "*C} {") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit = ##class(Sample.Audit).%New()") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.ClassName = """_tClassName_"""") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Id = {id}") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.UserName = tUsername") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Field = """_tColumnName_"""") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.Date = +$Horolog") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.OldValue = {"_tProperty.SqlFieldName_"*O}") Do %code.WriteLine($Char(9,9,9,9)_"Set tAudit.NewValue = {"_tProperty.SqlFieldName_"*N}") Do %code.WriteLine($Char(9,9,9,9)_"Set tSC = tAudit.%Save()") do %code.WriteLine($Char(9,9,9,9)_"If $$$ISERR(tSC) $$$ThrowStatus(tSC)") Do %code.WriteLine($Char(9,9,9)_"}") } Set tProperty = %compiledclass.Properties.GetNext(.tKey) } Do %code.WriteLine($Char(9,9)_"}") Do %code.WriteLine($Char(9)_"} Catch (tException) {") Do %code.WriteLine($Char(9,9)_"Set %msg = tException.AsStatus()") Do %code.WriteLine($Char(9,9)_"Set %ok = 0") Do %code.WriteLine($Char(9)_"}") Set %ok = 1}}
Data Class (Persistent Class)
Esta es la clase de datos del usuario, en la que el usuario (la aplicación) realiza modificaciones, genera y elimina historiales, o hace cualquier cosa que se le permita hacer :)
En resumen, esta generalmente es su clase %Persistent.
Para iniciar el seguimiento y registro de los cambios, necesitará que la clase persistente se herede de la clase abstracta (Sample.AuditBase).
Class Sample.Person Extends (%Persistent, %Populate, Sample.AuditBase){ Property Name As %String [ Required ]; Property Age As %String [ Required ]; Index NameIDX On Name [ Data = Name ];}
Prueba
Como se ha heredado la clase de datos (Sample.Person) de la clase de Audit Abstract Class (Sample.AuditBase) se podrá insertar datos, realizar modificaciones y analizar los cambios que se registraron en Audit Class (Sample. Audit).
Para comprobar esto, es necesario crear un método Test() para la clase, en la clase Sample.Person o en cualquier otra clase se elija.
ClassMethod Test(pKillExtent = 0){ If pKillExtent '= 0 { Do ##class(Sample.Person).%KillExtent() Do ##class(Sample.Audit).%KillExtent() } &SQL(INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01')) Write "INSERT INTO Sample.Person (Name, Age) VALUES ('TESTE', '01')",! Write "SQLCODE: ",SQLCODE,!!! Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person") Do tRS.%Display() &SQL(UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE') Write !!! Write "UPDATE Sample.Person SET Name = 'TESTE 2' WHERE Name = 'TESTE'",! Write "SQLCODE:",SQLCODE,!!! Set tRS = $SYSTEM.SQL.Execute("SELECT * FROM Sample.Person") Do tRS.%Display() Quit}
Ejecutar el método Test():
d ##class(Sample.Person).Test(1)
Parameter 1 will kill extent from Sample.Person and Sample.Audit classes.
El método Test para clase realiza lo siguiente:
Introduce una nueva persona con el nombre "TEST";
Muestra el resultado de la introducción;
Actualiza la persona "TEST" a "TEST ABC";
Muestra el resultado de la actualización;
Ahora se podrá verificar la tabla de control de cambios. Para ello, entra en el Portal de Administración del Sistema -> Explorador del Sistema -> SQL. (No olvides cambiarse a su namespace)
Ejecuta el siguiente comando en SQL y verifica los resultados:
SELECT * FROM Sample.Audit
Debes Tener en cuenta que la variable OldValue es "TEST" y la NewValue es "TEST ABC". A partir de ahora podrás realizar tus propias pruebas al cambiar el nombre "TEST ABC" por "Su propio nombre" y/o cambiar, por ejemplo, los valores de Age. Consulte:
UPDATE Sample.Person SET Name = 'Fabio Goncalves' WHERE Name = 'TEST ABC'
Generación del Código
Teniendo en cuenta que hemos implementado el procedimiento de control de cambios que se muestra a continuación, inicia Studio (o Atelier) en tu equipo, abre la clase persistente (Sample.Person) y analiza el código intermedio que se generó después de compilar la clase Sample.Person. Para hacerlo, presiona Ctrl + Shift + V (Consultar otro código fuente), y analiza .INT. Desplázate hacia abajo hasta la etiqueta zSaveAuditAfterExecute y échele un vistazo al código que se generó:
Ventajas
Es muy sencillo implementar el control de cambios basado en el despliegue de datos antiguos. No es necesario tablas adicionales. El mantenimiento también es sencillo. Si decides eliminar datos antiguos, entonces debes utilizar un SQL.
Si se necesita implementar el control de cambios en más tablas, solo es necesario heredarlo desde la clase abstracta (Sample.AuditBase)
Realiza los cambios conforme a tus necesidades, por ejemplo, registra los cambios en Streams.
Registra solamente los campos que se han modificado. No guarda todo el historial de cambios.
Desventajas
Un problema puede ser que cuando se modifican los datos, también se copia todo el historial, es decir, también se copian los datos que no se modificaron.
Si la tabla persona tiene una columna "foto" con los datos binarios (stream) que contiene la fotografía, entonces cada vez que el usuario cambie la imagen también se registrará la función stream (la cual consume espacio en el disco).
Otra desventaja es que también aumenta la complejidad con cada tabla complementaria que añade en el control de cambios. Deberas tener en cuenta, todo el tiempo, que no es tan sencillo recuperar los historiales. Siempre debes utilizar la cláusula SELECT con el condicional: "...WHERE Status = active" o considere algún "DATE INTERVAL"
Todas las modificaciones que se realicen a los datos se registran en una tabla común.
Piensa en las translocaciones y los rollbacks.
El control de cambios es un requisito importante para que algunas aplicaciones sean eficientes. Por lo general, para determinar si hubo alguna modificación en los datos, los desarrolladores deben implementar un método de seguimiento personalizado en sus aplicaciones, mediante una combinación de triggers, columnas para registrar la hora y tablas adicionales. Crear estos procedimientos normalmente requiere mucho trabajo para su implementación, conduce a actualizaciones de esquemas y con frecuencia, implica una disminución en el rendimiento. Este es un ejemplo sencillo que te podría ayudar a establecer tu propia estructura.
Este artículo ha sido etiquetado como "Best practices"
(Los artículos con la etiqueta "Best practices" incluyen recomendaciones sobre cómo desarrollar, probar, implementar y administrar mejor las soluciones de InterSystems).