Foros Club Delphi

Foros Club Delphi (https://www.clubdelphi.com/foros/index.php)
-   Conexión con bases de datos (https://www.clubdelphi.com/foros/forumdisplay.php?f=2)
-   -   Combinación fallida de Filter, RevertRecord y UpdateStatus (https://www.clubdelphi.com/foros/showthread.php?t=69162)

Al González 30-07-2010 00:51:05

Combinación fallida de Filter, RevertRecord y UpdateStatus
 
Hola a todos.

Hace tiempo que no escribía sobre TClientDataSet, una clase nativa sobre la que he trabajado considerablemente y que aún sigue dándome una que otra sorpresa (la mayoría buenas).

Hoy encontré una posible falla en la versión de Delphi 7 de ese componente, la cual me gustaría compartir aquí. Primero que nada para advertir sobre su existencia y proponer un sencillo remedio (work around), pero también para saber si se trata de un defecto conocido y si éste persiste en las nuevas versiones de Delphi.

Con un objeto TClientDataSet nativo que ya se encuentre abierto y ordenado por su campo llave (ID en este ejemplo), hagamos lo siguiente:

Código Delphi [-]
  With CDS1 Do
  Begin
    // Vaciamos la memoria del conjunto de datos
    EmptyDataSet;

    // Agregamos un primer registro en memoria
    Append;
    FieldValues ['ID'] := 10;
    // Aquí otras asignaciones que fueran necesarias
    Post;

    // Agregamos un segundo registro en memoria
    Append;
    FieldValues ['ID'] := 11;
    // Aquí otras asignaciones que fueran necesarias
    Post;

    { Filtramos el conjunto de datos para que solamente el primer registro
      sea visible }
    Filter := 'ID = 10';
    Filtered := True;

    { Si en este punto revisamos la propiedad UpdateStatus, notaremos que
      su valor es usInserted (pues el registro es nuevo y no ha sido
      aplicado a la base de datos) }
    If UpdateStatus = usInserted Then
      ShowMessage ('El registro ' + FieldByName ('ID').AsString +
        ' es nuevo.');

    // Llamamos al método RevertRecord para deshacernos del registro 10
    RevertRecord;

    If Not Locate ('ID', 10, []) Then
      ShowMessage ('Nos deshicimos del registro 10 (el filtro sigue ' +
        'siendo ' + QuotedStr (Filter) + ').');

    { Desactivamos el filtro.  Al hacer esto, el registro 10 (que ya
      habíamos eliminado con RevertRecord) ¡vuelve a aparecer y con
      estado usUnmodified! }
    Filtered := False;
    Filter := '';

    If Locate ('ID', 10, []) And (UpdateStatus = usUnmodified) Then
      ShowMessage ('Quitamos el filtro, y el registro ' +
        FieldByName ('ID').AsString + ' reapareció y como NO nuevo.');
  End;

El remedio que encontré fue casi obvio: usar Delete en lugar de RevertRecord.

La ayuda de Delphi no especifica si el método RevertRecord es aplicable solamente a registros que tienen el estado usModified, o si también puede ser utilizado con los que tienen el estado usInserted. No obstante, parece funcionar muy bien en el segundo caso mientras el conjunto de datos no tenga un filtro activo.

Así pues, mi recomendación sería que se utilice el método Delete en lugar de RevertRecord si se desea eliminar de la lista de cambios a algún registro nuevo (al menos mientras exista el problema planteado). Por otra parte, con los registros traídos de la base de datos y modificados en memoria (estado usModified), no parece haber problema al usar RevertRecord para deshacer sus cambios aunque haya un filtro presente.

Espero sea de ayuda lo anterior, y de antemano gracias por cualquier información relacionada que puedan proporcionar.

Un abrazo irreversible.

Al González. :)

Al González 10-08-2010 09:19:41

Volviendo a este tema. ¿Alguien podría hacer la misma prueba en una versión superior a Delphi 7?

Se los agradezco.

¡Saludos!

Al González 13-08-2010 09:40:33

Hola de nuevo.

Esto puede ser de alguna utilidad.

Haciendo más pruebas encontré que este defecto tiene cierto patrón. Ocurre solamente con registros nuevos (creados en memoria y aún no aplicados al servidor), que pueden o no tener modificaciones tras haber sido guardados en memoria (con o sin ediciones tras el primer Post). Pero NO ocurre si el registro en cuestión es el último en la lista de cambios (change log) del conjunto de datos.

Es decir, aparentemente, si el nuevo registro es la última operación realizada entre todas las altas, bajas y cambios que están pendientes de ser aplicados al servidor, RevertRecord funcionará sin conflicto alguno con el filtraje usado, eliminando el nuevo registro de la memoria del conjunto de datos y de su lista de cambios.

Los siguientes son cinco ejemplos de listas de cambios en el orden en el que son hechas las operaciones.

Ejemplo 1:
Alta A
Alta B
(puede ser revertido B, pero no A)

Ejemplo 2:
Alta A
Alta B
Baja C
(no puede ser revertido ni A ni B)

Ejemplo 3:
Alta A
Alta B
Cambio B
(puede ser revertido B, pero no A)

Ejemplo 4:
Alta A
Alta B
Cambio C
(no puede ser revertido ni A ni B)

Ejemplo 5:
Alta A
Alta B
Baja C
Cambio A
(puede ser revertido A, pero no B)

El remedio de llamar al método Delete con los registros nuevos para evitar el problema, si bien puede resultar una solución práctica, tiene el inconveniente de que mantiene el registro invisible ocupando memoria en la lista de cambios, sin hablar de los eventos y otras acciones llevadas a cabo por el método Delete.

Se me ha ocurrido que puede reemplazarse el método RevertRecord nativo por uno que haga lo mismo, pero que, cuando el registro sea nuevo, revise si éste se encuentra hasta el final de la lista de cambios (para ello puede utilizarse un cursor clonado con la propiedad StatusFilter llena), y, en caso de no ser así, lo modifique silenciosamente (sin disparar eventos), forzándolo a quedar al final de la lista de cambios antes de ejecutar la reversión.

Grosso modo, algo como:

Código Delphi [-]
Procedure TMyClientDataSet.RevertRecord;
Begin
  ...
  If RegistroEsNuevoYNoUltimoCambio Then
  Begin
    UpdateCursorPos;

    // Limpiamos un campo para provocar el cambio
    Check (DSCursor.PutField (ActiveBuffer, NumeroDelPrimerCampoNoVacio,
      Nil));

    { Guardamos el registro modificado en la memoria del conjunto de datos
      sin actualizar los buffers de registros (la interfaz de usuario no se
      entera) }
    Check (DSCursor.ModifyRecord (ActiveBuffer));
  End;

  Check (DSCursor.RevertRecord);
  ...
End;

Lo sé, después de ver todo esto, la solución de usar un simple Delete no parece tan mala, pero lo aquí planteado puede llegar a servirle a alguien.

Finalmente agradecería cualquier prueba que puedan hacer en Delphi 2009 o 20010. Por cierto, tengo entendido que ésta última trae el código fuente de la unidad MidasLib. En ella podría estar la clave de este defecto del método RevertRecord de la interfaz IDSCursor.

¿Alguien se anima a participar en esta investigación? Yo la dejaré hasta aquí por ahora.

Saludos.

Al González. :)

Casimiro Notevi 13-08-2010 11:13:00

Cita:

Empezado por Al González (Mensaje 373446)
[..] Finalmente agradecería cualquier prueba que puedan hacer en Delphi 2009 o 20010 [..]

Amigo Al, yo puedo probarlo en delphi 2007, no sé si te puede servir ya que especificas 2009 y 2010.

andres1569 14-08-2010 19:54:41

Probaré con Delphi 2010 y te diré Al go.

Saludos

Al González 15-08-2010 02:29:05

Cita:

Empezado por Casimiro Notevi (Mensaje 373451)
Amigo Al, yo puedo probarlo en delphi 2007, no sé si te puede servir ya que especificas 2009 y 2010.

Gracias por el ofrecimiento Antonio, ahora mismo lo probé con Delphi 2007 y el problema persiste. También lo he hecho con tres diferentes versiones de Midas.dll obteniendo los mismos resultados.

Andrés, si tienes el archivo MidasLib.pas en tu Delphi 2010, ¿podrías echar una mirada a la clase que implementa los métodos de la interfaz IDSCursor, especialmente a los que intervienen en el change log? (y ya de paso corregir este problema y enviarle la solución a Embarcadero :p).

Lo que daría por estar viendo ese código ahora. Tendré que esperar hasta estar frente a una computadora con Delphi 2010. :(

Casimiro Notevi 15-08-2010 02:47:00

Cita:

Empezado por Al González (Mensaje 373592)
[..] Lo que daría por estar viendo ese código ahora. Tendré que esperar hasta estar frente a una computadora con Delphi 2010. :(

¿No te sirve la versión trial?

Al González 15-08-2010 03:56:52

Cita:

Empezado por Casimiro Notevi (Mensaje 373593)
¿No te sirve la versión trial?

Tengo una versión de prueba que bajé e instalé hace varios meses, pero con ella no vienen los fuentes de la VCL, creo que no los incluyen en este tipo de distribuciones.

De todas formas no hay urgencia en esto, Antonio, con los trucos de borrar en lugar de revertir o el de modificar antes de revertir, se le puede sacar la vuelta a ese defecto. Eso sí, para mí sería interesante conocer más sobre el asunto. :)

andres1569 16-08-2010 21:13:02

He probado con Delphi 2010 y el comportamiento es el mismo, dándose esa disfunción en el uso de los filtros.

Por cierto, siento desilusionarte, Al, pero el archivo MidasLib.pas que viene con Delphi 2010 (al menos en el Professional) ocupa apenas 953 bytes, ya que tan solo declara la función externa DllGetDataSnapClassObject para registrarla mediante RegisterMidasLib, pero nada más que rascar. Esta unit hace uso del fichero binario Midas.obj, donde supongo está todo el nucleo de Midas.

Respecto a RevertRecord y la aplicación de un filtro, ahí hay un bug gordo que más bien parece estar relacionado con el uso de los filtros en esta clase de Datasets (TClientDataSet). He hecho la prueba de desmenuzar ese código que pones en cuatro partes, y lanzar cada una desde botones diferentes: en la primera se vacía el Dataset y se añaden los dos registros; en la segunda se aplica el filtro; en la tercera se llama a RevertRecord; y por último, en la cuarta se quita el filtro.

La sorpresa me la he llevado al cambiar el orden de estas llamadas: 1er paso, creo los dos registros, a continuación selecciono desde el grid el primer registro (ID = 10) y me voy al tercer paso, llamando a RevertRecord; todo ello sin haber aplicado el filtro, de ahí que seleccione el nº 10 desde el grid para asegurarme de que RevertRecord se aplica sobre dicho registro. Bien, en teoría esto debería borrar el registro ID=10 y dejar el ID=11 solamente ¿no?. Pues efectivamente, así sucede. Lo SORPRENDENTE es que si luego aplico el filtro resulta que ... ¡magia potagia! el registro nº 10, que ya estaba eliminado, vuelve a aparecer (lo lógico es que el grid se quedara vacío al aplicar este filtro), y cuando finalmente quito el filtro ahí están los dos, el nº 10 y el nº11.

A todo esto quiero añadir que, para hacer ciertas pesquisas, me he servido de una técnica propuesta por Ian Marteens en su libro "La Cara Oculta de Delphi 6", pgs 469 y 470, que consiste en volcar la propiedad Delta (matriz donde se almacenan los datos temporales, caché) de un ClientDataset sobre la propiedad Data (matriz donde se almacenan los datos confirmados) de otro ClientDataset, de forma que podamos ver, tras dicho volcado, qué registros hay presentes en cada momento en la caché del primero. Siguiendo esta técnica, después de cada paso que daba, pulsaba un botón que realizaba dicho volcado y los valores arrojados eran los esperados: es decir, que después de aplicar RevertRecord sobre el registro con ID nº 10, en la caché sólo quedaba el nº 11, por lo que entiendo que el 10 se daba por eliminado correctamente. Lo curioso es que luego, al aplicar el filtro, dicho registro vuelva a aparecer en el Grid, aunque ya no aparece en la Caché, ¿de dónde sale si ya había sido borrado y no estaba ni en la caché ni aparecía en el Grid?

Pongo aquí el código que vuelca el Delta del primer ClieentDataSet sobre el Data del segundo, por si quieres hacer pruebas, basta con que enlaces un Grid a este segundo ClientDataSet para ir viendo los registros pendientes de confirmar en el primero:

Código Delphi [-]
procedure TForm2.BtnVerDeltaClick(Sender: TObject);
begin
  try
    CDS2.Data := CDS1.Delta;
  except
    CDS2.Data := Null;
  end;
end;

Saludos

Al González 16-08-2010 21:40:42

Gracias Andrés.

Es muy interesante lo que describes, y, en efecto, así pasa también en Delphi 7. Ya había echado un vistazo al Delta de la manera que indicas (aunque no lo vi con mucho detalle).

Un amigo me dejó ver unos archivos .cpp, que al parecer son los fuentes del archivo .obj que mencionas (creí haber leído que todo el DataSnap estaba reescrito en Delphi). No los he mirado con dedicación porque no dispongo de mucho tiempo ahora (aunado a que le he perdido algo de práctica a C++), pero estoy seguro que dentro de ellos se encuentra la clave de todo este misterio.

Sabes, finalmente he decidido crear mi propio método RevertRecord con un parámetro Boolean opcional llamado FilterSafe (10 minutos para definir ese nombre ;)). Si es True, antes de revertir un registro nuevo, haré un par de llamadas de bajo nivel (DSCursor.PutField y DSCursor.ModifyRecord) para poner en blanco el último campo del registro que no lo esté, pues, como comenté arriba, el problema se evita si modificas el registro nuevo antes de revertirlo.

Por cierto, ¿podrías comprobar si eso último es aplicable también a Delphi 2010?

Saludos.

Al. :)

andres1569 19-08-2010 12:42:26

Cita:

Empezado por Al González (Mensaje 373675)
Por cierto, ¿podrías comprobar si eso último es aplicable también a Delphi 2010?

Pues sí, lo probé en Delphi 2010 y sucede igual, si realizas una modificación a un registro nuevo (ojo, habiendo sido este confirmado mediante Post), RevertRecord lo elimina realmente y ya no aparece en los filtros.

Saludos


La franja horaria es GMT +2. Ahora son las 03:17:55.

Powered by vBulletin® Version 3.6.8
Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
Traducción al castellano por el equipo de moderadores del Club Delphi