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)
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? 🙂
Comentarios recientes