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.
Excelente!
ResponderBorrar