Limpieza en archivos de base de datos (II)
Javier Mora   21 Junio 2009
Lo prometido es deuda. En el artículo anterior expliqué para qué servía el mandato CLNPFM y su funcionamiento. También prometí que profundizaría en el uso de algunas de las APIs utilizadas y en algunas cosas más. Mi intención era que esta segunda parte no fuera muy extensa pero no lo he conseguido. Espero que, al menos, sea un poco entretenida e interesante. Bueno, ¡pues allá vamos!

Antes de empezar quiero aclarar algunas cosas. Esta utilidad se podría haber construido en RPG sin utilizar ni una sola API. Es muy probable que a alguno de los lectores le pueda parecer mi solución un poco rebuscada. ¡Sí, es posible! Pero funciona y me sirvió, en su día, para aprender algo nuevo. Para el que quiera un estudio más profundo le recomiendo que lea los manuales de IBM. Entremos en materia.

¿Cómo se tratan los registros de los ficheros?

En lugar de emplear los mecanismos habituales de RPG, ya sea READ, WRITE y DELETE, junto con la hoja F, he utilizado las funciones de C que pertenecen a la biblioteca de tiempo de ejecución. Como ya comenté en el artículo anterior, con un programa tradicional RPG me obligaba a definir los registros de E/S de un tamaño lo suficientemente grande para alojar el registro más grande de mi base de datos. Además, se generaban mensajes indeseables en las anotaciones de trabajo anunciando que la longitud del registro leído no correspondía con el definido. Con las funciones de C no quedo atado a esta restricción.

Las fuciones son:

_Ropen()
Para abrir el archivo de base de datos.
_Rclose()

Para cerrar el archivo de base de datos.
_Rreadn()

Para leer el siguiente registro del archivo. Es el READ de RPG.
_Rwrite()

Para grabar un registro. Es el WRITE de RPG.
_Rdelete()

Para suprimir un registro. Es el DELETE de RPG.

En el manual ILE C for AS/400 Run-Time Library Reference (SC41-5607-00) vienen todas muy bien explicaditas. Estas funciones permiten manipular un fichero de base de datos en bruto, es decir, el registro es simplente una secuencia de bytes en donde no hay diferencia entre datos numéricos o alfabéticos. Además, hay que indicar dónde poner estos datos (el buffer) y cuantos bytes hay que leer o grabar. Realmente, no se necesita más para construir la utilidad.

He definido cinco envoltorios sobre estas cinco funciones. Con ello consigo unos subprocedimientos fáciles de utilizar, que realizan mucho trabajo por mí y que facilitan la lectura del código. Explicando uno de estos subprocedimientos quedará más claro. Veamos las tripas de OpenUpdFile().

     P OpenUpdFile     B
  D                 PI                        Like( Handle_T )
  D   lib                               Const Like( TypeName )
  D   file                              Const Like( TypeName )
  D   mbr                               Const Like( TypeName )

  D libtmp          S                   Like( lib )
  D fichero         S             32A   Varying
  D modo            S             32A   Varying
  D hdl             S                   Like( Handle_T )
  D                                     Inz( *NULL )

  C                   If        lib = *BLANKS                                >1
  C                   Eval      libtmp = '*LIBL'                             -1
  C                   Else                                                   *1
  C                   Eval      libtmp = lib                                 -1
  C                   Endif                                                  <1

  C                   Eval      fichero = %Trim( libtmp ) + '/'
  C                                     + %Trim( file ) + '('
  C                                     + %Trim( mbr ) + ')'

  C*  Con rtncode=Y se asegura que al llegar al final del archivo
  C*  no se producirá una excepción que no es capturable desde este
  C*  programa (al menos yo no he podido).
  C*  Este comportamiento sólo se pruduce cuando el CL llamador está
  C*  en ILE y, además, supervisa los mensajes de error.
  C*  De esta forma intentamos manejar la excepción en cualquier
  C*  ambiente, ILE o OPM.
  C                   Eval      modo    = 'rr+, rtncode=Y'

  C                   Eval      hdl = Ropen( fichero: modo )

  C                   If        hdl = *NULL                                  >1
  C                   Callp     ReportError()                                -1
  C                   Endif                                                  <1

  C                   Return    hdl

  P OpenUpdFile     E

Esta es una envoltura de la función _Ropen() cuyo propósito es abrir un archivo de base de datos para actualización. Tiene tres parámetros de entrada y un valor de retorno. Los primeros son el nombre de la biblioteca, archivo y miembro que queramos abrir. El retorno es un descriptor que me permitirá identificar a este archivo dentro del programa. En este caso es un puntero que apunta a una estructura de datos de control.

Como se puede apreciar, primero comprueba el nombre de la biblioteca y se ajusta a *LIBL si está en blanco. A continuación se monta el nombre del archivo con el siguiente formato: biblioteca/archivo(miembro). Con el modo indico el tipo de apertura y un atributo especial que luego explicaré. Después, abro el archivo y superviso si todo ha ido bien, informando si se produjo algún error. Finalmente, devuelvo el descriptor, que a su vez, corresponde a un valor proporcionado por_Ropen(). Ahora sólo queda utilizar este subprocedimiento:

     C                   Eval      hinp = OpenUpdFile( oriBibl
  C                                               : oriArch
  C                                               : '*FIRST' )

Como puede observar, queda muy limpio el código. Si se produjera un error, el gestor de errores se activa y no sigue con la ejecución de la siguiente sentencia.

Esta misma técnica se ha ido aplicando a cada una de las funciones de C. Todos los prototipos están en el miembro fuente RECIO_H que ya proporcioné en elartículo sobre APIs.

Al final, el trabajo duro lo realizan estas seis sentencias:

     C                   Dow       ReadFile( hinp: wpInput )                    >1
  C                   If        haySalida                                    >--2
  C                   Callp     WriteRec( hout: wpInput )                    ---2
  C                   Endif                                                  <--2
  C                   Callp     DeleteRec( hinp )                            -1
  C                   Enddo                                                  <1

Leer, grabar y borrar. Simple, ¿no le parece?

¿Cómo se seleccionan los registros?

Durante mucho tiempo, este fue un gran problema. Si quería una utilidad genérica, en donde hasta el último momento se desconoce que registros eliminar y en función de qué criterio, la selección era un factor clave. O construía algún sistema desde cero o tenía que aprovecharme de alguna herramienta suministrada con el sistema o de terceros. OPNQRYF vino a mi rescate: permite realizar una selección de registros de forma dinámica en tiempo de ejecución.

Es posible que haya otras soluciones mejores, pero es la utilizada y ¡funciona! Aunque no fue fácil hacerla funcionar. Por ejemplo, si quería mover los registros de un fichero a otro con:

CLNPFM FROMFILE(BIB1/A) TOFILE(BIB2/A) QRYSLT(*ALL)

Obtenía un resultado inesperado, los dos archivos estaban vacíos. ¿Por qué? Fíjese que utilizo nombres de archivo iguales pero en bibliotecas distintas. El OPNQRYFrequiere de una alteración temporal con SHARE(*YES) y _Ropen() no es seguro respecto a las alteraciones. Es decir, el OVRDBF provocaba que tanto el archivo de entrada como el de salida fueran el mismo. Bueno, antes no he dicho toda la verdad, _Ropen() si es seguro respecto a las alteraciones si se indica explícitamente:

     C                   Eval      modo    = 'ar, secure=Y'

  C                   Eval      hdl = Ropen( fichero: modo )

de esta forma consigo que al archivo de destino no le afecte la alteración temporal necesaria para el archivo de origen.

Manejo de excepciones

En este programa se pueden producir diferentes excepciones y errores. Algunos se intentan solucionar cuando suceden pero ¿qué hacemos con los impredecibles o inesperados? Simplemente, los reenvío al programa llamador. Para construir este manejador utilizo las APIs relacionadas con el manejo de mensajes:

QMHMOVPM (MovPgmMsg)
Mueve al programa llamador mensajes de diagnóstico, información, finalización, etc.
QMHRSNEM (RsnEscMsg)

Reenvía a otra entrada de la pila de llamadas el último mensaje de escape recibido.
QMHSNDPM (SndPgmMsg)

Envía un mensaje de programa.

Se utilizan dos métodos distintos: con MONITOR o verificando la ejecución de la función. El primero me permite codificar un manejador estándar de errores para casos de emergencia, es decir, los inesperados:

     C                   Monitor
  C* ...
  C                   On-error
  C* ...

  C*  Mover los mensajes de diagnóstico al programa llamador
  C                   Reset                   error
  C                   Callp(e)  MovPgmMsg( *BLANKS
  C                                      : '*DIAG'
  C                                      : 1
  C                                      : '*PGMBDY'
  C                                      : 1
  C                                      : error
  C                                      )
  C*  Reenviar al programa lladador los mensajes de escape
  C                   Reset                   error
  C                   Callp     RsnEscMsg( *BLANKS
  C                                      : error
  C                                      : RSNM0100
  C                                      : %Size( RSNM0100 )
  C                                      : 'RSNM0100'
  C                                      : *NULL: 0
  C                                      )
  C                   Endmon                                                

Cualquier mensaje de escape capturado es reenviado automáticamente al programa llamador, incluido los mensajes de diagnóstico.

El segundo método simplemente comprueba si la ejecución de la función o subprocedimiento inmediatemente anterior terminó correctamente. Por ejemplo:

     C                   Eval      rc = Rclose( handle )                        -1
  C                   If        rc = RECIO_EOF                               >--2
  C                   Callp     ReportError()                                ---2
  C                   Endif                                                  <--2

ReportError() es un procedimiento genérico de respuesta encargado de informar que se producido un error. Esta técnica se utiliza, en este programa, para las funciones de C.

Los curiosos ya se habrán dado cuenta que en el programa ILE CL también utilizo estas mismas APIs para controlar los mensajes inesperados.

Algunas vicisitudes

Con este programa me han sucedido cosas extrañísimas durante su desarrollo y no ha funcionado del todo bien hasta hace un año. Alguna ya la he comentado en párrafos anteriores. Enseguida detallo el resto.

Funciones de C y parámetros

Hay que tener mucho cuidado con las funciones de C. Utilizan un sistema de paso de parámetros distinto al que estamos acostumbrados. Todo lo pasan por valor, esto nos obliga a pasar punteros cuando necesita simular el paso por referencia.

En la utilización de estas funciones estaba el bug más importante. Inicialmente se me olvidó declarar el último parámetro de _Rreadn():

     D Rreadn          PR              *   ExtProc( '_Rreadn' )
  D   Rfile                         *   Value
  D   Buffer                        *   Value
  D   BufLen                      10U 0 Value
  D   Options                     10I 0 Value

El compilador de RPG no comprueba estos olvidos, ni en la ejecución se produce ningún error, pero ¿qué valor recibe un parámetro obligatorio de una función de C que no se ha declarado en el prototipo de RPG? No lo sé. Lo que si sé es que durante tres o cuatro años estuvo funcionando así y sin problemas. Este error se descubrió durante un cambio de máquina (que no de sistema operativo). Misteriosamente, la utilidad dejó de funcionar, además de forma aleatoria.

Final de fichero

Curiosamente, la utilidad generaba un mensaje de escape al llegar al final de archivo. No fui capaz de averiguar que identificador de mensaje era ni de capturarlo en el ILE CL. Hice un par de pruebas: si el CL lo compilaba como OPM el problema se solucinaba. Si en ILE CL no incluía el MONMSG inicial del programa tampoco.

Pero como soy cabezón y quería que el CL fuera ILE y también capturar las excepciones inesperadas mediante una rutina estándar de errores, tuve que investigar un poco. Al abrir el archivo de origen (vease OpnUpdFile()) he necesitado indicar un valor especial en el parámetro modo:

     C                   Eval      modo    = 'rr+, rtncode=Y'

  C                   Eval      hdl = Ropen( fichero: modo )

Con rtncode=Y estoy indicando que quiero evitar el proceso normal de manejo y generación de excepciones cuando se llegue al final de archivo o no se encuentre un registro. Además mejorará el rendimiento en estos casos. No sé cual es el procedimiento normal pero al utilizar esta palabra clave entiendo que es el programador quien debe detectar esta situación de fin de archivo. Con este pequeño cambio se solucionó este estraó suceso.

Algunas dudas

Durante la redacción de este artículo me han surgido algunas dudas:
1.        ¿Qué ocurriría si el programa se cortara en mitad de la ejecución?
2.        En ese hipotético caso, ¿los registros borrados quedan definitivamente marcados como tal?
3.        ¿Qué habrá ocurrido con los registros movidos? ¿Se habrán grabado todos definitivamente?

Son cuestiones que la utilidad debiera resolver satisfactoriamente. Lo dejo pendiente para una futura versión

Aquí acabo. Espero no haber aburrido al personal. ¡Hasta la próxima!

SIDRA400
Entrada