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):

borradoseguro

borradoKeysOnly

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.

5 comentarios sobre “Cero contra quinientos sesenta ( Anexo)

  1. Por fin alguien habla de las cosas del día a día de la programación con delphi utilizando datasnap, a ver si los de Borland, perdon, Codegear, que digo… Embarcadero ven esto y dan una solución porque solo quieren sacar versiones nuevas que no aportan nada, más que recaudación de dinero para mantener el negocio, bueno algo aportan, pero a ver si arreglan los bugs, que los hay desde Delphi 7 o antes. Por cierto si alguien lee este artículo y le ha pasado lo mismo que hable, que no se lo va a comer nadie, porque parace que nadie trabaje con DataSnap, que Salvador y yo seamos unos bichos raros que experimenta con la parte alucinogena de la programación. Saludos Manuel

  2. ¡Hola!

    Me da gusto haber encontrado esta bitácora, muchas felicidades por los estupendos artículos que publicas.

    Me llamó la atención éste, en particular, por una situación muy similar que se presentó hace unos días en los foros de Club Delphi, y en la cual, modestia aparte, ayudé a encontrar la causa del “extraño” error:

    http://www.clubdelphi.com/foros/showthread.php?t=63945

    * * * Para Manuel: * * *

    Te tomo la palabra. Muchas veces me he sentido con una especie de frustración por las mismas razones que comentas, pero hace tres años que tomé la iniciativa y me puse manos a la obra. Quizá quieras echarle un vistazo a la lista de características de Magia Data, una biblioteca de componentes basada (en parte) en dbExpress y DataSnap:

    http://www.sistemasgh.com

    (pido disculpas si se toma como mera publicidad)

    Sin embargo, creo que ya muchos desarrolladores Delphi estamos usando dbExpress y TClientDataSet, lo noto sobre todo en los foros. Aunque claro, me gustaría que fuéramos más.

    Un saludo respetuoso.

    Al González. 🙂

  3. Hola Al:

    Muchas gracias por el comentario y me alegro que te hayan gustado el contenido del blog.
    Si no tienes problema, te he añadido a los links de las bitacoras de Delphi.

    Respecto a al link que has incluido ¡ojala lo hubieramos encontrado, porque perdimos varios días hasta localizar el problema.

    Y seguro que Manuel se alegra de leer tu comentario.

    Un abrazo a todos los programadores hispanos, en especial a la comunidad de Mexico
    (para los que no lo sepan Al Gonzalez es un miembro destacado de la comunidad de programadores de Delphi en Mexico y colaborador habitual de Club Delphi)

    Salvador

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s