Archivo del Autor: PabloE

Interactive Report + Column confirm button

Introducción

Vamos a convertir una columna de un Interactive Report (IR) de APEX en un botón de cambio de estado con confirmación previa.

Partimos de un IR convencional, sobre una copia de la tabla EMP a la que hemos añadido una columna ACTIVE con el valor por defecto ‘YES’:

apex_ir_emp_step01

Las imágenes que nos servirán para el botón se han cargado en el apartado de recursos compartidos de la aplicación (Shared Components) asociadas a la aplicación:

apex_ir_emp_step02

Nota: Es importante acordarse de descargar los scripts de instalación y desintalación de cada una de las imágenes para incluirlos en el apartado «Supporting Objects». De esta forma, las imágenes se incorporarán de manera automática a los procesos de exportación / importación de la aplicación.

Paso 1: Preparamos el enlace

En nuestro ejemplo vamos a reemplazar la columna EMP.ACTIVE del informe por un botón que nos permita cambiar el valor entre ‘YES’ y ‘NO’ pulsando sobre él. Además, incluiremos una petición de confirmación previa para evitar que se ejecute por error.

El primer paso es crear una función PL/SQL que nos facilite la generación del código HTML con el que vamos reemplazar la información actual de la columna. Las siguientes líneas nos servirán:

En este ejemplo se construye un enlace HTML con las siguientes características:

  • Ejecuta una función JavaScript para solicitar la confirmación del usuario.
  • Incluye una etiqueta de imagen con la referencia al fichero cargado en Shared Componens y que está asociado al estado actual del registro:
    • La imagen «switch_on_2.gif» para los valores ‘YES’.
    • La imagen «switch_off_2.gif» para el resto de valores.

Para indicar la ubicación de la imagen se utiliza el parámetro «p_img_prefix». Por norma general, las imágenes utilizadas en una aplicación APEX se suelen alojar en uno de estos tres lugares:

  • #WORKSPACE_IMAGES#: Cargadas en Shared Components y asociadas al espacio de trabajo.
  • #APP_IMAGES#: Cargadas en Shared Components y asociadas a la aplicación.
  • #IMAGE_PREFIX#: Ruta definida para las imágenes estáticas. Por defecto es «/i/».

Una vez compilada la función, debemos modificar la consulta del informe para invocarla en lugar de la columna ACTIVE:

apex_ir_emp_step03

Al guardar estos cambios y volver a cargar el informe de empleados debería aparecernos algo similar a lo siguiente:

apex_ir_emp_step04

¡Vamos bien! Tal y como estaba configurada la columna ACTIVE en el informe, APEX no puede interpretar el código HTML que está generando la función GET_LINK_CHNG_ACTIVE. Pero esto tiene fácil solución. Solo tenemos que editar los atributos del informe y sustituir el valor «Display as a text» por «Standard Report Column» en la definición de esta columna:

apex_ir_emp_step05

Una vez aceptados los cambios, si volvemos a cargar el informe deberíamos encontrarnos con una imagen como la siguiente:

apex_ir_emp_step05

Paso 2: Control de la acción

De momento, si pulsamos sobre la imagen no ocurrirá nada. El código HTML que hemos generado invoca una función JavaScript que aun no hemos declarado. Para que nuestra aplicación tenga constancia de ella deberemos acceder a las propiedades de la página y en el espacio «Function and Global Variable Declaration» incluir el código necesario:

function chng_confirm(p_empno, p_active) {
  apex.confirm(
    'Change EmpNO '+p_empno+' Active status?',
    {request:'CHNG_STAT',
     set:{'P25_EMPNO_CHNG_STAT':p_empno,
          'P25_ACTIVE_CHNG_STAT':p_active}
    }
  );
}

Esta función se encarga de solicitar la confirmación al usuario y, en caso de obtenerla, ejecutar el submit de la página asignando los siguientes valoresde:

  • Item P25_EMPNO_CHNG_STAT = p_empno
  • Item P25_ACTIVE_CHNG_STAT = p_active
  • REQUEST = ‘CHNG_STAT’

Estos Items no existen aun en nuestra página. Así que vamos a crearlos con las siguientes características:

apex_ir_emp_step06

Nota: Es importante tener en cuenta que la propiedad «Value Protected» de cada un de estos Items debe ser ‘NO’ para permitir al código JavaScript modificar su valor después de que el usuario confirme la acción.

Paso 3: Modificación de los datos

Por último, deberemos crear el proceso que se encargue de ejecutar la acción y modifique los datos pertinentes. Es recomendable que este proceso se encuentre compilado en base de datos y, de esta forma, separar las operaciones sobre el modelo de datos del apartado visual. El código podría ser como el que sigue:

create procedure SET_ACTIVE (
    p_empno  in number,
    p_active in varchar2
    )
as
begin

  if p_empno is not null and p_active is not null
  then

    update emp_test set
      active = decode(p_active,
                      'YES', 'NO',
                      'NO', 'YES',
                      active)
    where empno = p_empno
      and active = p_active;

  end if;

end SET_ACTIVE;

Por último, necesitamos indicar al motor de APEX cómo y en qué momento debe ejecutar este bloque de código. De esto se va a encargar un proceso de página, o Page Processing, con las siguientes características:

  • Type: «PL/SQL anonymous block»
  • Process point: «On Submit and Before Computation»
  • Conditions: «Request = Expression 1» y con la expresión igual a la cadena’CHNG_STAT’ que hemos indicado anteriormente en nuestra función JavaScript.

El código PL/SQL a ejecutar en este caso es muy breve:

begin
  SET_ACTIVE(:P25_EMPNO_CHNG_STAT, :P25_ACTIVE_CHNG_STAT);
end;

Si volvemos a cargar el informe, ya podremos activar y desactivar registros de empleados tras aceptar la confirmación que se nos va a solicitar.

apex_ir_emp_confirm

SQL vs R

Introducción

Vamos a realizar una breve comparativa entre SQL y R. Para ello nos centraremos en los aspectos básicos del acceso a datos, mostrando ejemplos de las mismas operaciones en ambos lenguajes.

En junio de 2015 Oracle publicó el documento SQL – the natural language for analysis en el que se describen los fundamentos del lenguaje SQL y las ventajas que ofrece en el campo del acceso y análisis de datos:

http://www.oracle.com/technetwork/database/bi-datawarehousing/wp-sqlnaturallanguageanalysis-2431343.pdf

Éste documento será nuestra guía en la pequeña comparativa que sigue a continucaión.

Preparación del entorno

El juego de datos que se utiliza en los ejemplos del documento son las conocidas tablas EMP y DEPT, que tradicionalmente han acompañado a Oracle. Desde R se puede acceder a una gran variedad de fuentes de datos, entre las que se incluyen bases de datos, ficheros, recursos web, etc.

En este caso, para acceder a los datos de las tablas Oracle desde R, primero los volcaremos a un fichero con estructura CSV y los cargaremos utilizando la librería de R «DPLYR».

Las siguientes sentencias SQL nos pueden servir para generar la estructura CVS:

select '"EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO"' from dual
union all
select EMPNO||',"'||ENAME||'","'||JOB||'",'||MGR||','||HIREDATE||','||SAL||','||COMM||','||DEPTNO
from emp;
select '"DEPTNO","DNAME","LOC"' from dual
union all
select DEPTNO||',"'||DNAME||'","'||LOC||'"'
from dept;

Después de guardar los datos de EMP y DEPT en disco con formato CSV será necesario copiar ambos ficheros en una ruta a la que se tenga acceso desde el entorno de ejecución de R (por ejemplo, un subdirectorio «./data» dentro del espacionde trabajo) y ejecutar los siguientes comandos:

library(dplyr)
emp <- tbl_dt(read.csv("./data/emp.csv",stringsAsFactors = FALSE))
dept <- tbl_dt(read.csv("./data/dept.csv",stringsAsFactors = FALSE))

Principios de SQL

El lenguaje SQL se compone de cuatro operadores básicos:

  • Projection
  • Filter
  • Join
  • Aggregate

Projection

El operador projection permite indicar las columnas que queremos recuperar de una tabla. En su forma básica, se indican las columnas existentes por su nombre o posición en la tabla.

SELECT ename, job, hiredate
FROM emp;
ENAME	JOB		HIREDATE
-----	---------	--------------------
KING	PRESIDENT	17-NOV-1981 00:00:00
BLAKE	MANAGER		01-MAY-1981 00:00:00
CLARK	MANAGER		09-JUN-1981 00:00:00
JONES	MANAGER		02-APR-1981 00:00:00
-- Next lines were deleted
emp %>%
select(ENAME, JOB, HIREDATE)
Source: local data table [14 x 3]

    ENAME       JOB             HIREDATE
    (chr)     (chr)                (chr)
1    KING PRESIDENT 17-NOV-1981 00:00:00
2   BLAKE   MANAGER 01-MAY-1981 00:00:00
3   CLARK   MANAGER 09-JUN-1981 00:00:00
4   JONES   MANAGER 02-APR-1981 00:00:00
# Next lines were deleted

La proyección, en su forma extendida, permite generar columnas dinámicas aplicando operaciones que son calculadas en tiempo de ejecución.

SELECT ename, sal, comm, comm/sal*100 as COMM_RATE
FROM emp;
ENAME	SAL	COMM	COMM_RATE
-----	----	----	---------
KING	5000	- 	-
BLAKE	2850	- 	-
CLARK	2450	- 	-
JONES	2975	- 	-
SCOTT	3000	- 	-
FORD	3000	- 	-
SMITH	800	- 	-
ALLEN	1600	300	18.75
WARD	1250	500	40
MARTIN	1250	1400	112
TURNER	1500	0	0
ADAMS	1100	- 	-
JAMES	950	- 	-
MILLER	1300	- 	- 
emp %>%
mutate(COMM_RATE = COMM/SAL*100) %>%
select(ENAME,SAL,COMM,COMM_RATE)
Source: local data table [14 x 4]

    ENAME   SAL  COMM COMM_RATE
    (chr) (int) (int)     (dbl)
1    KING  5000    NA        NA
2   BLAKE  2850    NA        NA
3   CLARK  2450    NA        NA
4   JONES  2975    NA        NA
5   SCOTT  3000    NA        NA
6    FORD  3000    NA        NA
7   SMITH   800    NA        NA
8   ALLEN  1600   300     18.75
9    WARD  1250   500     40.00
10 MARTIN  1250  1400    112.00
11 TURNER  1500     0      0.00
12  ADAMS  1100    NA        NA
13  JAMES   950    NA        NA
14 MILLER  1300    NA        NA

Filter

El operador filter permite indicar una serie de reglas que deben cumplir los datos almacenados para ser recuperados en la consulta. En su forma más sencilla comparan los valores de la tabla con una expresión.

SELECT ename, job, hiredate
FROM emp
WHERE job = 'CLERK';
ENAME	JOB	HIREDATE
------	-----	--------------------
SMITH	CLERK	17-DEC-1980 00:00:00
ADAMS	CLERK	12-JAN-1983 00:00:00
JAMES	CLERK	03-DEC-1981 00:00:00
MILLER	CLERK	23-JAN-1982 00:00:00
emp %>%
filter(JOB == "CLERK") %>%
select(ENAME, JOB, HIREDATE)
Source: local data table [4 x 3]

   ENAME   JOB             HIREDATE
   (chr) (chr)                (chr)
1  SMITH CLERK 17-DEC-1980 00:00:00
2  ADAMS CLERK 12-JAN-1983 00:00:00
3  JAMES CLERK 03-DEC-1981 00:00:00
4 MILLER CLERK 23-JAN-1982 00:00:00

Join

El operador join relaciona distintas tablas a través de la comparación de uno o varios de sus campos. Existen varios tipos de join dependiendo de la forma en la que va a realizar el cruzado de los datos: inner, full outer, outer left, outer right y cross.

SELECT d.deptno, d.dname, e.empno, e.ename
FROM dept d INNER JOIN emp e
 ON (e.deptno = d.deptno);
DEPTNO	DNAME		EMPNO	ENAME
------	----------	-----	------
10	ACCOUNTING	7782	CLARK
10	ACCOUNTING	7934	MILLER
10	ACCOUNTING	7839	KING
20	RESEARCH	7902	FORD
20	RESEARCH	7788	SCOTT
-- Next lines were deleted
emp %>%
inner_join(dept, by = c("DEPTNO"="DEPTNO")) %>%
select(DEPTNO,DNAME,EMPNO,ENAME)
Source: local data table [14 x 4]

   DEPTNO      DNAME EMPNO  ENAME
    (int)      (chr) (int)  (chr)
1      10 ACCOUNTING  7839   KING
2      10 ACCOUNTING  7782  CLARK
3      10 ACCOUNTING  7934 MILLER
4      20   RESEARCH  7566  JONES
5      20   RESEARCH  7788  SCOTT
# Next lines were deleted

Aggregate

El operador aggregate ejecuta operaciones sobre grupos, o sets, de datos.

SELECT SUM(sal) AS total_salary
FROM emp;
TOTAL_SALARY
------------
29025
emp %>%
summarise(total_salary = sum(SAL))
Source: local data table [1 x 1]

  total_salary
         (int)
1        29025

Este operador es especialmente importante en el análisis de datos porque permite acotar las operaciones a rangos o agrupaciones de los datos sobre los que estamos trabajando.

SELECT deptno,
       COUNT(empno) AS no_of_employees,
       SUM(sal) AS total_salary,
       AVG(sal) AS average_salary
FROM emp
GROUP BY deptno;
DEPTNO	NO_OF_EMPLOYEES	TOTAL_SALARY	AVERAGE_SALARY
------	---------------	------------	----------------------------------------
30	6		9400		1566.66666666666666666666666666666666667
20	5		10875		2175
10	3		8750		2916.66666666666666666666666666666666667
emp %>%
group_by(DEPTNO) %>%
summarise(no_of_employees = length(EMPNO),total_salary = sum(SAL), average_salary = mean(SAL))
Source: local data table [3 x 4]

  DEPTNO no_of_employees total_salary average_salary
   (int)           (int)        (int)          (dbl)
1     10               3         8750       2916.667
2     30               6         9400       1566.667
3     20               5        10875       2175.000

APEX y ORA-02020: too many database links in use

Introducción

El entorno APEX en el que se produjo el error está compuesto por varios espacios de trabajo (o workspaces) independientes entre sí. Cada espacio de trabajo cuenta con un enlace de base de datos para conectarlo a la instancia de la que extrae los datos de trabajo. De esta forma, tanto los desarrolladores APEX como los usuarios finales de las aplicaciones web solo tienen acceso a los datos a través de la instancia en la que se ejecuta APEX y se puede controlar de forma centralizada la seguridad en lo relativo al acceso a los datos.

Esta arquitectura nos permite interponer una capa entre el acceso web de los usuarios finales y las bases de datos de producción, además de descargar a estas bases de datos de la tarea de generar el contenido web y controlar la navegación de los usuarios. En definitiva, separamos por capas cada tarea:

  • Navegación: servidor web (cualquiera de los soportados por APEX).
  • Generación de contenidos HTML: instancia APEX.
  • Repositorios de datos: instanicas de producción (bases de datos remotas).

Arquitectura APEX con acceso remoto a los datos

Error ORA-02020: too many database links in use

A medida que los usuarios accedían a los distintos espacios de trabajo iba aumentando el número de conexiones que se establecían entre la instancia de base de datos que aloja APEX y las distintas instancias a las que se conecta para obtener los datos. Esto no debía constituir ningún problema ya que se utlizaba un servidor web con un pool de conexiones para gestionar las peticiones HTML y las conexiones a través de data base links para acceder a los datos. El entorno debería responder bien ante la concurrencia de conexiones propia de los entornos web. Pero algo no iba bien. Empecé a recibir correos de los usuarios quejándose de que les aparecía el siguiente error en sus navegadores:

ORA-20001: get_dbms_sql_cursor error ORA-02020: too many database links in use

Nos pusimos a investigar las causas el problema.

Primer intento: aumentar el número de conexiones permitido

Lo primero fue acudir a la documentación oficial sobre errores Oracle. Y en ella se puede leer lo siguiente:

ORA-02020: too many database links in use
Cause: The current session has exceeded the INIT.ORA open_links maximum.
Action: Increase the open_links limit, or free up some open links by committing or rolling back the transaction and canceling open cursors that reference remote databases.

Estaba claro que se abrían muchos enlaces de base de datos. Nuestra arquitectura lo propiciaba. Pero por el mismo motivo, metiante el pool de conexión, el sistema debería compartir conexiones y nos sorprendía que se acabara produciendo este error. La primera medida fue seguir la recomendación y aumentar el número de conexiones permitidas que define el parámetro open_links. Durante unos días dejamos de recibir correos de los usuarios con la incidencia, pero la tregua no duró mucho. Con el paso de los días y el aumento en el uso de las aplicaciones APEX instaladas, los correos con el error por exceso de conexiones abiertas volvieron a llegar.

Estaba claro que aumentar el límite de conexiones no era la solución. Siempre se puede poner un número máximo de conexiones desorbitado, y no preocuparse más. Pero no es una solución muy adecuada si queremos crear un entorno robusto y seguro. La segunda alternativa que propone la documentación oficial del error tampoco nos daba muchas pistas. Nuestras conexiones desde APEX a las instancias remotas eran casi en su totalidad conexiones para consulta de datos: vistas locales que lanzaban selects sobre el modelo de datos remoto. No abríamos transacciones remotas y, por lo tanto, descartamos la necesidad de incluir sentencias commit o rollback.

Segundo intento: apex_util.close_open_db_links

Así que seguimos investigando. Esta vez acudimos a My Oracle Support (MOS), antiguo Metalink, y allí encotramos la nota Doc ID 1950408.1 de la que resumo el apartado que utilizamos para aplicarlo a nuestro entorno:

Oracle REST Data Services (ORDS) / APEX Listener

1. Add the following entries to the defaults.xml associated with your ORDS / APEX Listener configuration.

<entry key="procedure.postProcess">apex_util.close_open_db_links</entry>
<entry key="procedure.preProcess">apex_util.close_open_db_links</entry>

2. Restart ORDS / the APEX Listener where it is deployed, so the new entries will be recognized.

Parecía algo sencillo de aplicar, que atacaba exactamente el problema que teníamos y que lo hacía mediante un paquete PL/SQL oficial de APEX. Nos pusimos manos a la obra: incluimos las entradas en el fichero de configuración y realizamos una primera batería de pruebas. Esta vez parecía que todo iba a funcionar bien. Pero pronto observamos que en algunos apartados de las aplicaciones que íbamos probando aparecían nuevos errores aunque, esta vez, no se mostraban en pantalla. Tras un examen del log del servidor web encontramos las siguientes trazas:

oracle.dbtools.rt.web.WebErrorResponse internalError
ORA-02080: database link is in use
[...]
java.sql.SQLSyntaxErrorException: ORA-02080: database link is in use
[...]

El proceso apex_util estaba intentando cerrar conexiones cuando todavía se encontraban en uso. Como ya hemos comentado, las aplicaciones que ejecuta nuestra instancia APEX apenas generan inserciones, actualizaciones o borrado de datos en las instancias remotas. Por lo tanto, no deberían quedarse transacciones abiertas. ¡Qué estaba pasando!

Tercer y último intento: una modificación del MOS ID 1950408.1

Volvimos a buscar en MOS y Google para intentar acotar el origen del problema. Las vistas que utilizan nuestras aplicaciones APEX para acceder a las tablas de las instancias remotas, a pesar de ser una select, parece que si generan una transacción y esta impide el cierre de la conexión si no ejecutamos previamente un commit o un rollback. Es un efecto de utilizar data base links: la instancia remota asigna un bloque de undo a la conexión abierta y no se libera hasta recibir un comando de finalización de transacción.

La alternativa de invocar el cierre de los enlaces de base de datos desde un procedimiento dentro de cada aplicación APEX (un procedimiento de aplicación o un procedimiento que se ejecutara en las páginas que abrieran conexiones remotas) había sido descartada desde el principio porque nos obligaba a modificar el código de cada una de las aplicaciones y, además, identificar el nombre de cada database link en los distintos entornos. Resultaba poco práctico y difícil de mantener. Pero ante el problema de las transacciones abiertas y los nuevos errores que generaba la segunda solución que aplicamos, decidimos montar un escenario de test. Y funcionó a la perfección.

De todas formas, el cierre de los enlaces desde el interior de cada aplicación APEX seguía sin convencernos. Así que decidimos hacer una prueba más. Desactivamos el proceso de aplicación con el cierre de conexiones remotas que habíamos creado en el escenario de pruebas. Y volvimos a activar el parámetro en el fichero defaults.xml del servidor web. Pero en esta ocasión solo lo activamos para el preprocesado:

<entry key="procedure.preProcess">apex_util.close_open_db_links</entry>

Con esta configuración por fin logramos solucionar el problema de los database links abiertos sin tener que modificar cada una de las aplicaciones APEX.

Extraer datos del XML Facturae

Introducción

Oracle permite acceder a datos de un XML desde una sentencia SELECT. Para ello utiliza el estandar XPath descrito por la W3C. El acceso a los datos del XML se puede realizar mediante el uso de la función EXTRACT, que ha sido deprecada a partir de Oracle 11g Release 2, y la función XMLQuery que viene a reemplazarla y a ampliar sus posibilidades.

Un ejemplo muy sencillo podría ser el siguiente:

<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
    <node01 Attr01="Atribute 01">Node 01 text</node01>
    <node02>Node 02 text</node02>
</root>

Con la función EXTRACT de Oracle:

select dbms_xmlgen.convert(
         xmltype('<?xml version="1.0" encoding="ISO-8859-1"?>
                  <root>
                    <node01 Attr01="Atribute 01">Node 01 text</node01>
                    <node02>Node 02 text</node02>
                  </root>').extract( '/root/node01/text()' ).getstringval(), 1
       ) as "Extract node01"
from dual;
Extract node01
--------------
Node 01 text

Con la función XMLQuery de Oracle:

select xmlcast(
         xmlquery( '/root/node01/text()'
                   PASSING xmltype('<?xml version="1.0" encoding="ISO-8859-1"?>
                                    <root>
                                      <node01 Attr01="Atribute 01">Node 01 text</node01>
                                      <node02>Node 02 text</node02>
                                    </root>')
                   RETURNING CONTENT
                 ) as VARCHAR2(4000)
       ) as "XMLQuery node01"
from dual;
XMLQuery node01
---------------
Node 01 text

El modelo de factura XML en España: Facturae y FACe

Facturae es un esquema XML de factura electrónica diseñado en primer lugar por la Asociación CCI (Centro de Cooperación Interbancaria) y posteriormente asumido por la Agencia Tributaria española, con el objetivo de fijar un estándar nacional para el intercambio de facturas electrónicas. A día de hoy nos encontramos en la versión 3 del estándar, del que se han publicado varias revisiones. La más reciente es la revisión 3.2.1.

El pasado 15 de enero de 2015 se produjo un importante paso en la implantación de Facturae como modelo de referencia en España. Este día entró en vigor la obligatoriedad de enviar factura electrónica a la administración pública. Para ello, los distintos niveles de administración se han dotado de puntos generales de entrada de facturas como FACe, el punto de entrada de la administración central, y otros sistemas desarrollados por administraciones autonómicas o locales. Estos puntos de entrada admiten generalmente el modelo Facturae versión 3, con distintos grados de aceptación de las revisiones que se han liberado del mismo.

Otro factor importante que se ha introducido, junto a los puntos de entrada de facturas, es la designación del receptor de la factura mediante el sistema DIR3. Este sistema utiliza tres códigos para designar a todos los organismos que forman parte de la administración pública. Para informar los códigos DIR3 en la factura electrónica se han utilizado los nodos de centros administrativos del comprador disponibles en el modelo Facturae.

Oracle, SQL y el modelo XML Facturae

Como ya se ha comentado en la introducción, Oracle permite consultar datos de una estructura XML desde SQL mediante el estándar XPath. Los ejemplos que se muestran a continuación extraen los datos más relevantes del formato Facturae v3.x de cara a las integraciones con FACe y el uso de DIR3:

  • CIF del comprador
  • Nombre del comprador
  • Datos DIR3: Oficina Contable (OC), Órgano Gestor (OG) y Unidad Tramitadora (UT)
  • Número de la factura
  • Serie de la factura
  • Fecha de la factura
  • FileReference o Referencia de registro

Para estos ejemplos se ha utilizado el XML que propone la web oficial del estandar Facturae, incluyendo algunas modificaciones para poder mostrar el tratamiento de los centros administrativos que marca DIR3.

SELECT + EXTRACT

Ejemplo de select directa sobre una tabla con el XML en un campo Clob y la función EXTRACT:

select dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/TaxIdentification/TaxIdentificationNumber/text()').getstringval(), 1) as "CIF Comprador",
       decode( xmltype(data_clob).extract( '//Parties/BuyerParty/TaxIdentification/PersonTypeCode/text()').getstringval(),
               'J', dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/LegalEntity/CorporateName/text()').getstringval(), 1),
               dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/Individual/Name/text()').getstringval(), 1) || ' ' ||
               dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/Individual/FirstSurname/text()').getstringval(), 1)
             ) as "Nombre Comprador",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "01"]/CentreCode/text()').getstringval(), 1) as "Oficina Cont.",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "02"]/CentreCode/text()').getstringval(), 1) as "Órgano Ges.",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "03"]/CentreCode/text()').getstringval(), 1) as "Unidad Tr.",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Invoices/Invoice[1]/InvoiceHeader/InvoiceNumber/text()').getstringval(), 1) as "Número",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Invoices/Invoice[1]/InvoiceHeader/InvoiceSeriesCode/text()').getstringval(), 1) as "Serie",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Invoices/Invoice[1]/InvoiceIssueData/IssueDate/text()').getstringval(), 1) as "Fecha",
       dbms_xmlgen.convert(xmltype(data_clob).extract( '//Invoices/Invoice[1]/Items/InvoiceLine[1]/FileReference/text()').getstringval(), 1) as "Referencia"
from test_xml;
CIF Comprador   Nombre Comprador   Oficina Cont.   Órgano Ges.   Unidad Tr.   Número   Serie   Fecha        Referencia
0000000000B     Juana Mauriño      E00000012       E00000034     E00000033    18       -       2010-03-10   000298172

La función DBMS_XMLGEN.CONVERT permite codificar y decodificar las entidades XML (p. ej: la secuencia «& amp;» para el carácter &). Los valores que admite el parámetro flag son:

  -- DBMS_XMLGEN.CONVERT conversion types
  ENTITY_ENCODE CONSTANT conversionType := 0;
  ENTITY_DECODE CONSTANT conversionType := 1;

SELECT + XMLQUERY

Ejemplo de select directa sobre tabla con el XML en un campo Clob y la función XMLQuery:

select xmlcast(xmlquery( '//Parties/BuyerParty/TaxIdentification/TaxIdentificationNumber/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "CIF Comprador",
       xmlcast(xmlquery( '//Parties/BuyerParty/Individual/Name/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) ||' '||
       xmlcast(xmlquery( '//Parties/BuyerParty/Individual/FirstSurname/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Nombre Comprador",
       xmlcast(xmlquery( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "01"]/CentreCode/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Oficina Cont.",
       xmlcast(xmlquery( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "02"]/CentreCode/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Órgano Ges.",
       xmlcast(xmlquery( '//Parties/BuyerParty/AdministrativeCentres/AdministrativeCentre[RoleTypeCode = "03"]/CentreCode/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Unidad Tr.",
       xmlcast(xmlquery( '//Invoices/Invoice[1]/InvoiceHeader/InvoiceNumber/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Número",
       xmlcast(xmlquery( '//Invoices/Invoice[1]/InvoiceHeader/InvoiceSeriesCode/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Serie",
       xmlcast(xmlquery( '//Invoices/Invoice[1]/InvoiceIssueData/IssueDate/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Fecha",
       xmlcast(xmlquery( '//Invoices/Invoice[1]/Items/InvoiceLine[1]/FileReference/text()' PASSING xmltype(data_clob) RETURNING CONTENT) as VARCHAR2(4000)) as "Referencia"
from test_xml;
CIF Comprador   Nombre Comprador   Oficina Cont.   Órgano Ges.   Unidad Tr.   Número   Serie   Fecha        Referencia
0000000000B     Juana Mauriño      E00000012       E00000034     E00000033    18       -       2010-03-10   000298172

Si el XML se encuentra en una columna de tipo Blob, debemos indicar el ID del juego de caracteres al utilizar XMLType:

select dbms_xmlgen.convert(xmltype(data_blob, NLS_CHARSET_ID('AL32UTF8')).extract('//FileHeader/SchemaVersion/text()').getstringval(), 1) as "Versión"
from test_xml;

Recursos

Ejemplo de XML Facturae: http://descemp.pistigu.com/recursos/ejemplos/facturae-v3-2-ejemplo-dir3/

PL/SQL generate XML output

Para generar XML desde PL/SQL existen diversos métodos que van desde una select que utilice funciones XML_TYPE al empleo de los paquetes PL/SQL integrados en Oracle. En nuestro ejemplo vamos a utilizar DBMS_XMLDOM para montar un XML como este:

<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
  <node01 Attr01="Atribute 01" Attr02="Atribute 02">Node 01 text</node01>
  <node02>Node 02 text</node02>
</root>

Paquete de utilidades XML

El primer paso consiste en compilar el siguiente paquete en la base de datos. Esto nos va a permitir agilizar la creación de los nodos utilizando una serie de tablas para registrar cada nodo y el procedimiento de de escritura de estos nodos en el XML que estemos generando:

CREATE OR REPLACE PACKAGE create_xml IS
 
-- Declare XML types
   type ry_xmlattrib is record (
                                p_attrib_name     varchar2(512),
                                p_attrib_value    varchar2(32767)
                               );
   type ty_xmlattrib is table of ry_xmlattrib;
 
   type ry_xmlelement is record (
                                 p_parent         dbms_xmldom.DOMNode,
                                 p_element_name   varchar2(512),
                                 p_element        dbms_xmldom.DOMElement,
                                 p_attribs        ty_xmlattrib,
                                 p_node           dbms_xmldom.DOMNode,
                                 p_text_value     varchar2(32767),
                                 p_text           dbms_xmldom.DOMText,
                                 p_textnode       dbms_xmldom.DOMNode
                                );
   type ty_xmlelement is table of ry_xmlelement;

   type ty_varchar2 is table of varchar2(32767) index by varchar2(64);
 

-- Add node to ty_xmlelement
    procedure add_node(
                       p_tb_xmlelements  in out  ty_xmlelement,
                       p_tb_index_nodes  in out  ty_varchar2,
                       p_index_id        in      varchar2,
                       p_parent_index_id in      varchar2,
                       p_node_name       in      varchar2,
                       p_text_value      in      varchar2,
                       p_attribs         in      ty_xmlattrib
                      );
-- Write node to XML
   procedure write_node(
                        p_domdoc    in out   dbms_xmldom.DOMDocument,
                        p_reg_nodo  in out   ry_xmlelement
                       );
 
END create_xml;

CREATE OR REPLACE PACKAGE BODY create_xml AS

-- Add node to ty_xmlelement
procedure add_node(
                   p_tb_xmlelements  in out  ty_xmlelement,
                   p_tb_index_nodes  in out  ty_varchar2,
                   p_index_id        in      varchar2,
                   p_parent_index_id in      varchar2,
                   p_node_name       in      varchar2,
                   p_text_value      in      varchar2,
                   p_attribs         in      ty_xmlattrib
                  )
is
begin

   -- Add node
   p_tb_xmlelements.extend;
   p_tb_index_nodes( p_index_id ) := p_tb_xmlelements.last;
   p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_attribs := create_xml.ty_xmlattrib();
   p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_parent := p_tb_xmlelements( p_tb_index_nodes( p_parent_index_id ) ).p_node;
   p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_element_name := p_node_name;
   p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_text_value := p_text_value;
   
   -- Add attribs
   if p_attribs is not null then
      p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_attribs := create_xml.ty_xmlattrib();
      for i_att in 1..p_attribs.count() loop
          p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_attribs.extend();
          p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_attribs(i_att).p_attrib_name := p_attribs(i_att).p_attrib_name;
          p_tb_xmlelements( p_tb_index_nodes( p_index_id ) ).p_attribs(i_att).p_attrib_value := p_attribs(i_att).p_attrib_value;
      end loop;
   end if;
   
end add_node;

-- Write node to XML
procedure write_node(
                     p_domdoc    in out   dbms_xmldom.DOMDocument,
                     p_reg_nodo  in out   ry_xmlelement
                    )
is
begin
   
   -- Write node
   p_reg_nodo.p_element := dbms_xmldom.createElement(p_domdoc, p_reg_nodo.p_element_name );
     
   if p_reg_nodo.p_attribs is not null then
       if p_reg_nodo.p_attribs.count() > 0 then
          for i_att in 1..p_reg_nodo.p_attribs.count() loop
             dbms_xmldom.setAttribute(
                                      p_reg_nodo.p_element, 
                                      p_reg_nodo.p_attribs(i_att).p_attrib_name, 
                                      p_reg_nodo.p_attribs(i_att).p_attrib_value
                                     );
          end loop;
       end if;
   end if;
     
   p_reg_nodo.p_node := dbms_xmldom.appendChild(p_reg_nodo.p_parent,dbms_xmldom.makeNode(p_reg_nodo.p_element));
   p_reg_nodo.p_text := dbms_xmldom.createTextNode(p_domdoc, p_reg_nodo.p_text_value );
   p_reg_nodo.p_textnode := dbms_xmldom.appendChild(p_reg_nodo.p_node,dbms_xmldom.makeNode(p_reg_nodo.p_text));

end write_node;

END create_xml;

Ejemplo

El siguiente procedimiento utiliza el paquete create_xml para generar el XML de ejemplo con el que se ha iniciado esta entrada:

-- Generate XML (example)
CREATE OR REPLACE PROCEDURE xml_example
IS
 
  l_domdoc dbms_xmldom.DOMDocument;
  l_root_node dbms_xmldom.DOMNode;
  
  tb_xmlelements create_xml.ty_xmlelement := create_xml.ty_xmlelement();
  tb_index create_xml.ty_varchar2;
  tb_attribs create_xml.ty_xmlattrib := create_xml.ty_xmlattrib();
    
  v_buffer varchar2(32767);
  
BEGIN
  
   -- Create an empty XML document
   l_domdoc := dbms_xmldom.newDomDocument;
  
   -- Set version
   dbms_xmldom.setVersion(l_domdoc, '1.0');
   dbms_xmldom.setCharset(l_domdoc, 'ISO-8859-1');
  
   -- Create a root node
   l_root_node := dbms_xmldom.makeNode(l_domdoc);
   tb_xmlelements.extend;
   tb_index('root') := tb_xmlelements.last;

   tb_xmlelements(tb_index('root')).p_attribs      := create_xml.ty_xmlattrib();
   tb_xmlelements(tb_index('root')).p_parent       := l_root_node;
   tb_xmlelements(tb_index('root')).p_element_name := 'root';
   tb_xmlelements(tb_index('root')).p_text_value   := '';
       
   create_xml.write_node(l_domdoc, tb_xmlelements(tb_index('root')));
       
   -- Create a new node with text and attributes
   tb_attribs.extend();
   tb_attribs(1).p_attrib_name := 'Attr01';
   tb_attribs(1).p_attrib_value := 'Atribute 01';
  
   tb_attribs.extend();
   tb_attribs(2).p_attrib_name := 'Attr02';
   tb_attribs(2).p_attrib_value := 'Atribute 02';

   create_xml.add_node(tb_xmlelements, tb_index, 'node01', 'root', 'node01', 'Node 01 text', tb_attribs );
   create_xml.write_node(l_domdoc, tb_xmlelements(tb_index('node01')));
       
   -- Create a additional node with text
   tb_attribs.delete;
   create_xml.add_node(tb_xmlelements, tb_index, 'node02', 'root', 'node02', 'Node 02 text', tb_attribs );
   create_xml.write_node(l_domdoc, tb_xmlelements(tb_index('node02')));

   -- Result
   dbms_xmldom.writeToBuffer(l_domdoc, v_buffer);
   dbms_output.put_line(v_buffer);
   dbms_xmldom.freeDocument(l_domdoc);
 
END xml_example;

Encoding

Hay que tener especial cuidado con la declaración del encoding del XML. La forma de declararlo varía dependiendo del destino al que se vaya a enviar el XML: salida dbms_output, un XMLType o un Lob. En algunos conextos, en lugar de utilizar el procedimiento dbms_xmldom.setCharset(l_domdoc, ‘ISO-8859-1’) hay que manipular el procedimiento setVersion de la siguiente forma:

dbms_xmldom.setVersion(l_domdoc, '1.0" encoding="ISO-8859-1');

Oracle Day 2014 Madrid – Disrupción

En el encuentro Oracle Day 2014 de Madrid se han repasado las distintas tecnologías que en estos momentos están produciendo un cambio de paradigma en la forma que tienen las empresas de trabajar y en la relación que tradicionalmente establecían los distintos actores del mercado. Estas tecnologías han ido creciendo en los últimos años a gran velocidad y en estos momentos ya nos encontramos inmersos en sus efectos. Son las tecnologías que, en la jerga del sector, se denominan IoT (Internet of Things), BigData, Cloud y Movilidad.

La información siempre ha estado ahí, junto a nosotros, pero es ahora cuando empezamos a tener:

  • IoT y BigData: Las herramientas que nos permiten capturar datos de múltiples fuentes.
  • Cloud: Almacenarlos y procesarlos a un coste asumible.
  • Movilidad: Para convertirlos en información que podamos ofrecer de manera sencilla al usuario final allí donde sea necesaria.

Generar SHA de una cadena (PL/SQL + Java)

El paquete DBMS_CRYPTO nos permite generar hash de una cadena de texto utilizando distintos algoritmos como MD4, MD5 y SHA1. Pero si queremos generar un hash mediante SHA-256, SHA-384 o SHA-512 tenemos que apoyarnos en Java. Los siguientes bloques de código incorporan la posibilidad de generar estos hash desde SQL o PL/SQL.

Los algoritmos disponibles son los siguientes:

  • MD5
  • SHA-1
  • SHA-256
  • SHA-384
  • SHA-512

El primer paso es crear la clase Java que nos permita generar hash más complejos y compilarla en la base de datos:

create or replace and compile java source named encoder as
package com.test;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import sun.misc.BASE64Encoder;

public class Encoder {

    public static String encode(String message, String algorithm) {
        
        String encodedMessage;
        
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(algorithm);
            byte[] hash = messageDigest.digest(message.getBytes("UTF-8"));
            encodedMessage = (new BASE64Encoder()).encode(hash);
        
        } catch (Exception e) {
            encodedMessage = null;
        }
        
        return encodedMessage;
    }
} 

Una vez compilada la clase Java en nuestra base de datos, es necesario crear una función PL/SQL de enlace para poder invocar la clase desde una sentencia SQL o desde código PL/SQL:

create or replace function encoder(p_string    in varchar2,
                                   p_algorithm in varchar2) return varchar2 as
language java name 'com.test.Encoder.encode(java.lang.String, java.lang.String) return String';

Por último, un ejemplo de uso:

select Encoder('Hello', 'SHA-256') "hash_Hello",
       Encoder('HELLO', 'SHA-256') "hash_HELLO"
from dual;
hash_Hello
--------------------------------------------
GF+NsyJx/iX1Yab8k4suJkMG7DBO2lGAB9F2SCY4GWk=

hash_HELLO
--------------------------------------------
NzPNl3/46xi5hzV+Is7Zn0YJfzHssjnoeK5jdg6D5NU=

Ver también Generar SH1 de una cadena y Generar el HASH de una cadena

Buscar intervalos – Lead analytic function

¿Cómo encontrar intervalos libres en una secuencia temporal?

Ésta fue la pregunta que surgió en el transcurso de un proyecto en el que los usuarios tenían que dar de alta periodos temporales con una duración variable y que no tenían por qué seguir un orden. Estos periodos temporales se registraron en una tabla general de periodos, en la que se asociaba a cada usuario los periodos que iba dando de alta con las fechas de inicio y final de periodo. Cada vez que un usuario iba a registrar un nuevo periodo el sistema debía comprobar si este periodo existía ya o era necesario darlo de alta. Pero, ¿cómo buscar si el nuevo periodo de tiempo estaba ya comprendido entre los registrados anteriormente por el usuario?

La respuesta que finalmente encontramos fue utilizar las funciones analíticas MAX y LEAD. Gracias a estas dos funciones, una simple SELECT nos podía devolver los intervalos libres entre los periodos ya registrados en la tabla.

Para demostrar su funcionamiento, primero vamos a crear un pequeño escenario de pruebas. En la tabla «lead_test» vamos a insertar una serie de registros con fechas de inicio y final que nos permitan probar la solución:

-- Clean environment
drop table lead_test;

-- Create test environment
create table lead_test (id number, date_start date, date_end date);

insert into lead_test values(1, to_date('01/01/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/02/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(3, to_date('01/03/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/04/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(4, to_date('15/03/2014 22:00:00', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/05/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));
insert into lead_test values(5, to_date('01/05/2014 12:00:01', 'dd/mm/yyyy hh24:mi:ss'), to_date('01/06/2014 12:00:00', 'dd/mm/yyyy hh24:mi:ss'));

commit;

-- Alter default date format
alter session set nls_date_format = 'dd/mm/yyyy hh24:mi:ss';

Una vez ejecutadas las sentencias que preparan el entorno de pruebas vamos a comprobar cómo queda nuestra tabla «lead_test:

-- Show test data
select * from lead_test;

        ID DATE_START          DATE_END
---------- ------------------- -------------------
         1 01/01/2014 12:00:01 01/02/2014 12:00:00
         3 01/03/2014 12:00:01 01/04/2014 12:00:00
         4 15/03/2014 22:00:00 01/05/2014 12:00:00
         5 01/05/2014 12:00:01 01/06/2014 12:00:00

Estos datos nos proporcionan una casuística variada:

  • Hemos omitido el registro con ID = 2 para dejar el intervalo libre.
  • Los registros con ID 3 y 4 se solapan en el tiempo.
  • Los registros con ID 4 y 5 empiezan y acaban en el mismo momento, justo uno a continuación del otro pero sin solapamiento.

En una primera aproximación, se elaboró la siguiente consulta que nos permitía obtener los intervalos que no se encontraban cubiertos por ninguno de los periodos registrados en la tabla:

-- Search All Gaps
select *
from (
      select max(l.date_end) over (order by l.date_start) gap_start,
             lead(l.date_start) over (order by l.date_start) gap_end
      from lead_test l
     )
where gap_start < gap_end;

Como resultado obtendremos el intervalo que existe entre los registros con ID 1 y 3. Pero también el intervalo entre los registros con ID 4 y 5, puesto que no hay solapamiento:

GAP_START           GAP_END
------------------- -------------------
01/02/2014 12:00:00 01/03/2014 12:00:01
01/05/2014 12:00:00 01/05/2014 12:00:01

Con una pequeña modificación en la consulta podemos regular la duración mínima de los intervalos que queremos encontrar. Y, de este modo, podemos descartar aquellos intervalos que resulten demasiado pequeños para nuestros requerimientos:

-- Search Gaps > 1 sec.
select *
from (
      select max(l.date_end) over (order by l.date_start) gap_start,
             lead(l.date_start) over (order by l.date_start) gap_end
      from lead_test l
     )
where gap_start < gap_end
      and (gap_end - gap_start) > (1/24/60/60);

Al incluir como condición que el tiempo entre final de un intervalo y el inicio del intervalo que le sigue sea mayor a 1 segundo (la fórmula 1/24/60/60 representa un segundo en operaciones con fechas en las consultas SQL de Oracle) obtendremos solo el primer intervalo, entre los periodos con ID 1 y 3:

GAP_START           GAP_END
------------------- -------------------
01/02/2014 12:00:00 01/03/2014 12:00:01

Generar SH1 de una cadena

El paquete DBMS_CRYPTO nos permite, entre otras cosas, generar SHA1 de cualquier cadena de texto. Admite los siguientes parámetros:

  • src – [RAW|BLOB|CLOB]: datos a los que aplicar el hash
  • typ – PLS_INTEGER: algoritmo hash a aplicar

Los algoritmos disponibles son los siguientes:

  • HASH_MD4 := 1
  • HASH_MD5 := 2
  • HASH_SH1 := 3

Nota: Es necesario conceder permisos de ejecución sobre este paquete a los usuarios que vayan a utilizarlo.

select DBMS_CRYPTO.hash(utl_raw.cast_to_raw ('Hello'),
                        3) "hash_Hello",
       DBMS_CRYPTO.hash(utl_raw.cast_to_raw ('HELLO'),
                        3) "hash_HELLO"
from dual;
hash_Hello
----------------------------------------
F7FF9E8B7BB2E09B70935A5D785E0CC5D9D0ABF0

hash_HELLO
----------------------------------------
C65F99F8C5376ADADDDC46D5CBCF5762F9E55EB7

Ver también Generar el HASH de una cadena