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:
- Las transacciones sólo pueden usarse con tablas unidas a bases de datos; para tablas libres no aplica.
- Las transacciones aplican para archivos memo (FPT) e índices (CDX) como para los DBF.
- 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 .
- 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.
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.
- 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.
- 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.
- 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.
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


