11 de septiembre de 2017

Detectar y solucionar conflictos de actualización en VFP

Detectar y solucionar conflictos de actualización en VFP

Artículo original: Detecting and resolving update conflicts in VFP
http://weblogs.foxite.com/andykramek/archive/2006/02/18/1155.aspx
Autor: Andy Kramek
Traducido por: Ana María Bisbé York


En el último artículo en esta serie veré el problema acerca de cómo detectar y solucionar que Visual FoxPro piensa actualizar conflictos. La idea detrás de esto es que queremos asegurar que cuando VFP informa aquí el conflicto es realmente uno. La razón por la que aquí existe una duda es que VFP va a reportar un conflicto de actualización en algún cambio se ha hecho en la copia de disco del registro que se va a guardar - aunque los cambios hechos por el usuario actual realmente crean un conjunto con los datos generados o no. Como un ejemplo, considere el siguiente caso:

  • Usuario # 1 abre un registro cliente para modificar un número de teléfono
  • Usuario # 2 abre el mismo registro cliente para agregar el código postal y cuatro valores
  • El Usuario # 1 guarda sus cambios en el número de teléfono.
  • El usuario # 2 trata de guardar sus cambios del código postal y puede ser un error de "conflicto de actualización", incluso pensando que el valor que este usuario está cambiando no estuvo afectado por los cambios y fueron hechos y guardados por el usuario # 1.

Con un pequeño esfuerzo, puede detectar y solucionar este tipo de errores y solamente rechaza un cambio de usuario cuando realmente entra en conflicto con el dato existente. Para hacer esto podría aprovechar la capacidad de Visual FoxPro para devolver los valores CurVal() y OldVal() para cada columna (para una explicación del rol de estas funciones vea mi artículo Diciembre 2005 en "Handling buffered data in Visual FoxPro").

El proceso consiste en varios pasos y es mostrado aquí (por claridad) como un simple programa con alguna función asociada, por supuesto, puede ser creado en una clase e instanciado como un objeto. El programa mostrado aquí solamente devuelve Verdadero cuando el conflicto ha sido solucionado. Si devuelve Falso indica o que un error ocurre, o que se mantiene este conflicto. En el último caso un cursor contiene el detalle de todo el conflicto no resuelto.

[1] Lo primero que necesita verificar es que tenemos un nombre de tabla que está actualmente en buffer. Por tanto, como veremos, la solución del problema se basa en utilizar TableUpdate(), podemos solamente controlar los conflictos para tablas en buffer. El programa crea un cursor, el que utilizaremos para guardar todos los detalles de cualquier conflicto que no podemos solucionar programáticamente hasta su culminación, pueden ser presentados por el usuario para su revisión y decisión.

LPARAMETERS tuTable
LOCAL llRetval, lnBuffMode, lcTable, lnOldArea, lnNextRec, lnRows
*** Verificar parámetros
IF EMPTY(tuTable)
  *** No pasa nada, utiliza la tabla actual
  lcTable = ALLTRIM( ALIAS() )
  IF EMPTY( lcTable )
    *** No hay tabla, por tanto no podemos dar 
    *** seguimiento a ningún conflicto - return 
    RETURN lLRetVal
  ENDIF
ELSE
  DO CASE
    CASE TYPE( "tuTable" ) = "C"
      *** Asume que la cadena de caracteres es el Alias requerido
      lcTable = ALLTRIM( tuTable)
    CASE TYPE( "tuTable" ) = "N"
      *** Toma el Alias para el área de trabajo especificado
      lcTable = ALIAS( tuTable )
    OTHERWISE
      *** Parámetro no válido
      RETURN llRetVal
  ENDCASE
ENDIF 
*** Crea un cursor local para guardar los conflictos
CREATE CURSOR curcflix ( ;
  cfxRecNum C ( 8), ; && Número del conflicto
  cfxFldNam C (200), ; && Nombre del campo
  cfxOldVal C (200), ; && Valor original 
  cfxCurVal C (200), ; && Valor actual en el disco
  cfxUsrVal C (200), ; && Cambia en el buffer
  cfxForcit N ( 1) ) && Acción definida por el usuario
*** Verifica BufferMode
llRetVal = .T.
lnBuffMode = CURSORGETPROP( 'Buffering', lcTable )
IF lnBuffMode < 2
  *** Si la tabla no tiene Buffer devuelve Falso
  ***  no podemos confiar en TableUpdate() 
  RETURN .F.
ELSE
  *** Guarda el área de trabajo actual y 
  *** selecciona la tabla requerida
  lnOldArea = SELECT()
  SELECT (lcTable)
ENDIF

[2] La otra parte del proceso depende del tipo de buffering que va a ser utilizado. Si la tabla tiene buffer de filas, entonces podemos necesitas solamente verificar la fila actual. Cuando está en efecto de Buffer de Tabla necesitamos procesar todos los registros con cambios pendientes. Esto significa envolver la verificación a nivel de fila dentro del código que utiliza GetNextModified() para encontrar todos los registros con cambios pendientes.

*** Si tiene buffer de fila, sólo procesa esta fila
IF lnBuffMode < 4
  *** Buffer de fila
  llRetVal = CheckRow( RECNO(), lcTable )
ELSE
  *** Buffer de Tabla - necesita encontrar todos los registros modificados
  *** Lo que significa llamar a GetNextModified() hasta tanto devuelva 0
  *** indicando que no existen más registros con cambios
  lnNextRec = 0
  DO WHILE .T.
    lnNextRec = GETNEXTMODIFIED( lnNextRec )
    IF lnNextRec = 0
      EXIT
    ENDIF
    *** Intenta y actualiza el registro
    llRetVal = CheckRow( lnNextRec, lcTable )
    IF ! llRetVal
      *** Si falla, salir 
      EXIT
    ENDIF
  ENDDO
ENDIF

[3] La función CheckRow() (o el método si está creando un objeto para esto) es donde se hace el trabajo real. Para cada campo en el registro, este método lee el valor actual del usuario en el buffer, los valores OldVal() y CurVal() y los pasa a través de una verificación lógica, como sigue:

SI el usuario no ha cambiado este campo, y los valores Old y Current son idénticos.
  ignora este campo - igualmente, no ocasionará un conflicto 
SI_NO
  SI el usuario no ha cambiado el campo, pero los valores Old y Current son diferentes
    actualiza el buffer directamente con el valor Current
  SI_NO
    SI el usuario no ha cambiado este campo, pero el valor en el buffer es en realidad idéntico
      al valor Current, ignora este cambio
    SI_NO
      Esto ES realmente un conflicto, así que lo grabamos

Pero, esto se basa en saber si el usuario actual cambia realmente cualquier campo dado. Para obtener la lista de los campos que VFP recuerda que están siendo cambiados utilizamos una función llamada GetUserChanges() que devuelve una lista separada por comas de columnas con cambios pendientes:

FUNCTION GetUserChanges
LOCAL lcRetVal, lcTable, lcFldState, lnCnt, lcStatus
*** Initializa el valor de retorno
lcRetVal = ''
*** Y el alias actual - que está controlada en el código llamado antes
lcTable = ALIAS()
*** Primero verifica los campos que VFP ve al tener valores cambiados
lcFldState = NVL( GETFLDSTATE( -1, lcTable ), "")
IF EMPTY( CHRTRAN( lcFldState, '1', ''))
  *** Nada; pero '1', por tanto no cambia nada
  RETURN lcRetVal
ENDIF
*** Entonces, TENEMOS, al menos un campo modificado! 
*** Pero primero tenemos que controlar el indicador DELETED.
*** Aquí podemos utilizar "DELETED()" como nombre de campo!
IF ! INLIST( LEFT( lcFldState, 1), "1", "3" )
  lcRetVal = "DELETED()"
ENDIF
*** Ahora podemos olvidarnos del indicador de borrado
lcFldState = SUBSTR( lcFldState, 2 )
*** Toma el nombre de los campos para los campos modificados
FOR lnCnt = 1 TO FCOUNT()
  *** Hacemos un lazo con los campos
  lcStatus = SUBSTR( lcFldState, lnCnt, 1 )
  IF INLIST( lcStatus, "2", "4" )
    lcRetVal = lcRetVal + ;
      IIF( ! EMPTY( lcRetVal ), ",", "") + FIELD( lnCnt )
  ENDIF
NEXT
*** Devuelve la lista de campos modificados
RETURN lcRetVal

Vea que cuando utilizamos la función nativa DELETED() como un nombre de campo en esta función. Ambos, CurVal() y OldVal() aceptarán este como "nombre válido" de campo (devolviendo un valor lógico indicando si el campo fue eliminado en la tabla original) así que pueda verificar tanto lo datos eliminados, como los datos modificados.

[4] Como ha visto antes, la función CheckRow se llama una vez por cada línea que necesite verificar y es donde se toma la decisión y si la intervención del usuario es requerida o no. Para las filas donde un conflicto no se puede solucionar por programa escribimos los detalles del cursor que creamos antes. Esta es la función:

FUNCTION CheckRow( tnRecNum, tcTable )
LOCAL lnCnt, luCurVal, luOldVal, lnRows, llRetVal, lcFldList, lcFldName, luUsrVal
*** Fuerza el registro correcto a ser el actual
SELECT (tcTable)
IF RECNO() # tnRecNum
  GOTO tnRecNum
ENDIF
*** Toma la lista de campos cambiados por el usuario actual
lcFldList = ""
lcFldList = ThisForm.GetUserChanges( tcTable )
*** Recorre todos los campos
FOR lnCnt = 1 TO FCOUNT()
  lcFldName = FIELD( lnCnt )
  luCurVal = CURVAL( FIELD( lnCnt ))
  luOldVal = OLDVAL( FIELD( lnCnt ))
  luUsrVal = EVAL( FIELD( lnCnt ))
  *** ¿Este campo causará un conflicto?
  IF luCurVal == luOldVal
    *** No se han hecho cambios en este campo
    *** Por tanto, no hay problemas
  ELSE
    *** Se han hecho cambios en este campo
    IF ! FIELD( lnCnt ) $ lcFldList
      *** Pero el usuario actual no ha modificado este campo
      *** Por lo que podemos actualizarlo con el valor de CurVal()
      REPLACE (FIELD(lnCnt)) WITH luCurVal
    ELSE
      *** ¡Algo ha cambiado! La pregunta es ¿QUÉ?
      IF EVAL( FIELD(lnCnt) ) == luCurval
        *** El usuario no ha cambiado nada
        LOOP
      ELSE
        *** Este es un conflicto que no podemos solucionar por programa
        *** Entonces, escribe la información como cadena de caracteres al cursor
        INSERT INTO curcflix ( cfxRecNum, cfxFldNam, cfxOldVal, cfxCurVal, ;
          cfxUsrVal, cfxForcit) VALUES ;
          ( TRANSFORM(RECNO()), lcFldName, TRANSFORM(luOldVal), ;
          TRANSFORM(luCurVal), TRANSFORM(luUsrVal), 2 )
      ENDIF 
    ENDIF
  ENDIF
NEXT

[5] La sección final del proceso principal meramente verifica la cantidad de registros en el cursor de conflicto para ver si algo necesita ser hecho por el usuario. Si no existen registros en ese cursor, todos los conflictos se han resuelto al forzar el buffer de usuario para que coincida con el dato original donde haya discrepancias. Entonces, nosotros ahora utilizamos TableUpdate() e incluimos el parámetro FORCE para actualizar el dato y limpia el buffer:

*** Verifica el cursor de conflictos
IF RECCOUNT( "curcflix") = 0
  *** No hay conflictos sin resolver, 
  *** por tanto fuerza la actualización
  llRetVal = TableUpdate( .T., .T., lcTable )
ELSE
  GO TOP IN curcflix
  llRetVal = .F.
ENDIF
*** Devuelve el estado final
RETURN llRetVal

Obviamente si hay registros en el cursor de conflictos necesita permitir al usuario que decida qué hacer y entonces, o fuerza la actualización, o la revierte. Pero esto es un ejercicio para usted, el lector.


1 comentario :

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