28 de diciembre de 2017

Buffering de Datos y Multiusuarios (Parte III)

Autor: Doug Hennig
Traducido por: Germán Giraldo G.


Escribir a una Tabla Buffered
Como vimos antes, tableupdate(.T.) intenta escribir todos los registros de un buffer de tabla al disco. Al igual que la versión buffer de registro, devolverá .F. si no puede actualizar un registro a causa de que otro usuario lo ha cambiado (entre otras razones). La rutina que vimos antes para atrapar errores trabaja bien para buffering de fila, debido a que nos preocupamos por un único registro a la vez. Sin embargo, con buffering de tabla, tenemos que mirar cada registro uno por uno. Debido que podríamos tener en el buffer una mezcla de registros modificados y no modificados, cómo sabemos cuáles registros se actualizarán? Para hacerlo mas complicado, si tableupdate(.T.) falla, no sabemos cuáles registros fallaron; algunos registros pueden haberse guardado y allí podría haber mas de que un registro en conflicto. La nueva función getnextmodified() nos informará exactamente lo que necesitamos saber: el número de registro para el siguiente registro modificado. Si devuelve 0, no hay mas registros modificados en el buffer. Esta función acepta dos parámetros: el primero es el número de registro después del cual buscará los siguientes registros modificados, y el segundo es el alias o área de trabajo dónde buscar. Inicialmente, usted debería pasar 0 como el primer parámetro así getnextmodified() encuentra el primer registro modificado. Para hallar el siguiente, pase el número de registro del registro actual. Aquí está un ejemplo de la anterior rutina para el manejo de conflictos, modificada para manejar los cambios en una tabla buffered cuando tableupdate(.T.) falla:

* Hallar el primer registro modificado, entonces procesar cada uno.
lnChanged = getnextmodified(0)
do while lnChanged <> 0
    * Mover al registro y tratar de bloquearlo.
    go lnChanged
    if rlock()
    * Verificar cada campo para ver cuál tiene un conflicto.
       llConflict = .F.
       for lnI = 1 to fcount()
          lcField      = field(lnI)
          llOtherUser  = oldval(lcField)   <> curval(lcField)
          llThisUser   = evaluate(lcField) <> oldval(lcField)
          llSameChange = evaluate(lcField) == curval(lcField)
          do case
             * Otro usuario ha editado este campo pero este usuario,
             * no ha grabado los nuevos valores.
          case llOtherUser and not llThisUser
             replace (lcField) with curval(lcField)
             * Otro usuario no ha editado este campo, o ambos han hecho
             * el mismo cambio, así no necesitamos hacer algo.
           case not llOtherUser or llSameChange
             * Uh-oh, ambos usuarios han cambiado este campo, pero a diferentes valores.
           otherwise
              llConflict = .T.
           endcase
        next lnI
      * Tenemos un conflicto, manejarlo. Si no, a diferencia del caso de buffering
      * de fila, no hacemos algo ahora debido a que todos los registros se
      * escribirán mas tarde.
      if llonflict
        lnChoice = messagebox('Otro usuario también cambió este ' + ;
                   'registro ' + ltrim(str(lnChanged)) + '. Desea sobre-escribir ' +;
                   'sus cambios (Si), no sobre-escribir pero ver los cambios (No),'+;
                   ' o cancelar sus cambios (Cancelar)?', 3 + 16, ;
                   'Problema Guardando Registro!')
        do case
           * Sobre-escribir los cambios: actualmente no necesitamos hacer algo por que
           * lo haremos todo mas tarde (este case está aquí sólo para aclarar).
           case lnChoice = 6
                 * Ver los cambios: traer otra instancia del formulario.
           case lnChoice = 7
               do form MYFORM name oName
           * Cancelar los cambios en este registro solamente.
           otherwise
               = tablerevert()
               unlock record lnChanged
           endcase
        endif llConflict
        * No podemos bloquear el registro, entonces cancelamos los cambios sólo para
        * para este registro.
     else
        = messagebox("Lo siento, no puedo guardar el registro #" + ;
                    ltrim(str(lnChanged)))
        = tablerevert()
        unlock record lnChanged
     endif rlock()
     * Hallar el siguiente registro modificado t procesarlo.
     lnChanged = getnextmodified(lnChanged)
enddo while lnChanged <> 0

* Debido a que revertimos algunos cambios donde hallamos un conflicto
* y el usuario desea cancelar sus propios cambios, se permite forzar el resto
* de las actualizaciones.

= tableupdate(.T., .T.)

Si una tabla ha cambiado en un buffer de tabla que no se ha escrito a disco y usted intenta cerrar la tabla o cambiar el modo buffering, obtendrá un error (#1545): “Buffer de tabla para alias <Alias> contiene cambios no confirmados”.

Transacciones

Como hemos visto, el buffering de tabla es una forma conveniente para hacer buffer de un número de cambios a una tabla y entonces escribir o abandonar todos los cambios al mismo tiempo. Sin embargo, hay un defecto en esta estrategia —qué sucede si uno de los registros está bloqueado o ha sido editado por otro usuario? En este caso, tableupdate(.T.) devolverá .F. y se llama a la rutina de errores. El problema: algunos registros fueron guardados y algunos no. Ahora usted tiene una confusión bastante complicada en sus manos si necesita deshacer lo cambios que se hicieron. Aquí está un ejemplo de tal problema: usted va al banco para transferir dinero desde su cuenta de ahorros a su cuenta de cheques. El programa actualiza su cuenta de ahorros reduciendo el saldo en la cantidad apropiada, entonces trata de incrementar el saldo de su cuenta de cheques por la misma cantidad. El programa podría ver algo como esto:
      seek M.ACCOUNT1
      replace BALANCE with BALANCE - M.AMOUNT
      seek M.ACCOUNT2
      replace BALANCE with BALANCE + M.AMOUNT
      llSuccess = tableupdate(.T.)
      if not llSuccess
         do ERROR_ROUTINE
       endif not llSuccess
 
Entretanto, un programa de limpieza automático ha estado procesando su cuenta de cheques, y ha reducido su saldo por la cantidad de varios cheques. El programa detecta el conflicto y decide abandonar la actualización usando tablerevert(.T.). Sin embargo, debido a que la cuanta de ahorros se actualizó con éxito, sus cambios no están el buffer, y por tanto los cambios permanecen. Ahora el banco tiene una situación de “sobregiro” que dificultará el rastreo, y un cliente muy disgustado cuando obtiene su extracto bancario al final del mes. Afortunadamente, VFP provee un mecanismo que puede resolver este problema: la transacción. Una transacción es un grupo específico de cambios que deben hacerse a la vez o abandonarse todos. Una transacción arranca con el comando begin transaction. Cualquier cambio de una tabla después de usar este comando, aún aquellos hechos con tableupdate(), no se escribirá en disco hasta que se encuentre un comando end transaction. Piense en una transacción como un “buffer de buffer”. La transacción se mantiene hasta que usted determine que todos los cambios pueden hacerse con éxito e invoque un end transaction. Si el programa falla o se reinicia el computador antes de encontrar end transaction, o si su programa llama un comando rollback por que uno de los cambios no puede hacerse con éxito, ninguno de los cambios se escribe en el disco. Miremos el ejemplo de la actualización del banco pero esta vez usemos una transacción como una cubierta para la actualización:

        begin transaction
        seek M.ACCOUNT1
        replace BALANCE with BALANCE - M.AMOUNT
        seek M.ACCOUNT2
        replace BALANCE with BALANCE + M.AMOUNT
        llSuccess = tableupdate(.T.)
        if llSuccess
            end transaction
        else
            rollback
        endif llSuccess

Si el saldo de la primer se cambia pero el de la segunda no se puede cambiar con éxito, llSuccess será .F., y el comando rollback evitará que los primeros cambios se escriban en el disco. Si todo está bien, end transaction escribirá ambos cambios a la vez. Aquí hay algunos otros conceptos relativos a transacciones:
  1. Las transacciones sólo pueden usarse con tablas unidas a bases de datos; para tablas libres no aplica.
  2. Las transacciones aplican para archivos memo (FPT) e índices (CDX) como para los DBF.

  3. Los comandos y funciones que alteran la base de datos, la tabla, o los índices de la tabla no pueden usarse durante una transacción. Por ejemplo, usar alter table ,delete tag, index on,tablerevert(),o close databases durante una transacción generará un error. Vea la documentación de VFP para una lista completa de comandos restringidos .
  4. Puede anidar transacciones hasta cinco niveles de profundidad (“Las buenas noticias: usted ya no tiene sólo cinco niveles de read. Las malas noticias: ...” <g>). Cuando se completa un nivel interior de transacción, sus cambios se agregan al caché de cambios para el siguiente nivel de transacción en lugar de escribirse en el disco. Sólo cuando se alcanza el end transaction final se escriben todos los cambios en disco. Usted puede usar la función txnlevel() para determinar el nivel de la transacción actual.

  5. A diferencia de otras construcciones estructuradas de VFP (tal como for/next o scan/endscan), begin transaction, end transaction, y rollback no tienen que estar localizado en el mismo programa. Usted podría, por ejemplo, tener una rutina común para arrancar transacciones y otra para terminarlas. Las transacciones deben mantenerse tan cortas como sea posible, sin embargo, debido a que cualquier registro se puede actualizar durante una transacción no están disponibles para otros usuarios. Ni siquiera para sólo lectura.

  6. Los registros bloqueados automáticamente por VFP durante una transacción se desbloquean automáticamente cuando se completa la transacción. Algunos bloqueo que haga manualmente no se desbloquean automáticamente; usted es responsable por desbloquear aquellos registros usted mismo. Si usted usa unlock durante una transacción, el registro actual permanece bloqueado hasta que la transacción sea hela, en ese momento todos los registros especificados se desbloquean.
  7. Aunque las transacciones le dan tanta protección como ellas pueden, aún es posible una falla de hardware o una falla del servidor durante la escritura al disco después de end transaction lo que puede causar pérdida de datos.
  8. Las transacciones sólo aplican para tablas locales. Las transacciones para tablas remotas se controlan usando los comandos sqlsetprop(), sqlcommit(), y sqlrollback(). Procesar transacciones con tablas remotas está mas allá del alcance de esta sesión.
Aquí está otra mirada a la rutina “Guardar” y la rutina de manejo de errores (en la rutina de error, el código en el ciclo do while no se muestra debido a que es la misma de la versión previa):
begin transaction
   if tableupdate(.T.)
      end transaction
   else
      rollback
      do ERROR_ROUTINE
   endif tableupdate(.T.)

procedure ERROR_ROUTINE
* Haga aquí la instalación completa, incluyendo verificar qué sucedió.
* Si hallamos un error #1585, hacemos el siguiente código.
    lnChanged = getnextmodified(0)
    do while lnChanged <> 0
       ...
    enddo while lnChanged <> 0
* Debido a que revertimos algunos cambios donde se hallaron conflictos y
* el usuario deseaba cancelar sus propios cambios, se permite forzar la
* la actualización del resto y entonces desbloquea todos los registros que
* que hemos bloqueado manualmente.
     begin transaction
     if tableupdate(.T., .T.)
        end transaction
        * Ahora han ocurrido algunos otros errores, entonces rollback los cambios
        * y muestra un mensaje de error apropiado (también podría tratar de manejarlos
        * aquí si lo desea).
     else
        = aerror(laError)
        rollback
        = messagebox('Error #' + ltrim(str(laError[1])) + ': ' + laError[2] + ;
                ' occurred while saving.')
     endif tableupdate(.T., .T.)



Otros Asusntos

Como vimos antes, getfldstate() puede usarse para determinar si ha cambiado algo en el registro actual. Esto le permite crear formularios que no necesiten un modo “Editar”; los datos en el formulario siempre están disponibles para editar. Cada evento InteractiveChange de los campos podría habilitar los botones “Guardar” y “Cancelar” sólo si el usuario ha cambiado algo, usando un código similar a:
if getfldstate(-1) = replicate('1', fcount() + 1)
  * deshabilita los botones, debido a que no ha cambiado nada
else
  * habilita los botones
endif getfldstate(-1) = replicate('1', fcount() + 1)

Para crear un formulario sin un modo editar, usted también necesita tener código que maneje el caso cuando el usuario cierra la ventan, sale de la aplicación, o se mueve a otro registro (si usa buffering de fila). El evento QueryUnload puede ayudar con las primeras dos, este evento ocurre cuando el usuario hace clic en el botón cerrar del formulario o sale de la aplicación. Podría poner código en este evento que guarde los registros antes de cerrar el formulario. En el caso de mover el puntero del registro, usted ha modificado sus rutinas de navegación de registros (primero, último, siguiente, anterior, buscar, etc.) para verificar si algún campo ha cambiado (y si es así, guarda el registro) antes de mover el puntero de registro. Usted probablemente tendría un método común en el formulario que hace todas estas verificaciones y guardar, y guardarlo donde sea necesario. Un asunto relacionado con esto es que getfldstate() podría indicar erróneamente que nada ha cambiado cuando en efecto el usuario ha cambiado el valor en el campo. Esto puede suceder si usted provee un item de menú o un botón de una barra de herramientas para guardar el registro o mover el puntero del registro. VFP sólo copia el valor en un control (tal como un Textbox) a el buffer del registro cuando el control pierde el foco. Si el usuario cambia el valor en un campo y entonces hace clic en el botón Siguiente en la barra de herramientas, el Textbox no pierde el foco (debido a que la barra de herramientas nunca recibe el foco), entonces el nuevo valor no se copia al buffer del registro y VFP no sabe que los datos han cambiado. La solución a este problema es forzar a que el valor del control actual se copie al buffer del registro antes de usar getfldstate() usando un código similar al siguiente en el evento clic del botón en la barra de herramientas:
with _screen.ActiveForm.ActiveControl
   if type('.ControlSource') <> 'U' and not empty(.ControlSource) and ;
          not evaluate(.ControlSource) == .Value
          replace (.ControlSource) with .Value
   endif type('.ControlSource') <> 'U' ...
endwith

1 comentario :

  1. En VFP 9 ya se puede hacer que tablas libres puedan ser parte de transacciones. Para esto se usan las funciones MAKETRANSACTABLE() y ISTRANSACTABLE().

    Saludos.

    ResponderEliminar

Los comentarios son moderados, por lo que pueden demorar varias horas para su publicación.