Seminario Web de 18 de Septiembre

septiembre 19, 2009 en Delphi, Entrada Diario, Noticias, Noticias Delphi, Seminario web, Videos

No se si llegaste a asistir o no. Si no es así, tan poco te preocupes ya que imagino que en próximos días lo podremos tener disponible desde las paginas de Embarcadero, para poder repasarlo con mas detenimiento, pues siempre quedan algunos detalles que se te escapan durante la celebración del evento.

No obstante, creo que la impresión general que se recibió fue muy positiva. El seminario duró tres horas y fue mantenido por Andreano Lanusse, de sobra conocido para todos los que formamos parte de esta comunidad de programadores, como máximo representante de Embarcadero para América latina o dicho de otro modo, para la comunidad de programadores hispanos de Delphi.

Fue un detalle que a nadie escapó, durante el inicio del acto, escuchar a David Intersimone, tras una breve introducción en ingles, intentar comunicarse con los oyentes con algunas frases en español, lo cual creo que se recibió como una muestra de respeto hacia nuestra comunidad. Una muestra simpática y bonita que seguro es agradecida.

Luego, ya respecto al contenido se dividió en dos partes claramente separadas, por un respiro de varios minutos en los que se aprovechó para responder algunas preguntas. La primera parte para comentar las novedades del IDE. Y ya la segunda, mas concreta, para ver las novedades de datasnap.

Yo creo que ha estado bien el seminario. Fue positivo. Quizás lo único que pudo deslucirlo un poco fue la mala recepción del sonido de las dos sesiones grabadas que fueron emitidas. Es decir, que había una diferencia grande entre la calidad del sonido en directo, oyendo a Andreano Lanusse y la del video, que hacia que en muchos momentos concreto no pudieras seguir las explicaciones y perdieras el hilo. Por lo demás, fue un buen seminario, muy positivo, donde se pudieron ver algunos detalles muy interesantes. A mi particularmente, lo que mas me gustó fue todo los relacionado con el soporte para dispositivos táctiles, que abre un abanico de posibilidades y alternativas a nuestros interfaces con un minimo coste de desarrollo.

Os animo, si no habéis podido asistir, a que  en próximos días no os lo perdáis ya que casi con seguridad va a estar disponible.

Así que no me queda mas que felicitar a Embarcadero, a Andreano Lanusse y a David Intersimone. ¡Enhorabuena por el seminario!. ¡Enhorabuena!

:-)

Por curiosidad (y Parte III)

abril 8, 2009 en Ado Express & DataSnap, Advertencia, Artículos, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

Nos habiamos quedado en la propiedad Delta…

Tanto la propiedad Data como Delta se definen como OleVariants, y se organizan internamente como un array de bytes, y es esta característica la que va a dotar de flexibilidad a las dos estructuras, que soportaran por un lado los datos, el contenido real, en el caso de la propiedad Data, y un registro de actualizaciones que representa a la propiedad Delta. Dicho registro, logicamente es de solo lectura, dado que es la unica forma de garantizar que es tan solo manipulable por el propio dataset. Tambien es por esa razón, ya que no nos es permitido modificarlo, es por la que existen metodos que nos permiten limpiar esa cache de datos que contiene los registros de cambios.

Pero lo mejor es que lo veamos con unos cambios. Lo primero que he hecho es modificar el navegador de articulos para que nos deje insertar, habiltando todas las opciones. Como solo vamos a poner nuestra atención en la tabla de articulos nos podemos olvidar de los detalles y del mecanismo para transmitir el valor a las claves ajenas desde la inserción del articulo. Sobre este punto, yo os volvería a remitir a los cursos de Ian Marteens, accesibles desde su web a un precio razonable. Esto con independencia de que podamos mas adelante comentar este punto. El framework que desarrolla Martens en cada capitulo de esos cursos aborda una buena cantidad de detalles necesarios para trabajar con el esquema DataSnap, tanto a dos como a mas capas.

Os propongo unos cambios en los datos de la tabla articulos. Los hago y vemos los resultados:

* Añadimos un registro a la tabla articulos.
  Articulo Descripcion
(Valor nuevo) A0006 Articulo 0006
* Modificamos el articulo con codigo A0005.
  Articulo Descripcion
(Valor anterior) A0005 Articulo 0005
(Valor nuevo) Art5 Otra descripcion
* Eliminamos tambien el artículo con codigo A0002.
  Articulo Descripcion
(Valor anterior) A0002 Articulo 0002

 
Ver delta

La rejilla inferior os muestra el contenido del registro de actualizaciones. La estructura es identica a nivel de campos, a la que contiene la propiedad Data, como ya hemos comentado. Pero en el delta, por cada registro existente en el data, pueden existir máximo un par de registros vinculados a éste. Segun el caso, ya que para las inserciones o los borrados tan solo necesitamos un registro y mantener en algun “sitio” algo que nos diga si es un borrado o una inserción (en ese punto interviene UpdateStatus con los valores [usModified, usInserted, usDeleted]). En el caso de las modificiones, el primero registro representa el contenido original y el segundo los nuevos valores que toma.

Si yo modificara repetidamente el registro correspondiente al articulo A0005, los cambios sobrescribirian repetidamente el segundo integrante del par, lo cual no es ni bueno ni malo, pero si es esclarecedor, si en algun momento piensas, al revertir los cambios, un comportamiento similar al de cualquier deshacer (ej Word en office) de las aplicaciones mas habituales.

Y retomando ya el motivo que originó estas tres entradas, ahora que ya estamos en contexto de poder razonarlo, parece dificil encontrar la forma de generar un metodo que de forma limpia y sencilla provoque el refresco de uno de los detalles cualquiera sin tener que liarse a crear nuevos conjuntos de datos auxiliares que invoquen en un segundo plano el contenido a importar. Quiero decir que no parece sencillo añadir un nuevo metodo o una nueva función que nos permita decirle:
- ¡Oye, majo!, que no quiero los detalles de la tabla composicion. Haga Vd el favor de mostrarme los valores correctos…

Y el problema en mi opinión (y como siempre digo es mi opinión y no tiene porque coincidir ni ser acertada) es el diseño en bloque de ambas cachés. Si tuviera que buscar una analogia quizás lo compararia a esas muñecas rusas que nunca sabes por el exterior cuantas van a contener salvo que las vayas abriendo de una en una hasta llegar hasta la que tu deseas.

Muñecas rusas

Nunca llueve a gusto de todos, decía mi padre. Así que me queda la pregunta si hubiera podido existir una alternativa que hubiera contemplado que estas cachés se hubieran repartido de forma equitativa a cada uno de los componentes ClientDataSet, dejando que cada palo hubiera aguantado su vela.

Ni idea… :-(

Por curiosidad (Parte II)

abril 7, 2009 en Ado Express & DataSnap, Advertencia, Artículos, ¿Sabías que...?, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

Seguimos el experimento, intentado satisfacer nuestra curiosidad. :-)

Vamos a hacer lo siguiente:

* En el modulo de datos eliminamos los dos componentes TClientDataSet que contienen los detalles. Es decir, cdsComposicion y cdsComponentes desaparecen. Tambien eliminamos todos los campos persistentes del dataset maestro cdsArticulos Logicamente, una vez hecho esto, los dos componentes TDataSource ligados a los mismos (a cdsComposicion y a cdsComponentes) ahora han perdido las referencias a los dataset y apuntan a nil.

Cambio en el módulo de datos

* Vamos a añadir en el evento OnCreate del modulo principal un par de lineas para suplir las dos asignaciones perdidas.

procedure TMainTest.FormCreate(Sender: TObject);
begin
dsComposicion.DataSet:=
   TDataSetField(dsArticulos.DataSet.FieldByName('qComposicion')).NestedDataSet;

 dsComponentes.DataSet:=
   TDataSetField(dsComposicion.DataSet.FieldByName('qComponente')).NestedDataSet;
end;

El resultado: Todo sigue igual que estaba y podemos ver los datos en las rejillas de datos (tanto en el dataset maestro como en los detalles) porque la existencia de cdsComposicion y cdsComponentes parece meramente anecdótica.

Podeis verlo por vosotros mismos si lo ejecutais.

Segunda ejecución

Así que una de las primeras conclusiones o reflexiones al vuelo de estos comentarios es que nuestro dataset maestro (cdsArticulos) es el jefazo del grupo, nuestro hombre importante y el resto de detalles son tan solo una ventana a lo que se está cociendo en su interior. Son meros hombres de paja o si se prefiere, actores de relleno que hacen que el batallon de romanos tenga mas credibilidad. Lo cual no quiere decir para nada que se deban eliminar pues son una ayuda necesaria en la parte de diseño de nuestra desarrollo. Solo hablo de tomar conciencia de un detalle que nos puede pasar desapercibido, lo cual sí parece importante en el transfondo de la curiosidad que mueve este grupo de entradas.

La tercera parte tendrá que ver sobre el delta y de alguna forma, la reflexión casi cae por si misma, por su propio peso, si haceis cualquier modificación en uno de los registros, y tras el oportuno post, es pulsado el boton VerDelta. La asignación en:

procedure Tdatos.Verdelta;
begin
  try
    Delta.Data := cdsArticulos.Delta;
  except
    Delta.Data := Null;
  end;
end;

nos permitirá visualizar el esquema conceptual y fisico que usa nuestro proveedor de datos para generar las oportunas sentencias sql, es decir el registro de actualizaciones. Esa será nuestra tercera parada. Tambien de cuando en cuando hay que dormir… :-)

Por curiosidad (Parte I)

abril 6, 2009 en Ado Express & DataSnap, Advertencia, Artículos, Código, Consejo, Delphi, Entrada Diario

Por curiosidad, ya que hablamos de AdoExpres y Datasnap, vamos a perder unos minutos en un par de comentarios que me parecen interesantes y que me hicieron reflexionar durante varios días de la semana anterior. Como muchos de los comentarios que hemos compartido, han surgido a raiz del trabajo diario. En este caso, concretamente al modificar la ficha de Clientes.

Estaba frente al típico esquema maestro-detalle donde el maestro era la tabla de clientes y los detalles eran cualquier otra tabla relacionada con ésta a través de una clave ajena. Una de estas tablas, mostraba en un dbGrid las comisiones que habían generado las ventas a determinado cliente, y en el interior de la pestaña que contenia la rejilla, existía un botón para recalcularlas y corregir errores generados en la modificación de las facturas. Resumiendo un poco, el proceso iba a disparar un procedimiento en la base de datos cuya misión era recalcular las comisiones que se mostraban en la rejilla y, en principio, no iba a retornar un conjunto de datos sino unicamente el exito en la finalización o el codigo del error de existir éste. Esto era problematico en el sentido de que, dado que nuestro conjunto de datos se ubica en la cache local y el procedimiento es disparado en nuestro servidor, los valores visualizados por el usuario podian en determinados casos diferir de los reales tras la ejecución del proceso, y podríamos necesitar invalidar el contenido de aquel dataset para mostrar los nuevos datos.

Además, si estamos en esos momentos en el contexto de nuestra aplicación, será habitual que el usuario se encuentre frente a una ventana modal (la ficha del cliente) que actualiza los cambios tras “aceptar” (que finalmente cerrará la ventana modal y actualizará los cambios pendientes). Ese esquema que parece el mas habitual y funciona bien en la mayoría de los casos, nos pone en una tesitura un tanto peculiar y problemática. O bien obligamos a nuestro usuario a confirmar cambios cerrando la ventana modal para que visualice los nuevos datos, lo cual no deja de ser una pequeña chapuza. O bien, creamos un datasets auxiliar que nos permitirá importar los registros que han cambiado y que añadiremos a la cache local, bien actualizando los existentes, bien insertando si fueran nuevos. Esta ultima opción es trabajo extra que a nadie le apetece hacer, sobretodo si consideramos la enorme cantidad de tablas que se implican en una aplicación normal y corriente (las que se utilizan en los ejemplos de los libros no tienen parecido alguno con las del mundo real) :-)

En estos casos, lo habitual es acceder a Internet con el fin de comprobar si alguien ha tenido una inquietud similar y ha comentado algo al respecto. Y así lo hice. Y encontré casualmente un post en el foro del Club Delphi donde se comentaba este problema.

http://www.clubdelphi.com/foros/showthread.php?p=330090

En dicho post, participaba un programador de México, Al Gonzalez, que hace poco ha compartido algunos comentarios con nosotros, precisamente cuando hablabamos de Datasnap. El codigo final que se muestra en dicho post corrobora un poco lo comentado en las lineas anteriores.

Veamos un fragmento de dicho post:

procedure TForm1.Button12Click(Sender: TObject);
Var
  xClave:Integer;
begin
   //asegurase de que no existan pendientes
   //ya que MergeChangeLog lo eliminaria del Delta
    if ClientDataset3.ChangeCount>0 then
       showmessage('Hay datos pendientes en caché, aplicarlos antes del refresh')
    else begin
       //salvo el contenido del campo clave del registro a refrescar
       xClave:=ClientDataset3fcod_mast.AsInteger;
       //Leer de la Base de datos el registro a refrescar
       CDS_refresh.Close;
       CDS_refresh.Params.ParamByName('xClave').AsInteger:=xClave;
       CDS_refresh.Open;
       //borrar el registro que deseo refrescar
       ClientDataset3.Delete;
       //borrar datos de Delta
       ClientDataset3.MergeChangeLog;
       // añadir el registro - RefreshRecord
       ClientDataset3.AppendData (CDS_refresh.Data, True);
    end;
end;

Aparte de estos comentarios, y los que pude leer repasando los cursos de Marteens sobre [AdoExpress/DBExpress] y DataSnap (o bien lo escrito por el autor en las distintas versiones de “La cara oculta de…”), el resto de información no es demasiado abundante ni clara. Marteens es una excepción en ese escenario ya que no conozco todavía a nadie que le haya leido y que piense que la compra de ese libro o ese curso ha sido un gasto inutil. Todo lo contario. Un problema que tenemos, in eternis, es el de que resulta facil encontrar textos que describen nuestras herramientas pero que dificilmente nos enseñan a usarlas en el mundo real. En el simil del martillo, encontrariamos cien mil textos que nos explicarian el origen del martillo en la historia de la humanidad. Doble cantidad hablando de forma elocuente sobre las distintas formas del mango. Y contados escritos (se podrían contar con los dedos de la mano) explicando como clavar un clavo sin machacarse los dedos. :-)

Vamos a montarnos un pequeño ejemplo que nos permitirá experimentar y extrapolar el problema que hemos comentado. Imaginemos tres tablas, Articulos, Componentes y Composicion. Las relaciones entre ellas son muy sencillas. Lo mejor es verlo con una imagen.

Ejemplo

Es decir, la tabla Composición muestra una relación entre un articulo y los componentes de los que está compuesto.

Podéis descargar este zip que contiene el código fuente del ejemplo y un script para generar la base de datos en sqlserver:Fuente

Ejecutamos el programa y seguimos comentando. Esto nos muestra la ventana principal.

El primer comentario o duda que nos puede asaltar es preguntarnos si no es posible actualizar un registro detalle de un forma limpia y sencilla. Ejecutad la aplicación. Luego editad cualquier campo y confirmad con un post, (por ejemplo la descripción del componente en la ficha del componente y en lugar del texto “Componente 1″ poneis “Componente 1 modificado” o cualquier cosa que se os ocurra. Y finalmente, tras editar, pulsais el boton “Incrementa Cantidad” que ejecutará el procedimiento en el servidor que actualizará el campo cantidad en 10 unidades de cada uno de los componentes que forman parte de cada articulo. Es entonces cuando nos encontraremos el “problema”. Los registros de la tabla Composición han sido modificados por el usuario en un segundo plano pero el contenido de la rejilla no puede ser actualizado a menos que ejecutemos un ApplyUpdates. Solo entonces, es posible refrescar los datos.

Y no perdais de vista que nuestra ventana pueda ser modal, que puede ser una dificultad adicional con la que quizás tengamos que lidiar, por las razones comentadas al principio.

Vamos a hacer un pequeño alto en el camino, nos damos un pequeño respiro en este punto en el que ya tenemos planteada la inquietud. Os invito a seguir reflexionando sobre este tema en la siguiente entrada.

Cero contra quinientos sesenta ( Anexo)

marzo 26, 2009 en Ado Express & DataSnap, Advertencia, Artículos, Ayuda, ¿Sabías que...?, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

No iba a escribir esta entrada pero me ha pasado algo similar a lo que me ocurrió con los comentarios sobre JediVCL, donde despues de haber acabado el segundo artículo, sentía que quedaban cosas que comentar y redacté un pequeño anexo con el que dejaba lo que me parecía mas importante zanjado. El resto de funciones se podían ver sobre la marcha. Es más cuestión del día a día, a medida que te van surgiendo las dudas. Con este tema, siento que pasa algo similar.

Hemos compartido dos entradas donde comentabamos como se generaban, y en base a que criterios, las sentencias sql que el proveedor (TDataSetProvider) finalmente “ejecuta” (o manda ejecutar). Bueno. Ya visteis que no él, sino que se apoya en una clase que crea a demanda, que se llama TSQLResolver. Concretamente, y para entrar un poco más en detalle se iniciaba todo desde la función CreateResolver

function CreateResolver: TCustomResolver; override;

Las llamadas tanto a CheckResolver que en último extremo pueden crear la clase, como a FreeResolver que se encarga de liberar el objeto instanciado, se lanzan desde la clase ascendente de nuestro TDataSetProvider.

procedure TBaseProvider.CheckResolver;
begin
  if not Assigned(FResolver) then
         FResolver := CreateResolver;
end;

procedure TBaseProvider.FreeResolver;
begin
  FResolver.Free;
  FResolver := nil;
end;

Así que esa pieza clave en la generación de la sentencia SQL, queda habilmente oculta tras nuestro TDataSetProvider, que se alza como un muro casi infranqueable (*). Atrás quedan algunos comentarios compartidos con otros compañeros donde se dejaba caer que en ocasiones podría ser necesario sobrescribir la sentencia SQL, o que les gustaría ver la cadena que se ha generado, etc….

(*) Eso de infranqueable no es del todo cierto ya que es un poco una expresión, sobretodo desde el momento en el que podemos redefinir el método protegido

function CreateResolver: TCustomResolver; override;

Podriamos obtener una instancia de dicha clase y buscar la forma de, en un descendiente de la misma, acceder al punto que deseamos. Todo es posible dedicando horas y paciencia.

Y yo desde luego, no soy demasiado partidario de tocar el codigo fuente, más que nada porque luego, las posibles actualizaciones pueden sobrescribir los cambios que podamos hacer. En este caso, y para mi uso y disfrute, he modificado unas lineas del módulo Provider. El riesgo también puede ser el de generar errores adicionales a los ya existentes (que seguro los hay). Además, no siempre recordamos pasados meses y meses desde que se hizo el cambio, exactamente las lineas que estaban implicadas por lo que cualquier actualización puede hacer que el código fuente empiece a crear problemas.

Bueno… prometo que no lo volveré a hacer. :-)

Os comenté la idea de duplicar un par de procedimientos y reservarlos para el borrado. Concretamente yo en casa me he duplicado UseFieldInWhere y GenWhereSQL y les he añadido Delete para distinguirlos pero es una solución casera.

function UseFieldInWhereDelete(Field: TField;
Mode: TUpdateMode): Boolean; virtual;

procedure GenWhereSQLDelete(Tree: TUpdateTree; SQL: TWideStrings;
 Params: TParams;  GenUpdateMode: TUpdateMode; Alias: string); virtual;

Lo correcto quizás sería cambiar la firma de esos metodos y añadir sucesivamente el parametro UpdateKind (TUpdateKind) que nos hace falta para distinguir desde UseFieldInWhere cuando se está borrando de cuando se está modificando un registro. Yo siempre digo en casos así que cada uno haga de su capa un sayo, que en cristiano viene a decir algo así como cada cual se la pele como bien pueda. ;-)

Sea como sea, entraríamos en una discusión cuasi filosófica entre los que abogarían por coger siempre en el borrado (en el where) los campos de las claves primarias y aquellos que tienen valor, para compararlos (o si prefiere… todos), como forma mas segura de saber que nada ha pasado desde el momento que cargamos en la cache de nuestro TClientDataSet el registro y fue borrado, hasta que se han aplicado los cambios. Si en ese transcurso de tiempo indeterminado el registro es modificado por otro usuario, el borrado fallaría, garantizando que el usuario que ha borrado dicho registro lo hará con el conocimiento de que se ha intentado modificar.

Ahora bien, siempre existirá tambien el que piense que no vale la pena semejante alboroto para tan pocas nueces. Si me basta conocer las claves primarias que intervienen para localizar el registro, qué necesidad puede haber de utlizar en el where de la consulta todos, o parte de los campos, en el borrado de un registro. Todo depende un poco del cristal desde el que observemos y analicemos la realidad. Si nuestra vida dependiera de que un alcaide modificara el campo perdonado para que no se ejecutase nuestra sentencia al ser borrado el registro, sin duda escogeriamos un borrado seguro, pero si fuera el caso que la información a manejar fuera intrascendente prefeririamos ir al grano y olvidarnos de historias.

Yo me he dado la oportunidad de tener lo mejor de ambos mundos por si cambio de opinión. :-)

En primer lugar he creado la propiedad en la parte publica del TDataSetProvider:

property BorradoSeguro: Boolean read FBorradoSeguro write SetBorradoSeguro;

Y puesto que he hecho cambios en el modulo provider ¡que mas me da hacer unos pocos mas!

Veamos…

procedure TSQLResolver.GenDeleteSQL(Tree: TUpdateTree; SQL: TWideStrings;
  Params: TParams; Alias: string);
begin
  with PSQLInfo(Tree.Data)^ do
  begin
    SQL.Clear;
    if Tree.IsNested then
    begin
      Alias := NestAlias;
      SQL.Add(WideFormat('delete the (select %s FROM %s %s',[QuoteFullName(Tree.Name, QuoteChar),
        PSQLInfo(Tree.Parent.Data).QuotedTable, DefAlias]));       { Do not localize }
      GenWhereSQL(Tree.Parent, SQL, Params, upWhereKeyOnly, DefAlias);
      SQL.Add(WideFormat(') %s',[Alias]));
    end else
      SQL.Add(WideFormat('delete from %s %s', [QuotedTable, Alias]));  { Do not localize }

    if GetProvider.BorradoSeguro then
      GenWhereSQL(Tree, SQL, Params, Provider.UpdateMode, Alias)
    else GenWhereSQLDelete(Tree, SQL, Params, Provider.UpdateMode, Alias);

  end;
end;

La condición ( if GetProvider.BorradoSeguro then ) y la asignación previa de la propiedad BorradoSeguro me ayudará en esta tarea.

Puestos a cambiar, también se me ocurrió sobre la marcha, incluir un evento en el TDataSetProvider que nos permita acceder a la sentencia SQL. Veamos las lineas añadidas:

  TSobrescribeSQLEvent = procedure(Sender: TObject; var SQL: TWideStringList;Params: TParams;
UpdateKind: TUpdateKind; const ATablename, AliasTablename: String) of Object;

   ...

  TDataSetProvider = class(TBaseProvider)
    ...
  private
    FOnNeedChangeSQLCommand: TSobrescribeSQLEvent;
     ...
  published
    ...
    property BorradoSeguro: Boolean read FBorradoSeguro write SetBorradoSeguro;
    ...
   property OnNeedChangeSQLCommand: TSobrescribeSQLEvent read FOnNeedChangeSQLCommand
write FOnNeedChangeSQLCommand;
  end;

Solo nos quedaría localizar donde se va a ejecutar y previamente a esa instrucción, dejar que se transmita el evento al proveedor, para que el usuario pueda acceder al mismo.

procedure TSQLResolver.InternalDoUpdate(Tree: TUpdateTree; UpdateKind: TUpdateKind);
var
  Alias: string;
  FTableName: string;
  PS2 : IProviderSupport2;
begin
  if (Supports(Tree.Source, IProviderSupport2, PS2) and
 (not (PS2.PSUpdateRecord(UpdateKind, Tree.Delta)))) or 
   (not (Tree.Source as IProviderSUpport).PSUpdateRecord(UpdateKind, Tree.Delta)) then
  begin
    if (PSQLInfo(Tree.Data)^.QuotedTable = '') and not Tree.IsNested then
      DatabaseError(SNoTableName);
    if PSQLInfo(Tree.Data)^.HasObjects then Alias := DefAlias else Alias := '';
    FSQL.Clear;
    FParams.Clear;
    case UpdateKind of
      ukModify: GenUpdateSQL(Tree, FSQL, FParams, Alias);
      ukInsert: GenInsertSQL(Tree, FSQL, FParams);
      ukDelete: GenDeleteSQL(Tree, FSQL, FParams, Alias);
    end;

    if FSQL.Text <> '' then begin
      FTableName:= PSQLInfo(Tree.Data)^.QuotedTable;
      DoSobrescribeSQL(FSQL, FParams, UpdateKind, FTableName, Alias);

      DoExecSQL(FSQL, FParams);
    end;
  end;
end;

Las lineas añadidas son:

      FTableName:= PSQLInfo(Tree.Data)^.QuotedTable;
      DoSobrescribeSQL(FSQL, FParams, UpdateKind, FTableName, Alias);

Yo he usado el método virtual y protegido DoSobrescribeSQL, con la idea de poder sobrescribirlo si me hiciera falta, pero en realidad solo encubre el disparo del evento.

procedure TSQLResolver.DoSobrescribeSQL(var SQL: TWideStringList; Params: TParams;
UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);
begin
  if Assigned(GetProvider.FOnNeedChangeSQLCommand) then
     GetProvider.FOnNeedChangeSQLCommand(Self,
                                          SQL,
                                          Params,
                                          UpdateKind,
                                          ATablename,
                                          AliasTablename);
end;

En cualquier caso, el evento haría recaer sobre el programador la responsabilidad de la manipulación de la cadena, sea para el uso que sea. Incluso puede haber a quien se le ocurra algo similar para elaborar un log de movimientos reales de flujo de datos, por sesión de usuario. ¡Qué se yo lo que puede haber dentro de la cabecita de un programador!, ¡Nada bueno! :-)

Al final, cogí un pequeño ejemplo y puse en práctica el artefacto, haciendo un borrado en la tabla vArticulos, que es la tabla que me servía de muestra en las entradas anteriores.

Podeis ver una imagen de cada ejecución (una con borrado seguro y la otra sin borrado seguro):

Borrado seguro

Borrado con KeysOnly

Y estas son las pocas lineas de código que las han generado:

unit USQLDatos;

interface

uses
  SysUtils, Classes, DB, ADODB, DBClient, TConnect, Provider, WideStrings;

type
  TsqlDatos = class(TDataModule)
    dspArticulos: TDataSetProvider;
    qArticulos: TADOQuery;
    Local: TLocalConnection;
    conexion: TADOConnection;
    qArticulosIdArticulo: TIntegerField;
    qArticulosCodigoAlternativo: TStringField;
    qArticulosDescripcion: TStringField;
    qArticulosCoste: TBCDField;
    qArticulosPorcentajeBeneficio: TFloatField;
    qArticulosPrecioVenta: TBCDField;
    qArticulosFechaUltimaVenta: TDateTimeField;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }
public
    { Public declarations }
     procedure OnNeedChangeSQL(Sender: TObject; var SQL: TWideStringList;
Params: TParams; UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);

  end;

var
  sqlDatos: TsqlDatos;

implementation
{$R *.dfm}

uses Dialogs;

procedure TsqlDatos.DataModuleCreate(Sender: TObject);
begin
   dspArticulos.OnNeedChangeSQLCommand:= OnNeedChangeSQL;
   dspArticulos.BorradoSeguro:= True;
end;

procedure TsqlDatos.OnNeedChangeSQL(Sender: TObject; var SQL: TWideStringList; Params: TParams;
  UpdateKind: TUpdateKind; const ATablename, AliasTablename: String);
var
  i: Integer;
  sValores: String;
begin

   for i := 0 to params.Count - 1 do
    if Not Params[i].IsNull then
       sValores:= sValores + #13#10 +
                              IntToStr(params[i].Index) + '=' + Params[i].AsString;

   case UpdateKind of
     ukModify: ;
     ukInsert: ;
     ukDelete: begin

                  if ATablename = 'vArticulos' then
                       ShowMessage(SQL.Text + #13#10 + sValores);
                //     Otra posibilidad...
                //     SQL.Clear;
                //     SQL.Add('Delete from vArticulos where IDArticulo = 1');

               end;
   end;
end;
end.

Así que despues de probarlo y de comprobar que efectivamente puedo sobrescribir la sentencia SQL me quedo más tranquilo.

Me vienen a la mente numerosas ocasiones que pensé en la necesidad de poder acceder a la cadena SQL. Cuando por ejemplo he necesitado transformar una sentencia de borrado en una actualizacion (marcando por ejemplo un campo que identifique el registro como borrado) y no ejecutar un borrado físico del registro. Con ese pequeño cambio que he hecho me podría valer. Pero también podría valerme para simplemente visualizar en una depuración la sentencia y el valor de los parámetros que realmente estan enviandose a la base de datos, sin necesidad de hacer una traza en una herramienta adicional, que puede no existir.

Espero que os puedan ayudar estos comentarios.

Cero contra quinientos sesenta ( y Parte 2)

marzo 21, 2009 en Ado Express & DataSnap, Advertencia, Artículos, ¿Sabías que...?, ¿Sabías que...? [Delphi], Código, Consejo, Delphi, Entrada Diario

En esta segunda parte de la historia, debería pienso, empezar a explicar el por qué del título, que seguramente os puede haber causado extrañeza. Todo tiene una explicación.

Vereis, en estos casos, cuando existe un error y no tienes una explicación lógica que lo justifique, es cuando hacemos uso de herramientas de depuración que casi nos hacen sentirnos como sabuesos. Nos vemos a media tarde, siguiendo paso a paso la depuración del código o como en esté caso concreto nos paso, haciendo uso de una herramienta que nos permitía husmear entre bastidores, como puede ser el analizador de sql de SQL Server.

Así que ejecutamos el programa y reproducimos el error y voila, aparecía la siguiente ventana (la imagen que ahora veis es la de un ejemplo que puede representar la que teniamos en ese momento Manuel y yo)

Analizador de SQL

Como se que no se puede ver claramente en la imagen, el texto que figura en la linea remarcada es:

exec sp_executesql N’delete from vArticulos
where
IdArticulo = @P1 and
CodigoAlternativo = @P2 and
Descripcion = @P3 and
Coste = @P4 and
PorcentajeBeneficio = @P5 and
PrecioVenta = @P6 and
FechaUltimaVenta = @P7
‘, N’@P1 int,@P2 varchar(20),@P3 varchar(40),@P4 money,@P5 float,@P6 money,@P7 datetime’, 6, ‘arti’, ‘khjk’, $0.0000, 0.000000000000000e+000, $0.0000, ‘Mar 21 2009 12:10AM’

Es decir que no solo estaba enviando en la parte where de la consulta sql solamente la clave primaria de la tabla (en este caso concreto es IDArticulo), sino que comparaba tambien el resto de campos. Nosotros pensabamos en nuestra ignorancia, fruto de haberlo leido en alguna documentación, que esto no era así. Pero claro, pensaréis, Palomo, el protagonista de la historia, debería haber sido ajusticiado y sigue vivito y coleando.

Vamos a ver el por qué:

Si os fijais el campo FechaUltimaVenta se evalua sobre el valor ‘Mar 21 2009 12:10AM’.

Como ha fallado la ejecución de la sentencia vamos a ver de nuevo el contenido del registro por lo que ejecutamos en el analizador de consultas sql la siguiente instrucción:

select IDArticulo, FechaUltimaVenta from vArticulos
where (IDArticulo = 6)

¿Sabeis que valor nos devuelve el campo?

IDArticulo FechaUltimaVenta
—————————————————
6 2009-03-21 00:10:03.560

Muchos de vosotros ya habeis adivinado cual es el problema. Nuestro TDataSetProvider ha elaborado una sentencia de borrado donde ha despreciado los milisegundos (560) enviando la cantidad (0) milisegundos. Cero contra quinientos sesenta es lo mismo que error, porque el registro ha cambiado y no lo encuentra al ser ejecutada la sentencia, y claro, el componente lanza el error lógico que tiene planificado en ese caso “Record not found…”

Así que nos quedamos planchados, tras dos o tres días perdidos dandole vueltas a este error sin encontrar el por qué fallaba para al fin llegar a una conclusión similar a la que tuvimos en la entrada en la que hablabamos de la longitud del campo. Se te queda cara de tonto y te dan ganas de cerrar el entorno, olvidarte de la programación y marcharte al campo a respirar aire sano y saludable.

Ahora bien… ya sabeis que el espiritu que intentamos transmitir desde esta página no es el de quedarnos de brazos cruzados sino el de ser positivos, el de seguir adelante e intentar aprender de nuestra experiencia. En el despacho de Manuel y mio, tenemos una pequeña pizarra que nos sirve para razonar. En la parte superior escribí un lema que me parece interesante recordar día a día: Un viaje de un millar de kilometros empieza por un solo paso.

Así que hoy vamos a intentar dar ese paso, proponiendonos saber que criterio sigue el TDataSetProvider en las actualizaciones segun la selección TUpdateMode deseada.

Veamos…

Hagamos varios puntos de parada en la ejecución. Descubriremos que el responsable de generar la cadena SQL es la clase TSQLResolver. Estas son las lineas claves en la ejecución del borrado.

procedure TSQLResolver.GenDeleteSQL(Tree: TUpdateTree; SQL: TWideStrings;
  Params: TParams; Alias: string);
begin
  with PSQLInfo(Tree.Data)^ do
  begin
    SQL.Clear;
    if Tree.IsNested then
    begin
      Alias := NestAlias;
      SQL.Add(WideFormat('delete the (select %s FROM %s %s',
                         [QuoteFullName(Tree.Name, QuoteChar),
                           PSQLInfo(Tree.Parent.Data).QuotedTable, DefAlias]));
      GenWhereSQL(Tree.Parent, SQL, Params, upWhereKeyOnly, DefAlias);
      SQL.Add(WideFormat(') %s',[Alias]));
    end else
      SQL.Add(WideFormat('delete from %s %s', [QuotedTable, Alias]));
      GenWhereSQL(Tree, SQL, Params, Provider.UpdateMode, Alias);
  end;
end;
procedure TSQLResolver.GenWhereSQL(Tree: TUpdateTree; SQL: TWideStrings;
  Params: TParams; GenUpdateMode: TUpdateMode; Alias: string);

  function AddField(Field: TField; InObject: Boolean): Boolean;
  var
    i: Integer;
    BindText: WideString;
  begin
    Result := False;
    with PSQLInfo(Tree.Data)^ do
    begin
      if Field.DataType = ftADT then
      begin
        for i := 0 to TObjectField(Field).FieldCount - 1 do
          if AddField(TObjectField(Field).Fields[i], True) then
            Result := True;
      end else
      if UseFieldInWhere(Field, GenUpdateMode) and
            (Field.DataSize < dsMaxStringSize) then
      begin
        Result := True;
        if InObject then
        begin
          if VarIsNull(Field.OldValue) then
            BindText := WideFormat(' %s.%s is null and', [Alias,
              QuoteFullName(Field.FullName, QuoteChar)])
          else
          begin
            BindText := WideFormat(' %s.%s = ? and',[Alias,
              QuoteFullName(Field.FullName, QuoteChar)]);
            TParam(Params.Add).AssignFieldValue(Field, Field.OldValue);
          end;
        end else
        begin
          if VarIsNull(Field.OldValue) or (not IsSQLBased and
             (Field.DataType = ftString) and (Length(Field.OldValue) = 0)) then
            BindText := WideFormat(' %s%s%s%1:s is null and',
              [PSQLInfo(Tree.Data)^.QuotedTableDot, QuoteChar, Field.Origin])
          else
          begin
            BindText := WideFormat(' %s%s%s%1:s = ? and',
              [PSQLInfo(Tree.Data)^.QuotedTableDot, QuoteChar, Field.Origin]);
            TParam(Params.Add).AssignFieldValue(Field, Field.OldValue);
          end;
        end;
        SQL.Add(BindText);
      end;
    end;
  end;

var
  I: Integer;
  TempStr: WideString;
  Added: Boolean;
begin
  with PSQLInfo(Tree.Data)^ do
  begin
    SQL.Add('where');
    Added := False;
    for I := 0 to Tree.Delta.FieldCount - 1 do
      if AddField(Tree.Delta.Fields[I], Alias = NestAlias) then
        Added := True;
    if not Added then
      DatabaseError(SNoKeySpecified);
    { Remove last ' and'}
    TempStr := SQL[SQL.Count-1];
    SQL[SQL.Count-1] := Copy(TempStr, 1, Length(TempStr) - 4);
  end;
end;
function TSQLResolver.UseFieldInWhere(Field: TField; Mode: TUpdateMode): Boolean;
const
  ExcludedTypes = [ftDataSet, ftADT, ftArray, ftReference, ftCursor, ftUnknown];
begin
  with Field do
  begin
    Result := not (DataType in ExcludedTypes) and not IsBlob and
      (FieldKind = fkData) and (Tag <> tagSERVERCALC);
    if Result then
      case Mode of
        upWhereAll:
          Result := pfInWhere in ProviderFlags;
        upWhereChanged:
          Result := ((pfInWhere in ProviderFlags) and
                             not VarIsClear(NewValue)) or
                             (pfInKey in ProviderFlags);
        upWhereKeyOnly:
          Result := pfInKey in ProviderFlags;
      end;
  end;
end;

Es decir:

La implementación del procedimiento GenDeleteSQL, hará la llamada a la GenWhereSQL, que será la responsable de generar la parte final de la cadena sql, decidiendo que campos deben enlazarse en la condicion where. Dentro de la implementación de GenWhereSQL se evalua AddField para cada campo y finalmente, será esta función la que llame a UseFieldInWhere que decidirá si el campo debe ser incluido en función de la relacion ProviderFlags (en Tfield) / UpdateMode (en TDataSetProvider)

Así que todas las sentencias, tanto de inserción, de actualización como de borrado van a ser filtradas a través de UseFieldInWhere. Lo cual nos llevaría a considerar si realmente queremos que en los borrados se evaluen solo las claves primarias añadir quizas un procedimiento nuevo

function TSQLResolver.UseFieldInWhereDelete(Field: TField; Mode: TUpdateMode): Boolean;
const
  ExcludedTypes = [ftDataSet, ftADT, ftArray, ftReference, ftCursor, ftUnknown];
begin
  with Field do
  begin
    Result := not (DataType in ExcludedTypes) and not IsBlob and
      (FieldKind = fkData) and (Tag <> tagSERVERCALC);
    if Result then
      case Mode of
        upWhereAll:
          Result := pfInWhere in ProviderFlags;
        upWhereChanged,  upWhereKeyOnly:
          Result := pfInKey in ProviderFlags;
      end;
  end;
end;

Y utilizarlo solo en las llamadas a GenDeleteSQL mientas que en el resto, en GenInsertSQL y GenUpdateSQL dejar que invocara a la existente.

Y respecto al tipo DateTime de SQL Server, quizás podría evitarnos el error la selección de SmallDateTime que, como Delphi, no va a considerar los milisegundos. O seguir utilizando el tipo Datetime pero evitar asignaciones de funciones que implicarán la gestión de los milisegundos, como GetDate( ) y tenerlo en cuenta. Eso sí, en el caso de que optemos por SmallDateTime habrá que llevar cuidado porque tan solo nos alcanzaría hasta el año 2080 (creo recordar) fecha en la que nuestra aplicación fallecería de muerte natural. ;-)

Pero bueno, cualquier idea que se nos ocurra haría que Palomo Confiado dejará de tener buena estrella… ¿Tú que harías? :-)