Limpiar filtro
Artículo
Ricardo Paiva · 26 mar, 2020
Este es el primero de dos artículos sobre los índices SQL.
Parte 1 - Conoce tus índices
¿Qué es un índice?
Recuerda la última vez que fuiste a una biblioteca. Normalmente, los libros están ordenados por temática (y luego autor y título) y cada repisa tiene un cartel en el extremo con un código que describe la temática de los libros. Si necesitaras libros de un cierto tema, en lugar de caminar por cada pasillo y leer la descripción en la parte interior de cada libro, podrías dirigirte directamente al estante cuyo cartel describa la temática que buscas y elegir tus libros de allí. Sin esos carteles, el proceso de encontrar los libros que quieres, habría sido muy lento.
Un índice SQL tiene la misma función general: mejorar el rendimiento, al ofrecer una referencia rápida del valor de los campos para cada fila de una tabla.
Configurar índices es uno de los pasos más importantes a la hora de preparar tus clases para un rendimiento óptimo de SQL.
En este artículo veremos:
¿Qué es un índice y por qué/cuando se deben usar?
¿Qué tipo de índices existen y en qué situaciones son perfectos?
¿A qué se parece un índice?
¿Cómo se puede crear uno?
Y cuando tengo índices, ¿qué hago con ellos?
Me referiré a las clases de nuestro esquema Sample, que se incluye en el namespace Samples en instalaciones de Caché y Ensemble. Puedes descargar estas clases de nuestro repositorio en Github:
https://github.com/intersystems/Samples-Data
Lo fundamental
Puedes indexar cualquier propiedad persistente y cualquier propiedad que puede ser calculada de forma fiable a partir de datos persistentes.
Supongamos que queremos indexar la propiedad TaxID (identificador impositivo) en Sample.Company. En Studio o Atelier, añadiríamos lo siguiente a la definición de la clase:
Index TaxIDIdx On TaxID [ Type = index, Unique ];
La sentencia DDL SQL equivalente se vería como algo así:
CREATE UNIQUE INDEX TaxIDIdx ON Sample.Company (TaxID);
La estructura del índice global predeterminado es la siguiente:
^Sample.CompanyI("TaxIDIdx ",<TaxIDValueAtRowID>,<RowID>) = ""
Ten en cuenta que hay menos subíndices para leer que campos en un global de datos típicos.
Mira la consulta “SELECT Name,TaxID FROM Sample.Company WHERE TaxID = 'J7349'”. Es lógica y simple, y el plan de consulta para ejecutar esta consulta lo refleja:
Este plan dice, básicamente, que buscamos columnas con el valor dado de TaxID en el global del índice, y luego volvemos a buscar en el global de datos ("master map") para recuperar la fila coincidente.
Ahora mira la misma consulta sin un índice en TaxIDX. El plan de consulta resultante es, como se esperaría, menos eficiente:
Sin índices, la ejecución de consultas subyacente de Caché se basa en leer en la memoria y aplicar la condición de la cláusula WHERE a cada fila de la tabla. Y como TaxID es único, ¡estamos haciendo todo este trabajo tan solo para una fila!
Por supuesto, tener índices significa tener datos de índices y filas en disco. Dependiendo de en qué tengamos una condición y cuántos datos contenga nuestra tabla, esto puede generar sus propios desafíos al crear y poblar un índice.
Entonces, ¿cuándo agregamos un índice a una propiedad?
El caso general es cuando condicionamos con frecuencia en base a una propiedad. Algunos ejemplos son identificar información como el número de seguridad social (SSN) de una persona o su número de cuenta bancaria. También puede pensar en fechas de nacimiento o en los fondos de una cuenta. Volviendo a Sample.Company, quizás la clase se vería beneficiada de indexar la propiedad Revenue (ingresos) si quisiéramos recopilar datos sobre organizaciones con altos ingresos. Por otra parte, las propiedades que es poco probable que condicionemos son menos adecuadas para indexar, como por ejemplo el eslogan o descripción de una empresa.
Simple, ¡excepto que también debemos considerar qué tipo de índice es el mejor!
Tipos de índices
Hay seis tipos principales de índices que describiré aquí: estándar, bitmap, compuesto, recopilación, bitslice y datos. También describiré brevemente los índices iFind, que se basan en flujos. Aquí hay posibles solapamientos, y ya hemos visto los índices estándar con el ejemplo anterior.
Compartiré ejemplos de cómo crear índices en tu definición de clase, pero añadir nuevos índices a una clase es más complejo que tan solo añadir una línea a tu definición de clase. En la próxima parte analizaremos todas las consideraciones adicionales.
Usemos Sample.Person como ejemplo. Ten en cuenta que Person tiene la subclase Employee (empleado), que será relevante para entender algunos ejemplos. Employee comparte su almacenamiento de global de datos con Person, y todos los índices de Person son heredados por Employee. Esto significa que Employee usa el global de índices de Person para estos índices heredados.
Si no estás familiarizado con ellas, esta es una descripción general de las clases: Person tiene propiedades SSN (número de seguridad social), DOB (fecha de nacimiento), Name (nombre), Home (un objeto embebido de dirección tipo Address que contiene State (estado) y City (ciudad)), Office (oficina, también de tipo Address) y la colección de listas FavoriteColors (colores favoritos). Employee tiene la propiedad adicional Salary (salario, que definí yo misma).
Estándar
Index DateIDX On DOB;
Aquí uso "estándar" de forma de forma poco precisa, para referirme a índices que almacenan el valor sencillo de una propiedad (a diferencia de una representación binaria). Si el valor es una cadena, se almacenará bajo alguna compilación (collation) – SQLUPPER por defecto.
En comparación con índices bitmap o bitslice, los índices estándar son mucho más fáciles de leer por una persona y su mantenimiento es bastante sencillo. Tenemos un nodo global para cada fila de la tabla.
A continuación se muestra cómo se almacena DateIDX a nivel global.
^Sample.PersonI("DateIDX",51274,100115)="~Sample.Employee~" ; Date is 05/20/81
Ten en cuenta que el primer subscript después del nombre del índice es el valor de la fecha, el último subscript es el ID de Person con esa DOB (fecha de nacimiento) y el valor almacenado en este nodo global indica que esta persona también es miembro de la subclase Sample.Employee. Si esa persona no fuera miembro de ninguna subclase, el valor en el nodo sería una cadena vacía.
Esta estructura base será consistente con la mayoría de los índices que no sean bits, en los cuales los índices en más de una propiedad crean más subscripts en el global y tener más de un valor almacenado en el nodo genera un objeto $listbuild, por ejemplo:
^Package.ClassI(IndexName,IndexValue1,IndexValue2,IndexValue3,RowID) = $lb(SubClass,DataValue1,DataValue2)
Bitmap – Una representación bit a bit (bitwise) del conjunto de IDs que corresponden al valor de una propiedad.
Index HomeStateIDX On Home.State [ Type = bitmap];
Los índices de bitmap se guardan por valor único, a diferencia de los índices estándar, que se almacenan por fila.
Continuando con el ejemplo anterior, digamos que la persona con el ID 1 vive en Massachusetts, el ID 2 en Nueva York, ID 3 en Massachusetts e ID 4 en Rhode Island. HomeStateIDX básicamente se almacena así:
ID
1
2
3
4
(…)
(…)
0
0
0
0
-
MA
1
0
1
0
-
NY
0
1
0
0
-
RI
0
0
0
1
-
(…)
0
0
0
0
-
Si quisiéramos que una consulta devuelva datos de las personas que viven en New England, el sistema realizaría un OR bit a bit (bitwise) sobre las filas relevantes del índice bitmap. Se puede ver rápidamente que debemos cargar en memoria los objetos Person con ID 1, 3 y 4 como mínimo.
Los bitmaps pueden ser eficientes para operadores AND, RANGE y OR en tus claúsulas WHERE.
Si bien no hay un límite oficial sobre la cantidad de valores únicos que puede tener para una propiedad antes de que un índice tipo bitmap sea menos eficiente que un índice estándar, la regla general es de hasta unos 10.000 valores distintos. Entonces, mientras un índice tipo bitmap podría ser efectivo en un estado de los EE. UU., un índice bitmap para una ciudad o condado no sería tan útil.
Otro concepto a tener en cuenta es la eficiencia de almacenamiento. Si piensas añadir o eliminar filas de tu tabla con frecuencia, el almacenamiento de tu índice tipo bitmap podría volverse menos eficiente. Veamos el ejemplo anterior: si elimináramos muchas filas por algún motivo y ya no tenemos a nadie en nuestra tabla que viva en estados menos poblados como Wyoming o Dakota del Norte, el bitmap entonces tendría varias filas solo con ceros. Por otra parte, crear nuevas filas en tablas grandes al final puede volverse más lento, ya que el almacenamiento de bitmaps grandes debe alojar más valores únicos.
En estos ejemplos tengo unas 150.000 filas en Sample.Person. Cada nodo global almacena hasta 64.000 ID's, por lo que el global del índice bitmap en el valor MA está dividido en tres partes:
^Sample.PersonI("HomeStateIDX"," MA",1)=$zwc(135,7992)_$c(0,(...))
^Sample.PersonI("HomeStateIDX"," MA",2)=$zwc(404,7990,(…))
^Sample.PersonI("HomeStateIDX"," MA",3)=$zwc(132,2744)_$c(0,(…))
Caso especial: Extent Bitmap
Un bitmap extent, a menudo llamado $<ClassName>, es un índice tipo bitmap sobre los ID de una clase. Esto brinda a Caché una forma rápida de saber si una fila existe y puede ser útil para consultas COUNT o consultas sobre subclases. Estos índices se generan cuando un índice tipo bitmap se añade a la clase. También puedes crear manualmente un índice extent bitmap en una definición de clase de la siguiente forma:
Index Company [ Extent, SqlName = "$Company", Type = bitmap ];
O mediante la DDL keyword BITMAPEXTENT:
CREATE BITMAPEXTENT INDEX "$Company" ON TABLE Sample.Company
Compuesto – Índices basados en dos o más propiedades
Index OfficeAddrIDX On (Office.City, Office.State);
El caso de uso general de los índices compuestos es tener consultas frecuentes con condiciones sobre dos o más propiedades.
El orden de las propiedades en un índice compuesto importa, debido a la forma en que se almacena el índice en un nivel global. Tener primero la propiedad más selectiva es más eficiente para el rendimiento, ya que ahorrará lecturas de disco iniciales del global de índices. En este ejemplo. Office.City está primero debido a que hay más ciudades únicas que estados en los EE. UU.
Tener primero una propiedad menos selectiva es más eficiente para el espacio. En términos de estructura global, el árbol de índices estaría mejor equilibrado si el estado (State) estuviera primero. Piénsalo: cada estado contiene varias ciudades, pero algunos nombres de ciudades pertenecen a un único estado.
También puedes considerar si esperas ejecutar consultas frecuentes que condicionan solo una de las propiedades. Esto puede ahorrarte definir otro índice más.
Este es un ejemplo de la estructura global del índice compuesto:
^Sample.PersonI("OfficeAddrIDX"," BOSTON"," MA",100115)="~Sample.Employee~"
¿Índice compuesto o índices de bitmap?
Para consultas con condiciones sobre múltiples propiedades, puede que también quieras evaluar si índices tipo bitmap separados serían más efectivos que un único índice compuesto.
Las operaciones bit a bit en dos índices distintos podrían ser más eficientes, considerando que los índices bitmap se adecuan bien a cada propiedad.
También puedes tener índices tipo bitmap compuestos: son índices tipo bitmap en los que el valor único es la intersección de las múltiples propiedades sobre las que estás indexando. Por ejemplo, la tabla de la sección anterior, pero si en lugar de estados tenemos cada par posible de estado y ciudad (p. ej. Boston, MA, Cambridge, MA, incluso Los Angeles, MA, etc.) y las celdas reciben valores 1 por las filas que cumplen ambos valores.
Recopilación – Índices basados en propiedades de recopilación
Aquí tenemos la propiedad FavoriteColors definida de la siguiente forma:
Property FavoriteColors As list Of %String(JAVATYPE = "java.util.List", POPSPEC = "ValueList("",Red,Orange,Yellow,Green,Blue,Purple,Black,White""):2");
Con cada uno de los siguientes índices definidos con fines de demostración:
Index fcIDX1 On FavoriteColors(ELEMENTS);Index fcIDX2 On FavoriteColors(KEYS);
Aquí uso "recopilación" para referirme de forma más amplia a propiedades de una celda que contienen más de un valor. Las propiedades List Of y Array Of son relevantes aquí e incluso las cadenas delimitadas.
Las propiedades de recopilación se analizan automáticamente para construir sus índices. Para propiedades delimitadas, como un número telefónico, deberá definir este método, <PropertyName>BuildValueArray(value, .valueArray), de forma explícita.
Dado el ejemplo anterior para FavoriteColors, fcIDX1 se vería como algo así para una persona cuyos colores favoritos fueran azul y blanco:
^Sample.PersonI("fcIDX1"," BLUE",100115)="~Sample.Employee~"
(…)
^Sample.PersonI("fcIDX1"," WHITE",100115)="~Sample.Employee~"
fcIDX2 se vería así:
^Sample.PersonI("fcIDX2",1,100115)="~Sample.Employee~"
^Sample.PersonI("fcIDX2",2,100115)="~Sample.Employee~"
En este caso, como FavoriteColors es una colección de List, un índice basado en sus claves es menos útil que un índice basado en sus elementos.
Consulta nuestra documentación para cuestiones más específicas sobre crear y gestionar índices basados en propiedades de recopilación:
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_collections
Bitslice – Representación bitmap de la representación de la cadena de bits de datos numéricos
Index SalaryIDX On Salary [ Type = bitslice ]; //In Sample.Employee
A diferencia de los índices bitmap, que contienen marcas (flags) que representan qué filas contienen un valor específico, los índices bitslice primero convierten valores numéricos de decimal a binario y luego crean un bitmap en cada dígito del valor binario.
Tomemos el ejemplo anterior y, para ser realistas, simplifiquemos Salary (salario) como unidades de $1000. Así, si el salario de un empleado se guarda como 65, se entiende que representa $65.000.
Digamos por ejemplo que tenemos Employee con ID 1 y salario 15, ID2 con salario 40, ID 3 con salario 64 e ID 4 con salario 130. Los valores de bits correspondientes son:
15
0
0
0
0
1
1
1
1
40
0
0
1
0
1
0
0
0
64
0
1
0
0
0
0
0
0
130
1
0
0
0
0
0
1
0
Nuestra cadena de bits abarca 8 dígitos. La representación bitmap correspondiente (los valores de índices bitslice) se almacena básicamente así:
^Sample.PersonI("SalaryIDX",1,1) = "1000" ; Row 1 has value in 1’s place
^Sample.PersonI("SalaryIDX",2,1) = "1001" ; Rows 1 and 4 have values in 2’s place
^Sample.PersonI("SalaryIDX",3,1) = "1000" ; Row 1 has value in 4’s place
^Sample.PersonI("SalaryIDX",4,1) = "1100" ; Rows 1 and 2 have values in 8’s place
^Sample.PersonI("SalaryIDX",5,1) = "0000" ; etc…
^Sample.PersonI("SalaryIDX",6,1) = "0100"
^Sample.PersonI("SalaryIDX",7,1) = "0010"
^Sample.PersonI("SalaryIDX",8,1) = "0001"
Ten en cuenta que las operaciones que modifican Sample.Employee o los salarios de sus filas (es decir, INSERT, UPDATES y DELETE) ahora requieren actualizar cada uno de estos nodos globales, o bitslices. Añadir un índice bitslice a múltiples propiedades de una tabla o una propiedad que se modifica con frecuencia puede conllevar riesgos de rendimiento. En general, mantener un índice bitslice es más costoso que mantener índices de tipo estándar o bitmap.
Los índices bitslice son altamente especializados y por eso tienen casos de uso específicos: consultas que deben realizar cálculos agregados (p.ej. SUM, COUNT o AVG).
Además, solo pueden usarse de forma efectiva sobre valores numéricos (las cadenas de caracteres se convierten a un 0 binario).
Si es necesario leer la tabla de datos, en lugar de los índices, para verificar la condición de una consulta, los índices bitslice no se elegirán para ejecutar la consulta. Digamos que Sample.Person no tiene un índice en Name. Si quisiéramos calcular el salario medio de los empleados cuyo apellido es Smith (“SELECT AVG(Salary) FROM Sample.Employee WHERE Name %STARTSWITH 'Smith,' “) necesitaríamos leer filas de datos para aplicar la condición WHERE, por lo que en la práctica no se usaría el índice bitslice.
Hay varios problemas de almacenamiento similares para índices bitslice y bitmap en tablas en las que se crean o eliminan filas frecuentemente.
Datos - Índices con datos almacenados en sus nodos globales.
Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];
En varios de los ejemplos anteriores, puedes haber observado la cadena “~Sample.Employee~” almacenada como el valor del propio nodo. Recuerda que Sample.Employee hereda índices de Sample.Person. Cuando hacemos una consulta sobre los empleados (Employees) en particular, leemos el valor en los nodos de índices que coinciden con la condición de nuestra propiedad para verificar que dicha persona (Person) también sea un empleado.
También podemos definir explícitamente qué valores almacenar. Definir los datos en tus nodos globales de índices puede ahorrarte lecturas de todos los datos globales. Esto puede ser útil para consultas ordenadas o selectivas frecuentes.
Tomamos como ejemplo el índice anterior. Si quisiéramos extraer información identificatoria sobre una persona dado su nombre completo o parcial (p. ej. para buscar información del cliente en una aplicación de atención al público), podríamos tener una consulta como “SELECT SSN, Name, DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J' ORDER BY Name”. Como las condiciones de nuestra consulta sobre Name y los valores que estamos recuperando se encuentran todos dentro de los nodos globales QuickSearchIDX, solo necesitamos leer nuestro global I para ejecutar esta consulta.
Ten en cuenta que los valores de datos no pueden almacenarse con índices bitmap o bitslice.
^Sample.PersonI("QuickSearchIDX"," LARSON,KIRSTEN A.",100115)=$lb("~Sample.Employee~","555-55-5555",51274,"Larson,Kirsten A.")
Índices iFind
¿Alguna vez ha oído hablar de ellos? Yo tampoco. Los índices iFind se usan en propiedades de flujo, pero para usarlos se deben especificar sus nombres con palabras clave en la consulta.
Podría explicarlo más en detalle, pero Kyle Baxter tiene un artículo muy útil sobre esto:
https://community.intersystems.com/post/free-text-search-way-search-your-text-fields-sql-developers-are-hiding-you
Continúa leyendo la Parte 2, sobre la gestión de índices definidos.
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).
Artículo
Rizmaan Marikar · 21 abr, 2022
Hay varias maneras de generar ficheros Excel usando tecnología InterSystems: por ejemplo utilizando informes generados con InterSystems Reports, o los antiguos informes ZEN, o incluso haciendo uso de librerías Java de terceros. Las posibilidades son casi infinitas.
Pero, ¿qué pasa si quieres crear una sencilla hoja de cálculo sólo con ObjectScript? (sin aplicaciones de terceros)
En mi caso, necesito generar informes que contengan muchos datos sin procesar (a los financieros les encantan), pero mi antiguo informe ZEN fallaba y me da lo que me gusta llamar un "archivo con cero bytes". Básicamente, Java se queda sin memoria y provoca una sobrecarga en el servidor de informes.
Esto se puede hacer usando Office Open XML (OOXML). El formato Office Open XML está compuesto por un número de archivos XML dentro de un paquete ZIP. Así que, básicamente, necesitamos generar estos archivos XML y comprimirlos renombrandolos a .xslx. Así de fácil.
Los archivos siguen un sencillo conjunto de convenciones llamadas Open Packaging Conventions (OPC). Hay que declar los tipos de contenido de las partes, así como indicar a la aplicación que lo consumirá donde debería empezar.
Parar crear una sencilla hoja de cálculo, necesitamos un mínimo de 5 ficheros:
workbook.xml
worksheet.xml
[Content_Types].xml
styles.xml
_rels
.rels
workbook.xml.rels
workbook.xmlEl workbook es el contenedor de diferentes worksheets. El workbook es donde puedes referenciar estilos, tablas de cadenas de texto compartidas, y otras piezas de información cuyo ámbito es la totalidad de la hoja de cálculo.
ClassMethod GenerateWorkbookXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"workbook.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<workbook xmlns='http://schemas.openxmlformats.org/spreadsheetml/2006/main' xmlns:r='http://schemas.openxmlformats.org/officeDocument/2006/relationships'>")
do stream.WriteLine("<sheets> <sheet name='"_workSheetName_"' sheetId='1' r:id='rId1'/>")
do stream.WriteLine("</sheets> </workbook>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
_rels/workbook.xml.relsSólo necesitamos crear una relación que tenga un id de rId1 de manera que coincida con la referencia desde la parte de workbook.xml
ClassMethod CreateRelsXML(){
set status =$$$OK
set isunix=$zcvt($p($zv," ",3,$l($p($zv," (")," ")),"U")["UNIX"
if isunix {
set ext="/"
}else{
set ext="\"
}
set xmlfile = fileDirectory_"_rels"_ext_"workbook.xml.rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>")
do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet' Target='worksheet.xml'/>")
do stream.WriteLine("<Relationship Id='rId2' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles' Target='styles.xml' />")
do stream.WriteLine("</Relationships>")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
set xmlfile = fileDirectory_"_rels"_ext_".rels"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Relationships xmlns='http://schemas.openxmlformats.org/package/2006/relationships'>")
do stream.WriteLine("<Relationship Id='rId1' Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument' Target='workbook.xml'/>")
do stream.WriteLine("</Relationships>")
try{
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
[Content_Types].xmlArchivo estático (por el momento, aunque debería ser un archivo dinámico dependiendo del número de worksheets) que vincula workbook, worksheet y estilos. Cada archivo Office Open XML debe declarar los tipos de contenido usados en el paquete ZIP. Eso se hace con el archivo [Content_Types].xml.
ClassMethod GenerateConntentTypesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"[Content_Types].xml"
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
try{
do stream.WriteLine("<?xml version='1.0' encoding='UTF-8' standalone='yes'?>")
do stream.WriteLine("<Types xmlns='http://schemas.openxmlformats.org/package/2006/content-types'>")
do stream.WriteLine("<Default Extension='rels' ContentType='application/vnd.openxmlformats-package.relationships+xml'/>")
do stream.WriteLine("<Override PartName='/workbook.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml'/>")
do stream.WriteLine("<Override PartName='/worksheet.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml'/>")
do stream.WriteLine("<Override PartName='/styles.xml' ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml' />")
do stream.WriteLine("</Types>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
styles.xmlTodo lo necesario para el formateo se coloca aquí. Por el momento hay varios estilos estáticos (aunque debería ser dinámico dependiendo del workbook)
Excel Styles
ID
Style
Excel Format
1
default
Text
2
#;[Red]-#
Number
3
#.##;[Red]-#.##
Number
4
yyyy/mm/dd
Date
5
hh:mm
Date
6
Header and Center Aligned
Text
7
Header 2 Left Aligned
Text
8
Good(Green Highlight)
General
9
Bad(Red Highlight)
General
10
Neutral(Orange Highlight)
General
11
yyyy/mm/dd hh:mm
Date
ClassMethod CreateStylesXML(){
set status =$$$OK
set xmlfile = tempDirectoryPath_"styles.xml"
try{
set stream = ##class(%Stream.FileCharacter).%New()
set sc=stream.LinkToFile(xmlfile)
do stream.WriteLine("<?xml version=""1.0"" encoding=""UTF-8"" standalone=""yes""?>")
do stream.WriteLine("<styleSheet xmlns=""http://schemas.openxmlformats.org/spreadsheetml/2006/main"" xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006"" mc:Ignorable=""x14ac x16r2 xr"" xmlns:x14ac=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"" xmlns:x16r2=""http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"" xmlns:xr=""http://schemas.microsoft.com/office/spreadsheetml/2014/revision"">")
do stream.WriteLine("<numFmts count=""4"">")
do stream.WriteLine("<numFmt numFmtId=""166"" formatCode=""#,##0;[Red]\-#,##0""/>")
do stream.WriteLine("<numFmt numFmtId=""168"" formatCode=""#,##0.00;[Red]\-#,##0.00""/>")
do stream.WriteLine("<numFmt numFmtId=""169"" formatCode=""dd\/mm\/yyyy;@""/>")
do stream.WriteLine("<numFmt numFmtId=""170"" formatCode=""dd/mm/yyyy\ hh:mm""/></numFmts>")
do stream.WriteLine("<fonts count=""5"" x14ac:knownFonts=""1"">")
do stream.WriteLine("<font><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF006100""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C0006""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><sz val=""10""/><color rgb=""FF9C5700""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font>")
do stream.WriteLine("<font><b/><sz val=""10""/><color theme=""1""/><name val=""Calibri""/><family val=""2""/><scheme val=""minor""/></font></fonts>")
do stream.WriteLine("<fills count=""5"">")
do stream.WriteLine("<fill><patternFill patternType=""none""/></fill>")
do stream.WriteLine("<fill><patternFill patternType=""gray125""/></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFC6EFCE""/></patternFill></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFC7CE""/></patternFill></fill>")
do stream.WriteLine("<fill><patternFill patternType=""solid""><fgColor rgb=""FFFFEB9C""/></patternFill></fill></fills>")
do stream.WriteLine("<borders count=""1""><border><left/><right/><top/><bottom/><diagonal/></border></borders>")
do stream.WriteLine("<cellStyleXfs count=""4"">")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""1"" fillId=""2"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" applyNumberFormat=""0"" applyBorder=""0"" applyAlignment=""0"" applyProtection=""0""/></cellStyleXfs>")
do stream.WriteLine("<cellXfs count=""12""><xf numFmtId=""0"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" quotePrefix=""1"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""166"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""168"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""169"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""20"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1""/>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""4"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1"" applyFont=""1"" applyAlignment=""1""><alignment horizontal=""center""/>")
do stream.WriteLine("</xf>")
do stream.WriteLine("<xf numFmtId=""49"" fontId=""1"" fillId=""2"" borderId=""0"" xfId=""1"" applyNumberFormat=""1""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""2"" fillId=""3"" borderId=""0"" xfId=""2""/>")
do stream.WriteLine("<xf numFmtId=""0"" fontId=""3"" fillId=""4"" borderId=""0"" xfId=""3""/>")
do stream.WriteLine("<xf numFmtId=""170"" fontId=""0"" fillId=""0"" borderId=""0"" xfId=""0"" applyNumberFormat=""1""/></cellXfs>")
do stream.WriteLine("<cellStyles count=""4""><cellStyle name=""Bad"" xfId=""2"" builtinId=""27""/>")
do stream.WriteLine("<cellStyle name=""Good"" xfId=""1"" builtinId=""26""/><cellStyle name=""Neutral"" xfId=""3"" builtinId=""28""/>")
do stream.WriteLine("<cellStyle name=""Normal"" xfId=""0"" builtinId=""0""/></cellStyles><dxfs count=""0""/>")
do stream.WriteLine("<tableStyles count=""0"" defaultTableStyle=""TableStyleMedium2"" defaultPivotStyle=""PivotStyleLight16""/> ")
do stream.WriteLine("<extLst><ext uri=""{EB79DEF2-80B8-43e5-95BD-54CBDDF9020C}"" xmlns:x14=""http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"">")
do stream.WriteLine("<x14:slicerStyles defaultSlicerStyle=""SlicerStyleLight1""/></ext><ext uri=""{9260A510-F301-46a8-8635-F512D64BE5F5}"" xmlns:x15=""http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"">")
do stream.WriteLine("<x15:timelineStyles defaultTimelineStyle=""TimeSlicerStyleLight1""/></ext></extLst>")
do stream.WriteLine("</styleSheet>")
do stream.%Save()
}catch{
set status=$$$NO
}
kill stream
return status
}
worksheet.xmlAquí es donde se colocan nuestros datos. La primera fila tendrá los títulos de las columnas. Las siguientes filas sólo tendrán datos.Aquí definiremos los anchos de columna para cada columna; si no, las columnas por defecto se configurarán con ajuste automático.
Worksheet de muestra en xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet xmlns="https://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="https://schemas.openxmlformats.org/officeDocument/2006/relationships">
<sheetData>
<row>
<c t="inlineStr">
<is>
<t>Name</t>
</is>
</c>
<c t="inlineStr">
<is>
<t>Amount</t>
</is>
</c>
</row>
<row>
<c t="inlineStr">
<is>
<t>Jhon Smith</t>
</is>
</c>
<c>
<v>1000.74</v>
</c>
</row>
<row>
<c t="inlineStr">
<is>
<t>Tracy A</t>
</is>
</c>
<c>
<v>6001.74</v>
</c>
</row>
</sheetData>
</worksheet>
Excel de muestra
Las fórmulas dentro de la worksheet las podemos incluir utilizando una etiqueta <f>
<c >
<f>B2*0.08</f >
</c >
<c >
<f>B2+C2</f >
</c>
y finalmente lo empaquetamos y renombramos a .xlsx (usando unix zip)
set cmd ="cd "_fileDirectory_" && find . -type f | xargs zip .."_ext_xlsxFile
Cómo generar un documento Excel
Este código de muestra genera un documento Excel.
set file = "/temp/test.xlsx"
set excelObj = ##class(XLSX.writer).%New(file)
do excelObj.SetWorksheetName("test1")
set status = excelObj.BeginWorksheet()
set row = 0
set row = row+1
;----------- excelObj.Cells(rowNumber,columnNumber,style,content)
set status = excelObj.Cells(row,1,1,"Header1")
set row = row+1
set status = excelObj.Cells(row,1,2,"Content 1")
set status = excelObj.EndWorksheet()
W !,excelObj.fileName
Podéis encontrar el código en :
https://github.com/RizmaanMarikar/ObjectScriptExcelGenerator
Artículo
Estevan Martinez · 11 feb, 2020
Esta publicación es el resultado directo de trabajar con un cliente de InterSystems que acudió a mí con el siguiente problema:
SELECT COUNT(*) FROM MyCustomTable
Esto tarda 0.005 segundos, con 2300 filas en total. Sin embargo:
SELECT * FROM MyCustomTable
Tardó algunos minutos. La razón de que esto sucediera es lo suficientemente sutil e interesante para mí como para escribir una publicación sobre ello. Esta publicación es extensa, pero si va hasta el final verá que escribí un resumen rápido. Por ello, si llegó hasta aquí y cree que ya leyó demasiado, desplácese hasta el final para leer únicamente las ideas principales. Busque la frase que se encuentra en negrita.
Cuando esté creando sus clases, hay un aspecto a tener cuenta relacionada con el almacenamiento. Como muchos ya saben, todos los datos en Caché se almacenan en globals.
<Paréntesis>
Si no sabe esto, creo que esta publicación va a ser un poco extensa. Recomiendo echar un vistazo a este excelente tutorial que se encuentra en nuestra documentación:
ObjectScript Tutorial
Si nunca ha utilizado Caché/Ensemble/HealthShare, el tutorial anterior es muy útil. E incluso si ya los ha utilizado, ¡vale la pena revisarlo!
</Paréntesis>
Ahora, debido a que todos los datos se almacenan en globals, es importante que comprenda cómo las definiciones de su clase se mapean con los globals. ¡Creemos una aplicación juntos! Repasaremos algunos de los errores más comunes y discutiremos cómo el desarrollo de su clase afecta a sus estrategias de almacenamiento, con atención especial al rendimiento de SQL.
Imaginemos que somos la Oficina del Censo de Estados Unidos y necesitamos una base de datos para almacenar la información de todas las personas que viven en el país. Entonces construiremos una clase como esta:
Class USA.Person extends %Persistent
{
Property Name as %String;
Property SSN as %String;
Property Address as %String;
Property DateOfBirth as %Date;
}
SSN significa "Social Security Number (Número de Seguridad Social)" que, aunque originariamente no estaba destinado a funcionar como un número para identificar a las personas, en la práctica es su número de identificación. Sin embargo, somos tradicionalistas, así que no lo utilizaremos para el ID. Dicho esto, a la vez queremos que se indexe, ya que es una excelente manera de buscar a alguien. Sabemos que algunas veces tendremos que buscar a las personas por su nombre, así que también queremos un índice de nombres. Y como a nuestro jefe le encantan los informes basados en grupos de edad, creemos que también sería bueno incluir un índice para la fecha de nacimiento (DOB). Así que vamos a añadir todo esto a nuestra clase
Class USA.Person extends %Persistent
{
Property Name as %String;
Property SSN as %String;
Property Address as %String;
Property DateOfBirth as %Date;
Index NameIDX On Name;
Index SSNIDX On SSN [Unique];
Index DOBIDX on DateOfBirth;
}
Muy bien. Agreguemos una fila y veamos cómo son nuestros globals. Nuestra sentencia INSERT se ve de la siguiente manera:
INSERT INTO USA.Person (Name,SSN,Address,DateOfBirth) VALUES
('Baxter, Kyle','111-11-1111','1 Memorial Drive, Cambridge, MA 02142','1985-07-20')
Y el global:
USER>zw ^USA.PersonD
^USA.PersonD=1
^USA.PersonD(1)=$lb("","Baxter, Kyle","111-11-1111","1 Memorial Drive, Cambridge, MA 02142",52796)
El almacenamiento predeterminado para una clase almacena sus datos en ^Package.ClassD. Si el nombre de la clase es demasiado extenso, puede descomponerse en otros más pequeños, y puede encontrarlo en la Definición de Almacenamiento que se encuentra en la parte inferior de su definición de clase. ¿Cómo se ven los índices?
USER>zw ^USA.PersonI
^USA.PersonI("DOBIDX",52796,1)=""
^USA.PersonI("NameIDX"," BAXTER, KYLE",1)=""
^USA.PersonI("SSNIDX"," 111-11-1111",1)=""
¡Genial! Nuestro almacenamiento se ve bastante bien hasta ahora. De modo que añadimos nuestros 320 millones de personas y podemos conseguir más personas muy rápidamente. Pero ahora tenemos un problema, porque queremos tratar al presidente y a todos los ex presidentes con algunas consideraciones especiales. Entonces, agregaremos una clase especial para el presidente:
Class USA.President extends USA.Person
{
Property PresNumber as %Integer;
Index PresNumberIDX on PresNumber;
}
Bien. Debido a la herencia, obtuvimos todas las propiedades de USA.Person, y agregamos una que nos permitiera saber qué número de presidente era. Ya que quiero ser lo menos político posible, colocaré INSERT para añadir a nuestro SIGUIENTE presidente. Esta es la sentencia:
INSERT INTO USA.President (Name,SSN,DateOfBirth,Address,PresNumber) VALUES ('McDonald,Ronald','221-18-7518','01-01-1963','1600 Pennsylvania Ave NW, Washington, DC 20006',45)
Nota: Su SSN significa 'burger'. Lo siento mucho si es su SSN.
¡Genial! Veamos el global de su presidente:
USER>zw ^USA.PresidentD
¡No hay datos! Y aquí es donde llegamos al motivo de esta publicación. Debido a que PRIMERO decidimos heredarlo desde USA.Person, no solo heredamos sus propiedades e índices, ¡también obtuvimos su almacenamiento! Entonces, para localizar al presidente McDonald tenemos que buscarlo en ^USA.PersonD. Podemos ver lo siguiente:
^USA.PersonD(2)=$lb("~USA.President~","McDonald,Ronald","221-18-7518","1600 Pennsylvania Ave NW, Washington, DC 20006",44560)
^USA.PersonD(2,"President")=$lb(45)
Aquí destacamos dos cosas importantes. La primera es que podemos ver que el nodo (2) tiene toda la información que ya se almacenó en USA.Person. Mientras que el nodo (2, "President") solamente tiene la información específica para la clase USA.President.
¿Qué significa esto en la práctica? Bueno, si queremos realizar: SELECT * FROM USA.President entonces NECESITAREMOS revisar toda la tabla de personas. Si esperamos que la tabla de personas tenga 320,000,000 filas, y la tabla de presidentes 45, ¡entonces debemos hacer más de 320,000,045 referencias globales para extraer 45 filas! De hecho, si miramos el plan de la consulta:
Read master map USA.President.IDKEY, looping on ID.
For each row:
Output the row.
Vemos exactamente lo que esperábamos. Sin embargo, ya vimos que esto significa realizar búsquedas en todo el global ^USA.PersonD. Por tanto, esta será una referencia global de más de 320,000,000 ya que necesitamos probar CADA ^USA.PersonD para comprobar si existen datos en ^USA.PersonD(i, "Presidente"), pues no sabemos cuáles de entre todas las personas serán los presidentes. Esto no está muy bien. ¡No es lo que queríamos hacer! ¡¿Qué podemos hacer ahora?! Bueno, tenemos 2 opciones:
Opción 1
Agregar un índice de extensión. Si lo hacemos, obtendremos una lista de los ID, de modo que sabremos qué personas son presidentes y podemos utilizar esa información para leer nodos específicos del global ^USA.Person. Debido a que tengo un almacenamiento predeterminado, puedo utilizar un índice para mapas de bits, el cual hará que este proceso se realice aún más rápidamente. Agregamos el índice de esta manera:
Index Extent [Type=Bitmap, Extent];
Y cuando buscamos SELECT * FROM USA.President en nuestro plan de consulta, podemos observar:
Read extent bitmap USA.President.Extent, looping on ID.
For each row:
Read master map USA.President.IDKEY, using the given idkey value.
Output the row.
Muy bien, ahora esto va a ser rápido. Una referencia global para revisar la extensión, y después 45 más para los presidentes. Eso es bastante eficiente.
¿Cuáles son los inconvenientes? Bueno, unir esta tabla lo vuelve un poco más problemático y puede involucrar más tablas temporales de las que le gustaría tener.
Opción 2
Cambiar la definición de clase por:
Class USA.President extends (%Persistent, USA.Person)
Hacer que %Persistent sea la primera clase extendida significaría que USA.President obtendrá su propia definición de almacenamiento. Entonces, los presidentes se almacenarán de la siguiente manera:
USER>zw ^USA.PresidentD
^USA.PresidentD=1
^USA.PresidentD(1)=$lb("","McDonald,Ronald","221-18-7518","1600 Pennsylvania Ave NW, Washington, DC 20006",44560,45)
Esto está bien, porque seleccionarlo desde USA.President. significa que podrá leer los 45 miembros de este global. Es agradable, fácil y tiene un diseño limpio.
¿Cuáles son los inconvenientes? Bueno, ahora los Presidentes NO están en la tabla de personas (Person). De modo que si quiere la información acerca de los presidentes y las personas que no son presidentes, es necesario que haga lo siguiente SELECT ... FROM USA.Person UNION ALL SELECT ... FROM USA.President
Si dejó de leer al principio, siga leyendo aquí:
Cuando creamos herencias, tenemos dos opciones
Opción 1: Heredar desde la primera superclase. Esto almacena los datos en el mismo global que la superclase. Hacer esto es útil si quiere tener toda la información junta, y puede mitigar los problemas de rendimiento en la subclase mediante un índice de extensión.
Opción 2: Heredar primero desde %Persistent. Esto almacena los datos en un nuevo global. Hacer esto es útil si va a consultar mucho la subclase. Sin embargo, si quiere ver los datos tanto de la superclase como de la subclase, necesita utilizar una consulta de tipo UNION.
¿Cuál de estas opciones es mejor? Bueno, eso depende de cómo va a utilizar su aplicación. Si va a querer hacer muchas consultas sobre todos los datos juntos, entonces probablemente prefiera el primer método. Sin embargo, si cree que nunca tendrá que consultar todos los datos juntos, entonces probablemente prefiera utilizar el segundo método. Ambos son bastante buenos, siempre y cuando recuerde el índice de extensión en la Opción 1.
¿Preguntas? ¿Comentarios? ¿Se siente confundido? Puede escribir en la parte de abajo.
¡Muchas gracias!
Artículo
Javier Lorenzo Mesa · 25 jun, 2020
¡Hola desarrolladores!
El desarrollo de una aplicación web Full-Stack en JavaScript con Caché requiere que juntes los bloques correctos para construirla. En la primera parte de este artículo, creamos una aplicación básica para desarrollar el frontal de usuario (front-end) en React. En esta segunda parte, mostraré cómo elegir la tecnología más adecuada para desarrollar la parte servidora (back-end) de tu aplicación. Verás que Caché te permite utilizar diferentes enfoques para vincular tu front-end con tu servidor de Caché, dependiendo de las necesidades de tu aplicación. También configuraremos un back-end mediante Node.js/QEWD y CSP/REST. En la siguiente parte, vamos a mejorar nuestra aplicación básica web y la conectaremos a Caché usando estas tecnologías.
Caché es una base de datos multi-modelo muy potente y flexible, además de un servidor para aplicaciones que cuenta con un enfoque único. Permite conectar el front-end de tu aplicación utilizando muchas tecnologías diferentes. Esta es una gran ventaja, pero también puede hacer que sea más difícil decidir cuál de estas tecnologías se debe utilizar para desarrollar el back-end.
Como desarrollador, quieres elegir una tecnología que permanezca a lo largo del tiempo, te haga ser productivo durante el desarrollo, mantenga estable la base de tu código y permita que tu aplicación se ejecute en tantos dispositivos y plataformas como sea posible utilizando el mismo código fuente.
Estos requisitos te conducen hoy en día, inevitablemente, a escribir las aplicaciones con JavaScript porque es el lenguaje que se utiliza en los navegadores (lo que hace que esté disponible en todo tipo de plataformas y sea tan popular). La aparición de Node.js hizo que también fuera posible utilizar JavaScript del lado del servidor, lo que permite a los desarrolladores usar el mismo lenguaje tanto en el front-end como en el back-end.
Básicamente, puedes escribir un sitio web o una aplicación web de dos maneras diferentes: usando la tecnología del lado del servidor o del lado del cliente (rendering). El renderizado del lado del servidor es una excelente opción para los sitios web habituales, mientras que el renderizado del lado del cliente es la mejor elección cuando se escribe una aplicación web (dado que también necesita funcionar sin conexión). Las páginas CSP de Caché son un buen ejemplo de la tecnología del lado del servidor, mientras que React es una tecnología del lado del cliente (aunque actualmente también puede utilizarse para el renderizado del lado del servidor). Esta es una decisión que deberás tomar y depende totalmente de tu aplicación.
Por ahora, nos centraremos en la escritura de las aplicaciones web, no hablaré de las páginas CSP aquí.
En el back-end, también deberás elegir qué tecnologías utilizarás para tu aplicación web: ¿usarás únicamente el servidor para aplicaciones de Caché y las llamadas CSP/REST o usarás Node.js como tu servidor para aplicaciones de modo que funcione con Caché? Al utilizar Node.js como tu servidor para la aplicación tendrás lo mejor de ambos mundos: te permite escribir el código de tu back-end en JavaScript con Caché, como una base de datos y un servidor para aplicaciones detrás de él.
¿Qué hace que esta combinación sea tan potente?
Utiliza el mismo lenguaje (JavaScript) tanto para el front-end como para el back-end
Utiliza todos los módulos estándares disponibles de Node.js en tus aplicaciones (esto aumenta la funcionalidad de tu aplicación hacia todos los tipos de dispositivos y servicios externos que puedas imaginar)
Te permite reutilizar tu código COS existente mediante envoltorios de función muy pequeños
Integra tu aplicación con otros servicios y protocolos estándar
...
Escribir tus aplicaciones con estas tecnologías no te limita a una única tecnología: de hecho, ¡puedes combinarlas todas en la misma aplicación!
La siguiente pregunta es cómo puedes vincular tu front-end con tu back-end. Actualmente, existen diversas opciones, cada una con sus (des)ventajas:
WebSockets: crea un canal bidireccional (con estados) para el back-end y tu servidor puede "impulsar" (push) un mensaje hacia el front-end, no es necesario que tu aplicación inicie primero una solicitud. Esta es una excelente opción para las aplicaciones internas que cuentan con una conexión estable a la red. La biblioteca socket.io de Node.js también proporciona una "degradación elegante", ya que cuenta con una función de retroceso para las llamadas de Ajax
REST: tu front-end está ligeramente acoplado a su back-end (sin estados), no es posible la comunicación bidireccional y la aplicación tiene que iniciar cada solicitud. Esta es una buena opción para las aplicaciones que no requieren de sesiones, redes inestables,...
Ajax: aún se sigue utilizando ampliamente, y también sirve como una función de retroceso para las conexiones de WebSockets
Como primer ejemplo, presentaré las características de un back-end en Node.js usando el módulo QEWD:
crea un canal de comunicación seguro entre tu aplicación web y el back-end a través de WebSockets, de manera predeterminada, mediante la biblioteca que está incorporada en socket.io, y te permite cambiar al modo Ajax de forma transparente (¡sin que sea necesario modificar el código de tu aplicación!)
conecta el código de tu aplicación desde el back-end hacia tu base de datos en Caché
contiene un potente servidor Express REST estándar, que proporciona los servicios REST en el mismo servidor de back-end, y también te permite crear llamadas federadas hacia los servidores remotos subyacentes y combinarlo todo en una sola respuesta (¡incluso te permite interceptar y modificar la respuesta según tus propias necesidades!)
es completamente modular y se integra con los módulos de Node.js que ya existen; también le permite utilizar todos los demás módulos de Node.js en tus aplicaciones. Incluso te permite reemplazar el servidor Express por algún otro, como Ember.js
simplifica enormemente el desarrollo de código en el back-end, ya que resuelve el problema de la falta de sincronización durante la codificación en JavaScript: esto te permite escribir tu código de una manera completamente sincronizada
crea un procesamiento múltiple del back-end para ti, con tantos procesos de Node.js como sean necesarios - la funcionalidad de Node.js en sí no lo proporciona de manera predeterminada
te permite utilizar cualquier tipo de estructura del lado del cliente (front-end), como React, Angular, ExtJS, Vue.js,...
Antes de que empecemos, hay que tener en cuenta una consideración importante: en la parte 1 construimos una pequeña demo de una aplicación en React mediante el módulo create-react-app. Notarías que este módulo contiene un servidor de programación en Node.js que se ejecuta en el localhost:3000. Es importante tener en cuenta que este servidor únicamente sirve para el front-end - el servidor back-end QEWD que configuraremos aquí es un servidor back-end (para aplicaciones) completamente distinto y que se ejecuta por separado en un puerto diferente (normalmente en el localhost:8080 para el desarrollo). Durante el desarrollo, tanto el servidor de programación React en el puerto 3000 como el servidor para aplicaciones QEWD en el puerto 8080 se ejecutarán en la misma máquina, ¡no te confundas con los dos servidores! En producción, la situación será diferente: tu aplicación React se ejecutará desde el navegador del cliente y tu servidor QEWD recibirá en el servidor de Caché las conexiones que provengan del cliente.
A continuación, mostraré todos los pasos necesarios para comenzar nuestro ejemplo de una aplicación en React.
Primero, abre una línea de comandos en Windows para instalar QEWD en tu sistema:
Cuando se termine la instalación, verás un par de advertencias que puedes ignorar. Observa cómo el módulo QEWD utiliza los módulos Node.js que ya existen como bloques de construcción (echa un vistazo dentro de la carpeta C:\qewd\node_modules). El módulo qewd-monitor se utilizará más adelante para monitorizar tu servidor QEWD y el módulo cors será necesario para las solicitudes REST.
El QEWD necesita conectarse con Caché, por lo que debemos copiar el conector cache.node que usamos durante el primer script de prueba entre Node.js y Caché en C:\qewd\node_modules. Esto permitirá que QEWD inicie nuestros nuevos procesos o "procesos hijos" en Node.js, que se conectarán con Caché.
Solo necesitamos crear un pequeño script de inicio en JavaScript, que nos permita iniciar el servidor para aplicaciones QEWD. Primero, añade la carpeta del proyecto C:\qewd en tu editor Atom. Después, dentro de la carpeta C:\qewd, crea un nuevo archivo qewd-start.js y ábrelo para editarlo.
Copia/pega el siguiente código en este archivo y guárdalo:
// define the QEWD configuration (adjust directories if needed)
let config = {
managementPassword: 'keepThisSecret!',
serverName: 'My first QEWD Server',
port: 8080,
poolSize: 1,
database: {
type: 'cache',
params: {
path:"C:\\InterSystems\\Cache\\Mgr",
username: "_SYSTEM",
password: "SYS",
namespace: "USER"
}
}
};
// include the cors module to automatically add CORS headers to REST responses
let cors = require('cors');
// define the QEWD Node.js master process variable
let qewd = require('qewd').master;
// define the internal QEWD Express module instance
let xp = qewd.intercept();
// define a basic test path to test if our QEWD server is up- and running
xp.app.get('/testme', function(req, res) {
console.log('*** /testme query: ', req.query);
res.send({
hello: 'world',
query: req.query
});
});
// start the QEWD server now ...
qewd.start(config);
* Para tener una introducción completa al servidor para aplicaciones QEWD para Node.js/Caché, recomiendo consultar la documentación, disponible en el apartado "Training" en el menú superior.
Ahora podemos iniciar nuestro servidor para aplicaciones QEWD en la línea de comandos:
Si recibes alguna advertencia del firewall de Windows, concede permiso a los procesos de Node.js para que tengan acceso a la red.
Ahora abre Chrome, e instala la extensión (app) Advanced REST client (ARC) para depurar las solicitudes REST que enviaremos hacia nuestro servidor para aplicaciones.
Abre una nueva pestaña en la página y haz clic sobre la opción "Apps" en la barra de herramientas. Verás que la nueva extensión está disponible allí. Haz clic en el icono para iniciar la aplicación ARC.
Ahora escribe la url http://localhost:8080/testme?nodejs=hot&cache=cool en la solicitud de línea de texto que se encuentra en la parte superior y observa la respuesta del módulo QEWD/Express:
¡Enhorabuena! Si ves esta respuesta, tu servidor QEWD se inició satisfactoriamente ¡y el módulo Express en su interior también está funcionando!
Hasta ahora, aún no nos hemos conectado a Caché: solo probamos el proceso maestro QEWD, en el que se recibirán todas las solicitudes. Este proceso está recibiendo las conexiones de WebSockets y el módulo Express está listo para atender las solicitudes REST.
La primera opción que implementaremos ahora para acceder a nuestro back-end será mediante el servidor QEWD REST (consulta la documentación): crear un archivo para el módulo testrest.js dentro de C:\qewd\node_modules
module.exports = {
restModule: true,
handlers: {
isctest: function(messageObj, finished) {
// this isctest handler retrieves text from the ^nodeTest global and returns it
let incomingText = messageObj.query.text;
let nodeTest = new this.documentStore.DocumentNode('nodeTest');
let d = new Date();
let ts = d.getTime();
nodeTest.$(ts).value = incomingText;
finished({text: 'You sent: ' + incomingText + ' at ' + d.toUTCString()});
}
}
};
Guarda este archivo y edita el archivo qewd-start.js en C:\qewd. Aquí necesitamos añadir un endpoint para nuestro módulo REST:
// define the QEWD configuration (adjust directories if needed)
let config = {
managementPassword: 'keepThisSecret!',
serverName: 'My first QEWD Server',
port: 8080,
poolSize: 1,
database: {
type: 'cache',
params: {
path:"C:\\InterSystems\\Cache\\Mgr",
username: "_SYSTEM",
password: "SYS",
namespace: "USER"
}
}
};
// include the cors module to automatically add CORS headers to REST responses
let cors = require('cors');
// define the QEWD Node.js master process variable
let qewd = require('qewd').master;
// define the internal QEWD Express module instance
let xp = qewd.intercept();
// define a basic test path to test if our QEWD server is up- and running
xp.app.get('/testme', function(req, res) {
console.log('*** /testme query: ', req.query);
res.send({
hello: 'world',
query: req.query
});
});
// define REST endpoint for /testrest requests
xp.app.use('/testrest', cors(), xp.qx.router());
// start the QEWD server now ...
qewd.start(config);
La línea que añadimos enrutará todas las solicitudes que empiecen con la ruta /testrest hacia nuestro módulo testrest.js. Para activar la nueva ruta, es necesario que reiniciemos el servidor QEWD presionando Ctrl-C en la ventana de la línea de comandos y reiniciarlo:
* Ten en cuenta que incluso puedes reiniciar un servidor QEWD, cuando los clientes estén conectados, en caso de que lo necesites (no se recomienda hacer este procedimiento en un sistema ocupado): todas las conexiones de los clientes se reiniciarán/reconectarán automáticamente. Por cierto, también existe una manera más sencilla para añadir/(re)definir dinámicamente las rutas en un servidor QEWD que esté en ejecución, aunque esto requiere de un pequeño módulo de enrutamiento, que le proporcionará más flexibilidad sin la necesidad de detener el servidor QEWD.
Ahora, vuelve al ARC REST client y envía una nueva solicitud para http://localhost:8080/testrest/isctest?text=REST+call+to+cache
En la línea de comandos donde se está ejecutando el servidor QEWD, verás que aparecen mensajes de la llamada entrante:
Ahora puedes ver que QEWD comenzó un proceso de trabajo para esta solicitud. ¿Por qué? El proceso maestro (el servidor Express) recibe tu solicitud y se la entrega al middleware QEWD Express cors(), en primer lugar, para añadir los encabezados CORS y después para agregar la función de enrutamiento QEWD/REST xp.qx.router() en este momento, que añade la solicitud a la lista de solicitudes en espera e inicia un "proceso hijo" (el worker). Estos workers se encargan de gestionar tus solicitudes y procesan el código de tu aplicación (en testrest.js). Esto permite el procesamiento múltiple de las solicitudes, ya que QEWD puede iniciar tantos workers como sea necesario.
Probablemente te diste cuenta de que, en la salida del espacio de trabajo, el proceso maestro recibió la solicitud REST y entregó dicha solicitud al worker. Entonces, el worker carga el módulo de tu aplicación, llama al controlador de las solicitudes correcto y envia la respuesta.
Ahora que ya definimos un endpoint para QEWD REST, añadiremos una segunda opción para llamar a nuestro back-end utilizando WebSockets: crear un archivo para el módulo test.js en C:\qewd\node_modules, para procesar las solicitudes (mensajes) que entran mediante las conexiones de WebSockets:
module.exports = {
handlers: {
isctest: function(messageObj, session, send, finished) {
// get the text coming in from the message request
var incomingText = messageObj.params.text;
// instantiate the global node ^nodeTest as documentStore abstraction
let nodeTest = new this.documentStore.DocumentNode('nodeTest');
// get the current date & time
let d = new Date();
let ts = d.getTime();
// save the text from the request in the ^nodeTest global (subscripted by the current timestamp)
nodeTest.$(ts).value = incomingText;
// return the response to the client using WebSockets (or Ajax mode)
finished({text: 'You sent: ' + incomingText + ' at ' + d.toUTCString()});
}
}
};
Este controlador de solicitudes es casi igual al controlador de solicitudes REST, sin embargo, notarás que hay algunas diferencias sutiles: en los parámetros que definen las funciones del controlador, ahora están disponibles un objeto de sesión y un método send(). El método send() permite que el back-end envíe mensajes "push" hacia el front-end, y también tenemos un objeto de sesión ahora, porque las conexiones de WebSockets tienen estados.
No es posible probar este controlador de solicitudes de WebSockets mediante la herramienta ARC, porque necesitamos configurar una conexión de WebSockets para probarlo. En la siguiente parte, donde mejoraremos el front-end de nuestra aplicación en React, verás cómo funciona el controlador para iniciar una conexión de WebSockets con nuestro back-end QEWD.
Para tener información más detallada y formación sobre el funcionamiento del servidor QEWD/Express, te recomiendo que consultes la cdocumentación, disponible en el apartado "Training" en el menú superior.
La tercera opción para establecer una conexión con nuestro back-end es utilizar las llamadas CSP/REST: ve al portal del Sistema y en la sección Administración del sistema, ve a las Seguridad - Aplicaciones - Aplicaciones web y define una nueva aplicación web (consulta también la documentación sobre CSP/REST):
Abre Caché Studio, crea una clase App.TestRestHandler en el namespace USER y añade este código:
Class App.TestRestHandler Extends %CSP.REST
{
Parameter HandleCorsRequest = 1;
XData UrlMap
{
<Routes>
<Route Url="/isctest/:text" Method="GET" Call="GetIscTest"/>
</Routes>
}
ClassMethod GetIscTest(text As %String = "") As %Status
{
#Dim e as %Exception.AbstractException
#Dim status as %Status
Try {
Set d = $now()
Set ts = $zdt(d,-2)
Set ^nodeTest(ts) = text
If $Data(%response) Set %response.ContentType="application/json"
Write "{"
Write " ""text"" : ""You sent: ",text," at ",$zdt(d,5,1),""""
Write "}"
} Catch (e) {
Set status=e.AsStatus()
Do ..ErrorHandler(status)
}
Quit $$$OK
}
ClassMethod ErrorHandler(status)
{
#Dim errorcode, errormessage as %String;
set errorcode=$piece(##class(%SYSTEM.Status).GetErrorCodes(status),",")
set errormessage=##class(%SYSTEM.Status).GetOneStatusText(status)
Quit ..ErrorHandlerCode(errorcode,errormessage)
}
ClassMethod ErrorHandlerCode(errorcode, errormessage) As %Status
{
Write "{"
Write " ""ErrorNum"" : """,errorcode,""","
Write " ""ErrorMessage"" : """,errormessage,""""
write "}"
If $Data(%response) {
Set %response.ContentType="application/json"
}
quit $$$OK
}
}
* Gracias a Danny Wijnschenk por este ejemplo sobre CSP/REST
Vamos a probar este back-end REST también, usando ARC:
¡Enhorabuena! Hemos creado tres posibles maneras para establecer una conexión entre nuestro back-end y Caché:
utilizando el servidor para aplicaciones Node.js/QEWD/Express mediante llamadas REST
utilizando el servidor para aplicaciones Node.js/QEWD/Express mediante WebSockets (con funciones de retroceso opcionales para las llamadas de Ajax)
utilizando el puerto de enlace CSP/REST con Caché como el servidor para aplicaciones, mediante clases para el controlador CSP/REST
Como puedes ver, con Caché hay muchas maneras de lograr nuestro objetivo. Puedes decidir cuál de las opciones es la más apropiada para ti.
Si tú (o la mayoría de tu equipo de desarrollo) estás familiarizado con JavaScript y quieres desarrollar tu aplicación utilizando el mismo lenguaje, pero con los potentes frameworks de JavaScript para el front-end, y/o tu aplicación necesita las funciones o servicios externos que ya estén disponibles en los módulos de Node.js, entonces un servidor para aplicaciones Node.js que funcione con Caché es tu mejor opción, porque tendrás lo mejor de ambos mundos y podrás conservar y reutilizar tu código, las clases y el SQL en Caché. El servidor para aplicaciones QEWD se diseñó específicamente para escribir aplicaciones empresariales con Caché y es tu mejor opción para integrar el front-end y el back-end.
Si tu front-end está escrito en HTML simple, no estás utilizando los potentes frameworks de JavaScript y tu aplicación únicamente necesita llamadas REST hacia Caché, puedes conectarte directamente a Caché usando CSP/REST. Solamente recuerda que en este caso no podrás usar todos los módulos fácilmente disponibles de Node.js.
Ahora, ya estamos listos para la parte 3: ¡conectaremos el front-end de nuestra aplicación en React con los tres posibles back-ends!
Artículo
Kurro Lopez · 5 dic, 2019
¡Hola a tod@s!
En las partes anteriores (1, 2) de este artículo, hablamos de Globals como árboles. En esta tercera parte, los veremos como matrices dispersas.
Una matriz dispersa es un tipo de matriz donde la mayoría de los valores asumen un valor idéntico.
En la práctica, a menudo veréis matrices dispersas tan grandes que no tiene sentido ocupar memoria con elementos idénticos. Por lo tanto, tiene sentido organizar matrices dispersas de tal manera que no se desperdicie memoria al almacenar valores duplicados.
En algunos lenguajes de programación, las matrices dispersas son parte del lenguaje (por ejemplo, en J, MATLAB). En otros lenguajes, hay bibliotecas especiales que permiten usarlas. Para C ++, esos serían Eigen y similares.
Los Globals son buenos candidatos para implementar matrices dispersas por las siguientes razones:
Solo almacenan valores de nodos particulares y no almacenan valores indefinidos;
La interfaz de acceso para un valor de nodo es extremadamente similar a la que ofrecen muchos lenguajes de programación para acceder a un elemento de una matriz multidimensional.
Set ^a(1, 2, 3)=5
Write ^a(1, 2, 3)
Un Global es una estructura de nivel bastante bajo para almacenar datos, razón por la cual los Globals poseen características de rendimiento sobresalientes (cientos de miles a docenas de millones de transacciones por segundo dependiendo del hardware, ver 1)
Dado que una estructura Global es persistente, solo tiene sentido crear matrices dispersas sobre la base de situaciones en las que sabe de antemano que tendrá suficiente memoria para ellas.
Uno de los matices de la implementación de matrices dispersas es el retorno de un cierto valor por defecto si se dirige a un elemento indefinido.
Esto se puede implementar usando la función $GET en COS. Echemos un vistazo a una matriz tridimensional en este ejemplo.
SET a = $GET(^a(x,y,z), defValue)
Entonces, ¿qué tipo de tareas requieren matrices dispersas y cómo pueden ayudarlo los Globals?
Matriz de adyacencia
Tales matrices se utilizan para la representación gráfica:
Es obvio que cuanto más grande es un gráfico, más ceros habrá en la matriz. Si miramos un gráfico de una red social, por ejemplo, y lo representamos como una matriz o este tipo, consistirá principalmente en ceros, es decir, será una matriz dispersa.
Set ^m(id1, id2) = 1
Set ^m(id1, id3) = 1
Set ^m(id1, id4) = 1
Set ^m(id1) = 3
Set ^m(id2, id4) = 1
Set ^m(id2, id5) = 1
Set ^m(id2) = 2
....
En este ejemplo, guardaremos la matriz de adyacencia en el global ^m, así como el número de bordes de cada nodo (quién es amigo de quién y el número de amigos).
Si el número de elementos en el gráfico no supera los 29 millones (este número se calcula como 8 * longitud máxima del string), Incluso hay un método más económico para almacenar tales matrices: cadenas de bits, ya que optimizan los espacios grandes de una manera especial.
Las manipulaciones con cadenas de bits se llevan a cabo con la ayuda de la función $BIT .
; setting a bit
SET $BIT(rowID, positionID) = 1
; getting a bit
Write $BIT(rowID, positionID)
Tabla de conmutadores FSM
Dado que el gráfico de conmutadores FSM es un gráfico regular, la tabla de conmutadores FSM es esencialmente la misma matriz de adyacencia de la que hablamos anteriormente.
Autómata celular
El autómata celular más famoso es "El juego de la vida", donde las reglas (cuando una celda tiene muchos vecinos, muere) esencialmente la convierten en una matriz dispersa.
Stephen Wolfram cree que los autómatas celulares son un nuevo campo de la ciencia. En 2002, publicó un libro de 1280 páginas llamado "Un nuevo tipo de ciencia", donde afirma que los logros en el área de autómatas celulares no están aislados, pero son bastante estables y son importantes para todos los campos de la ciencia.
Se ha demostrado que cualquier algoritmo que pueda ser procesado por una computadora también puede implementarse con la ayuda de un autómata celular. Los autómatas celulares se utilizan para simular entornos y sistemas dinámicos, para resolver problemas algorítmicos y para otros fines.
Si tenemos un campo enorme y necesitamos registrar todos los estados intermedios de un autómata celular, tiene sentido usar Globals.
Cartografía
Lo primero que me viene a la mente cuando se trata de usar matrices dispersas es la cartografía.
Como regla general, los mapas tienen mucho espacio vacío. Si imaginamos que el mapa mundial se compone de píxeles grandes, veremos que el 71% de todos los píxeles de la Tierra estarán ocupados por una matriz dispersa del océano. Y si solo agregamos estructuras artificiales al mapa, habrá más del 95% del espacio vacío.
Por supuesto, nadie almacena mapas como matrices de mapas de bits, todos usan la representación vectorial en su lugar.¿Pero qué son los mapas vectoriales? Es una especie de marco junto con polilíneas y polígonos. En esencia, es una base de datos de puntos y relaciones entre ellos.
Una de las tareas más desafiantes en cartografía es la creación de un mapa de nuestra galaxia realizado por el telescopio Gaia. Hablando en sentido figurado, nuestra galaxia es un mamut de matriz dispersa : enormes espacios vacíos con puntos brillantes ocasionales: estrellas. Es 99,999999 .......% de espacio absolutamente vacío. Cache, una base de datos basada en Globals, fue seleccionada para almacenar el mapa de nuestra galaxia.
No sé la estructura exacta de Globals en este proyecto, pero puedo suponer que es algo así:
Set ^galaxy(b, l, d) = 1; star catalog number, if exists
Set ^galaxy(b, l, d, "name") = "Sun"
Set ^galaxy(b, l, d, "type") = "normal" ; Otras opciones pueden incluir un agujero negro, cuásar, enana roja y similar.
Set ^galaxy(b, l, d, "weight") = 14E50
Set ^galaxy(b, l, d, "planetes") = 7
Set ^galaxy(b, l, d, "planetes", 1) = "Mercury"
Set ^galaxy(b, l, d, "planetes", 1, weight) = 1E20
...
Donde b, l, d son coordenadas galácticas: latitud, longitud y distancia desde el sol.
La estructura flexible de Globals le permite almacenar cualquier característica de estrella y planeta, ya que las bases de datos globales no tienen esquemas.
Se seleccionó Caché para almacenar el mapa de nuestro universo no solo por su flexibilidad, sino también por su capacidad de guardar rápidamente un hilo de datos mientras se crean simultáneamente Globales de índice para una búsqueda rápida.
Si volvemos a la Tierra, se usaron Globals en proyectos centrados en mapas OpenStreetMap XAPI y FOSM, una bifurcación de OpenStreetMap.
Recientemente, en un hackathon de Caché, un grupo de desarrolladores implementó Geospatial indexes usando esta tecnología. Verr el artículo para mas detalles.
Implementación de índices geoespaciales usando Globals en OpenStreetMap XAPI
Se tomaron ilustraciones de esta presentación.
Todo el globo se divide en cuadrados, luego en subcuadros, luego en más subcuadros, y así sucesivamente. Al final, obtenemos una estructura jerárquica para la que se crearon Globals.
En cualquier momento, podemos solicitar instantáneamente cualquier cuadrado o vaciarlo, y todos los subcuadrados serán devueltos o vaciados también.
Un esquema detallado basado en Globals puede implementarse de varias maneras.
Variante 1:
Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 1) = idPointOne
Set ^m(a, b, a, c, d, a, b,c, d, a, b, a, c, d, a, b,c, d, a, 2) = idPointTwo
...
Variante 2:
Set ^m('abacdabcdabacdabcda', 1) = idPointOne
Set ^m('abacdabcdabacdabcda', 2) = idPointTwo
...
En ambos casos, no será un gran problema en COS/M solicitar puntos ubicados en un cuadrado de cualquier nivel. Será un poco más fácil limpiar segmentos cuadrados de espacio en cualquier nivel en la primera variante, pero esto rara vez se requiere.
Un ejemplo de un cuadrado de bajo nivel.:
Y aquí hay algunos Globals del proyecto XAPI: representación de un índice basado en Globals:
El Global ^way es utilizado para almacenar los vértices de polilíneas (caminos, ríos pequeños, etc.) y polígonos (áreas cerradas: edificios, bosques, etc.).
Una clasificación aproximada del uso de matrices dispersas en Globals.
Almacenamos las coordenadas de algunos objetos y su estado (cartografía, autómatas celulares).
Almacenamos matrices dispersas.
En la variante 2) cuando se solicita una determinada coordenada y no hay un valor asignado a un elemento, necesitamos obtener el valor predeterminado del elemento de la matriz dispersada.
Beneficios que obtenemos al almacenar matrices multidimensionales en Globals
Eliminación rápida y/o selección de segmentos de espacio que son múltiples de cadenas, superficies, cubos, etc.Para casos con índices enteros, puede ser conveniente poder eliminar rápidamente y/o seleccione segmentos de espacio que sean múltiples de cadenas, superficies, cubos y demás.
El comando Kill puede eliminar un elemento independiente, una cadena e incluso una superficie completa. Gracias a las propiedades de lo global, ocurre muy rápidamente, mil veces más rápido que la eliminación de elemento por elemento.
La ilustración muestra una matriz tridimensional en Global ^a y diferentes tipos de eliminaciones.
Para seleccionar segmentos de espacio por índices conocidos, puedes usar el comando Merge .
Selección de una columna de matriz en la variable Column:
; Let's define a three-dimensional 3x3x3 sparse array
Set ^a(0,0,0)=1,^a(2,2,0)=1,^a(2,0,1)=1,^a(0,2,1)=1,^a(2,2,2)=1,^a(2,1,2)=1
Merge Column = ^a(2,2)
; Let's output the Column variable
Zwrite Column
Output:
Column(0)=1
Column(2)=1
Lo interesante es que tenemos una matriz dispersa en la variable Column que puede abordar a través de $GET ya que los valores predeterminados no se almacenan allí.
La selección de segmentos de espacio también se puede hacer con la ayuda de un pequeño programa que utiliza la función $Order . Esto resulta especialmente útil en espacios con índices no cuantificados (cartografía).
Conclusión
Las realidades de hoy plantean nuevos desafíos. Los gráficos pueden consistir en miles de millones de vértices, los mapas pueden tener miles de millones de puntos, algunos incluso pueden querer lanzar su propio universo basado en autómatas celulares (1, 2).
Cuando el volumen de datos en matrices dispersas no puede exprimirse en la RAM, pero aún necesita trabajar con ellos, debe considerar implementar tales proyectos utilizando Globals y COS.
¡Gracias por su atención! Esperamos ver sus preguntas y solicitudes en la sección de comentarios.
Descargo de responsabilidad: Este artículo y mis comentarios reflejan solo mi opinión y no tienen nada que ver con la posición oficial de InterSystems Corporation.
Artículo
Alberto Fuentes · 7 abr, 2021
Hola a todos! Os traigo hoy un ejemplo de código que compartía [Robert Cemper](https://community.intersystems.com/user/robert-cemper-0) para mostrar por SQL los registros de error almacenados en `^ERRORS`.
Este es un ejemplo de código que funciona en Caché 2018.1.3 e IRIS 2020.2
No se mantendrá sincronizado con las nuevas versiones.
Es un ejemplo de código y como tal no está soportado por el Soporte de InterSystems
Los errores en IRIS/Caché/Ensemble se registran entre otros en la global `^ERRORS`. Como este mecanismo se remonta a muchas versiones atrás (décadas del milenio anterior!) su estructura está lejos de las estructuras de almacenamiento de SQL típicas.
El global se escribe mediante la rutina `^%ETN.int` y el contenido se hace visible desde la línea de comandos del terminal mediante la rutina `^%ERN` o en el Portal de Administración como *Log de Errores de la Aplicación*.
Por defecto no está disponible a través de SQL ya que no hay ninguna clase que lo presente como tabla. Por varias razones:
* Cuando se diseñó, era una buena práctica tener estructuras similares a índices en los mismos globals que los datos. Si digo "similares", significa que no sirve para SQL.
* El contenido de los objetos va a niveles más profundos que el resto. En consecuencia, la profundidad de los subíndices (normalmente IdKey) varía de 3 a 11.
`^ERRORS` es independiente en cada namespace.
Está estructurado por Day, SequenceByDay, Type, ItemName (Variable, OREF),Value.
**zrcc.ERRORStack** muestra esta información como tabla SQL.
El contenido más profundo de los objetos se hace visible por la consulta personalizada incluida.
El procedimiento SQL **zrcc.ERRORStack_Dump(Day,Sequence)** devuelve todo el contenido disponible y presenta subíndices y valores como se ve en la lista de globals.
A continuación veremos cómo sacar partido de estas utilidades:
Primero: localiza el día que te interese y el número de secuencia con la ayuda de SQL
Por ejemplo: `SELECT * FROM zrcc.ERRORStack where item='$ZE'`
```
Day Seq Stk Type Item Value
2020-07-02 1 0 V $ZE <WRITE>zSend+204^%Net.HttpRequest.1
2020-07-07 1 0 V $ZE <WRITE>zSend+204^%Net.HttpRequest.1
2020-07-15 1 0 V $ZE <WRITE>zSend+204^%Net.HttpRequest.1
2020-07-20 1 0 V $ZE <LOG ENTRY>
2020-07-26 1 0 V $ZE <WRITE>zSend+204^%Net.HttpRequest.1
```
A continuación, llama al procedimiento en SQL: `CALL zrcc.ERRORStack_Dump('2020-07-26',1)`
Row count: 541 Performance: 0.026 seconds 6557 global references
Ref Value
2020-07-26,1,"*STACK",0,"V","Routine") zSend+204^%Net.HttpRequest.1
2020-07-26,1,"*STACK",1,"I") 1^S^^^0^
2020-07-26,1,"*STACK",1,"L") 1 SIGN ON
2020-07-26,1,"*STACK",1,"S")
2020-07-26,1,"*STACK",1,"T") SIGN ON
2020-07-26,1,"*STACK",1,"V","%dsTrackingKeys","N","""Analyzer""") 6
2020-07-26,1,"*STACK",1,"V","%dsTrackingKeys","N","""Architect""") 7
2020-07-26,1,"*STACK",1,"V","%dsTrackingKeys","N","""DashboardViewer""") 8
2020-07-26,1,"*STACK",1,"V","%dsTrackingKeys","N","""ResultSet""") 9
2020-07-26,1,"*STACK",1,"V","%objcn") 2
2020-07-26,1,"*STACK",3,"V","Task") <OBJECT REFERENCE>[1@%SYS.Task]
2020-07-26,1,"*STACK",3,"V","Task","OREF",1) 142
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,0) 3671
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,1) +----------------- general information ---------------
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,2) | oref value: 1
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,3) | class name: %SYS.Task
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,4) | %%OID: $lb("13","%SYS.Task")
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,5) | reference count: 5
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,6) +----------------- attribute values ------------------
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,7) | %Concurrency =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,8) 4 <Set>
- - -
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,53) | EmailOutput =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,54) 0
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,55) | EndDate =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,56) ""
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,57) | Error =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,58) "<WRITE>zSend+204^%Net.HttpRequest.1"
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,59) | Expires =
- - -
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,111) | Status =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,112) "0 "_$lb($lb(5002,"POST to Server Failed",,,,,,,,$lb(,"%SYS",$lb("$^zSend+204^%Net.HttpRequest.1 +1","$^zPost+1^%Net.HttpRequest.1 +1","$^zSendData+20^FT.Collector.1 +1","$^zTransfer+12^FT.Collector.1 +1","$^zOnTask+3^%SYS.Task.FeatureTracker.1 +1","D^zRunTask+74^%SYS.TaskSuper.1 +1","$^zRunTask+54^%SYS.TaskSuper.1 +1","D^zRun+26^%SYS.TaskSuper.1 +1"))),$lb(6085,"ISC.FeatureTracker.SSL.Config","SSL/TLS error in SSL_connect(), SSL_ERROR_SSL: protocol error, error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed",,,,,,,$lb(,"%SYS",$lb("e^zSend+303^%Net.HttpRequest.1^1","e^zPost+1^%Net.HttpRequest.1^1","e^zSendData+20^FT.Collector.1^1","e^zTransfer+12^FT.Collector.1^1","e^zOnTask+3^%SYS.Task.FeatureTracker.1^1","e^zRunTask+74^%SYS.TaskSuper.1^1","d^zRunTask+54^%SYS.TaskSuper.1^1","e^zRun+26^%SYS.TaskSuper.1^1","d^^^0"))))/* ERROR #5002: Cache error: POST to Server Failed- ERROR #6085: Unable to write to socket with SSL/TLS configuration 'ISC.FeatureTracker.SSL.Config', error reported 'SSL/TLS error in SSL_connect(), SSL_ERROR_SSL: protocol error, error:14090086:SSL routines:ssl3_get_server_certificate:certificate verify failed' */
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,113) | SuspendOnError =
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,114) 0
2020-07-26,1,"*STACK",3,"V","Task","OREF",1,115) | Suspended =
- - -
2020-07-26,1,"*STACK",6,"V","Status1") 1
2020-07-26,1,"*STACK",6,"V","Task") <OBJECT REFERENCE>[1@%SYS.Task]
2020-07-26,1,"*STACK",6,"V","Task","OREF",1) 142
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,0) 3671
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,1) +----------------- general information ---------------
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,2) | oref value: 1
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,3) | class name: %SYS.Task
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,4) | %%OID: $lb("13","%SYS.Task")
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,5) | reference count: 5
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,6) +----------------- attribute values ------------------
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,7) | %Concurrency =
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,8) 4 <Set>
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,9) | DailyEndTime =
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,10) 0
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,11) | DailyFrequency =
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,12) 0
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,13) | DailyFrequencyTime =
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,14) ""
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,15) | DailyIncrement =
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,16) ""
2020-07-26,1,"*STACK",6,"V","Task","OREF",1,17) | DailyStartTime =
- - -
2020-07-26,1,"*STACK",12,"V","%00000","N","""JournalState""") 12
2020-07-26,1,"*STACK",13,"I") 13^Z^ETNERRB^%ETN^0
2020-07-26,1,"*STACK",13,"L") 13 ERROR TRAP S $ZTRAP="ETNERRB^%ETN"
2020-07-26,1,"*STACK",13,"S") S $ZTRAP="ETNERRB^%ETN"
2020-07-26,1,"*STACK",13,"T") ERROR TRAP
541 row(s) affected
Artículo
Alberto Fuentes · 14 jun, 2023
## Qué es el Web Scraping:
En términos sencillos, el **Web scraping**, también conocido como **recolección de datos de sitios web** o **extracción de datos de sitios web** es un proceso automatizado que permite la recopilación de grandes volúmenes de datos (no estructurados) de los sitios web. El usuario puede extraer datos de sitios web específicos, según sus necesidades. Los datos recopilados se pueden almacenar en un formato estructurado para su posterior análisis.
##
**Pasos necesarios para realizar Web scraping:**
1. Encontrar la URL de la página web de la que se desean extraer los datos
2. Seleccionar ciertos elementos mediante una inspección
3. Escribir el código para obtener el contenido de los elementos seleccionados
4. Almacenar los datos en el formato requerido
¡¡Así de sencillo!!
## **Las librerías/herramientas más populares para realizar Web scraping son:**
* Selenium: un *framework* para probar aplicaciones web
* BeautifulSoup: una librería de Python para obtener datos a partir de HTML, XML y otros lenguajes de marcado
* Pandas: una librería de Python para manipulación y análisis de datos
##¿Qué es Beautiful Soup?
Beautiful Soup es una librería de Python que sirve para extraer datos estructurados de un sitio web. Permite analizar datos a partir de archivos HTML y XML. Funciona como un módulo de ayuda e interactúa con HTML de forma similar pero mejor a como interactuarías con una página web utilizando otras herramientas disponibles para desarrolladores.
* Por lo general, ahorra horas o días de trabajo a los programadores, porque funciona con analizadores muy conocidos, como `lxml` y `html5lib`, para ofrecer formas compatibles con Python para navegar, efectuar búsquedas y modificar el árbol de análisis de elementos de la página.
* Otra potente y útil característica de Beautiful soup es la inteligencia con la que cuenta para convertir a Unicode los documentos obtenidos y a UTF-8 los documentos de salida. Como desarrollador, no tienes que ocuparse de eso a menos que el documento en sí no especifique una codificación o BeautifulSoup no pueda detectar una.
* También se considera que es más **rápido** en comparación con otras técnicas generales de análisis o extracción de datos.
## **En este artículo utilizaremos Python Embebido con ObjectScript para extraer de ae.indeed.com empresas y ofertas de empleo de Python**
**Paso 1 - Encontrar la URL de la página web de la que se desean extraer los datos**
URL = https://ae.indeed.com/jobs?q=python&l=Dubai&start=0
La página web de la que extraeremos los datos se ve así:
.png)
**_Para simplificar el proceso y con fines educativos, extraeremos los datos "Job" (Puesto de trabajo) y "Company" (Empresa). El resultado sería algo similar esto:_**

**Utilizaremos dos librerías de Python.**
* **requests** Requests es una librería HTTP para el lenguaje de programación Python. El objetivo del proyecto es hacer que las solicitudes HTTP sean más sencillas y amigables para el usuario.
* **bs4 para BeautifulSoup** BeautifulSoup es un paquete de Python para analizar documentos HTML y XML. Crea un árbol de análisis para las páginas analizadas, que puede utilizarse para extraer datos de HTML, lo que es útil para el proceso de *web scraping*.
**Vamos a instalar estos paquetes de Python (en Windows)**
irispip install --target C:\InterSystems\IRISHealth\mgr\python bs4
irispip install --target C:\InterSystems\IRISHealth\mgr\python requests
.png)
**Ahora importaremos las librerías de Python a ObjectScript**
<span class="hljs-keyword">Class</span> PythonTesting.WebScraper <span class="hljs-keyword">Extends</span> <span class="hljs-built_in">%Persistent</span>
{
<span class="hljs-comment">// pUrl = https://ae.indeed.com/jobs?q=python&l=Dubai&start=</span>
<span class="hljs-comment">// pPage = 0</span>
<span class="hljs-keyword">ClassMethod</span> ScrapeWebPage(pUrl, pPage)
{
<span class="hljs-comment">// imports the requests python library</span>
<span class="hljs-keyword">set</span> requests = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Import(<span class="hljs-string">"requests"</span>)
<span class="hljs-comment">// import the bs4 python library</span>
<span class="hljs-keyword">set</span> soup = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Import(<span class="hljs-string">"bs4"</span>)
<span class="hljs-comment">// import builtins package which contains all of the built-in identifiers</span>
<span class="hljs-keyword">set</span> builtins = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Import(<span class="hljs-string">"builtins"</span>)
}
**Recopilamos los datos HTML utilizando la librería requests**
_Nota: El "user agent" se encontró buscando en Google "my user agent"_
_La URL es "https://ae.indeed.com/jobs?q=python&l=Dubai&start=", donde pPage es el número de página_
Haremos una solicitud HTTP GET a la URL usando requests y almacenaremos la respuesta en "req"
<span class="hljs-keyword">set</span> headers = {<span class="hljs-string">"User-Agent"</span>: <span class="hljs-string">"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"</span>}
<span class="hljs-keyword">set</span> url = <span class="hljs-string">"https://ae.indeed.com/jobs?q=python&l=Dubai&start="</span>_pPage
<span class="hljs-keyword">set</span> req = requests.get(url,<span class="hljs-string">"headers="</span>_headers)
El objeto "req" tendrá el HTML que fue devuelto desde la página web.
**Ahora ejecutaremos el analizador de HTML de BeautifulSoup, para que podamos extraer la información del puesto de trabajo.**
<span class="hljs-keyword">set</span> soupData = soup.BeautifulSoup(req.content, <span class="hljs-string">"html.parser"</span>)
<span class="hljs-keyword">set</span> title = soupData.title.text
<span class="hljs-keyword">W</span> !,title
**El título se ve de la siguiente manera:**
.png)
**Paso 2: Seleccionar ciertos elementos mediante una inspección.**
En este escenario nos interesa la lista de trabajos que normalmente se encuentra en una etiqueta < div >. En el navegador se puede inspeccionar el elemento para encontrar la clase "div".
En nuestro caso, la información necesaria se almacena en <div class="cardOutline tapItem ...
**Paso 3: Escribir el código para obtener el contenido de los elementos seleccionados**
Utilizaremos la función find_all de BeautifulSoup para buscar todas las etiquetas < div > que contengan la clase con el nombre "cardOutline".
<span class="hljs-comment">//parameters to python would be sent as a python dictionary</span>
<span class="hljs-keyword">set</span> divClass = {<span class="hljs-string">"class"</span>:<span class="hljs-string">"cardOutline"</span>}
<span class="hljs-keyword">set</span> divsArr = soupData.<span class="hljs-string">"find_all"</span>(<span class="hljs-string">"div"</span>,divClass...)
Esto devolverá una lista que podremos examinar para extraer los Puestos de trabajo y la Empresa.
Paso 4: Almacenar los datos en el formato requerido
En el siguiente ejemplo escribiremos los datos en el terminal.
<span class="hljs-keyword">set</span> len = builtins.len(divsArr)
<span class="hljs-keyword">W</span> !, <span class="hljs-string">"Job Title"</span>,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>)_<span class="hljs-string">" --- "</span>_<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),<span class="hljs-string">"Company"</span>
<span class="hljs-keyword">for</span> i = <span class="hljs-number">1</span>:<span class="hljs-number">1</span>:len {
<span class="hljs-keyword">Set</span> item = divsArr.<span class="hljs-string">"__getitem__"</span>(i - <span class="hljs-number">1</span>)
<span class="hljs-keyword">set</span> title = <span class="hljs-built_in">$ZSTRIP</span>(item.find(<span class="hljs-string">"a"</span>).text,<span class="hljs-string">"<>W"</span>)
<span class="hljs-keyword">set</span> companyClass = {<span class="hljs-string">"class_"</span>:<span class="hljs-string">"companyName"</span>}
<span class="hljs-keyword">set</span> company = <span class="hljs-built_in">$ZSTRIP</span>(item.find(<span class="hljs-string">"span"</span>, companyClass...).text,<span class="hljs-string">"<>W"</span>)
<span class="hljs-keyword">W</span> !,title,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),<span class="hljs-string">" --- "</span>,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),company
}
Hay que tener en cuenta que estamos usando la función builtins.len() para obtener la longitud de la lista divsArr
Nombres de los identificadores:
Las reglas para nombrar a los identificadores son diferentes entre ObjectScript y Python. Por ejemplo, el guion bajo (_) está permitido en el nombre de los métodos de Python, y de hecho se utiliza ampliamente para los llamados métodos y atributos “dunder” (“dunder” es la abreviatura de “double underscore”), como __getitem__ o __class__. Para usar estos identificadores desde ObjectScript, hay que ponerlos entre comillas dobles:Documentación sobre los nombres de los identificadores
Ejemplo de Método de Clase.
ClassMethod ScrapeWebPage(pUrl, pPage)
<span class="hljs-comment">// pUrl = https://ae.indeed.com/jobs?q=python&l=Dubai&start=</span>
<span class="hljs-comment">// pPage = 0</span>
<span class="hljs-keyword">ClassMethod</span> ScrapeWebPage(pUrl, pPage)
{
<span class="hljs-keyword">set</span> requests = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Import(<span class="hljs-string">"requests"</span>)
<span class="hljs-keyword">set</span> soup = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Import(<span class="hljs-string">"bs4"</span>)
<span class="hljs-keyword">set</span> builtins = <span class="hljs-keyword">##class</span>(<span class="hljs-built_in">%SYS.Python</span>).Builtins()
<span class="hljs-keyword">set</span> headers = {<span class="hljs-string">"User-Agent"</span>: <span class="hljs-string">"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"</span>}
<span class="hljs-keyword">set</span> url = pUrl_pPage
<span class="hljs-keyword">set</span> req = requests.get(url,<span class="hljs-string">"headers="</span>_headers)
<span class="hljs-keyword">set</span> soupData = soup.BeautifulSoup(req.content, <span class="hljs-string">"html.parser"</span>)
<span class="hljs-keyword">set</span> title = soupData.title.text
<span class="hljs-keyword">W</span> !,title
<span class="hljs-keyword">set</span> divClass = {<span class="hljs-string">"class_"</span>:<span class="hljs-string">"cardOutline"</span>}
<span class="hljs-keyword">set</span> divsArr = soupData.<span class="hljs-string">"find_all"</span>(<span class="hljs-string">"div"</span>,divClass...)
<span class="hljs-keyword">set</span> len = builtins.len(divsArr)
<span class="hljs-keyword">W</span> !, <span class="hljs-string">"Job Title"</span>,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>)_<span class="hljs-string">" --- "</span>_<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),<span class="hljs-string">"Company"</span>
<span class="hljs-keyword">for</span> i = <span class="hljs-number">1</span>:<span class="hljs-number">1</span>:len {
<span class="hljs-keyword">Set</span> item = divsArr.<span class="hljs-string">"__getitem__"</span>(i - <span class="hljs-number">1</span>)
<span class="hljs-keyword">set</span> title = <span class="hljs-built_in">$ZSTRIP</span>(item.find(<span class="hljs-string">"a"</span>).text,<span class="hljs-string">"<>W"</span>)
<span class="hljs-keyword">set</span> companyClass = {<span class="hljs-string">"class_"</span>:<span class="hljs-string">"companyName"</span>}
<span class="hljs-keyword">set</span> company = <span class="hljs-built_in">$ZSTRIP</span>(item.find(<span class="hljs-string">"span"</span>, companyClass...).text,<span class="hljs-string">"<>W"</span>)
<span class="hljs-keyword">W</span> !,title,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),<span class="hljs-string">" --- "</span>,<span class="hljs-built_in">$C</span>(<span class="hljs-number">9</span>),company
}
}
Próximos pasos...
Usando Object Script y Python Embebido y creando unas pocas líneas de código, podríamos extraer fácilmente la información de las páginas web más populares para buscar trabajo, recoger el nombre del puesto de trabajo, la empresa, el sueldo, la descripción del trabajo y correos electrónicos/enlaces.
Esta información se puede añadir a un DataFrame de Pandas para eliminar los datos duplicados. Se pueden aplicar filtros basados en ciertas palabras clave que interesen.
Se pueden ejecutar estos datos a través de NumPy y obtener algunos gráficos de líneas.
O realizar una One-Hot encoding de los datos, y crear/entrenar modelos de Machine Learning para que si hay puestos de trabajo que interesan particularmente, envíen una notificación. 😉
¡Feliz desarrollo!
Artículo
Ricardo Paiva · 15 mayo, 2020
¡Hola desarrollador!
Si has leído la parte 1 de este artículo, ya tienes una buena idea del tipo de índices que necesitas para tus clases y cómo definirlos. Lo siguiente es saber cómo gestionarlos.
Plan de consultas
(RECUERDA: Al igual que cualquier modificación en una clase, añadir índices en un sistema en producción conlleva riesgos: si los usuarios están actualizando o accediendo a datos mientras se rellena un índice, podrían obtener resultados vacíos o incorrectos a sus consultas, o incluso dañar los índices que se están formando. Ten en cuenta que hay pasos adicionales para definir y usar índices en un sistema en producción. Estos pasos se analizarán en esta sección, y se detallan en nuestra documentación).
Cuando tengas un nuevo índice implementado, podemos ver si el optimizador de SQL decide que es el más eficiente para leer, al ejecutar la consulta. No hay que ejecutar la consulta para verificar el plan. Dada una consulta, puedes verificar el plan de forma programática:
Set query = 1
Set query(1) = “SELECT SSN,Name FROM Sample.Person WHERE Office_State = 'MA'”
D $system.SQL.ShowPlan(.query)
O siguiendo la interfaz en el Portal de Administración del Sistema desde System Explorer -> SQL.
Desde aquí, se puede ver qué índices están siendo usados antes de cargar los datos de la tabla (o "mapa maestro"). Ten en cuenta lo siguiente: ¿tu nueva consulta está en el plan tal como se esperaba? ¿La lógica del plan tiene sentido?
Una vez sepas que el optimizador SQL está usando tus índices, puedes verificar que estos índices están funcionando correctamente.
Construcción de índices
(Si aún estás en las fases de planificación y todavía no tienes datos, los pasos detallados aquí no serán necesarios ahora.)
Definir un índice no lo poblará o "construirá" automáticamente con los datos de tu tabla. Si el plan de consulta usa un índice que aún no se ha construido, corres el riesgo de obtener resultados incorrectos o vacíos a la consulta. Para "desactivar" un índice antes de que esté listo para usar, pon su elegibilidad de mapa en 0 (esto básicamente le indica al optimizador SQL que no puede usar este índice para ejecutar consultas).
write $SYSTEM.SQL.SetMapSelectability("Sample.Person","QuickSearchIDX",0) ; Set selectability of index QuickSearchIDX false
Ten en cuenta que puedes usar la llamada de arriba incluso antes de que añadas un nuevo índice. El optimizador SQL reconocerá este nuevo nombre del índice, sabrá que está inactivo y no lo usará para ninguna consulta.
Puedes rellenar un índice usando el método %BuildIndices (##class(<class>).%BuildIndices($lb("MyIDX"))) o en la página SQL del Portal de Administración del Sistema (bajo el menú desplegable "Actions").
El tiempo necesario para construir índices depende de la cantidad de filas de la tabla y de los tipos de índices que estás construyendo. Los índices bitslices generalmente requieren de más tiempo.
Una vez completado el proceso, puedes usar el método SetMapSelectivity una vez más (esta vez ajuste en 1) para reactivar tu índice ahora poblado.
Ten en cuenta que construir índices significa esencialmente emitir comandos KILL y SET para poblarlos. Puedes considerar desactivar journaling durante este proceso para evitar llenar el espacio de disco.
Puedes encontrar instrucciones más detalladas sobre construir índices, en particular en sistemas en producción, en nuestra documentación:
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices#GSQLOPT_indices_build_readwrite
Mantenimiento de índices
¡Ahora ya podemos usar nuestros índices! Algunos puntos a considerar son la eficiencia de ejecución de una consulta, la frecuencia de uso de un índice dado y los pasos a tomar si tus índices entran en un estado inconsistente.
Rendimiento
Primero, podemos evaluar cómo ha afectado este índice el rendimiento de la consulta. Si sabemos que el optimizador SQL está usando un nuevo índice para una consulta, podemos ejecutar la consulta para obtener sus estadísticas de desempeño: número de referencias globales, número de líneas leídas, tiempo para preparar y ejecutar la consulta y tiempo empleado en el disco.
Volvamos al ejemplo anterior:
“SELECT SSN,Name,DOB FROM Sample.Person WHERE Name %STARTSWITH 'Smith,J'”
Tenemos el siguiente índice para ayudar al desempeño de esta consulta:
Index QuickSearchIDX On Name [ Data = (SSN, DOB, Name) ];
Ya tenemos un índice NameIDX en la propiedad Name.
Intuitivamente, nos damos cuenta de que la consulta se ejecutará usando QuickSearchIDX. NameIDX seguramente sería una segunda opción, ya que está basado en la propiedad Name, pero no contiene ninguno de los valores de datos para SSN o DOB.
Para comprobar el rendimiento de esta consulta con QuickSearchIDX, basta con ejecutarla.
(Por otra parte: he depurado las consultas en caché antes de ejecutar estas consultas para mostrar mejor las diferencias de rendimiento. Cuando se ejecuta una consulta SQL, almacenamos el plan usado para ejecutarla, lo que mejora el rendimiento en su próxima ejecución. Los detalles de esto están fuera del alcance de este artículo, pero al final de este artículo incluiré recursos adicionales para otras consideraciones de desempeño de SQL.)
Cantidad de filas: 31 Rendimiento: 0.003 segundos 154 referencias globales 3264 líneas ejecutadas 1 latencia de lectura de disco (ms)
Simple: solo necesitamos QuickSearchIDX para ejecutar la consulta.
Vamos a comparar el desempeño de usar NameIDX en lugar de QuickSearchIDX. Para hacer esto podemos añadir una palabra clave %IGNOREINDEX para la consulta, que evitará que el optimizador SQL elija ciertos índices.
Reescribimos la consulta de la siguiente forma:
“SELECT SSN,Name,DOB FROM %IGNOREINDEX QuickSearchIDX Sample.Person WHERE Name %STARTSWITH 'Smith,J'”
El plan de consulta ahora usa NameIDX, y vemos que debemos leer desde el global de datos (“mapa maestro”) de Sample.Person usando los IDs de filas relevantes, encontrados mediante el índice.
Cantidad de filas: 31 Rendimiento: 0.020 segundos 137 referencias globales 3792 líneas ejecutadas 17 latencia de lectura de disco (ms)
Vemos que es necesario más tiempo para la ejecución, más líneas de código ejecutadas y una mayor latencia de disco.
A continuación, considera ejecutar esta consulta sin ningún índice. Ajustamos nuestra consulta de la siguiente forma:
“SELECT SSN,Name,DOB FROM %IGNOREINDEX * Sample.Person WHERE Name %STARTSWITH 'Smith,J'”
Sin usar índices, debemos comprobar nuestra condición en las filas de datos.
Cantidad de filas: 31 Rendimiento: 0.765 seconds 149999 referencias globales 1202681 líneas ejecutadas 517 latencia de lectura de disco (ms)
Esto muestra que un índice especializado, QuickSearchIDX, permite a nuestra consulta ejecutarse más de 100 veces más rápido que sin índice y casi 10 veces más rápido que con el más general NameIDX, y usar NameIDX permite una ejecución al menos 30 veces más rápido que sin usar índices.
Para este ejemplo específico, la diferencia de rendimiento entre usar QuickSearchIDX y NameIDX puede ser despreciable, pero para consultas que se ejecutan cientos de veces por día con millones de filas a consultar, veríamos que se ahorra un tiempo muy valioso cada día.
Análisis de índices existentes usando SQLUtilites
%SYS.PTools.SQLUtilities incluye procedimientos como IndexUsage, JoinIndices, TablesScans y TempIndices. Estos analizan las consultas existentes en cualquier namespace dado y ofrecen información sobre la frecuencia de uso de cualquier índice dado, qué consultas eligen iterar sobre cada fila de una tabla y qué consultas generan archivos temporales para simular índices.
Puedes usar estos procedimientos para determinar "gaps" (falta de datos) que un índice podría solucionar. De igual manera puedes usar estos procedimientos para eliminar algún indice que no se utilice o que no sea eficiente.
Puedes encontrar detalles de estos procedimientos y ejemplos de su uso en nuestra documentación:
https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_optquery_indexanalysis
¿Problemas de índices?
Validar índices confirma si un índice existe y está definido correctamente para cada fila de una clase. Ninguna clase debería entrar en un estado en el que los índices estén dañados, pero si descubres que las consultas están devolviendo conjuntos de resultados vacíos o incorrectos, podrías considerar verificar si los índices de las clases existentes son válidos actualmente.
Puedes validar índices de forma programática así:
Set status = ##class(<class>).%ValidateIndices(indices,autoCorrect,lockOption,multiProcess)
Aquí, el parámetro "índices" es una cadena vacía de forma predeterminada, lo que significa que validamos todos los índices o un objeto $listbuild que contenga los nombres de índices.
Ten en cuenta que autoCorrect tiene como valor predeterminado 0. Si es 1, cualquier error encontrado durante el proceso de validación será corregido. Si bien esta funcionalidad es la misma que reconstruir los índices, el rendimiento de ValidateIndices es más lento comparativamente.
Puedes consultar la documentación de la clase %Library.Storage para más información:
https://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25Library.Storage#%ValidateIndices
Eliminación de índices
Si ya no necesitas un índice (o si estás planeando hacer una gran cantidad de modificaciones a una tabla y quieres guardar para más tarde el impacto sobre el rendimiento de construir los índices relevantes) puedes simplemente eliminar la definición del índice de la clase en Studio y eliminar el nodo global de índice apropiado. O puedes ejecutar un comando DROP INDEX vía DDL, lo que también limpiará la definición y los datos del índice. Desde ahí, puedes depurar las consultas en caché para asegurarte de que ningún plan existente use el índice ya eliminado.
¿Y ahora qué?
Los índices son solo una parte del desempeño de SQL. En este mismo sentido, existen otras opciones para monitorizar el rendimiento y el uso de tus índices. Para entender el desempeño de SQL, también puedes aprender:Tune Tables – una herramienta que se ejecuta una vez poblada una tabla con datos representativos o si la distribución de los datos cambia radicalmente. Esto llena la definición de tu clase con metadatos. Por ejemplo, qué longitud esperamos que tenga un campo o cuántos valores únicos hay en un campo, lo que ayuda al optimizador SQL a elegir un plan de consulta que permita una ejecución eficiente.
Kyle Baxter escribió un artículo sobre esto: https://community.intersystems.com/post/one-query-performance-trick-you-need-know-tune-table
Query Plans – la representación lógica de cómo nuestro código subyacente ejecuta consultas SQL. Si tienes una consulta lenta, podemos analizar qué plan de consulta se está generando, si tiene sentido para tu consulta y qué se puede hacer para optimizar más este plan.
Cached queries – Declaraciones SQL dinámicas preparadas – las consultas guardadas en caché son esencialmente el código subyacente de los planes de consulta.cached queries are essentially the code underneath query plans.
Para saber más
Parte 1 de este artículo.
Documentación sobre definición y construcción de índices. Incluye pasos adicionales a considerar en sistemas de lectura-escritura en producción. https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQLOPT_indices
Comandos SQL ISC: vea CREATE INDEX y DROP INDEX por referencias sintácticas de manejo de índices vía DDL. Incluye permisos de usuario adecuados para ejecutar estos comandos. https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_COMMANDS
Detalles sobre Collation SQL en clases ISC. De forma predeterminada, los valores de cadenas se almacenan en globales de índices como SQLUPPER (“ STRING”): https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSQL_collation
[EDIT 05/06/2020: Corrections on index build performance and the DROP INDEX command.] 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).
Artículo
Kurro Lopez · 30 jul, 2020
Este es el primer artículo de una serie que se sumerge en herramientas de visualización y análisis de datos de series temporales. Obviamente, estamos más interesados en analizar los datos relacionados con el rendimiento que podemos recopilar de la familia de productos Caché. Sin embargo, como veremos más adelante, no estamos limitados a eso. Por ahora estamos explorando Python y las bibliotecas/herramientas disponibles dentro de ese ecosistema.
La serie está estrechamente vinculada a la excelente serie de Murray sobre el rendimiento y la supervisión de Caché. ([ver aquí](https://community.intersystems.com/post/intersystems-data-platforms-capacity-planning-and-performance-series-index)) y mas especificamente [este artículo](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting).
**Descargo de responsabilidad I**: Si bien hablaré de pasada sobre la interpretación de los datos que estamos viendo, hablar de eso en detalle distraería demasiado del objetivo real. Recomiendo encarecidamente la serie de Murray para comenzar, para obtener una comprensión básica del tema.
**Descargo de responsabilidad II**: Existen miles de millones de herramientas que le permiten visualizar los datos recopilados. Muchos de ellos trabajan directamente con los datos que obtiene de mgstat y sus amigos, o solo necesitan un ajuste mínimo. Esta no es una publicación de 'esta solución es la mejor'. Es solo una de las formas en que he encontrado útil y eficiente trabajar con los datos.
**Descargo de responsabilidad III**: La visualización y el análisis de datos es un campo altamente adictivo, emocionante y divertido para sumergirse. Puede perder algo de tiempo libre por esto. ¡Usted ha sido advertido!
Entonces, sin más preámbulos, profundicemos en ello.
# Prerequisitos
Para comenzar, necesitará algunas herramientas y bibliotecas:
* Jupyter notebooks
* Python (3)
* varias bibliotecas de Python que usaremos en el futuro
*Python (3)* Necesitará Python en su máquina. Hay numerosas formas de instalarlo en varias arquitecturas. Yo uso [homebrew](http://brew.sh/) en mi mac, lo que lo hizo fácil:
~~~
brew install python3
~~~
Solicite a google instrucciones para su plataforma favorita.
*[Jupyter notebooks](http://jupyter.org/install.html)*: Si bien no es técnicamente necesario, el Jupyter notebooks hace que trabajar en scripts de python sea muy fácil. Le permite ejecutar interactivamente y mostrar scripts de Python desde una ventana del navegador. También permite trabajar en colaboración en scripts. Como hace que sea muy fácil experimentar y jugar con código, es muy recomendable.
~~~
pip3 install jupyter
~~~
(de nuevo, habla con $search-engine ;)
*Librerías Python* Mencioné las diferentes bibliotecas de Python mientras las usamos en el futuro. Si obtiene un error en una declaración **import**, una buena primera aproximación es siempre asegurarse de tener instalada la biblioteca:
~~~
pip3 install matplotlib
~~~
# Empezando
Suponiendo que tiene todo instalado en su máquina, debería poder ejecutar
~~~
jupyter notebook
~~~
de un directorio.
Esto debería abrir automáticamente una ventana del navegador con una interfaz de usuario simple.

Continuaremos y crearemos un nuevo cuaderno a través del menú y agregaremos un par de declaraciones de importación a nuestra primera celda de código (New -> Notebooks -> Python3):

~~~python
import math
import pandas as pd
import mpl_toolkits.axisartist as AA
from mpl_toolkits.axes_grid1 import host_subplot
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.dates import DateFormatter
~~~
En cuanto a las bibliotecas que estamos importando, solo quiero mencionar algunas:
* [Pandas](http://pandas.pydata.org/) "es una biblioteca de código abierto con licencia BSD que proporciona estructuras de datos y herramientas de análisis de datos de alto rendimiento y fáciles de usar para el lenguaje de programación Python". Lo que permite trabajar eficientemente con grandes conjuntos de datos. Si bien los conjuntos de datos que obtenemos de pButtons, de ninguna manera son 'big data'. Sin embargo, nos consolaremos con el hecho de que podríamos ver muchos datos a la vez. Imagine que ha estado recolectando pButtons con muestreo de 24 h/2 segundos durante los últimos 20 años en su sistema. Podríamos graficar eso.
* [Matplotlib](http://matplotlib.org/) "matplotlib es una biblioteca de trazado 2D de python que produce cifras de calidad de publicación en una variedad de formatos impresos y entornos interactivos en todas las plataformas". Este será el principal motor de gráficos que vamos a utilizar (por ahora).
Si recibe un error al ejecutar la celda de código actual (shortcut: Ctrl+Enter) ([lista de atajos](https://www.cheatography.com/weidadeyue/cheat-sheets/jupyter-notebook/)), asegúrese de verificar que los tenga instalados.
También notará que cambié el nombre del cuaderno *Sin título*, para hacerlo, simplemente puede hacer clic en el título.
# Cargando algunos datos
Ahora que pusimos un poco de terreno, es hora de obtener algunos datos. Por suerte, Pandas proporciona una manera fácil de cargar datos CSV. Ya que [tenemos un conjunto de datos de mgstat por ahí](https://community.intersystems.com/post/extracting-pbuttons-data-csv-file-easy-charting) in csv-format, we'll just use that.
~~~python
mgstatfile = '/Users/kazamatzuri/work/proj/vis-articles/part1/mgstat.txt'
data = pd.read_csv(
mgstatfile,
header=1,
parse_dates=[[0,1]]
)
~~~
Nosotras estamos utilizando el [read_csv](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html) comando para leer directamente los datos de mgstat en un [DataFrame](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.html). Consulte la [documentación completa](http://pandas.pydata.org/pandas-docs/stable/generated/pandas.read_csv.html) para una descripción completa de las opciones. En resumen: simplemente estamos pasando el archivo para leerlo y decirle que la segunda línea (¡basada en 0!) Contiene los nombres de los encabezados.
Dado que mgstat divide los campos de fecha y hora en dos campos, también necesitamos combinarlos con el parámetro parse_dates.
~~~python
data.info()
~~~
~~~
RangeIndex: 25635 entries, 0 to 25634
Data columns (total 37 columns):
Date_ Time 25635 non-null datetime64[ns]
Glorefs 25635 non-null int64
RemGrefs 25635 non-null int64
GRratio 25635 non-null int64
PhyRds 25635 non-null int64
Rdratio 25635 non-null float64
Gloupds 25635 non-null int64
RemGupds 25635 non-null int64
Rourefs 25635 non-null int64
RemRrefs 25635 non-null int64
RouLaS 25635 non-null int64
RemRLaS 25635 non-null int64
PhyWrs 25635 non-null int64
WDQsz 25635 non-null int64
WDtmpq 25635 non-null int64
WDphase 25635 non-null int64
WIJwri 25635 non-null int64
RouCMs 25635 non-null int64
Jrnwrts 25635 non-null int64
GblSz 25635 non-null int64
pGblNsz 25635 non-null int64
pGblAsz 25635 non-null float64
ObjSz 25635 non-null int64
pObjNsz 25635 non-null int64
pObjAsz 25635 non-null int64
BDBSz 25635 non-null int64
pBDBNsz 25635 non-null int64
pBDBAsz 25635 non-null float64
ActECP 25635 non-null int64
Addblk 25635 non-null int64
PrgBufL 25635 non-null int64
PrgSrvR 25635 non-null int64
BytSnt 25635 non-null int64
BytRcd 25635 non-null int64
WDpass 25635 non-null int64
IJUcnt 25635 non-null int64
IJULock 25635 non-null int64
dtypes: datetime64[ns](1), float64(3), int64(33)
memory usage: 7.2 MB
~~~
nos da una buena visión general del DataFrame recopilado.
# Trabajando con los datos
Dado que algunos nombres de campo contienen espacios y guión bajo ("Date_ Time") es bastante difícil de manejar, seguiremos adelante y eiminamos espacios y cambiaremos el nombre de la primera columna:
~~~python
data.columns=data.columns.str.strip()
data=data.rename(columns={'Date_ Time':'DateTime'})
~~~
El Marco de datos predeterminado es un RangeIndex. Esto no es muy útil para ver nuestros datos. Como tenemos disponible una columna DateTime bastante práctica, seguiremos adelante y la configuraremos como índice:
~~~python
data.index=data.DateTime
~~~
Ahora estamos listos para crear una versión inicial de nuestra trama. Como esta es siempre una de las primeras cosas a tener en cuenta, usemos Glorefs para esto:
~~~python
plt.figure(num=None, figsize=(16,5), dpi=80, facecolor='w', edgecolor='k')
plt.xticks(rotation=70)
plt.plot(data.DateTime,data.Glorefs)
plt.show()
~~~
Primero le decimos a la biblioteca en qué tamaño queremos el gráfico. También queremos que las etiquetas del eje x giren un poco, para que no se superpongan.
Finalmente graficamos DateTime vs Glorefs y mostramos el gráfico. Esto nos da algo como el siguiente gráfico.

Podemos reemplazar fácilmente Glorefs con cualquiera de las otras columnas para tener una idea general de lo que está sucediendo.
# Combinando gráficos
En algún momento es bastante útil mirar múltiples gráficos a la vez. Entonces, la idea de dibujar varias parcelas en una sola gráfica parece natural.
Si bien es muy sencillo hacerlo solo con matplotlib:
~~~python
plt.plot(data.DateTime,data.Glorefs)
plt.plot(data.DateTime,data.PhyRds)
plt.show()
~~~
Esto nos dará más o menos la misma gráfica que antes. El problema, por supuesto, es la escala y. Dado que Glorefs sube a millones, mientras que los PhyRds generalmente están en los 100 (a miles), no los vemos.
Para resolver esto, necesitaremos usar el kit de herramientas axisartist previamente importado.
~~~python
plt.gcf()
plt.figure(num=None, figsize=(16,5), dpi=80, facecolor='w', edgecolor='k')
host = host_subplot(111, axes_class=AA.Axes)
plt.subplots_adjust(right=0.75)
par1 = host.twinx()
par2 = host.twinx()
offset = 60
new_fixed_axis = par2.get_grid_helper().new_fixed_axis
par2.axis["right"] = new_fixed_axis(loc="right",axes=par2,offset=(offset, 0))
par2.axis["right"].toggle(all=True)
host.set_xlabel("time")
host.set_ylabel("Glorefs")
par1.set_ylabel("Rdratio")
par2.set_ylabel("PhyRds")
p1,=host.plot(data.Glorefs,label="Glorefs")
p2,=par1.plot(data.Rdratio,label="Rdratio")
p3,=par2.plot(data.PhyRds,label="PhyRds")
host.legend()
host.axis["left"].label.set_color(p1.get_color())
par1.axis["right"].label.set_color(p2.get_color())
par2.axis["right"].label.set_color(p3.get_color())
plt.draw()
plt.show()
~~~
El breve resumen es: agregaremos dos ejes y al diagrama, que tendrán su propia escala. Si bien utilizamos implícitamente la subtrama en nuestro primer ejemplo, en este caso necesitamos acceder a ella directamente para poder agregar el eje y las etiquetas.
Establecemos un par de etiquetas y los colores. Después de agregar una leyenda y conectar los colores a las diferentes parcelas, terminamos con una imagen como esta:

# Comentarios finales
Esto ya nos da un par de herramientas muy poderosas para trazar nuestros datos. Exploramos cómo cargar datos de mgstat y crear algunos gráficos básicos. En la siguiente parte, jugaremos con diferentes formatos de salida para nuestros gráficos y obtendremos más datos.
Comentarios y preguntas son alentados! ¡Comparte tus experiencias!
-Fab
ps. el cuaderno para esto está disponible [aquí](https://github.com/kazamatzuri/vis-part1)
Artículo
Luis Angel Pérez Ramos · 3 abr, 2023
¡Hola Comunidad!
En este artículo configuraremos mediante programación un Apache Web Gateway con Docker, utilizando:
* El Protocolo HTTPS.
* TLS\SSL para asegurar la comunicación entre el Web Gateway y la instancia de IRIS.

Utilizaremos dos imágenes: una para el Web Gateway y la segunda para la instancia de IRIS.
Todos los archivos necesarios están disponibles en este [repositorio de GitHub](https://github.com/lscalese/docker-webgateway-sample).
Comencemos clonando el proyecto de git:
```bash
git clone https://github.com/lscalese/docker-webgateway-sample.git
cd docker-webgateway-sample
```
## Preparación del sistema
Para evitar problemas con los permisos, el sistema necesita un usuario y un grupo:
* www-data
* irisowner
Es necesario para compartir archivos de certificados con los contenedores. Si no existen en tu sistema, simplemente ejecútalos:
```bash
sudo useradd --uid 51773 --user-group irisowner
sudo groupmod --gid 51773 irisowner
sudo useradd –user-group www-data
```
## Generación de certificados
En este ejemplo, utilizaremos tres certificados:
1. Uso del servidor web HTTPS
2. Cifrado TLS\SSL en el cliente Web Gateway
3. Cifrado TLS\SSL en la Instancia de IRIS
Para generarlos, está disponible un *script* listo para usarse.
Sin embargo, hay que personalizar el asunto del certificado; simplemente edita el archivo [gen-certificates.sh](https://github.com/lscalese/docker-webgateway-sample/blob/master/gen-certificates.sh).
Esta es la estructura del argumento OpenSSL `subj`:
1. **C**: Código del país
2. **ST**: Estado
3. **L**: Ubicación
4. **O**: Organización
5. **OU**: Unidad de la organización
6. **CN**: Nombre común (básicamente el nombre de dominio o el nombre del *host*)
Se pueden cambiar estos valores.
```bash
# sudo is needed due chown, chgrp, chmod ...
sudo ./gen-certificates.sh
```
Si todo está bien, deberías ver dos nuevos directorios `./certificados/` y `~/webgateway-apache-certificados/` con certificados:
| Archivo | Contenedor | Descripción |
| ------------------------------------------------------ | --------------- | -------------------------------------------------------------------------------------------------------------- |
| ./certificates/CA_Server.cer | webgateway,iris | Certificado del servidor de autoridad |
| ./certificates/iris_server.cer | iris | Certificado para la instancia de IRIS (utilizado para el cifrado de la comunicación *mirror* y de webgateway) |
| ./certificates/iris_server.key | iris | Clave privada relacionada |
| ~/webgateway-apache-certificates/apache_webgateway.cer | webgateway | Certificado para el servidor web de apache |
| ~/webgateway-apache-certificates/apache_webgateway.key | webgateway | Clave privada relacionada |
| ./certificates/webgateway_client.cer | webgateway | Certificado para cifrar la comunicación entre webgateway e IRIS |
| ./certificates/webgateway_client.key | webgateway | Clave privada relacionada |
Ten en cuenta que si hay certificados autofirmados, los navegadores web mostrarán alertas de seguridad. Obviamente, si tienes un certificado emitido por una autoridad certificadora, puedes utilizarlo en vez de uno autofirmado (especialmente para el certificado del servidor de Apache).
## Archivos de configuración del Web Gateway
Echa un vistazo a los archivos de la configuración.
### CSP.INI
Puedes ver un archivo CSP.INI en el directorio `webgateway-config-files`.
Será introducido en la imagen, pero el contenido puede ser modificado durante el tiempo de ejecución.
Considera este archivo como una plantilla.
En este ejemplo, los siguientes parámetros se sobrescribirán cuando se inicie el contenedor:
* Ip_Address
* TCP_Port
* System_Manager
Consulta [startUpScript.sh](https://github.com/lscalese/docker-webgateway-sample/blob/master/startUpScript.sh) para más información. A grandes rasgos, la sustitución se realiza con la línea de comandos `sed`.
Además, este archivo contiene la configuración SSL\TLS para asegurar la comunicación con la instancia de IRIS:
```
SSLCC_Certificate_File=/opt/webgateway/bin/webgateway_client.cer
SSLCC_Certificate_Key_File=/opt/webgateway/bin/webgateway_client.key
SSLCC_CA_Certificate_File=/opt/webgateway/bin/CA_Server.cer
```
Estas líneas son importantes. Debemos asegurarnos de que los archivos del certificado estarán disponibles para el contenedor.
Lo haremos más tarde en el archivo `docker-compose` con un volumen.
### 000-default.conf
Se trata de un archivo de configuración de Apache. Permite utilizar el protocolo HTTPS y redirige las llamadas HTTP a HTTPS.
Los archivos del certificado y la clave privada se configuran en este archivo:
```
SSLCertificateFile /etc/apache2/certificate/apache_webgateway.cer
SSLCertificateKeyFile /etc/apache2/certificate/apache_webgateway.key
```
## Instancia de IRIS
Para nuestra instancia de IRIS, configuramos únicamente los requisitos mínimos para permitir la comunicación de SSL\TLS con el Web Gateway; esto incluye:
1. Configuración SSL `%SuperServer`
2. Habilitar la configuración de seguridad SSLSuperServer
3. Restringir la lista de IPs que pueden utilizar el servicio del Web Gateway
Para facilitar la configuración, se utiliza config-api con un archivo de configuración JSON sencillo.
```json
{
"Security.SSLConfigs": {
"%SuperServer": {
"CAFile": "/usr/irissys/mgr/CA_Server.cer",
"CertificateFile": "/usr/irissys/mgr/iris_server.cer",
"Name": "%SuperServer",
"PrivateKeyFile": "/usr/irissys/mgr/iris_server.key",
"Type": "1",
"VerifyPeer": 3
}
},
"Security.System": {
"SSLSuperServer":1
},
"Security.Services": {
"%Service_WebGateway": {
"ClientSystems": "172.16.238.50;127.0.0.1;172.16.238.20"
}
}
}
```
No es necesario realizar ninguna acción. La configuración se cargará automáticamente al iniciar el contenedor.
## Imagen tls-ssl-webgateway
### dockerfile
```
ARG IMAGEWEBGTW=containers.intersystems.com/intersystems/webgateway:2021.1.0.215.0
FROM ${IMAGEWEBGTW}
ADD webgateway-config-files /webgateway-config-files
ADD buildWebGateway.sh /
ADD startUpScript.sh /
RUN chmod +x buildWebGateway.sh startUpScript.sh && /buildWebGateway.sh
ENTRYPOINT ["/startUpScript.sh"]
```
De forma predeterminada, el punto de entrada es `/startWebGateway`, pero necesitamos realizar algunas operaciones antes de iniciar el servidor web. Recuerda que nuestro archivo CSP.ini es una `plantilla`, y necesitamos cambiar algunos parámetros (IP, puerto, administrador del sistema) durante el arranque. `startUpScript.sh` realizará estos cambios y después ejecutará el script del punto de entrada inicial `/startWebGateway`.
## Arranque de los contenedores
### Archivo Docker-compose
Antes de iniciar los contenedores, se debe modificar el archivo `docker-compose.yml`:
* `**SYSTEM_MANAGER**` debe estar configurado con la IP autorizada para tener acceso a **Web Gateway Management** https://localhost/csp/bin/Systems/Module.cxw Básicamente, es tu dirección IP (Puede ser una lista separada por comas).
* `**IRIS_WEBAPPS**` debe configurarse con la lista de tus aplicaciones CSP. La lista está separada por espacios, por ejemplo: `IRIS_WEBAPPS=/csp/sys /swagger-ui`. De forma predeterminada, solo se expone `/csp/sys`.
* Los puertos 80 y 443 están mapeados. Adáptalos a otros puertos si ya se utilizan en tu sistema.
```
version: '3.6'
services:
webgateway:
image: tls-ssl-webgateway
container_name: tls-ssl-webgateway
networks:
app_net:
ipv4_address: 172.16.238.50
ports:
# change the local port already used on your system.
- "80:80"
- "443:443"
environment:
- IRIS_HOST=172.16.238.20
- IRIS_PORT=1972
# Replace by the list of ip address allowed to open the CSP system manager
# https://localhost/csp/bin/Systems/Module.cxw
# see .env file to set environement variable.
- "SYSTEM_MANAGER=${LOCAL_IP}"
# the list of web apps
# /csp allow to the webgateway to redirect all request starting by /csp to the iris instance
# You can specify a list separate by a space : "IRIS_WEBAPPS=/csp /api /isc /swagger-ui"
- "IRIS_WEBAPPS=/csp/sys"
volumes:
# Mount certificates files.
- ./volume-apache/webgateway_client.cer:/opt/webgateway/bin/webgateway_client.cer
- ./volume-apache/webgateway_client.key:/opt/webgateway/bin/webgateway_client.key
- ./volume-apache/CA_Server.cer:/opt/webgateway/bin/CA_Server.cer
- ./volume-apache/apache_webgateway.cer:/etc/apache2/certificate/apache_webgateway.cer
- ./volume-apache/apache_webgateway.key:/etc/apache2/certificate/apache_webgateway.key
hostname: webgateway
command: ["--ssl"]
iris:
image: intersystemsdc/iris-community:latest
container_name: tls-ssl-iris
networks:
app_net:
ipv4_address: 172.16.238.20
volumes:
- ./iris-config-files:/opt/config-files
# Mount certificates files.
- ./volume-iris/CA_Server.cer:/usr/irissys/mgr/CA_Server.cer
- ./volume-iris/iris_server.cer:/usr/irissys/mgr/iris_server.cer
- ./volume-iris/iris_server.key:/usr/irissys/mgr/iris_server.key
hostname: iris
# Load the IRIS configuration file ./iris-config-files/iris-config.json
command: ["-a","sh /opt/config-files/configureIris.sh"]
networks:
app_net:
ipam:
driver: default
config:
- subnet: "172.16.238.0/24"
```
Ejecutamos el build e iniciamos:
```bash
docker-compose up -d --build
```
Los contenedores `tls-ssl-iris y tls-ssl-webgateway deben iniciarse.`
## Prueba del acceso web
### Página predeterminada de Apache
Abre la página [http://localhost](http://localhost). Serás redirigido automáticamente a [https://localhost](https://localhost).
Los navegadores muestran alertas de seguridad. Este es el comportamiento habitual con un certificado autofirmado, acepta el riesgo y continua.

### Página de administración del Web Gateway
Abre [https://localhost/csp/bin/Systems/Module.cxw](https://localhost/csp/bin/Systems/Module.cxw) y prueba la conexión con el servidor. 
### Portal de administración
Abre [https://localhost/csp/sys/utilhome.csp](https://localhost/csp/sys/utilhome.csp)

¡Excelente! ¡El ejemplo del Web Gateway funciona!
## IRIS Mirror con Web Gateway
En el artículo anterior construimos un entorno *mirror*, pero faltaba el Web Gateway. Ahora podemos solucionar eso.
Un nuevo repositorio [iris-miroring-with-webgateway](https://github.com/lscalese/iris-mirroring-with-webgateway) está disponible, incluyendo el Web Gateway y algunas mejoras más:
1. Los certificados ya no se generan sobre la marcha, sino en un proceso separado.
2. Las direcciones IP se sustituyen por variables de entorno en docker-compose y archivos de configuración JSON. Las variables se definen en el archivo ".env".
3. El repositorio puede utilizarse como plantilla.
Consulta el archivo [README.md](https://github.com/lscalese/iris-mirroring-with-webgateway) del repositorio para ejecutar un entorno como este:

Artículo
Luis Angel Pérez Ramos · 5 abr, 2023
Bienvenidos miembros de la comunidad a un nuevo artículo, en esta ocasión trataremos las capacidades de interoperabilidad que nos proporciona IRIS for Health para trabajar con ficheros DICOM.
Para ello vamos a montar un pequeño ejemplo haciendo uso de Docker. Al final del artículo podréis encontrar la URL de acceso a GitHub por si queréis verlo en acción en vuestros propios equipos.
Pero antes de empezar vamos a explicar que es DICOM:
DICOM corresponde a las iniciales de Digital Imaging and Communication in Medicine y es un estándar de transmisión de imágenes y datos médicos. Este protocolo incluye la definición del formato del fichero y el protocolo de comunicación está basado en TCP/IP.
Los ficheros DICOM dan soporte a imágenes y documentación clínica (los ficheros DICOM pueden incluir imágenes propiamente dichas o documentos "dicomizados").
El protocolo DICOM permite definir una serie de operaciones sobre los propios documentos DICOM, estás operaciones van desde el almacenamiento de una imagen (C-STORE) hasta la búsqueda (C-FIND) o el movimiento de imágenes entre sistemas (C-MOVE). Aquí podemos ver qué servicios tenemos a nuestra disposición.
Los sistemas involucrados en una comunicación DICOM esperarán recibir como respuesta un documento DICOM a su vez.
Un ejemplo típico de arquitectura de trabajo con DICOM sería el siguiente:
Dispondremos de una serie de "modalidades" (las cuales pueden ser los aparatos capturadores de imágenes o los sistemas receptores) identificados con el llamado AE Title o AET (Application Entity Title). Este AET será único para cada modalidad y deberá ser configurado en aquellas otras modalidades o sistemas que vayan a comunicarse con ella, de tal forma que se permite la comunicación entre ambas modalidades.
Como véis en el gráfico, las modalidades están configuradas para almacenar sus imágenes en un servidor de ficheros DICOM que puede pertenecer o no a un PACS (Picture Archiving and Communication System) que posteriormente es consultado desde una interfaz web del PACS. Cada vez es más común incluir un sistema de VNA (Vendor Neutral Archive) en las organizaciones que se encargue del almacenamiento y visualización centralizada de todos los archivos DICOM usados por la organización.
Por lo general en las modalidades más modernas se puede configurar el destino de las imágenes generadas, pero en muchas ocasiones puede ser necesario o bien realizar algún tipo de acción sobre los campos de la imagen DICOM (modificar el identificador del paciente, incluir el episodio clínico al con el que se relaciona, etc) o bien, por incapacidad de la modalidad, encargarse de la captura y el reenvio de la imagen generada al sistema responsable del archivado. Es en estos casos en los que es necesaria la existencia de un motor de integración que nos proporcione dicha funcionalidad, y ¡no hay ninguno mejor que IRIS for Health!
Para nuestro ejemplo vamos a plantear el siguiente escenario:
Una determinada modalidad está generando imágenes que necesitan ser enviadas a un PACS para su registro.
Nuestro servidor DICOM o PACS recibirá dichas imágenes y deberá reenviarlas a una determinada VNA.
Para simular nuestro PACS utilizaremos Orthanc, una herramienta open source que nos proveerá de las funcionalidades básicas de archivado y visualización de imágenes DICOM (más información aquí). Orthanc tiene la amabilidad de facilitarnos su uso mediante una imagen que podremos montar en Docker sin ninguna complicación. Finalmente desplegaremos un contenedor de IRIS for Health (depende de cuando leas este artículo es posible que la licencia haya podido caducar, en ese caso sólo deberás actualizar el fichero docker-compose del código) en el que podremos montar nuestra producción.
Vamos a echar un vistazo al docker-compose que hemos configurado:
version: '3.1' # Secrets are only available since this version of Docker Compose
services:
orthanc:
image: jodogne/orthanc-plugins:1.11.0
command: /run/secrets/ # Path to the configuration files (stored as secrets)
ports:
- 4242:4242
- 8042:8042
secrets:
- orthanc.json
environment:
- ORTHANC_NAME=orthanc
volumes:
- /tmp/orthanc-db/:/var/lib/orthanc/db/
hostname: orthanc
iris:
container_name: iris
build:
context: .
dockerfile: iris/Dockerfile
ports:
- "52773:52773"
- "2010:2010"
- "23:2323"
- "1972:1972"
volumes:
- ./shared:/shared
command:
--check-caps false
hostname: iris
secrets:
orthanc.json:
file: orthanc.json
El acceso al visor web de Orthanc se realizará por el puerto 8042 (http://localhost:8042), la IP destinada a recibir imágenes vía TCP/IP será el 4242 y su configuración se realizará desde el archivo orthanc.json. El portal de gestión de nuestro IRIS for Health será el 52773.
Veamos que contiene orthanc.json:
{
"Name" : "${ORTHANC_NAME} in Docker Compose",
"RemoteAccessAllowed" : true,
"AuthenticationEnabled": true,
"RegisteredUsers": {
"demo": "demo-pwd"
},
"DicomAssociationCloseDelay": 0,
"DicomModalities" : {
"iris" : [ "IRIS", "host.docker.internal", 2010 ]
}
}
Como podéis observar hemos definido un usuario demo con una contraseña demo-pwd y hemos declarado una modalidad llamada IRIS que usará el puerto 2010 para recibir las imágenes desde Orthanc, "host.docker.internal" es la máscara usada por Docker para tener acceso a otros contenedores desplegados.
Comprobemos que despues de ejecutar los docker-compose build y docker-compose up -d podemos acceder a nuestro IRIS for Health y a Orthanc sin problemas:
IRIS for Health está correctamente desplegado.
Orthanc también funciona, pues venga, ¡al lío!
Accedamos al namespace llamado DICOM y abramos su producción. En ella podremos observar que tenemos los siguientes business components:
De momento vamos a fijarnos en los necesarios para gestionar el primer caso que hemos presentado. Una modalidad que genera imágenes DICOM pero desde la que no los podemos enviar a nuestro PACS. Para ello utilizaremos un Business Service de la clase estándar EnsLib.DICOM.Service.File configurado para leer todos los archivos .dcm presentes en el directorio /shared/durable/in/ y remitirlos al Business Process de la clase Workshop.DICOM.Production.StorageFile.
Analicemos más en detalle el método principal de dicho Business Process:
/// Messages received here are instances of EnsLib.DICOM.Document sent to this
/// process by the service or operation config items. In this demo, the process is ever
/// in one of two states, the Operation is connected or not.
Method OnMessage(pSourceConfigName As %String, pInput As %Library.Persistent) As %Status
{
#dim tSC As %Status = $$$OK
#dim tMsgType As %String
do {
If pInput.%Extends("Ens.AlarmResponse") {
#; We are retrying, simulate 1st call
#; Make sure we have a document
Set pInput=..DocumentFromService
$$$ASSERT(..CurrentState="OperationNotConnected")
}
#; If its a document sent from the service
If pSourceConfigName'=..OperationDuplexName {
#; If the operation has not been connected yet
If ..CurrentState="OperationNotConnected" {
#; We need to establish a connection to the operation,
#; Keep hold of the incoming document
Set ..DocumentFromService=pInput
#; We will be called back at OnAssociationEstablished()
Set tSC=..EstablishAssociation(..OperationDuplexName)
} elseif ..CurrentState="OperationConnected" {
#; The Operation is connected
#; Get the CommandField, it contains the type of request, it should ALWAYS be present
Set tMsgType=$$$MsgTyp2Str(pInput.GetValueAt("CommandSet.CommandField",,.tSC))
If $$$ISERR(tSC) Quit
#; We are only handling storage requests at present
$$$ASSERT(tMsgType="C-STORE-RQ")
// set patientId = pInput.GetValueAt("DataSet.PatientID",,.tSC)
// Set ^PatientImageReceived(patientId) = pInput.GetValueAt("DataSet.PatientName",,.tSC)
#; We can forward the document to the operation
Set tSC=..SendRequestAsync(..OperationDuplexName,pInput,0)
}
} elseif pSourceConfigName=..OperationDuplexName {
#; We have received a document from the operation
Set tMsgType=$$$MsgTyp2Str(pInput.GetValueAt("CommandSet.CommandField",,.tSC))
If $$$ISERR(tSC) Quit
#; Should only EVER get a C-STORE-RSP
$$$ASSERT(tMsgType="C-STORE-RSP")
#; Now close the Association with the operation, we will be called back at
#; OnAssociationReleased()
Set tSC=..ReleaseAssociation(..OperationDuplexName)
#; Finished with this document
Set ..DocumentFromService="",..OriginatingMessageID=""
}
} while (0)
Quit tSC
}
Como podemos observar, esta clase está configurada para comprobar el origen del fichero DICOM, si no procede del Business Operation definido en el parámetro OperationDuplexName significará que debemos reenviarlo al PACS y por lo tanto el metadato del mensaje DICOM ubicado en la sección CommandSet bajo el nombre CommandField deberá ser del tipo C-STORE-RQ (solicitud de almacenamiento) previo establecimiento de la conexión. En esta URL podréis comprobar los diferentes valores que puede tomar dicho metadato (en hexadecimal).
En el caso de que el mensaje proceda del Business Operation indicado es señal de que corresponde con un mensaje DICOM de respuesta a nuestro DICOM enviado previamente, por ello está validando que el CommandField de dicho mensaje sea del tipo C-STORE-RSP.
Analicemos un poco más en detalle la configuración clave del Business Operation EnsLib.DICOM.Operation.TCP utilizado para el envío de nuestro DICOM a nuestro PACS vía TCP/IP:
Hemos declarado como IP el nombre del hostname especificado en el docker-compose en el que se encuentra desplegado Orthanc al igual que el puerto.
Hemos configurado dos elementos clave para el envío a PACS, el AET de nuestro IRIS for Health (IRIS) y el AET de nuestro PACS (ORTHANC). Sin esta configuración no es posible ningún envío de imágenes, ya que tanto IRIS como Orthanc validarán que la modalidad que envía/recibe tiene permiso para hacerlo.
¿Dónde se configura a qué modalidades se pueden enviar imágenes desde IRIS y qué modalidades nos pueden enviar imágenes? Muy sencillo, tenemos desde el portal de gestión de IRIS acceso a la funcionalidad de DICOM settings:
Desde este menú no sólo podemos indicar qué modalidades pueden enviarnos y a cuales podemos enviar imágenes DICOM, también podemos indicar qué tipo de imágenes vamos a poder enviar y recibir, de tal forma que podremos rechazar cualquier imagen que se salga de esa parametrización. Como veis en la imagen superior tenemos configurado conexiones tanto de IRIS a Orthanc como de Orthanc a IRIS. Por defecto Orthanc admite cualquier tipo de imagen, por lo que no necesitamos modificar nada en su configuración.
Para no tener problemas con las imagenes que podemos enviar y recibir desde IRIS configuraremos la llamada "Presentation Context", formada por "Abstract Syntax" compuestas por la combinación servicios DICOM (Store, Get, Find...) y un objeto (imágenes MR, CT, etc...) y la "Transfer Syntax" que define como se intercambia la información y como se representan los datos.
Bien, ya tenemos configurada cualquier posible conexión entre IRIS y Orthanc y viceversa. Procedamos a lanzar una prueba incluyendo un fichero DICOM en la ruta definida en nuestro Business Service:
¡Muy bien! Aquí tenemos registrados nuestros ficheros DICOM y podemos ver como han pasado por nuestra producción hasta ser enviados a Orthanc. Entremos en más detalles revisando un mensaje.
Aquí tenemos nuestro mensaje con su CommandField definido con el valor 1, correspondiente a C-STORE-RQ, revisemos ahora la respuesta que hemos recibido desde Orthanc:
Podemos ver que el valor del CommandFile 32769 corresponde en hexadecimal al 8001, que como hemos visto en esta URL equivale al tipo C-STORE-RSP. También podemos observar que el mensaje de respuesta es un mensaje DICOM que únicamente contiene los valores definidos en el Command Set.
Comprobemos desde Orthanc que hemos recibido correctamente los mensajes:
Aquí están nuestros mensajes archivados en nuestro PACS con éxito. ¡Objetivo conseguido! Ya podemos almacenar las imágenes DICOM de nuestra modalidad en nuestro PACS sin ningún problema.
En el próximo artículo trataremos el sentido opuesto de la comunicación, el envío desde el PACS a nuestra modalidad configurada en IRIS.
Aquí tenéis disponible el código utilizado para este artículo: https://github.com/intersystems-ib/workshop-dicom-orthanc
Pregunta
Yone Moreno · 14 oct, 2021
En primer lugar, gracias por su tiempo leyendo esta pregunta y gracias por su ayuda.
Tenemos el siguiente caso de uso: necesitaríamos enviar con MTOM un zip que contenga un csv
Hemos leído, investigado, indagado:
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls...
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls...
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls...
Necesitaríamos replicar cómo SOAPUI envía un zip con un csv como archivo adjunto en una solicitud SOAP, empleando Ensemble
Siendo la petición raw de la siguiente manera:
¿Podrían ayudarnos o indicarnos la documentación necesaria, ejemplos de código o proyectos de Ensemble para estudiar, comprender y desarrollar este comportamiento? 💭
¿Hay algunos ejemplos de código o documentación que nos pueda ayudar? 💭
Gracias por su tiempo, respuestas y ayuda. Hola Yone,
Entiendo que quieres implementar un cliente de WebService en Ensemble que utilice MTOM.
Supongo que ya tienes creado el cliente de WebService, el Business Operation y los Mensajes a partir del WSDL del WebService.
¿Has probado a sobreescribir el parámetro `MTOMREQUIRED` del cliente web y activarla?
En [Using MTOM for Attachments. Webclient](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOAP_mtom#GSOAP_mtom_example_webclient), tienes un ejemplo de un cliente de servicio web que al invocarlo activa la propiedad `MTOMRequired` y donde además ha cambiado los tipos de datos por derivados de `%Stream`:
Hola Alberto, gracias por responder porque nos apoya y orienta lo que nos explicas, en concreto el ejemplo de la documentación y las dos indicaciones a implementar
Hemos escrito el siguiente WebService:
Class WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP Extends %SOAP.WebClient [ ProcedureBlock ]
{
// Parameter LOCATION = "https://regvacube.sns.gob.es/regvacu/ws/FicheroVacuService";
/// This is the URL used to access the web service.
/// This is the namespace used by the Service
Parameter NAMESPACE = "http://ws.regvacuWs.ms.es/regvacu/ws/FicheroVacuService";
/// 20/09/21 Cambiamos a 0, con el objetivo de quitar el xsi:type
Parameter OUTPUTTYPEATTRIBUTE = 0;
/// Determines handling of Security header.
Parameter SECURITYIN = "ALLOW";
/// This is the name of the Service
Parameter SERVICENAME = "FicheroVacuService";
// Parameter SOAPVERSION = 1.2;
Parameter SOAPVERSION = 1.1;
/// This is the SOAP version supported by the service.
Parameter MTOMREQUIRED = 1;
// Method cargarFichero(fichero As %xsd.base64Binary(REQUIRED=1), ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
// Method cargarFichero(fichero As %GlobalBinaryStream, ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
/// 15 10 21 Edu explica que el fichero, el .zip con un .csv necesitamos enviarlo SIN CODIFICAR
/// quitamos %GlobalBinaryStream y ponemos %GlobalCharacterStream
///
Method cargarFichero(fichero As %GlobalCharacterStream, ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
{
//Header - Addresing
set addressing = ..crearAddressing()
set addressing.Action = "cargarFichero"
set ..AddressingOut = addressing
set ..AddressingOut.mustUnderstand = "1"
//Firma el XML (mensaje SOAP)
//do ..crearSignature()
set ..MTOMRequired=1
//24 09 21 para añadir parametro name en cabecera content type
//set ..ContentType="application/octet-stream; name=nombre"
//28 09 21 probamos a ajustarlo
//set ..ContentType="application/octet-stream; charset=latin1"
set ..ContentType="application/xhtml+xml; charset=latin1"
/*
27 09 21 con el objetivo de poner parametro name en cabecera content type
Genera excepcion: ERROR #5001: <INVALID OREF>zcargarFichero+16^WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP.1
*/
//set ..HttpRequest.ContentType="application/octet-stream; name=nombre"
//do ..HttpRequest.SetHeader("name","nombre")
Quit ..WebMethod("cargarFichero","CargarFicheroVacuRequest").Invoke($this,"http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero",.fichero,.ccaaId,.tipoFichero)
}
Method infoFichero(ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), infoFichero As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(REQUIRED=1), Output estado As EsquemasDatos.HistoriaClinica.tns.EstadoFicheroType(REQUIRED=1)) As %xsd.base64Binary(XMLNAME="fichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
{
set ..MTOMRequired=1
Quit ..WebMethod("infoFichero","InfoFicheroVacuRequest").Invoke($this,"http://ws.regvacuWs.ms.es/FicheroVacu/infoFichero",.ccaaId,.infoFichero,.estado)
}
Method crearAddressing() As %SOAP.Addressing.Properties
{
set IPRedSanitaria = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","IPRedSanitaria")
set puertoRespuestas = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","PuertoRespuestasSSL")
set ReplyTo = ##class(%SOAP.Addressing.EndpointReference).%New()
set ReplyTo.Address = "http://www.w3.org/2005/08/addressing/anonymous"
//set ReplyTo.Address = "https://"_IPRedSanitaria_":"_puertoRespuestas_"/csp/SNS/Servicios.ProgramasAsistenciales.SIFCOv02r00.cls"
set MessageId = ##class(Util.FuncionesComunes).getUID()
set addressing = ##class(%SOAP.Addressing.Properties).%New()
set addressing.MessageId = MessageId
set addressing.Destination = ..Location
set addressing.ReplyEndpoint = ReplyTo
Quit addressing
}
Method crearSignature() As %XML.Security.Signature
{
//Generamos el Binary Security Token a partir del mcertificado
set x509alias = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","aliasCertMSSSI")
set pwd = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","pwdCertMSSSI")
set cred = ##class(%SYS.X509Credentials).GetByAlias(x509alias,pwd)
set token = ##class(%SOAP.Security.BinarySecurityToken).CreateX509Token(cred)
//Creamos la firma
//set sig1=##class(%XML.Security.Signature).CreateX509(token,,$$$KeyInfoX509IssuerSerial)
//set sig2=##class(%XML.Security.Signature).CreateX509(token,$$$SOAPWSIncludeSoapBody,$$$SOAPWSReferenceDirect)
set sig2=##class(%XML.Security.Signature).CreateX509(token,$$$SOAPWSIncludeDefault,$$$SOAPWSReferenceDirect)
//do sig1.SetSignatureMethod($$$SOAPWSrsasha1)
do sig2.SetSignatureMethod($$$SOAPWSrsasha1)
//do sig1.SetDigestMethod($$$SOAPWSsha1)
do sig2.SetDigestMethod($$$SOAPWSsha1)
//Creamos la referencia al id del token generado a partir de la firma
//set algorithm=$$$SOAPWSEnvelopedSignature_","_$$$SOAPWSc14n
set reference=##class(%XML.Security.Reference).Create(token.Id)
do sig2.AddReference(reference)
//Crear TimeStamp
Set timestamp=##class(%SOAP.Security.Timestamp).Create()
//Se une
//do ..SecurityOut.AddElement(sig1)
do ..SecurityOut.AddToken(token)
do ..SecurityOut.AddElement(sig2)
Do ..SecurityOut.AddToken(timestamp)
}
}
El sistema destino, nos pide por favor, que implementemos un WebService con MTOM y que envie los zip con csv de la siguiente forma:
Al importar el WSDL del sistema destino, se nos generó la cabecera del método "cargarFichero" con "fichero" como un "%xsd.base64Binary"
Siguiendo tus indicaciones, hemos cambiado fichero por "%GlobalCharacterStream"
De esta forma se envía el fichero, el csv, sin codificar, dentro de un CDATA
Siendo la respuesta del sistema destino:
10/15/2021 08:57:24 *********************
Input to Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
--MIME_Boundary
Content-ID: <root.message@cxf.apache.org>
Content-Type: application/xop+xml; type="text/xml"; charset=utf-8
Content-Transfer-Encoding: 8bit
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Name must not be null</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
--MIME_Boundary--
---------------
Validate Security header: action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero, MethodName=cargarFichero
**** SOAP client return error. method=cargarFichero, action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
ERROR #6248: La respuesta de SOAP es un error de SOAP: faultcode=Server
faultstring=Name must not be null
faultactor=
detail=
Sin embargo, como observamos en la primera imagen, el sistema destino necesitaría, requeriría, que le enviemos el fichero codificado en binario, ya que pone:
Content-Transfer-Encoding: binary
Cuando adaptamos el fichero para que sea "%GlobalBinaryStream" vemos la siguiente traza, donde sí se codifica en binario:
Al enviar codificado en binario, el sistema destino también responde:
09/28/2021 16:56:43 *********************
Input to Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
--MIME_Boundary
Content-ID: <root.message@cxf.apache.org>
Content-Type: application/xop+xml; type="text/xml"; charset=utf-8
Content-Transfer-Encoding: 8bit
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<soap:Fault>
<faultcode>soap:Server</faultcode>
<faultstring>Name must not be null</faultstring>
</soap:Fault>
</soap:Body>
</soap:Envelope>
--MIME_Boundary--
---------------
Validate Security header: action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero, MethodName=cargarFichero
**** SOAP client return error. method=cargarFichero, action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
ERROR #6248: La respuesta de SOAP es un error de SOAP: faultcode=Server
faultstring=Name must not be null
faultactor=
detail=
En resumen, el sistema destino necesita de nosotros: "añadir el parámetro “name” dentro de la cabecera “Content-Type” al adjuntar el archivo. Vuestro cliente debería generar dicho parámetro para no obtener dicho error."
Siendo la comparativa completa entre lo enviado por ensemble (sin el parámetro "name", por lo tanto es incorrecto) a la izquierda; y lo generado por el SoapUI (con el parámetro name, correcto), a la derecha:
Por favor, ustedes ¿podrían indicarnos ejemplos, documentación, proyectos, código, que nos sirva de referencia para indagar, investigar y completar el desarrollo?
Además hemos investigado las siguientes respuestas:
https://community.intersystems.com/post/add-parameter-name-inside-content-type-header-when-we-send-mtom-attachment-using-soap-request
Muchas gracias por su tiempo, leyendo y respondiendo, gracias
Hola de nuevo Yone,
Por lo que comentas, quizá te falte añadir la cabecera *Content-Disposition* en la petición y ahí especificar el nombre del archivo.
Creo que en el ejemplo que habías hecho originalmente con SoapUI, esa cabecera tenía este valor:
```
Content-Disposition: attachment; name="application.zip"
```
En [Fine Tuning a WebClient](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOAP_cli_details#GSOAP_cli_details_http_headers) te habla de cómo puedes especificar cabeceras en tu cliente web utilizando el método `SetHttpHeader()` que hereda de `%SOAP.WebClient`.
Así que puedes probar a especificar con `SetHttpHeader()` la cabecera `Content-Disposition` y añadir el nombre del fichero que quieras que aparezca.
Gracias por responder Alberto,
es un apoyo, alivio y ayuda, la orientación que nos ofreces
Siguiendo tu criterio, y la explicación de la documentación, hemos añadido:
do ..SetHttpHeader("Content-Disposition","application.zip")
La línea la hemos escrito antes de:
Quit ..WebMethod("cargarFichero","CargarFicheroVacuRequest").Invoke($this,"http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero",.fichero,.ccaaId,.tipoFichero)
Al capturar el LOGSOAP observamos que se queda sin incluir la cabecera Content-Disposition y el nombre application.zip en lo que enviamos:
¿Por qué podría ser que al escribir la línea, se quede sin incluir la nueva cabecera Content-Disposition y el nombre del fichero application.zip? 💭🤔
Además, en otras pruebas, hemos escrito 3 formas adicionales, para tratar de resolverlo:
set ..ContentType="application/octet-stream; name=nombre"
Mediante la línea anterior, no se ve que añada el parámetro "name"
Probamos con la siguiente línea y generaría excepción Ensemble:
set ..HttpRequest.ContentType="application/octet-stream; name=nombre"
Genera excepcion: ERROR #5001: <INVALID OREF>zcargarFichero+16^WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP.1
y mediante:
do ..HttpRequest.SetHeader("name","nombre")
Nos da excepción también:
ERROR #5001: <INVALID OREF>zcargarFichero+16^WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP.1
La clase completa que hemos escrito del Servicio Web es:
Class WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP Extends %SOAP.WebClient [ ProcedureBlock ]
{
// Parameter LOCATION = "https://regvacube.sns.gob.es/regvacu/ws/FicheroVacuService";
/// This is the URL used to access the web service.
/// This is the namespace used by the Service
Parameter NAMESPACE = "http://ws.regvacuWs.ms.es/regvacu/ws/FicheroVacuService";
/// 20/09/21 Cambiamos a 0, con el objetivo de quitar el xsi:type
Parameter OUTPUTTYPEATTRIBUTE = 0;
/// Determines handling of Security header.
Parameter SECURITYIN = "ALLOW";
/// This is the name of the Service
Parameter SERVICENAME = "FicheroVacuService";
// Parameter SOAPVERSION = 1.2;
Parameter SOAPVERSION = 1.1;
/// This is the SOAP version supported by the service.
Parameter MTOMREQUIRED = 1;
// Method cargarFichero(fichero As %xsd.base64Binary(REQUIRED=1), ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
// Method cargarFichero(fichero As %GlobalBinaryStream, ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
/// 15 10 21 Edu explica que el fichero, el .zip con un .csv necesitamos enviarlo SIN CODIFICAR
/// quitamos %GlobalBinaryStream y ponemos %GlobalCharacterStream
///
Method cargarFichero(fichero As %GlobalCharacterStream, ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), tipoFichero As EsquemasDatos.HistoriaClinica.tns.TipoFicheroType(REQUIRED=1)) As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(XMLNAME="responseFichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
{
//Header - Addresing
set addressing = ..crearAddressing()
set addressing.Action = "cargarFichero"
set ..AddressingOut = addressing
set ..AddressingOut.mustUnderstand = "1"
//Firma el XML (mensaje SOAP)
//do ..crearSignature()
set ..MTOMRequired=1
//24 09 21 para añadir parametro name en cabecera content type
//set ..ContentType="application/octet-stream; name=nombre"
//28 09 21 probamos a ajustarlo
//set ..ContentType="application/octet-stream; charset=latin1"
//set ..ContentType="application/xhtml+xml; charset=latin1"
/*
27 09 21 con el objetivo de poner parametro name en cabecera content type
Genera excepcion: ERROR #5001: <INVALID OREF>zcargarFichero+16^WSCLIENTE.HistoriaClinica.FicheroVacuServiceSOAP.1
*/
//set ..HttpRequest.ContentType="application/octet-stream; name=nombre"
//do ..HttpRequest.SetHeader("name","nombre")
/*
15 10 21 seguimos la indicacion de Alberto Fuentes:
➕ añadir la cabecera Content-Disposition en la petición y ahí especificar el nombre del archivo.
https://es.community.intersystems.com/post/%C2%BFc%C3%B3mo-podr%C3%ADamos-usar-mtom-para-enviar-un-zip-con-un-csv-adentro#comment-169536
https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOAP_cli_details#GSOAP_cli_details_http_headers
*/
do ..SetHttpHeader("Content-Disposition","application.zip")
Quit ..WebMethod("cargarFichero","CargarFicheroVacuRequest").Invoke($this,"http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero",.fichero,.ccaaId,.tipoFichero)
}
Method infoFichero(ccaaId As EsquemasDatos.HistoriaClinica.tns.CCAAIdType(REQUIRED=1), infoFichero As EsquemasDatos.HistoriaClinica.tns.InfoFicheroType(REQUIRED=1), Output estado As EsquemasDatos.HistoriaClinica.tns.EstadoFicheroType(REQUIRED=1)) As %xsd.base64Binary(XMLNAME="fichero") [ Final, ProcedureBlock = 1, SoapBindingStyle = document, SoapBodyUse = literal, WebMethod ]
{
set ..MTOMRequired=1
Quit ..WebMethod("infoFichero","InfoFicheroVacuRequest").Invoke($this,"http://ws.regvacuWs.ms.es/FicheroVacu/infoFichero",.ccaaId,.infoFichero,.estado)
}
Method crearAddressing() As %SOAP.Addressing.Properties
{
set IPRedSanitaria = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","IPRedSanitaria")
set puertoRespuestas = ##class(Util.TablasMaestras).getValorMaestra("PARAMETROS","PuertoRespuestasSSL")
set ReplyTo = ##class(%SOAP.Addressing.EndpointReference).%New()
set ReplyTo.Address = "http://www.w3.org/2005/08/addressing/anonymous"
//set ReplyTo.Address = "https://"_IPRedSanitaria_":"_puertoRespuestas_"/csp/SNS/Servicios.ProgramasAsistenciales.SIFCOv02r00.cls"
set MessageId = ##class(Util.FuncionesComunes).getUID()
set addressing = ##class(%SOAP.Addressing.Properties).%New()
set addressing.MessageId = MessageId
set addressing.Destination = ..Location
set addressing.ReplyEndpoint = ReplyTo
Quit addressing
}
}
¿Cómo podríamos indagar, depurar, ajustar, y resolver juntos, esta situación?
Si nos pudieran orientar, se los agradeceríamos 🙇♂️🙏🙏
¿Cómo recomendarían ustedes seguir?
¿Qué documentación necesitamos estudiar, leer, entender, para completar esta parte?
Gracias por las respuestas Hola Yone,
En principio el `..SetHttpHeader()` debería ser suficiente.
Puedes probar con:
```
do ..SetHttpHeader("Content-Disposition", "attachment; name=""application.zip""")
```
¿El Servicio Web sigue devolviendo un error respecto al Name?
Supongo que estás utilizando `^ISCSOAP` para examinar lo que se envía al WebService. Dependiendo de tu versión de Ensemble o Health Connect podrás utilizar las opciones de ISCSOAP para que te muestre las cabeceras HTTP:
```
set ^ISCSOAP("Log")="ioshH"
set ^ISCSOAP("LogFile")="/tmp/soap.log"
```
Aquí en [InterSystems IRIS SOAP Log](https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOAP_debug#GSOAP_debug_info_soap_log) te explica las diferentes opciones. Comprueba en la documentación de tu versión específica si están las opciones para las cabeceras HTTP y así asegurarte de que se muestran.
He hecho una prueba rápida en una 2020.1 y en el log de ISCSOAP debería tener una pinta así:
```
10/15/2021 13:39:51 *********************
Output from Web client with SOAP action =
**** Output HTTP headers for Web Client
User-Agent: Mozilla/4.0 (compatible; InterSystems IRIS;)
Host: xyz.es
Accept-Encoding: gzip
Content-Disposition: attachment; name="application.zip"
SOAPAction:
```
Si en tu versión no se pueden mostrar las cabeceras en el log quizá puedes utilizar herramientas externas como WireShark, etc.
Échale un vistazo a esto, y si no logras avanzar escríbeme por correo y quedamos y lo revisamos. Gracias Alberto por tu respuesta y por dedicarnos tu tiempo,
Hemos añadido:
do ..SetHttpHeader("Content-Disposition", "attachment; name=""application.zip""")
Activamos el LOGSOAP:
set ^ISCSOAP("Log")="ioshH"
set ^ISCSOAP("LogFile") = "/opt/contenedor/15 10 21 3 Content-Disposition attachment name Cargar Fichero Vacunas.xml"
Observamos que unicamente se incluyen las cabeceras SOAP, y no las HTTP en la traza:
10/15/2021 13:17:59 *********************
Output from Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
----boundary1237.882352941176471407.7058823529411765--
Content-Type: application/xop+xml; type="text/xml"; charset="UTF-8"
Content-Transfer-Encoding: 8bit
Content-Id: <0.EFBCA6F8.2DB1.11EC.9A62.005056B672A4>
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:s='http://www.w3.org/2001/XMLSchema' xmlns:wsa='http://www.w3.org/2005/08/addressing'>
<SOAP-ENV:Header>
<wsa:Action>cargarFichero</wsa:Action>
<wsa:MessageID>efbc9f822db111ec9a62005056b672a4</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:To>https://regvacu-svc-pre.sns.gob.es/sssns/regvacu-bus-r01/PS/CCAA/Canarias/RegVacu_PS?wsdl</wsa:To>
</SOAP-ENV:Header>
Siguiendo el consejo que nos indicas, leemos la documentación de la versión que empleamos:
Cache for UNIX (Red Hat Enterprise Linux for x86-64) 2017.2.1 (Build 801_3_18358U) Tue Jul 24 2018 16:36:10 EDT
Y nos sale que la opción "H" en el ^ISCSOAP que nos permitiría observar las cabeceras HTTP, está sin incluir:
Tienes razón, Alberto, el Servicio Web destino, nos sigue respondiendo que faltaría la propiedad name:
10/15/2021 13:18:00 *********************
Input to Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
--MIME_Boundary
Content-ID: <root.message@cxf.apache.org>
Content-Type: application/xop+xml; type="text/xml"; charset=utf-8
Content-Transfer-Encoding: 8bit
<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
---------------
Validate Security header: action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero, MethodName=cargarFichero
**** SOAP client return error. method=cargarFichero, action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
ERROR #6248: La respuesta de SOAP es un error de SOAP: faultcode=Server
faultstring=Name must not be null
faultactor=
detail=
¿Cómo podríamos continuar?
¿De qué forma depuraríamos las cabeceras HTTP del envío del fichero mediante SOAP?
¿Ustedes como nos recomiendan que sigamos?
¿Cuáles serían los siguientes pasos?
Muchas gracias por tus respuestas Alberto, y sobre todo gracias por indicarnos la documentación y ejemplos concretos y prácticos
Hola Yone,
Lo que te indicaba antes. Necesitarás en ese caso otra herramienta como Wireshark que te permita capturar las cabeceras que se envían al servicio web y ver si están bien.
Dale una vuelta con eso y si no consigues avanzar me escribes por email y buscamos la forma de revisarlo conjuntamente. Disculpen, había cometido un error
Necesitamos usar el mecanismo llamado "Attachment" para enviar el fichero como un adjunto en la petición SOAP; y no el mecanismo alternativo, conocido como "MTOM"
Hemos añadido las siguientes líneas en el Cliente Web:
Set file=##class(%Library.FileCharacterStream).%New()
Set file.Filename="application.zip"
while (fichero.AtEnd '= 0){
do file.Write($system.Encryption.Base64Decode(fichero.Read()))
}
Set mim
epart=##class(%Net.MIMEPart).%New()
Set mimepart.Body=file
Do mimepart.SetHeader("Content-Disposition", "attachment; name=""application.zip""")
do ..Attachments.Insert(mimepart)
Siguiendo la documentación:
Using SOAP with Attachments: Web Client
¿Qué podría significar que el sistema destino nos devuelva un código http 511?
ERROR #6242: La solicitud HTTP a SOAP WebService ha devuelto un estado inesperado: 511.
Parece que podría indicar lo siguiente:
"El código de estado de respuesta HTTP 511 Network Authentication Required indica que el cliente necesita autenticarse para obtener acceso a la red."
En estos casos, ¿qué se suele necesitar?
En el LOGSOAP veríamos:
10/15/2021 15:51:56 *********************
Output from Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
----boundary1980.3529411764705881780.294117647058824--
Content-Type: text/xml; charset="UTF-8"
Content-Transfer-Encoding: 8bit
<?xml version="1.0" encoding="UTF-8" ?>
<SOAP-ENV:Envelope xmlns:SOAP-ENV='http://schemas.xmlsoap.org/soap/envelope/' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns:s='http://www.w3.org/2001/XMLSchema' xmlns:wsa='http://www.w3.org/2005/08/addressing'>
<SOAP-ENV:Header>
<wsa:Action>cargarFichero</wsa:Action>
<wsa:MessageID>7148b7422dc711eca7e1005056b672a4</wsa:MessageID>
<wsa:ReplyTo>
<wsa:Address>http://www.w3.org/2005/08/addressing/anonymous</wsa:Address>
</wsa:ReplyTo>
<wsa:To>https://regvacu-svc-pre.sns.gob.es/sssns/regvacu-bus-r01/PS/CCAA/Canarias/RegVacu_PS?wsdl</wsa:To>
</SOAP-ENV:Header>
<SOAP-ENV:Body>
<CargarFicheroVacuRequest xmlns="http://ws.regvacuWs.ms.es/regvacu/ws/FicheroVacuService">
<fichero>
<![CDATA[;;;47B7F6BF1C6D7174A920B3...;12010070;]]>
</fichero>
<ccaaId>01</ccaaId>
<tipoFichero>2</tipoFichero>
</CargarFicheroVacuRequest>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
----boundary1980.3529411764705881780.294117647058824--
Content-Disposition: attachment; name="application.zip"
----boundary1980.3529411764705881780.294117647058824----
10/15/2021 15:51:56 *********************
Input to Web client with SOAP action = http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
ERROR #6242: La solicitud HTTP a SOAP WebService ha devuelto un estado inesperado: 511.
<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"/>
<soapenv:Body/>
</soapenv:Envelope>**** SOAP client return error. method=cargarFichero, action=http://ws.regvacuWs.ms.es/FicheroVacu/cargarFichero
ERROR #6242: La solicitud HTTP a SOAP WebService ha devuelto un estado inesperado: 511.
¿se implementaría una autenticación desde nuestra Operacion SOAP o Cliente Web, hacia el sistema destino?
¿qué recomiendan ustedes?
¿cómo continuaríamos? ¿cuáles son los siguientes pasos?
Gracias por leernos, y por su ayuda con las respuestas Hola Yone, me alegro de que al final hayas encontrado el mecanismo de Attachment para enviarlo :)
Respecto a la autenticación, deberías revisar las especificaciones del servicio web al que te quieres conectar.
Creo que estás intentando conectar a un servicio web del Ministerio para enviar comunicaciones, por experiencias pasadas creo recordar que suelen pedir al menos:
* Cabeceras WSA (parece que ya tienes algunas puestas).
* Firma del mensaje con token y un timestamp. La firma debería incluir el timestamp, el cuerpo SOAP, el token y las cabeceras WSA.
Pero repito, confírmalo por favor con la documentación de las especificaciones del servicio web al que te quieras conectar.
Puedes echar un vistazo a la documentación aquí:
https://docs.intersystems.com/irisforhealthlatest/csp/docbook/DocBook.UI.Page.cls?KEY=GSOAPSEC_bodyencr
En cualquier caso lo dicho, si necesitas algún ejemplo específico o comentar alguna cosa concreta puedes escribirme por email y buscamos un rato para echarlo un ojo.
Artículo
Nancy Martínez · 28 jun, 2019
¡Hola Comunidad!En este artículo encontrarán algunos ejemplos de conversiones y operaciones que les pueden resultar útiles. También incluyo enlaces a la documentación donde se puede obtener más información.Cuando escribí esto, la hora del Este "Eastern Daylight Time" estaba activa en el Caché de mi sistema.Cómo guarda Caché la hora y la fechaCaché tiene un formato simple para la hora, con un rango más amplio para las fechas reconocidas, en comparación con otras tecnologías.La hora actual se actualiza con la variable especial $HOROLOG ($H):
USER>WRITE $H
64146,54027
USER>
El primer número entero corresponde al número de días transcurridos desde el 31 de diciembre de 1840. El segundo número entero corresponde al número de segundos transcurridos desde la medianoche del día en curso.
También se puede obtener la hora y la fecha actuales con $SYSTEM.SYS.Horolog().
Cómo establecer un registro de la hora
$HOROLOG registra el tiempo transcurrido en segundos. $ZTIMESTAMP es un formato similar a $HOROLOG, pero registra las fracciones de segundo en una porción de tiempo y guarda el Tiempo Universal Coordinado (UTC), en lugar de la hora local. La precisión de las fracciones de segundo depende de la plataforma que se esté usando.
Por esta razón, $ZTIMESTAMP proporciona un registro de las horas que es uniforme en todas las zonas horarias. El registro de la hora que ve en cualquier momento en el tiempo podría tener una fecha y hora diferente a su hora local actual. En este ejemplo, mi hora local es la hora del Este, que tiene cuatro horas de retraso respecto al Tiempo Universal Coordinado (UTC).
WRITE !,"$ZTIMESTAMP: "_$ZTIMESTAMP_" $HOROLOG: "_$HOROLOG
$ZTIMESTAMP: 64183,53760.475 $HOROLOG: 64183,39360
La diferencia (sin contar las fracciones de segundo) es de 14400 segundos. Por lo tanto, mi $HOROLOG tiene un "retraso" de cuatro horas con $ZTIMESTAMP.
Cómo realizar conversiones desde el formato interno al formato de visualización
Puede utilizar $ZDATETIME. Realizar conversiones de la fecha y la hora actuales desde el formato interno puede ser tan sencillo como
WRITE !, "With default date and time format: ",$ZDATETIME($HOROLOG)
With default date and time format: 09/22/2016 10:56:00
Esto toma la configuración predeterminada de la configuración local de Caché y del soporte de idiomas nativos (NLS).
El segundo y tercer argumento (opcional) tienen la función de especificar el formato de la fecha y la hora.
WRITE !, "With dformat 5 and tformat 5: ", $ZDATETIME($HOROLOG,5,5)
With dformat 5 and tformat 5: Sep 22, 2016T10:56:00-04:00
Por ejemplo, Time format 7 muestra la hora en el formato del Tiempo Universal Coordinado, como puede verse aquí.
WRITE !, "With dformat 5 and tformat 7: ", $ZDATETIME($HOROLOG,5,7)
With dformat 5 and tformat 7: Sep 22, 2016T14:56:00Z
Además de los formatos para la fecha y la hora, existen muchos otros argumentos opcionales que le permitirán controlar la visualización. Por ejemplo, puede:
Especificar los límites inferior y superior de las fechas válidas si también están permitidos por la ubicación actualEstablecer si los años se visualizan con dos o cuatro dígitosControlar la manera en que se visualizarán los errores
Cómo realizar conversiones entre un formato de visualización y el formato interno de Caché
Utilice $ZDATETIMEH para ir desde el formato de visualización a un formato interno, como $HOROLOG. La H que se encuentra al final es un recordatorio de que terminará con un formato $HOROLOG. Pueden utilizarse muchos formatos de entrada diferentes.
SET display = "09/19/2016 05:05 PM"
WRITE !, display_" is "_$ZDATETIMEH(display)_" in internal format"
WRITE !, "Suffixes AM, PM, NOON, MIDNIGHT can be used"
SET startdate = "12/31/1840 12:00 MIDNIGHT"
WRITE !, startdate_" is "_$ZDATETIMEH(startdate)_" in internal format"
09/19/2016 05:05 PM is 64180,61500 in internal format
Suffixes AM, PM, NOON, MIDNIGHT can be used
12/31/1840 12:00 MIDNIGHT is 0,0 in internal format
Cómo hacer conversiones UTC hacia y desde la hora local en un formato interno
Puede utilizar $ZDATETIME y $ZDATETIMEH con un especificador especial (-3) para el formato de la fecha en el segundo argumento.
La mejor manera para convertir la hora UTC a la hora local en el formato interno de Caché, es utilizar la función $ZDATETIMEH (con datetime, -3). Aquí, el primer argumento contiene la hora UTC en el formato interno.
SET utc1 = $ZTIMESTAMP
SET loctime1 = $ZDATETIMEH(utc1, -3)
WRITE !, "$ZTIMESTAMP returns a UTC time in internal format: ", utc1
WRITE !, "$ZDATETIMEH( ts,-3) converts UTC to local time: ", loctime1
WRITE !, "$ZDATETIME converts this to display formats: ", $ZDATETIME(utc1)
WRITE !, "which is "_$ZDATETIME(loctime1)_" in local time"
$ZTIMESTAMP returns a UTC time in internal format: 64183,53760.475
$ZDATETIMEH( ts,-3) converts UTC to local time: 64183,39360.475
$ZDATETIME converts this to display formats: 09/22/2016 14:56:00
which is 09/22/2016 10:56:00 in local time
Si se requiere hacer conversiones desde la hora local hacia el UTC, de nuevo en el formato interno, utilice $ZDATETIME(con datetime, -3). Aquí, datetime contiene la hora que indica tu zona horaria local en el formato interno.
SET loctime2 = $HOROLOG
SET utc2 = $ZDATETIME(loctime2, -3)
WRITE !, "$HOROLOG returns a local time in internal format: ", loctime2
WRITE !, "$ZDATETIME(ts, -3) converts this to UTC: ", utc2
WRITE !, "$ZDATETIME converts this to display formats:"
WRITE !, "Local: ", $ZDATETIME(loctime2)
WRITE !, "UTC: ", $ZDATETIME(utc2)
$HOROLOG returns a local time in internal format: 64183,39360
$ZDATETIME(ts, -3) converts this to UTC: 64183,53760
$ZDATETIME converts this to display formats:
Local: 09/22/2016 10:56:00
UTC: 09/22/2016 14:56:00
Se debe tener en cuenta estos puntos cuando se realiza conversiones entre la hora local y el UTC:
Las conversiones entre la hora local y el UTC deben utilizar las reglas vigentes de la zona horaria para la fecha y el lugar especificados. Caché depende del sistema operativo para registrar esos cambios en el tiempo. Si el sistema operativo no realiza esto de forma apropiada, las conversiones no serán correctas.Las conversiones de fechas y horas que se realicen en el futuro utilizaran las reglas que conserva actualmente el sistema operativo. Sin embargo, la reglas para las conversiones de los años, que se realicen en el futuro, pueden cambiar.
Cómo determinar la zona horaria en el sistema
Se puede obtener el desfase con la zona horaria actual al examinar el valor de $ZTIMEZONE o %SYSTEM.SYS.TimeZone(). El sistema operativo establece el valor predeterminado.
WRITE !, "$ZTIMEZONE is set to "_$ZTIMEZONE
WRITE !, "%SYSTEM.SYS.TimeZone() returns "_$System.SYS.TimeZone()
$ZTIMEZONE is set to 300
%SYSTEM.SYS.TimeZone() returns 300
No se debe modificar el valor de la función $ZTIMEZONE. Si se hace, podría afectar los resultados de IsDST(), $ZDATETIME, y $ZDATETIMEH, entre muchas otras consecuencias. La hora establecida por el proceso no se actualizará correctamente para el horario de verano. Realizar modificaciones en $ZTIMEZONE provocará que los cambios no se apliquen de manera uniforme en la zona horaria que utiliza Caché.
A partir de la versión 2016.1, Caché proporciona el método $System.Process.TimeZone(), el cual le permite establecer y recuperar la zona horaria para realizar un proceso específico utilizando la variable de entorno TZ. El método devuelve -1 si la variable TZ no se estableció.
WRITE !,$System.Process.TimeZone()
WRITE !, "Current Time: "_$ZDT($H)
WRITE !, "Set Central Time"
DO $System.Process.TimeZone("CST6CDT")
WRITE !, "New current time: "_$ZDT($H)
WRITE !, "Current Time Zone: "_$System.Process.TimeZone()
-1
Current Time: 10/03/2016 15:46:04
Set Central Time
New current time: 10/03/2016 14:46:04
Current Time Zone: CST6CDT
Cómo determinar si el horario de verano está activo
Utilice $SYSTEM.Util.IsDST(). En este caso, Caché también depende del sistema operativo para aplicar las reglas correctas y determinar si el horario de verano está activo.
SET dst = $System.Util.IsDST()
IF (dst = 1) {WRITE !, "DST is in effect"}
ELSEIF (dst = 0) { WRITE !, "DST is not in effect" }
ELSE { WRITE !, "DST cannot be determined" }
Cómo realizar cálculos con las fechas
Dado que el formato interno de Caché mantiene un recuento de los días y de los segundos que hay en cada día, usted puede calcular las fechas de forma directa. La función $PIECE le permite quitar los elementos de la fecha y hora del formato interno.
Esta es una rutina corta que utiliza $ZDATE y $ZDATEH para determinar cuál fue el último día del año pasado, de modo que pueda calcular el día actual del año en curso. Esta rutina utiliza métodos en la clase %SYS.NLS para establecer el formato que deseemos, obtener las separaciones para la fecha, y configurar nuevamente los valores predeterminados.
DATECALC ; Example of date arithmetic.
W !, "Extracting date and time from $H using $PIECE"
W !, "---------------------------------------------"
set curtime = $H
set today = $PIECE(curtime,",",1)
set now = $PIECE(curtime,",",2)
W !, "Curtime: "_curtime_" Today: "_today_" Now: "_now
W !, "Counting the days of the year"
W !, "-----------------------------"
; set to US format
SET rtn = ##class(%SYS.NLS.Format).SetFormatItem("DateFormat",1)
set sep = ##class(%SYS.NLS.Format).GetFormatItem("DateSeparator")
SET lastyear = ($PIECE($ZDATE($H),sep,3) - 1)
SET start = $ZDATEH("12/31/"_lastyear)
W !, "Today is day "_(today - start)_" of the year"
; put back the original date format
SET rtn=##class(%SYS.NLS.Format).SetFormatItem("DateFormat",rtn)
Cómo obtener y establecer otras configuraciones para NLS
Utilice la clase %SYS.NLS.Format para configurar cosas como el formato de la fecha, las fechas máximas y mínimas y realizar otros ajustes. La configuración inicial proviene de la ubicación actual y los cambios que realice con esta clase influirán únicamente en el proceso actual.
Fecha y hora en SQL
Caché proporciona varias funciones en SQL para trabajar con las fechas y las horas. Estas funciones también están disponibles en ObjectScript mediante la clase $System.SQL.
TO_DATE: Convierte una cadena de caracteres con formato CHAR o VARCHAR2 en una fecha. Esta función está disponible en ObjectScript utilizando $System.SQL.TODATE(“string”,”format”)
DAYOFYEAR: Devuelve el día del año para expresar un cierto año, el cual puede estar en varios formatos, como un entero para mostrar una fecha desde $HOROLOG.
DAYNAME: Devuelve el nombre del día que corresponde con una fecha específica.
W $ZDT($H)
10/12/2016 11:39:19
w $System.SQL.TODATE("2016-10-12","YYYY-MM-DD")
64203
W $System.SQL.DAYOFYEAR(+$H)
286
W $System.SQL.DAYNAME(+$H)
Wednesday
Hay mucha información sobre cómo utilizar estas funciones (y muchas más) en la documentación de Caché. Consulte las referencias en la siguiente sección.
Referencias
Enlaces a los documentos de soporte de InterSystems, disponibles "online":
$HOROLOG: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_vhorolog
$PIECE: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fpiece
Referencia a las funciones de SQL: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RSQL_FUNCTIONS
%SYS.NLS.Format: http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYS.NLS.Format
%SYSTEM.Process.TimeZone(): http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.Process#METHOD_TimeZone
%SYSTEM.SQL: http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.SQL
%SYSTEM.SYS.Horolog: http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.SYS
%SYSTEM.Util.IsDST(): http://docs.intersystems.com/latest/csp/documatic/%25CSP.Documatic.cls?PAGE=CLASS&LIBRARY=%25SYS&CLASSNAME=%25SYSTEM.Util#IsDST
$ZDATE: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdate
$ZDATEH: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdateh
$ZDATETIME: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdatetime
$ZDATETIMEH: http://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=RCOS_fzdatetimeh
Artículo
Mathew Lambert · 16 abr, 2020
¡Hola Comunidad!
Descubrí el Desarrollo Basado en Pruebas (TDD) hace casi 9 años y me enamoré del concepto inmediatamente.
Hoy se ha vuelto muy popular pero, desafortunadamente, muchas empresas no lo usan. Es más, muchos desarrolladores, sobre todo principiantes, ni siquiera saben exactamente qué es ni como usarlo.
Resumen
Mi objetivo con este artículo es mostrar cómo usar TDD con %UnitTest. Mostraré mi flujo de trabajo y explicaré cómo usar cosFaker, un proyecto de @Henry Pereira, usando Caché y que hace poco lo subió a OpenExchange.
Así que... ¡preparaos que allá vamos!
¿Qué es TDD?
El Desarrollo basado en pruebas (TDD) puede definirse como una práctica de programación que enseña a los desarrolladores a escribir código solo cuando haya fallado una prueba automática.Hay montones de artículos, ponencias, charlas, etc., sobre sus ventajas, y todas tienen razón.El código nace ya probado, te aseguras de que tu sistema realmente cumple con los requerimientos que se le definieron, evitas la sobreingeniería y obtienes feedback continuo.
Entonces... ¿por qué no se usa TDD? ¿Cuál es el problema con TDD? La respuesta es sencilla: ¡el coste! Cuesta mucho.
Al tener que escribir más líneas de código con TDD es un proceso lento. Pero con TDD tienes el coste final de un producto AHORA, sin tener que sumarle en un futuro.Si ejecutas las pruebas todo el tiempo, encontrarás los errores pronto, lo que reduce el coste de su corrección.Así que mi consejo es: ¡Hazlo Ya!
Configuración
InterSystems tiene documentación y un tutorial sobre cómo usar %UnitTest, puedes leerlo aquí.
Yo uso vscode para desarrollar. De esta forma, creo una carpeta separada para las pruebas. Agrego la ruta del código de mi proyecto a UnitTestRoot y, cuando ejecuto pruebas, paso el nombre de la subcarpeta de prueba. Y siempre paso el calificador loadudl
Set ^UnitTestRoot = "~/code"
Do ##class(%UnitTest.Manager).RunTest("myPack","/loadudl")
Pasos
Probablemente hayas oído hablar del famoso ciclo TDD: rojo ➡ verde ➡ refactor. Primero escribes una prueba que falla, luego escribes un código de producción simple para hacer que pase la prueba y luego refactorizas el código de producción.
Así que vamos a ponernos manos a la obra y crear una clase para hacer unas operaciones matemáticas y otra clase para probarla. La segunda de estas clases será una extensión de %UnitTest.TestCase.
Ahora crearemos un Método de clase que devuelva el cuadrado de un número entero:
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
}
}
Y probaremos qué sucede si le pasamos 2. Debería devolver 4.
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
}
Si ejecutas:
Do ##class(%UnitTest.Manager).RunTest("TDD","/loadudl")
La prueba Fallará.
¡Rojo! El siguiente paso es convertirlo en verde.Para hacer que funcione, devolvamos 4 como resultado de la ejecución de nuestro método Square.
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit 4
}
}
y volvamos a ejecutar nuestra prueba.
Probablemente no estés muy contento con esta solución, porque solo funciona para un escenario. ¡Bien! Vamos al siguiente paso. Creemos otro escenario de prueba, ahora pasándole un número negativo.
Class TDD.Math Extends %UnitTest.TestCase
{
Method TestSquare()
{
Do $$$AssertEquals(##class(Production.Math).Square(2), 4)
}
Method TestSquareNegativeNumber()
{
Do $$$AssertEquals(##class(Production.Math).Square(-3), 9)
}
}
Cuando ejecutamos la prueba:
Fallará nuevamente. Refactoricemos entonces el código de producción:
Class Production.Math
{
ClassMethod Square(pValue As %Integer) As %Integer
{
Quit pValue * pValue
}
}
y volvamos a ejecutar nuestras pruebas:
Ahora todo va bien... Este es el ciclo de TDD, en pequeños pasos.
En este punto te estarás preguntando: ¿por qué debo seguir estos pasos? ¿Por qué tengo que ver fallar la prueba?He trabajado en equipos que escribían el código de producción y después escribían las pruebas. Pero prefiero seguir estos pequeños pasos por los siguientes motivos:Uncle Bob (Robert C. Martin) dijo que escribir pruebas después de escribir el código no es TDD, sino que en realidad se le podría llamar "una pérdida de tiempo".Otro detalle es que, al ver fallar la prueba y luego ver que se pasa, estoy probando la propia prueba.Tu prueba no deja de ser un código, y también puede tener errores. Y la forma de ponerlo a prueba es garantizar que falla y pasa cuando debe fallar y pasar. Esta es la forma de "probar la prueba" y asegurarte de que no tienes falsos positivos.
cosFaker
Para escribir buenas pruebas, puede que primero tengas que generar datos de prueba. Una forma de hacer esto es generar un volcado de datos y usarlo en tus pruebas.Otra forma es usar cosFaker para generar fácilmente datos falsos cuando los necesites. https://openexchange.intersystems.com/package/CosFakerSolo necesitas descargar el archivo xml y luego ir a Management Portal -> System Explorer -> Classes -> Import. Elige el archivo xml file a importar, o arrastra el archivo en Studio.También puedes importarlo usando Terminal.
Do $system.OBJ.Load("yourpath/cosFaker.vX.X.X.xml","ck")
Traducciones (Localization)
cosFaker añadirá archivos de localización en la carpeta de aplicación CSP predeterminada. Por ahora solo hay dos idiomas: inglés y portugués de Brasil. El idioma de los datos se elige de acuerdo con la configuración de tu Caché.La localización de cosFaker es un proceso en desarrollo. Si quieres ayudar, no dudes en crear un proveedor localizado para tu propio idioma y envíar una pull request.Con cosFaker puedes generar textos aleatorios como palabras, párrafos, números telefónicos, nombres, direcciones, direcciones de correo electrónico, precios, nombres de productos, fechas, códigos de color hexadecimales, etc.
Todos los métodos se agrupan por asuntos en clases, es decir, para generar una Latitud, debes llamar al método Latitude en la clase Address
Write ##class(cosFaker.Address).Latitude()
-37.6806
También puedes generar un Json para tus pruebas
Write ##class(cosFaker.JSON).GetDataJSONFromJSON("{ip:'ipv4',created_at:'date.backward 40',login:'username', text: 'words 3'}")
{
"created_at":"2019-03-08",
"ip":"95.226.124.187",
"login":"john46",
"text":"temporibus fugit deserunt"
}
Esta es la lista completa de las clases y métodos de cosFaker:
cosFaker.Address
StreetSuffix
StreetPrefix
PostCode
StreetName
Latitude
Output: -54.7274
Longitude
Output: -43.9504
Capital( Location = “” )
State( FullName = 0 )
City( State = “” )
Country( Abrev = 0 )
SecondaryAddress
BuildingNumber
cosFaker.App
FunctionName( Group= “”, Separator = “” )
AppAction( Group= “” )
AppType
cosFaker.Coffee
BlendName
Output: Cascara Cake
Variety
Output: Mundo Novo
Notes
Output: crisp, slick, nutella, potato defect!, red apple
Origin
Output: Rulindo, Rwanda
cosFaker.Color
Hexadecimal
Output: #A50BD7
RGB
Output: 189,180,195
Name
cosFaker.Commerce
ProductName
Product
PromotionCode
Color
Department
Price( Min = 0, Max = 1000, Dec = 2, Symbol = “” )
Output: 556.88
CNPJ( Pretty = 1 )
CNPJ is the Brazilian National Registry of Legal Entities
Output: 44.383.315/0001-30
cosFaker.Company
Name
Profession
Industry
cosFaker.Dates
Forward( Days = 365, Format = 3 )
Backward( Days = 365, Format = 3 )
cosFaker.DragonBall
Character
Output: Gogeta
cosFaker.File
Extension
Output: txt
MimeType
Output: application/font-woff
Filename( Dir = “”, Name = “”, Ext = “”, DirectorySeparator = “/” )
Output: repellat.architecto.aut/aliquid.gif
cosFaker.Finance
Amount( Min = 0, Max = 10000, Dec = 2, Separator= “,”, Symbol = “” )
Output: 3949,18
CreditCard( Type = “” )
Output: 3476-581511-6349
BitcoinAddress( Min = 24, Max = 34 )
Output: 1WoR6fYvsE8gNXkBkeXvNqGECPUZ
cosFaker.Game
MortalKombat
Output: Raiden
StreetFighter
Output: Akuma
Card( Abrev = 0 )
Output: 5 of Diamonds
cosFaker.Internet
UserName( FirstName = “”, LastName = “” )
Email( FirstName = “”, LastName = “”, Provider = “” )
Protocol
Output: http
DomainWord
DomainName
Url
Avatar( Size = “” )
Output: http://www.avatarpro.biz/avatar?s=150
Slug( Words = “”, Glue = “” )
IPV4
Output: 226.7.213.228
IPV6
Output: 0532:0b70:35f6:00fd:041f:5655:74c8:83fe
MAC
Output: 73:B0:82:D0:BC:70
cosFaker.JSON
GetDataOBJFromJSON( Json = “” // JSON template string to create data )
Parameter Example: "{dates:'5 date'}"
Output: {"dates":["2019-02-19","2019-12-21","2018-07-02","2017-05-25","2016-08-14"]}
cosFaker.Job
Title
Field
Skills
cosFaker.Lorem
Word
Words( Num = “” )
Sentence( WordCount = “”, Min = 3, Max = 10 )
Output: Sapiente et accusamus reiciendis iure qui est.
Sentences( SentenceCount = “”, Separator = “” )
Paragraph( SentenceCount = “” )
Paragraphs( ParagraphCount = “”, Separator = “” )
Lines( LineCount = “” )
Text( Times = 1 )
Hipster( ParagraphCount = “”, Separator = “” )
cosFaker.Name
FirstName( Gender = “” )
LastName
FullName( Gender = “” )
Suffix
cosFaker.Person
cpf( Pretty = 1 )
CPF is the Brazilian Social Security Number
Output: 469.655.208-09
cosFaker.Phone
PhoneNumber( Area = 1 )
Output: (36) 9560-9757
CellPhone( Area = 1 )
Output: (77) 94497-9538
AreaCode
Output: 17
cosFaker.Pokemon
Pokemon( EvolvesFrom = “” )
Output: Kingdra
cosFaker.StarWars
Characters
Output: Darth Vader
Droids
Output: C-3PO
Planets
Output: Takodana
Quotes
Output: Only at the end do you realize the power of the Dark Side.
Species
Output: Hutt
Vehicles
Output: ATT Battle Tank
WookieWords
Output: nng
WookieSentence( SentenceCount = “” )
Output: ruh ga ru hnn-rowr mumwa ru ru mumwa.
cosFaker.UFC
Category
Output: Middleweight
Fighter( Category = “”, Country = “”, WithISOCountry = 0 )
Output: Dmitry Poberezhets
Featherweight( Country = “” )
Output: Yair Rodriguez
Middleweight( Country = “” )
Output: Elias Theodorou
Welterweight( Country = “” )
Output: Charlie Ward
Lightweight( Country = “” )
Output: Tae Hyun Bang
Bantamweight( Country = “” )
Output: Alejandro Pérez
Flyweight( Country = “” )
Output: Ben Nguyen
Heavyweight( Country = “” )
Output: Francis Ngannou
LightHeavyweight( Country = “” )
Output: Paul Craig
Nickname( Fighter = “” )
Output: Abacus
Vamos a crear una clase para el usuario con un método que devuelva su nombre de usuario, es decir, FirstName concatenado con LastName.
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
}
Haciendo refactor:
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
}
Ahora añadimos una fecha de vencimiento de la cuenta y la validamos.
Class Production.User Extends %RegisteredObject
{
Property FirstName As %String;
Property LastName As %String;
Property AccountExpires As %Date;
Method Username() As %String
{
Quit ..FirstName _ "." _ ..LastName
}
Method Expired() As %Boolean
{
}
}
Class TDD.User Extends %UnitTest.TestCase
{
Method TestUsername()
{
Set firstName = ##class(cosFaker.Name).FirstName(),
lastName = ##class(cosFaker.Name).LastName(),
user = ##class(Production.User).%New(),
user.FirstName = firstName,
user.LastName = lastName
Do $$$AssertEquals(user.Username(), firstName _ "." _ lastName)
}
Method TestWhenIsNotExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Forward(40)
Do $$$AssertNotTrue(user.Expired())
}
}
Hacemos refactor:
Method Expired() As %Boolean
{
Quit ($system.SQL.DATEDIFF("dd", ..AccountExpires, +$Horolog) > 0)
}
Ahora vamos a probar cuándo expira la cuenta:
Method TestWhenIsExpired() As %Status
{
Set user = ##class(Production.User).%New(),
user.AccountExpires = ##class(cosFaker.Dates).Backward(40)
Do $$$AssertTrue(user.Expired())
}
Y todo está verde...
Sé que estos ejemplos son un poco tontos, pero de esta forma conseguirás que no solo el código sea sencillo, sino también el diseño de la clase.
Conclusión
En este artículo hemos aprendido un poco más sobre el Desarrollo Basado en Pruebas (Test Driven Development / TDD) y cómo usar la clase %UnitTest. También tratamos cosFaker y cómo generar datos falsos para pruebas.
Hay mucho más para aprender sobre pruebas y TDD, cómo usar estas prácticas con código legacy, pruebas de integración, pruebas de aceptación (ATDD), bdd, etc. Si quieres saber más sobre estos temas, te recomiendo muy especialmente estos dos libros:
Test Driven Development Teste e design no mundo real com Ruby - Mauricio Aniche. No sé si este libro tiene una versión en inglés o español. Hay ediciones para Java, C#, Ruby y PHP. Este libró me explotó la cabeza de lo bueno que es.
Y, por supuesto, el libro Test Driven Development by Example, de Kent Beck.
No dudes en escribir tus comentarios o preguntas.Es todo, amigos.
Artículo
Ricardo Paiva · 29 nov, 2021
En la primera parte de esta serie de artículos, hablamos sobre cómo leer un fragmento "grande" de datos del contenido sin procesar de un método HTTP POST y guardarlo en una base de datos como una propiedad de flujo de una clase. Ahora veremos cómo guardar esos datos y metadatos en formato JSON.
Desafortunadamente, Advanced REST Client no permite configurar objetos JSON con datos binarios como valor de una clave (o quizá simplemente no he descubierto cómo hacerlo), así que decidí escribir un cliente simple en ObjectScript para enviar datos al servidor.
Creé una nueva clase llamada RestTransfer.Client y le añadí los parámetros Server = "localhost" y Port = 52773 para describir mi servidor web. Y creé un método de clases GetLink en el que creo una nueva instancia de la clase %Net.HttpRequest y establezco sus propiedades con los parámetros mencionados anteriormente.
Para enviar la solicitud POST al servidor, creé un método de clase SendFileDirect, que lee el archivo que quiero enviar al servidor y escribe su contenido en la propiedad EntityBody de mi solicitud.
Después de esto, llamo al método Post("/RestTransfer/file") y, si se completa correctamente, la respuesta estará en la propiedad HttpResponse.
Para ver el resultado devuelto por el servidor, llamo al método OutputToDevice de la respuesta.
Este es el método de la clase:
Class RestTransfer.Client
{
Parameter Server = "localhost";
Parameter Port = 52773;
Parameter Https = 0;
ClassMethod GetLink() As %Net.HttpRequest
{
set request = ##class(%Net.HttpRequest).%New()
set request.Server = ..#Server
set request.Port = ..#Port
set request.ContentType = "application/octet-stream"
quit request
}
ClassMethod SendFileDirect(aFileName) As %Status
{
set sc = $$$OK
set request = ..GetLink()
set s = ##class(%Stream.FileBinary).%New()
set s.Filename = aFileName
While 's.AtEnd
{
do request.EntityBody.Write(s.Read(.len, .sc))
Quit:$System.Status.IsError(sc)
}
Quit:$System.Status.IsError(sc)
set sc = request.Post("/RestTransfer/file")
Quit:$System.Status.IsError(sc)
set response=request.HttpResponse
do response.OutputToDevice()
Quit sc
}
}
Podemos llamar a este método para transferir los mismos archivos que se utilizaron en el artículo anterior:
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads \2020_1012_114732_020.JPG")
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Archive.xml")
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\arc-setup.exe")
Observaremos las respuestas del servidor para cada archivo:
HTTP/1.1 200 OK
CACHE-CONTROL: no-cache
CONTENT-LENGTH: 15
CONTENT-TYPE: text/html; charset=utf-8
DATE: Thu, 05 Nov 2020 15:13:23 GMT
EXPIRES: Thu, 29 Oct 1998 17:04:19 GMT
PRAGMA: no-cache
SERVER: Apache
{"Status":"OK"}
Y tendremos los mismos datos en los globals que antes:
Esto significa que nuestro cliente funciona y ahora lo podemos expandir para enviar JSON al servidor.
Introducción a JSON
JSON es un formato ligero para almacenar y transportar datos en el que los datos están en pares nombre/valor separados por comas. En este caso, el JSON será algo parecido a esto:
{
"Name": "test.txt",
"File": "Hello, world!"
}
IRIS tiene varias clases que permiten trabajar con JSON. Yo utilizaré la siguientes:
%JSON.Adaptor ofrece una forma de serializar un objeto habilitado para JSON como un documento JSON y viceversa.
%Library.DynamicObject ofrece una forma de ensamblar de forma dinámica datos que se pueden transferir cómodamente entre un cliente web y un servidor.
Puedes encontrar más información sobre estas y otras clases que te permiten trabajar con JSON en la sección Class Reference de la documentación y en las guías para desarrollar aplicaciones.
La principal ventaja de estas clases es que ofrecen una forma sencilla de crear JSON a partir de un objeto IRIS o de crear un objeto a partir de JSON.
Voy a mostrar dos enfoques sobre cómo se puede trabajar con JSON para enviar/recibir archivos, en concreto uno que utiliza %Library.DynamicObject para crear un objeto en el lado del cliente y serializarlo en un documento JSON y %JSON.Adaptor para deserializarlo de nuevo a RestTransfer.FileDesc en el lado del servidor, y otro en el que desarrollo de forma manual una cadena para enviar y analizar en el lado del servidor. Para que sea más legible, crearé dos métodos diferentes en el servidor para cada enfoque.
Por ahora, nos centraremos en la primera.
Cómo crear JSON con IRIS
Para empezar, debemos heredar la clase RestTransfer.FileDesc de %JSON.Adaptor. Esto nos permite utilizar el método de instancia %JSONImport() para deserializar un documento JSON directamente en un objeto. Ahora, esta clase tendrá el siguiente aspecto:
Class RestTransfer.FileDesc Extends (%Persistent, %JSON.Adaptor)
{
Property File As %Stream.GlobalBinary;
Property Name As %String;
}
Añadamos una nueva ruta a UrlMap en el broker de la clase:
<Route Url="/jsons" Method="POST" Call="InsertJSONSmall"/>
Esto especifica que, cuando el servicio recibe un comando POST con la URL /RestTransfer/jsons, debería llamar al método de clase InsertJSONSmall.
Este método espera recibir un texto en formato JSON con dos pares clave-valor donde los contenidos del fichero serán inferiores a la longitud máxima de una cadena. Definiré las propiedades Name y File del objeto, lo guardaré en la base de datos y devolveré el estado y un mensaje con formato JSON que indicará si es correcto o es un error.
Este es el método de la clase.
ClassMethod InsertJSONSmall() As %Status
{
Set result={}
Set st=0
set f = ##class(RestTransfer.FileDesc).%New()
if (f = $$$NULLOREF) {
do result.%Set("Message", "Couldn't create an instance of class")
} else {
set st = f.%JSONImport(%request.Content)
If $$$ISOK(st) {
set st = f.%Save()
If $$$ISOK(st) {
do result.%Set("Status","OK")
} else {
do result.%Set("Message",$system.Status.GetOneErrorText(st))
}
} else {
do result.%Set("Message",$system.Status.GetOneErrorText(st))
}
}
write result.%ToJSON()
Quit st
}
¿Qué hace este método? Recibe la propiedad Content del objeto %request y, usando el método %JSONImport heredado de la clase %JSON.Adaptor, la convierte en un objeto RestTransfer.FileDesc.
Si se producen problemas durante la conversión, configuramos un JSON con la descripción del error:
{"Message",$system.Status.GetOneErrorText(st)}
De lo contrario, guardamos el objeto. Si se guarda sin problemas, creamos un mensaje JSON {"Status","OK"}. En caso contrario, un mensaje JSON con la descripción del error.
Finalmente, escribimos el código JSON en una respuesta y devolvemos el estado st.
En el lado del cliente añadí un nuevo método de clase SendFile para enviar archivos al servidor. El código es muy similar al de SendFileDirect pero, en vez de escribir los contenidos del archivo directamente en la propiedad EntityBody, creo una nueva instancia de la clase %Library.DynamicObject, establezco su propiedad Name igual al nombre del archivo, y codifico y copio los contenidos del archivo en la propiedad File.
Para codificar los contenidos del archivo utilizo el método Base64EncodeStream() propuesto por Vitaliy Serdtsev.
Después, usando el método %ToJSON de la clase %Library.DynamicObject, serializo mi objeto en un documento JSON, lo escribo en el contenido de la solicitud y llamo al método Post("/RestTransfer/jsons").
Este es el método de la clase:
ClassMethod SendFile(aFileName) As %Status
{
Set sc = $$$OK
Set request = ..GetLink()
set s = ##class(%Stream.FileBinary).%New()
set s.Filename = aFileName
set p = {}
set p.Name = s.Filename
set sc = ..Base64EncodeStream(s, .t)
Quit:$System.Status.IsError(sc)
While 't.AtEnd {
set p.File = p.File_t.Read(.len, .sc)
Quit:$System.Status.IsError(sc)
}
do p.%ToJSON(request.EntityBody)
Quit:$System.Status.IsError(sc)
set sc = request.Post("/RestTransfer/jsons")
Quit:$System.Status.IsError(sc)
Set response=request.HttpResponse
do response.OutputToDevice()
Quit sc
}
Llamamos a este método y al método SendFileDirect para transferir varios archivos pequeños:
do ##class(RestTransfer.Client).SendFile("D:\Downloads\Outline Template.pdf")
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Outline Template.pdf")
do ##class(RestTransfer.Client).SendFile("D:\Downloads\pic3.png")
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\pic3.png")
do ##class(RestTransfer.Client).SendFile("D:\Downloads\Archive (1).xml")
do ##class(RestTransfer.Client).SendFileDirect("D:\Downloads\Archive (1).xml")
Obtendremos los siguientes resultados:
Como puedes observar, las longitudes son las mismas y, si guardamos esos archivos en un disco duro, veremos que no han cambiado.
Configuración manual de JSON
Ahora nos centraremos en el segundo enfoque, que consiste en configurar el JSON manualmente. Para hacerlo, añadiremos una nueva ruta a UrlMap en el broker de la clase:
<Route Url="/json" Method="POST" Call="InsertJSON"/>
Esto especifica que cuando el servicio recibe un comando POST con la URL /RestTransfer/json, debería llamar al método de clase InsertJSON. En este método, espero recibir el mismo JSON, pero no impongo ningún límite a la longitud del archivo. Este es el método de la clase:
ClassMethod InsertJSON() As %Status
{
Set result={}
Set st=0
set t = ##class(%Stream.TmpBinary).%New()
While '%request.Content.AtEnd
{
set len = 32000
set temp = %request.Content.Read(.len, .sc)
set:len<32000 temp = $extract(temp,1,*-2)
set st = t.Write($ZCONVERT(temp, "I", "RAW"))
}
do t.Rewind()
set f = ##class(RestTransfer.FileDesc).%New()
if (f = $$$NULLOREF)
{
do result.%Set("Message", "Couldn't create an instance of class")
} else {
set str = t.Read()
set pos = $LOCATE(str,""",")
set f.Name = $extract(str, 10, pos-1)
do f.File.Write($extract(str, pos+11, *))
While 't.AtEnd {
do f.File.Write(t.Read(.len, .sc))
}
If $$$ISOK(st)
{
set st = f.%Save()
If $$$ISOK(st)
{
do result.%Set("Status","OK")
} else {
do result.%Set("Message",$system.Status.GetOneErrorText(st))
}
} else {
do result.%Set("Message",$system.Status.GetOneErrorText(st))
}
}
write result.%ToJSON()
Quit st
}
¿Qué hace este método? Primero, creo una nueva instancia de un flujo temporal, %Stream.TmpBinary, y copio en él el contenido de la solicitud.
Como voy a trabajar con ella como una cadena, necesito deshacerme de las comillas finales (") y de las llaves (}). Para hacerlo, en el último fragmento del flujo dejo fuera los dos últimos caracteres: $extract(temp,1,*-2).
A la vez, convierto mi cadena usando la tabla de traducción "RAW": $ZCONVERT(temp, "I", "RAW").
Después creo una nueva instancia de mi clase RestTransfer.FileDesc y hago las mismas verificaciones que en los otros métodos. Ya conozco la estructura de mi cadena, así que extraigo el nombre del archivo y el archivo en sí y las defino en las propiedades correspondientes.
En el lado del cliente, modifiqué mi método de clase SendFile y, antes de crear el JSON, verifico la longitud del archivo. Si es inferior a los 2 000 000 de bytes (límite aparente del método %JSONImport), llamo a Post("/RestTransfer/jsons"). De lo contrario, llamo a Post("/RestTransfer/json").
Ahora el método se ve así:
ClassMethod SendFile(aFileName) As %Status
{
Set sc = $$$OK
Set request = ..GetLink()
set s = ##class(%Stream.FileBinary).%New()
set s.Filename = aFileName
if s.Size > 2000000 //3641144 max length of the string in IRIS
{
do request.EntityBody.Write("{""Name"":"""_s.Filename_""", ""File"":""")
While 's.AtEnd
{
set temp = s.Read(.len, .sc)
do request.EntityBody.Write($ZCONVERT(temp, "O", "RAW"))
Quit:$System.Status.IsError(sc)
}
do request.EntityBody.Write("""}")
set sc = request.Post("/RestTransfer/json")
} else {
set p = {}
set p.Name = s.Filename
set sc = ..Base64EncodeStream(s, .t)
Quit:$System.Status.IsError(sc)
While 't.AtEnd {
set p.File = p.File_t.Read(.len, .sc)
Quit:$System.Status.IsError(sc)
}
do p.%ToJSON(request.EntityBody)
Quit:$System.Status.IsError(sc)
set sc = request.Post("/RestTransfer/jsons")
}
Quit:$System.Status.IsError(sc)
Set response=request.HttpResponse
do response.OutputToDevice()
Quit sc
}
Para llamar al segundo método, creo la cadena con JSON de forma manual. Y para transferir el archivo en sí, en el lado del cliente también convierto los contenidos mediante la tabla de traducción "RAW": ($ZCONVERT(temp, "O", "RAW").
Ahora llamamos a este método y al método SendFileDirect para transferir varios archivos de diferentes tamaños:
do ##class(RestTransfer.Client).SendFile(“D:\Downloads\pic3.png”)
do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\pic3.png”)
do ##class(RestTransfer.Client).SendFile(“D:\Downloads\Archive (1).xml”)
do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\Archive (1).xml”)
do ##class(RestTransfer.Client).SendFile(“D:\Downloads\Imagine Dragons-Thunder.mp3”)
do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\Imagine Dragons-Thunder.mp3”)
do ##class(RestTransfer.Client).SendFile(“D:\Downloads\ffmpeg-win-2.2.2.exe”)
do ##class(RestTransfer.Client).SendFileDirect(“D:\Downloads\ffmpeg-win-2.2.2.exe”)
Obtendremos los siguientes resultados:
Podremos ver que las longitudes son las mismas por lo que todo funciona como se supone.
Conclusión
De nuevo, puedes leer más sobre la creación de servicios REST en la documentación. El código de ejemplo para ambos enfoques se encuentra en GitHub y en InterSystems Open Exchange.
Sobre la transferencia de resultados del módulo TWAIN que se comenta en el primer artículo de esta serie, depende del tamaño de los datos que recibas. Si la máxima resolución será limitada y los archivos serán pequeños, puedes utilizar las clases del sistema %JSON.Adaptor y %Library.DynamicObject. Pero para estar seguros, es mejor enviar un archivo directamente al cuerpo de un comando POST o configurar el JSON manualmente. También puede ser una buena idea utilizar solicitudes que abarquen varios archivos y dividirlos en varias partes, enviarlos por separado y volver a consolidarlos en un solo archivo en el lado del servidor.
En cualquier caso, si tienes alguna pregunta o sugerencia, puedes dejarla en la sección de comentarios.