Pente Fino na VCL - inspecionando o mecanismo de validação do TClientDataSet
Este artigo é do tipo dica e tem como objetivo explorar como é o comportamento de validações pré-definidas que o ClientDataSet realiza internamente.
E foi escrito motivado por uma situação em que eu não conseguia levantar uma exceção no lado servidor, embora estava forçando acontecer.
Mas não entendia porque a exceção era levantada antes de acontecer um ApplyUpdates.
A exceção que eu estava provocando era do tipo key violation em campo único.
Acompanhe os passos da brincadeira.
Estava eu brincando de levantar exceções e para tanto:
criei uma nova aplicação, coloquei um componente de conexão, um dataset com a instrução sql "SELECT * FROM BAIRRO ORDER BY BAIRRO" - use sua tabela preferida - (ambos componentes anteriores usei Interbase Objects - IBO), um TDataSetProvider, um TClientDataSet, um DataSource e um DBGrid, tudo ligado e devidamente configurado e funcionando.
Adicionalmente coloquei mais um ClientDataSet, um DataSource e um DBGrid para pegar o Delta também configurado e funcionando corretamente.
Coloquei mais 5 botões, shomessage em todos eventos relacionados a erros de todos componentes, uma linha de código no evento BeforePost do ClientDataSetBairro para poder depurar com breakpoint, código no evento ReconcileError do ClientDataSetBairro, código no evento DataRequest do DataSetProviderBairro e outros eventos que você pode ver no código completo mais abaixo.
Veja a lista de componentes e nomes:
IBODatabase: TIBODatabase;
IBOQueryBairro: TIBOQuery;
IBOQueryBairroCODIGO: TIntegerField;
IBOQueryBairroBAIRRO: TStringField;
DataSetProviderBairro: TDataSetProvider;
ClientDataSetBairro: TClientDataSet;
DataSourceBairro: TDataSource;
DBGridBairro: TDBGrid;
ClientDataSetDelta: TClientDataSet;
DataSourceDelta: TDataSource;
DBGridDelta: TDBGrid;
BtnAbrirClientDataSet: TButton;
BtnFecharClientDataSet: TButton;
BtnApplyUpdates: TButton;
BtnDataRequest: TButton;
BtnDelta: TButton;
Veja código completo:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DBXpress, FMTBcd, SqlExpr, DB, Provider, DBClient, StdCtrls,
Grids, DBGrids, IBODataset, IB_Components, recError;
type
TForm1 = class(TForm)
IBODatabase: TIBODatabase;
IBOQueryBairro: TIBOQuery;
IBOQueryBairroCODIGO: TIntegerField;
IBOQueryBairroBAIRRO: TStringField;
DataSetProviderBairro: TDataSetProvider;
ClientDataSetBairro: TClientDataSet;
DataSourceBairro: TDataSource;
DBGridBairro: TDBGrid;
ClientDataSetDelta: TClientDataSet;
DataSourceDelta: TDataSource;
DBGridDelta: TDBGrid;
BtnAbrirClientDataSet: TButton;
BtnFecharClientDataSet: TButton;
BtnApplyUpdates: TButton;
BtnDataRequest: TButton;
BtnDelta: TButton;
procedure BtnAbrirClientDataSetClick(Sender: TObject);
procedure BtnFecharClientDataSetClick(Sender: TObject);
procedure DataSetProviderBairroBeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
function DataSetProviderBairroDataRequest(Sender: TObject;
Input: OleVariant): OleVariant;
procedure BtnDataRequestClick(Sender: TObject);
procedure IBOQueryBairroBeforeOpen(DataSet: TDataSet);
procedure IBOQueryBairroBeforeClose(DataSet: TDataSet);
procedure BtnApplyUpdatesClick(Sender: TObject);
procedure ClientDataSetBairroReconcileError(DataSet: TCustomClientDataSet;
E: EReconcileError; UpdateKind: TUpdateKind;
var Action: TReconcileAction);
procedure BtnDeltaClick(Sender: TObject);
procedure IBOQueryBairroUpdateError(DataSet: TDataSet; E: EDatabaseError;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
procedure DataSetProviderBairroUpdateError(Sender: TObject;
DataSet: TCustomClientDataSet; E: EUpdateError;
UpdateKind: TUpdateKind; var Response: TResolverResponse);
procedure IBOQueryBairroEditError(DataSet: TDataSet; E: EDatabaseError;
var Action: TDataAction);
procedure IBOQueryBairroPostError(DataSet: TDataSet; E: EDatabaseError;
var Action: TDataAction);
procedure IBOQueryBairroError(Sender: TObject; const ERRCODE: Integer;
ErrorMessage, ErrorCodes: TStringList; const SQLCODE: Integer;
SQLMessage, SQL: TStringList; var RaiseException: Boolean);
procedure ClientDataSetBairroBeforePost(DataSet: TDataSet);
procedure IBOQueryBairroDeleteError(DataSet: TDataSet;
E: EDatabaseError; var Action: TDataAction);
private
{ Private declarations }
a: integer;
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
//código do ClientDataSetBairro
procedure TForm1.ClientDataSetBairroBeforePost(DataSet: TDataSet);
begin
a := 5; //truque para parar aqui com breakpoint. obs colocar a variável no private, caso colocar local, o otimizador ignora essa linha, pois a recebe uma atribuição e náo é usado em nenhum lugar
end;
procedure TForm1.BtnAbrirClientDataSetClick(Sender: TObject);
begin
ClientDataSetBairro.Open;
end;
procedure TForm1.BtnFecharClientDataSetClick(Sender: TObject);
begin
ClientDataSetBairro.Close;
end;
procedure TForm1.BtnDataRequestClick(Sender: TObject);
begin
ClientDataSetBairro.Data := ClientDataSetBairro.DataRequest('BAIRRO<>' + QuotedStr('CENTRO')); //pegando todos bairros execeto 'CENTRO'
end;
procedure TForm1.BtnApplyUpdatesClick(Sender: TObject);
begin
ClientDataSetBairro.ApplyUpdates(0);
end;
procedure TForm1.ClientDataSetBairroReconcileError(
DataSet: TCustomClientDataSet; E: EReconcileError;
UpdateKind: TUpdateKind; var Action: TReconcileAction);
begin
Action := HandleReconcileError(DataSet, UpdateKind, E);
end;
procedure TForm1.BtnDeltaClick(Sender: TObject);
begin
ClientDataSetDelta.Data := ClientDataSetBairro.Delta;
end;
//código do DataSetProviderBairro
function TForm1.DataSetProviderBairroDataRequest(Sender: TObject;
Input: OleVariant): OleVariant;
begin
showmessage('DataRequest' + input); //acompanhar o parâmetro e filtrar todos registros menos os "CENTRO"
with (Sender as TDataSetProvider) do
begin
DataSet.Open;
DataSet.Filter := Input;
DataSet.Filtered := True;
DataSet.First;
Result := Data;
DataSet.Filtered := False;
end;
end;
procedure TForm1.DataSetProviderBairroBeforeGetRecords(Sender: TObject;
var OwnerData: OleVariant);
begin
showmessage('BeforeGetRecords');
end;
procedure TForm1.DataSetProviderBairroUpdateError(Sender: TObject;
DataSet: TCustomClientDataSet; E: EUpdateError; UpdateKind: TUpdateKind;
var Response: TResolverResponse);
begin
showmessage('DataSetProvider1 UpdateError');
end;
//código do IBOQueryBairro
procedure TForm1.IBOQueryBairroBeforeOpen(DataSet: TDataSet);
begin
showmessage('IBOQueryBairro BeforeOpen');
end;
procedure TForm1.IBOQueryBairroBeforeClose(DataSet: TDataSet);
begin
showmessage('IBOQueryBairro BeforeClose');
end;
procedure TForm1.IBOQueryBairroError(Sender: TObject; const ERRCODE: Integer;
ErrorMessage, ErrorCodes: TStringList; const SQLCODE: Integer;
SQLMessage, SQL: TStringList; var RaiseException: Boolean);
begin
showmessage('IBOQueryBairro Error');
end;
procedure TForm1.IBOQueryBairroEditError(DataSet: TDataSet; E: EDatabaseError;
var Action: TDataAction);
begin
showmessage('IBOQueryBairro EditError');
end;
procedure TForm1.IBOQueryBairroPostError(DataSet: TDataSet; E: EDatabaseError;
var Action: TDataAction);
begin
showmessage('IBOQueryBairro PostError');
end;
procedure TForm1.IBOQueryBairroDeleteError(DataSet: TDataSet;
E: EDatabaseError; var Action: TDataAction);
begin
showmessage('IBOQueryBairro DeleteError');
end;
procedure TForm1.IBOQueryBairroUpdateError(DataSet: TDataSet; E: EDatabaseError;
UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
begin
showmessage('IBOQueryBairro UpdateError');
end;
end.
Neste ponto resolvi forçar uma exceção de registro único. Como tinha colocado ShowMessages em todos os eventos de erros do DataSet e do TDataSetProvider e nenhum estava sendo disparado, embora recebia a mensagem de violação, comecei desconfiar que as exceções estavam sendo levantadas pelo post do TClientDataSet.
Aí comecei debugar a VCL https://www.devmedia.com.br/post-15832-Pente-fino-na-VCL--desvendando-de-forma-pratica-segredos-ocultos.html e percebi que a exceção era levantada no método abaixo na linha do Check, considerando que eu editei o campo BAIRRO de um registro para ele ficar igual a outro registro.
procedure TCustomClientDataSet.InternalPost;
begin
inherited;
if State = dsEdit then
Check(FDSCursor.ModifyRecord(ActiveBuffer)) else /AQUI NO CHECK
Check(FDSCursor.InsertRecord(ActiveBuffer));
if AggregatesActive then
DoAggUpdates(State = dsEdit);
end;
procedure TCustomClientDataSet.Check(Status: DBResult);
var
ErrMsg: array[0..2048] of Char;
begin
if Status <> 0 then
begin
FDSBase.GetErrorString(Status, ErrMsg);
raise EDBClient.Create(ErrMsg, Status);
end;
end;
Mas não entendia porque o TClientDataSet estava levantando a Exceção se ele é para manter os dados em cache e sendo que o ApplyUpdates ainda não havia sido disparado neste ponto, alias estava longe de ser disparado.
Aí fui perguntar em fóruns:
"Alguém saberia explicar porque ele preconiza esta checagem? Se alguém já ouviu ou leu alguma explicação para o tal comportamento, comente aqui."
Do pouco que entendia quem deveria tomar conta disso é o TDataSetProvider e seu Resolver interno, neste caso, junto com o TSQLResolver.
Enquanto isso seguia a saga de desvendar o mistério. E me perguntava: será que haveria um conjungo de checagens para problemas recorrentes que seriam previamente feitas pelo próprio TClientDataSet para evitar que o Delta vá até o DataSetProvider para só lá colher a exceção?
Enquanto aguardava uma resposta fui dormir. Mas à noite me ocorreu o seguinte: será que o TClientDataSet já estaria verificando entre os registros que estão em CACHE por inconsistências e tentando amenizer o trafego até o lado server? Pensei, amanhã cedo vou simular o seguinte: não vou selecionar o registro que contém "CENTRO' e vou modificar um dos registros para 'CENTRO' e vou invocar o ApplyUpdates e observar se neste caso a exceção vai levantar no lado server.
E a conslusão foi que, de fato, o objetivo é aliviar o trafego até o servidor, nos casos em que há uma inconsistência nos dados, como eu imaginava.
Considerando a tabela Bairro e os campos CODIGO e BAIRRO, veja os comportamentos nas duas simulações:
Na primeira simulação, selecionei todos registros. Um deles era 'CENTRO'. Aí eu modificava outro registro também para 'CENTRO'.
O comportamente é assim: como foi modificado basta trocar de registro que vai acontecer um Post. Isso quase todos sabem. Porém, quando ocorre o post são efetuadas algumas regras de validação, neste caso específico, o TClientDataSet "verifica/pesquisa/compara/confronta" todos os registros que estão em sua cache procurando por violações de campos únicos.
Se houver algum repetido, uma exceção é levantada antes de enviar o Delta até o Servidor ou muito antes de se pensar em executar o ApplyUpdates.
Na segunda simulação, seleceionei todos os registros diferente de 'CENTRO'. Modifiquei outro registro para "CENTRO'. Neste caso não houve exceção no post do ClientDataSet e a exceção só foi levantada após invocar o ApplyUpdates. Os eventos UpdateError do DataSetProviderBairro e ReconcileError do ClientDataSetBairro foram disparados.
Então, em resumo, o TClientDataSet faz algumas verificações em sua cache antes mesmo do ApplyUpdates. E uma destas validações é de campos únicos. Mas esta validação também será feita no lado servidor, pois um suposto registro repetido poderia não estar na chache do ClientDataSet, mas poderia estar no banco de dados.
Lembrando que outras validações são realizadas no TClientDataSet, mas meu objetivo aqui foi expôr que há uma preconização de validação ainda no lado cliente numa tentativa de evitar o trafego do delta até o server e antecipar uma série de validações possíveis.
Delta = registros modificados (inseridos, editados, deletados) e mantidos em cache pelo ClientDataSet até que seja executado um ApplyUpdates.
Abraços e até a próxima
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Artigo