Cero contra quinientos sesenta ( y Parte 2)

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)

analizadordesql

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? 🙂

Los comentarios están cerrados.

Blog de WordPress.com.

Subir ↑

Recetas y consejos nutricionales

Indicadas para personas con diabetes, recomendadas para todos.

¡Buen camino!

ANÉCDOTAS Y REFLEXIONES SOBRE UN VIAJE A SANTIAGO…

http://lfgonzalez.visiblogs.com/

Algunas reflexiones y comentarios sobre Delphi

It's All About Code!

A blog about Delphi and related technologies

The Podcast at Delphi.org

The Podcast about the Delphi programming language, tools, news and community.

Blog de Carlos G

Algunas reflexiones y comentarios sobre Delphi

The Road to Delphi

Delphi - Free Pascal - Oxygene

La web de Seoane

Algunas reflexiones y comentarios sobre Delphi

El blog de cadetill

Cosas de programación....... y de la vida

Delphi-losophy

A Lover of Delphi Wisdom

Delphi en Movimiento

Algunas reflexiones y comentarios sobre Delphi

marcocantu.blog

Algunas reflexiones y comentarios sobre Delphi

/*Prog*/ Delphi-Neftalí /*finProg*/

Blog sobre programación de Neftalí -Germán Estévez-

Press F9

Algunas reflexiones y comentarios sobre Delphi

El blog de jachguate

Un blog sobre tecnología y la vida en general

A %d blogueros les gusta esto: