Buffering de Datos y Multiusuarios

24/03/2020

 

Autor: Doug Hennig

Generalidades

En FoxPro 2.x, los desarrolladores editaban los registros usando scatter memvar, editando las variables de memoria, y gather memvar. El propósito de de esta edición indirecta de campos era proteger el registro haciendo buffering. Con Visual FoxPro, el buffering de datos está incluido, así los campos pueden editarse directamente. Esta sesión discutirá cómo trabaja el buffering de datos y explora estrategias para seleccionar cuál mecanismo de buffering usar y cómo manejar conflictos multiusuario.


Introducción

Si usted ha pasado algún tiempo trabajando con VFP, una de las cosas que probablemente ha aprendido es que aunque usted puede continuar haciendo las cosas en la “vieja” forma si usted lo desea, VFP le da una forma mejor para realizar la misma tarea. Cómo editar los registros en un formulario es un perfecto ejemplo de esto.

Aquí está la “vieja” forma que yo usaba para escribir código para editar un registro en una pantalla de entrada de datos:

  1. La pantalla tiene objetos get para variables de memoria con el mismo nombre de los campos de la tabla (por ejemplo, M.CUST_ID y M.NAME).
  2. Cuando el usuario posiciona la tabla en un registro particular (por ejemplo, usando el botón “Siguiente”), usar scatter memvar para transferir el registro a las variables de memoria y show gets para refrescar los valores en la pantalla. El usuario no puede editar las variables (ellas están deshabilitadas o tienen una cláusula when que se evalúa a F.) a causa de que el usuarios actualmente está en el modo “ver”.
  3. Cuando el usuario elige el botón “Editar”, trata de bloquear el registro; muestra un mensaje apropiado si no podemos. Verificar el valor de cada campo contra su variable de memoria —si ellos no coinciden, otro usuario podría haberlo editado y guardado el registro desde que nosotros lo mostramos por primer vez. En ese cado, muestra un mensaje apropiado y usa scatter memvar y show gets de nuevo para que el usuario vea el contenido actual del registro.
  4. Si los campos y las variables de memoria coinciden, se habilitan los objetos get o hace que su cláusula when se evalúe a .T. para que el usuario pueda editar las variables.
  5. Cuando el usuario elige el botón “Guardar”, se hace alguna validación para asegurar que todo se entró de acuerdo a sus reglas, entonces usar gather memvar para actualizar el registro desde las variables de memoria y desbloquea el registro. Deshabilitar los objetos get o hacer que su cláusula when se evalúe a .F. para que el usuario esté de nuevo en el modo “ver”.

Note que en este esquema nosotros no hacemos un read directo contra el registro. En su lugar, nosotros le permitimos al usuario editar variables de memoria y sólo copia aquellas variables de memoria al registro si todo está bien. La razón para usar este método es proteger la tabla; nosotros no permitimos que cualquier datos se almacene a menos que pase todas la reglas. También note que el registro está bloqueado mientras el usuario está editando las variables de memoria. Esto evita que otros usuarios editen el mismo registro al mismo tiempo. Hacerlo, sin embargo, adolece del síndrome de “salí a comer” —si el usuario empieza a editar, entonces se va a comer, el registro permanece bloqueado, no disponible para que otros usuarios lo editen. Desde luego, esta no es la única forma de editar registros. Usted podría hacer el bloqueo justo antes de guardar el registro en lugar de cuando inicia el modo editar. Esto minimiza el tiempo que el registro permanece bloqueado, permitiéndole a otros usuarios accederlo. Esto tiene sus propios inconvenientes, aunque: si el usuario edita las variables y hace clic en “Guardar”, qué sucede si mientras tanto algún otro usuario ha editado el registro? Usted sobre-escribe los cambios? Usted evita que el registro se guarde? Esto es un asunto de diseño que usted debe manejar caso-por-caso. El único propósito de todo este esfuerzo es proteger los datos. Si usted estuviera escribiendo una aplicación que sólo usted usará siempre, usted probablemente lo haría mucho mas simple —sólo un read contra el campo directamente en el registro. Esto hace que la pantalla actúe en forma equivalente a un browse, debido a que todo lo que usted digita se hace directamente en el registro. Sin embargo, debido a que nosotros no podemos confiar en esos usuarios molestos para saber lo que ellos pueden y no pueden entrar, nosotros tenemos que proteger los datos construyendo un "cortafuego" entre el usuario y la tabla. Crear este "cortafuego" en FoxPro 2.x tomaba una cantidad considerable de código. VFP provee un mecanismo “cortafuegos” que nos da lo mejor de ambos mundos: read directo contra un registro mientras que sólo permite que los datos se escriban después de pasar todas las pruebas. Este mecanismo es buffering.

 

Buffering

Usar variables de memoria para mantener el contenido de un registro puede considerarse como crear un buffer de datos. Los datos se transfieren desde el registro al “buffer” usando scatter memvar y desde el “buffer” al registro con gather memvar. VFP no sólo puede hacer este tipo de buffering de un único registro (llamado buffering de fila o de registro) automáticamente, él también soporta otro tipo de buffering (llamado buffering de tabla) en el cual múltiples registros se accedan a través de un buffer. El buffering de registro se usa normalmente cuando usted desea acceder o actualizar un único registro a la vez. Esto es común en mecanismos de entrada de datos como los descritos antes: el usuario puede mostrar o editar un único registro en el formulario. El buffering de tabla debe elegirse para actualizar varios registros a la vez. Un ejemplo común es un formulario con cabecera-detalle de factura. Usando buffering de tabla para la tabla detalle de factura, usted puede permitirle al usuario editar tantas líneas de detalle como él desee, y entonces guardar o cancelar todos los registros de detalle a la vez. Además de los dos mecanismos de buffering, hay dos mecanismos de bloqueo. La “vieja” forma que describí antes puede considerarse un esquema de bloqueo pesimista —el registro se bloquea tan pronto como el usuario elige “Editar”, y permanece bloqueado hasta que él elige “Guardar”. Esto asegura que nadie mas puede hacer cambios al registro mientras el usuario lo esté haciendo, esta forma puede o no ser una buena cosa, dependiendo de su aplicación. El otro método que yo describí antes es un mecanismo de bloqueo optimista —el registro sólo se bloquea por el breve tiempo que toma escribir el registro, y se desbloquea inmediatamente. Esto maximiza la disponibilidad del registro (esto también se conoce como maximizar la simultaneidad) pero significa que tenemos que manejar conflictos que ocurren si dos usuarios editan el registro al mismo tiempo. Como veremos en un momento, actualmente esto es fácil de hacer en VFP, así el buffering optimista probablemente será el mecanismo elegido para la mayoría de las aplicaciones. Debido a que los registros pueden ser buffered automáticamente, ya no es necesario usar el mecanismo de “buffer manual”. En otras palabras, ahora nosotros podemos hacer read directamente contra los campos en el registro sin preocuparnos por el mantenimiento de variables de memoria para cada uno. Para guardar los cambios, simplemente le informamos a VFP que escriba el buffer en la tabla, y para cancelar los cambios, le informamos que no lo haga. En un momento veremos cómo hacerlo. VFP implementa el buffering creando un “cursor” cuando se abre una tabla. El cursor se usa para definir propiedades para la tabla. En el caso de tablas locales, la única propiedad para el cursor es cómo se realiza el buffering; para vistas y tablas remotas hay propiedades adicionales que van mas allá del alcance de esta sesión. Estas propiedades se establecen usando la función cursorsetprop() y se examinan con cursorgetprop(). Veremos resumidamente el uso de estas funciones. El buffering de tabla tiene una implementación interesante respecto a agregar registros: cómo se agregan los registros al buffer, se les asigna un número de registro negativo. recno() devuelve -1 para el primer registro agregado, -2 para el segundo, y así sucesivamente. Puede usar go con un número negativo para posicionar el buffer en el registro agregado apropiado. Esto tiene una implicación en las rutinas que manejan números de registro —en lugar de probar para between(lnRecno, 1, reccount()) para asegurar que lnRecno es un número de registro válido, usted ahora probará para between(lnRecno, 1, reccount()) or lnRecno < 0.

 

Usar Buffering

Buffering está desactivado por definición, así VFP actúa como FoxPro 2.x en términos de cómo se escriben las actualizaciones en una tabla. Para usar buffering, usted debe activarlo específicamente. Buffering está disponible para ambos: tablas libres y aquellas unidas a una base de datos. Buffering requiere que usted establezca set multilocks on debido a que por definición también está desactivado; usted obtendrá un mensaje de error si olvida hacer esto. Puede poner multilocks = on en su CONFIG.FPW o usar la función Options en el menú Herramientas para guardar esta configuración por definición. Buffering se controla usando cursorsetprop('Buffering', <n>, <Alias>). No tiene que especificar <Alias> si está configurando buffering para la tabla actual, <n> es uno de los siguiente valores dependiendo del método de buffering y bloqueo que desee usar:


Buffering/Método de Bloqueo <n>
sin buffering 1
registro, pesimista 2
registro, optimista 3
tabla, pesimista 4
tabla, optimista 5

 

Por ejemplo, para habilitar buffering de registro optimista, use cursorsetprop('Buffering', 3). Para determinar qué buffering está en uso en la tabla actual, use cursorgetprop('Buffering'). Para habilitar buffering en un formulario, podría especificar cursorsetprop() para cada tabla en el método Load del formulario, pero la estrategia preferida es establecer la propiedad BufferMode del formulario como optimista o pesimista (el predefinido es “ninguno”). El formulario entonces automáticamente usa buffering de tabla para todas las tablas ligadas a grids y buffering de registro para todas las otras tablas. Si usa un DataEnvironment para el formulario, puede sobre-escribir el BufferMode del formulario para una tabla particular estableciendo su BufferModeOverride como desee. Mientras un usuario esté cambiando los datos de un registro buffered (esté en medio de la edición de un registro), usted tiene acceso no sólo al valor que él ha entrado en cada campo, si no también al valor anterior de cada campo y su valor actual (el valor actualmente en disco). Dos nuevas funciones, oldval() y curval(), fueron agregadas para este propósito. Aquí está cómo se obtienen los valores apropiados:

Para obtener: Use:
el valor que el usuario ha entrado (el valor en el buffer) <fieldname> or <alias.fieldname>
el valor antes que el usuario cambiara algo oldval('<fieldname>')
el valor actual en el registro curval('<fieldname>')

 

curval() y oldval() sólo pueden usarse con buffering optimista. Usted puede extrañarse que el valor devuelto por curval() podría diferir del devuelto por oldval(). Esto obviamente no ocurriría si sólo un usuario está ejecutando la aplicación. Sin embargo, en una red y con bloqueo optimista, es posible que después de que el usuario ha iniciado la edición de un registro, otro usuario ha editado el mismo registro y ha guardado los cambios. Aquí hay un ejemplo: Bob trae el registro #2 de CONTACTS.DBF y hace clic en el botón “Editar”:

Campo Valor Oldval() curval()
LAST_NAME Jones Jones

Jones

FIRST_NAME Bill Bill Bill

 

Bob cambia el primer nombre a Sam pero todavía no ha guardado el registro:

Campo Valor Oldval() curval()

LAST_NAME

Jones Jones Jones
FIRST_NAME Sam Bill Bill<


Mary trae el registro #2 de CONTACTS.DBF, hace clic en el botón “Editar”, cambia el primer nombre a Eric, y guarda. En la máquina de Bill:

Field

Value

Oldval()

curval()

LAST_NAME

Jones

Jones

Jones

FIRST_NAME

Sam

Bill

Eric

 

Observe que FIRST_NAME, oldval('FIRST_NAME'), y curval('FIRST_NAME') devuelven valores diferentes. Teniendo acceso al valor original, el valor buffered, y el valor actual para cada campo en el registro, usted puede:

  1. determinar cuáles campos cambió el usuario comparando el valor buffered con el valor original; y
  2. detectar si otros usuarios en la red han hecho cambios al mismo registro después de iniciar la edición, comparando el valor original con el valor actual.

Si no le interesan los valores anterior y actual y sólo desea detectar si un campo fue editado por el usuario, use getfldstate(). Esta nueva función devuelve un valor numérico indicando si algo del registro actual ha cambiado. getfldstate() se llama en la siguiente forma: getfldstate(<FieldName> | <FieldNumber> [, <Alias> | <WorkArea>]) y devuelve uno de los siguientes valores:

Valor

Descripción

1 Sin cambios
2 El campo fue editado el estado de borrado del registro ha cambiado
3 Se agregó un registro pero el campo no fue editado y el estado de borrado no ha cambiado.
4 Se agregó un registro y el campo fue editado o el estado de borrado del registro ha cambiado.

 

Cambiar el estado de borrado significa ambos: borrar o recuperar (delete/recall) el registro. Note que borrar y recuperar inmediatamente el registro resultará en un valor de 2 o 4 aunque no haya un efecto de red para el registro. Si no especifica un alias o área de trabajo, getfldstate() opera sobre la tabla actual. Especifique 0 para <FieldNumber> para devolver el estado de agregado o borrado del registro actual. Si especifica -1 para <FieldNumber>, la función devolverá una cadena con el primer dígito representando el estado de la tabla y un dígito para el estado de cada campo. En el ejemplo mencionado antes, donde Bill edita el segundo campo, getfldstate(-1) podría devolver “112”. El primer dígito indica que el registro no fue agregado o borrado, el segundo que el primer campo no ha cambiado, y el tercero que el segundo campo ha cambiado.

 

Escribir un Registro Buffered

Continuando con el ejemplo anterior, ahora Bill hace clic en el botón “Guardar”. Cómo informamos a VFP que escriba el buffer al registro? Con buffering de registro, la tabla se actualiza cuando usted mueve el puntero del registro o usa la nueva función tableupdate(). Con buffering de tabla, moviendo el puntero del registro no actualiza la tabla (debido a que todo el punto del buffering de tabla es que varios registros son buffered al mismo tiempo), así la forma usual es llamar la función tableupdate(). Es mejor usar tableupdate() aún para buffering de registro debido a que usted tiene mas control sobre lo que sucede. tableupdate() devuelve .T. si el buffer se escribió con éxito al registro. Si el buffer de registro no ha cambiado (el usuario no ha editado algún campo, agregado un registro, o cambiado el estado de borrado para el registro), tableupdate() devuelve .T. pero no hace nada. tableupdate() puede recibir unos pocos parámetros opcionales: tableupdate(<AllRows>, <Forced>, <Alias> | <Workarea>) El primer parámetro indica qué registros actualizar: .F. informa que sólo se actualice el registro actual, mientras que .T. significa actualizar todos los registros (sólo tiene efecto si se usa buffering de tabla). Si el segundo parámetro es .T., cualquier cambio de otro usuario se sobre-escribirá por los cambios del usuario actual. A menos que se especifique el tercer parámetro, tableupdate() actualizará la tabla actual. Cómo cancelar los cambios que ha hecho el usuario? Con la estrategia de variables de memoria, usted sólo usa scatter memvar de nuevo para restaurar las variables de memoria a los valores almacenados en disco. Con buffering, use la función tablerevert() para realizar lo mismo para el buffer.

 

Manejo de Errores

Continuando con el ejemplo de “Bill y Mary”, el código ejecutado cuando Bill hace clic en el botón “Guardar” usa la función tableupdate() para tratar de escribir el buffer al registro. Recuerde que Mary editó el registro y guardó sus cambios mientras Bill estaba editando el mismo registro. Cuando Bill hace clic en “Guardar”, tableupdate() devolverá .F., significando que el no escribió el buffer. Por qué? VFP no escribirá el buffer al registro bajo las siguientes condiciones:

  1. Otro usuario haya cambiado y guardado el registro mientras este usuario lo estaba editando (como sucedió en el ejemplo). VFP automáticamente compara oldval() y curval() para cada campo. Si detecta alguna diferencia, nosotros tenemos un conflicto..
  2. El usuario ha entrado un duplicado del valor una clave primaria o candidata.
  3. Se violó una regla de un campo o tabla, o un campo que no soporta valores null es null.
  4. Falló un desencadenante (trigger).
  5. Otro usuario ha bloqueado el registro. Esto puede minimizarse evitando el bloqueo manual de registros con rlock() y usando el mismo mecanismo de bloqueo para buffer, para una tabla, en todos los formularios y programas que la acceden.
  6. Otro usuario cambió el estado borrado del registro.

Usted debe decidir qué hacer cuando tableupdate() falla. También, si su aplicación le permite al usuario hacer clic en botones “Siguiente” o “Anterior” mientras está editando un registro y aquellas funciones no usan tableupdate(), usted debe manejar el error que ocurrirá cuando se intenta el guardado automático. En ambos casos, el lugar apropiado para manejar esto en un una rutina que atrape los errores.

El manejo de errores ha sido mejorado en VFP. La vieja forma para atrapar un error (la cual usted aún puede usar en VFP) es usar el comando on error para especificar un procedimiento que se ejecute cuando ocurre un error. Esta rutina de error podría típicamente podría mirar en error() y message() para determinar qué sucedió, y tomar la acción apropiada. VFP ahora provee un mecanismo automático de manejo de errores: el método Error. Si existe un método Error para un objeto o formulario, él se ejecutará automáticamente cuando ocurre un error sin tener que atraparlo manualmente. aerror() es una nueva función que ayuda a entender que está mal. Usted le pasa un nombre de matriz y el crea o actualiza la matriz con los siguientes elementos:

 

Elemento Tipo Descripción
1 Numeric El número de error (la mismo que error()).
2 Character El mensaje de error (lo mismo que message()).
3 Character El parámetro con error (por ejemplo, un nombre de campo) si el error tiene uno (lo mismo que sys(2018)) o .NULL. si ninguno.
4 Numeric or Character El área de trabajo en la cual ocurrió el error si apropiado, de lo contrario .NULL.
5 Numeric or Character El desencadenante que falló (1 para insert, 2 para update, o 3 para delete) si falla un desencadenante (error 1539), o .NULL. si no.
6 Numeric or Character .NULL. (usado para errores OLE y ODBC).
7 Numeric .NULL. (usado para errores OLE).

 

Por ejemplo, aerror(laERROR) creará o actualizará una matriz llamada laERROR. Aquí están los errores comunes que pueden ocurrir cuando VFP intenta escribir el buffer a la tabla:

Error # Mensaje de Error Comentario
109 Registro está siendo usado por otro
1539 Falló Desencadenante (Trigger) Verifique elemento 5 para determinar cuál trigger falló
1581 Campo no acepta valores nulos Verifique elemento 3 para determinar cuál campo está involucrado.
1582 Violada regla de validación de Campo Verifique elemento 3 para determinar cuál campo está involucrado.
1583 Violada regla de validación de Registro
1585 Registro fue modificado por otro
1884 Violada unicidad de índice. Verifique elemento 3 para determinar cuál campo está involucrado.

 

El manejo de estos errores es directo: informe al usuario el problema y envíelo al modo editar para corregir el problema o cancelar. Para el error #1585 (registro fue modificado por otro), hay varias formas en que puede manejar el error:

  1. Usted le informa a alguien que otro ha modificado el registro y entonces cancela su edición usando tablerevert(). Yo sospecho que la mayoría de usuarios no estarían felices con esta estrategia <g>.
  2. Puede forzar la actualización del registro usando tableupdate(.F., .T.). Esto causa que se sobre-escriban los cambios de otros usuarios con las modificaciones del usuario actual. Este usuario podría estar feliz, pero los otros usuarios probablemente no lo estarían.
  3. Puede mostrar en una copia del mismo formulario los cambios que el otro usuario ha hecho al registro (fácil de hacer con la habilidad de VFP para crear múltiples instancias del mismo formulario). El usuario puede decidir entonces si los cambios del otro usuario deberían mantenerse o no, y usted puede usar tableupdate(.F., .T.) para forzar la actualización o tablerevert() para cancel.
  4. Un esquema mas inteligente involucra determinar si nosotros tenemos un conflicto “real” o no. Por “real” yo quiero decir: ambos usuarios cambian el mismo campo o no. Si los campos que ellos actualizan son diferentes, podríamos informar a VFP que sólo actualice el campo que este usuario ha cambiado, manteniendo intactos los cambios del otro usuario. Un ejemplo podría se un sistema de proceso de órdenes. Un usuario puede haber editado la descripción de un producto mientras otro usuario ha entrado una orden para el producto, por esta razón disminuye la cantidad en existencia. Estos cambios no son mutuamente excluyentes —si hacemos la actualización de nuestra tabla menos gruesa (esto es, no actualizamos un registro completo al mismo tiempo, sólo los campos que modificados), podemos satisfacer a ambos usuarios.

Aquí está cómo trabaja la lógica:

  1. Hallar un campo donde oldval() es diferente que curval(), esto significa que el campo fue editado por otro usuario. Si el valor buffered de los campos es el mismo de oldval(), este usuario no puede cambiar el campo, así podemos evitar sobre-escribir su nuevo valor poniendo el valor buffered como curval().
  2. Hallar un campo donde el valor buffered sea diferente a oldval(). Este es un campo que el usuario ha modificado Si oldval() es igual a curval(), el otro usuario no ha cambiado este campo, así podemos sobre-escribirlo con seguridad.

  3. Si hallamos un campo donde el valor buffered es diferente del valor oldval() pero es el mismo de curval(), ambos usuarios hicieron el mismo cambio. Esto puede verse improbable, un ejemplo puede ser cuando alguien envía un aviso de cambio de domicilio a una compañía y de algún modo dos usuarios deciden poner al día el registro al mismo tiempo. Debido a que los cambios son idénticos, nosotros podríamos estar habilitados a sobre-escribir el campo. Sin embargo, en el caso de una cantidad que está siendo actualizada por la misma cantidad (por ejemplo, dos órdenes por la misma cantidad fueron entradas al mismo tiempo), usted no desearía sobre-escribir el campo, y debería considerar esto como un conflicto “real”.
  4. Si tenemos un caso donde el valor buffered de un campo es diferente de ambos oldval() y curval(), además oldval() y curval() no son los mismos (iguales), ambos usuarios han cambiado el mismo campo pero con diferentes valores. En este caso, tenemos un conflicto “real”. Usted tiene que decidir cómo manejar el conflicto.
  5. En el caso de una cantidad de inventario existente o cuenta de balance, una posibilidad es aplicar al valor buffered el mismo cambio que el otro usuario ha hecho. Por ejemplo, si oldval() es 10 y curval() es 20, el otro usuario ha incrementado la cantidad por 10. Si el valor buffered es 5, este usuario está disminuyendo la cantidad en 5. El nuevo valor buffered sin embargo debería ser value + curval() - oldval(), o 15.
  6. En el caso de campos Fecha, las reglas de negocios y el sentido común podrían ayudar. Por ejemplo, en un programa de citas de pacientes con un campo conteniendo la fecha de la próxima visita de un paciente, la primera de las dos fechas en conflicto es probablemente la correcta para usar, a menos que sea anterior a la fecha actual, en cuyo caso la fecha posterior será la correcta.
  7. Otros tipos de campos, específicamente campos Character y Memo, a menudo no pueden resolverse sin preguntar al usuario que decida si sobre-escribe los cambios del otro usuario o abandona los suyos. Permitiéndole al usuario ver los cambios del otro usuario (como se mencionó antes) puede ayudarlo a tomar la decisión.

Aquí tiene algún código que resuelve este tipo de conflictos (este código asume que nosotros hemos determinado que el problema es un error #1585, el registro ha sido modificado por otro usuario):

  * Verifica 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
  * Si tenemos un conflicto, manejarlo.
  if llConflict
    lnChoice = messagebox('Otro usuario también cambió este ' + ;
               'registro. 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.
        case lnChoice = 6
           = tableupdate(.F., .T.)
        * Ver los cambios: trayendo otra instancia del formulario.
        case lnChoice = 7
           do form MYFORM name oName
        * Cancelar los cambios.
        otherwise
           = tablerevert()
        endcase
      * Sin conflictos, entonces forzar la actualización.
  else
      = tableupdate(.F., .T.)
  endif llConflict  

 

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  

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

 

Vea también

 

Descarga


Multi-User and Data Buffering Issues - Artículo original en inglés

Buffering de Datos y Multiusuarios - Este documento

 


 

Referencias

Artículo original: Multi-User and Data Buffering Issues

Autor: Doug Hennig

Traducido por: Germán Giraldo G.

 


 

 

 



error: Contenido protegido