Showing posts with label DataSnap. Show all posts
Showing posts with label DataSnap. Show all posts

Wednesday, October 13, 2010

Maldito bug do ConnectionBroker!

Há algum tempo venho experimentando uns crashes constantes do IDE do Delphi (tanto D6 quanto BDS2006) ao trabalhar com um projeto multi-camadas baseado no DataSnap.
De forma a abstratir a conexão do ClientDataSet do protocolo (DCOM, Socket ou WebConnection), eu utilizo um TConnectionBroker em cada DataModule onde tem TClientDataSet ligado ao servidor.

Pois bem, reparei que ao clicar no item de menu "Close All" do Delphi sempre ocorria um Access Violation, que com frequência obrigava-me a reiniciar o IDE. Resolvi identificar a causa do erro. Instalei um novo ExceptionHandler para o IDE, baseado no JclDebug, que me permitiria obter o stack trace.
Em seguida, quando o erro tornou a ocorrer, salvei o stack trace e pude então identificar a fonte do AV. Ele sempre ocorria no método TConnectionBroker.GetConnected.
Bastou 5 minutos investigando o código do TConnectionBroker para verificar onde está o problema: O ConnectionBroker não seta a property Connection para NIL internamente se o objeto Connection for destruído, caso os dois tenham Owners diferentes (estejam em DataModules diferentes).
Em design time, dentro do IDE do Delphi, frequentemente o objeto Connection é destruído ANTES do ConnectionBroker. Como a property ConnectionBroker.Connection continua diferente de NIL, qualquer referência ao Connection irá gerar um AV.

Considero isto um bug. O correto seria utilizar o mecanismo de notificação que existe no TComponent, utilizando FreeNotification e RemoveFreeNotification, de forma que o TDispatchConnection notifique o TConnectionBroker sempre que for destruído. Assim o ConnectionBroker poderá setar internamente o Connection para nil. Detalhe: Este problema só ocorre em DesignTime, dentro do IDE. Em testes não consegui fazer o problema se repetir dentro de aplicações.

Para implementar este comportamento em um descendente customizado do TConnectionBroker seria necessário sobrecarregar o método SetConnection, que é privado. Logo, tive que criar uma "cópia" do TConnectionBroker em outra unit, e dar outro nome para ele, TConectionBrokerEx.
O método SetConnection do meu TConnectionBrokerEx ficou assim (o resto do código é cópia exata do código do TConnectionBroker):
procedure TConnectionBrokerEx.SetConnection(const Value: TCustomRemoteServer);
resourcestring
SNoConnectToBroker = 'Connection not allowed to TConnectionBroker';
begin
if FConnection <> Value then
begin
if Value = Self then
raise Exception.Create(SNoCircularConnection)
else
if Assigned(Value) and (Value is TConnectionBroker) then
raise Exception.Create(SNoConnectToBroker);
// As linhas abaixo marcadas com {*} foram adicionadas para promover a
// notificação entre Connection e ConnectionBroker quando os dois possuem
// Owners diferentes (estão em DataModules/Forms diferentes)
if Assigned(FConnection) then {*}
FConnection.RemoveFreeNotification(Self); {*}
FConnection := Value;
if Assigned(FConnection) then {*}
FConnection.FreeNotification(Self); {*}
end;
end;
Substituí todos os TConnectionBroker do meu projeto por TConnectionBrokerEx e pus um fim definitivo nos AV's que me forçavam reiniciar o IDE dezenas de vezes por dia! :-)

Thursday, November 19, 2009

Faça sua aplicação ADO + DataSnap Voar, revisited!

Tenho andado sem tempo de postar, mas vamos lá.
A minha modificação referente ao meu post anterior sobre melhoria de performance no DataSnap, mais especificamente quando está obtendo dados de um DataSet ADO (dbGo), é chamar o DisableControls do DataSet ANTES de obter os dados.
Porquê isto tem significativo impacto na performance do ADO? A resposta está na unit ADODB.pas mais especificamente no método InternalGetRecord, como mostrado abaixo:


if (BookmarkSize > 0) and ((adRecDeleted and RecordStatus) = 0) then
begin
BookmarkFlag := bfCurrent;
Bookmark := Recordset.Bookmark;
if ControlsDisabled then
RecordNumber := -2 else
RecordNumber := Recordset.AbsolutePosition;
end else
BookmarkFlag := bfNA;


Note que no interior do método, um trecho do código que SEMPRE é executado contém "if ControlsDisabled". Quando ControlsDisabled retorna FALSE, é chamado o método AbsolutePosition do Recordset que é lentoooooooo, muito lento!

Durante o processo de prover os registros do ADODataSet não é necessário saber a posição absoluta no RecordSet, portanto não há necessidade nenhuma disso!

Teríamos duas soluções: Editar o ADODB.pas uma vez que o método InternalGetRecord não é virtual ou dinâmico (não cabendo então override), ou chamar DisableControls na mão, antes de abrir o ADODataSet a partir do DataSet provider.

Preferi não mexer no ADODB.pas (por enquanto!), e sim criar um descendente do DataSetProvider, chamado por mim de TDataSetProviderEx, que chamasse explicitamente o DisableControls ANTES de prover os registros.

Um porém é: Não se pode chamar DisableControls em um DataSet master que participa de uma relação Master-Detail, pois o detalhe não ficaria sincronizado com o master. Então, como fazer? Bem, a minha solução basicamente está em fazer um novo método InternalGetRecords do DataSetProviderEx, como abaixo:


function TDataSetProviderEx.InternalGetRecords(Count: Integer; out RecsOut: Integer;
Options: TGetRecordOptions; const CommandText: WideString;
var Params: OleVariant): OleVariant;
var
CanDisableControls: boolean;
begin
CanDisableControls := DSCanDisableControls;
if CanDisableControls then
DataSet.DisableControls;
try
Result := inherited InternalGetRecords(Count, RecsOut, Options, CommandText, Params);
finally
if CanDisableControls then
DataSet.EnableControls;
end;
end;


onde DSCanDisableControls é o método:


function TDataSetProviderEx.DSCanDisableControls: boolean;
begin
Result := SmartDisableControls and not IsMaster;
end;


SmartDisableControls é uma propriedade que incorporei à classe TDataSetProviderEx e indica se é desejável tentar chamar DisableControls antes de prover os registros ou não. Esta propriedade é TRUE por default pois não vejo motivo para não chamar DisableControls antes. O método IsMaster verifica se existem detalhes do DataSet, ou seja, se o DataSet participa como MASTER numa relação Master/Detail:


function TDataSetProviderEx.IsMaster: boolean;
var
List: TList;
begin
Result := False;
if Assigned(DataSet) then
begin
List := TList.Create;
try
DataSet.GetDetailDataSets(List);
Result := List.Count > 0;
finally
List.Free;
end;
end;
end;


Caso o DataSet não seja Master e caso a propriedade SmartDisableControls for TRUE, então DisableControls será chamado antes de se prover os registros. O ganho de performance é ENORME em DataSets ADO. Em alguns casos que eu testei chegou a 500% de ganho (DataSets com 30.000 registros ou um pouco mais). O ganho é tão mais perceptível quanto maior for o DataSet ADO que proverá os registros.

A minha classe TDataSetProviderEx já está em vários sistemas em produção, entre eles Windows Services rodando 24x7, há pelo menos 6 meses, ou seja, funciona! E o ganho de performance é considerável! Lembrando: O ganho de performance ocorre para DataSets que usam ADO (dbGo) do Delphi. DBExpress e outros mecanismos de acesso à dados não possuem este "defeito de nascença" e portanto não serão afetados em sua performance.

Agora só falta acabar de empacotar o código do DataSetProviderEx e fazer o seu upload! :-)

Sunday, August 2, 2009

Make your ADO + DataSnap application FLY!

Note: These tests are valid if you are using DataSnap in conjunction with ADO for DB access.

I've been using DataSnap very successfully for years now. Lately I've been optimizing it for the best possible performance, specially when dealing with a large number of records.

Andreas Hausladen did a great job with his Midas SpeedFix, but there is more.

I have a simple table named "streets" containing some fields: ID (Integer), NAME (varchar[50]) and a few other fields (it is a large DataSet contaning all street names of all cities of my state). Well, I'm using ADO (dbGo) to access it. The table has 50,000 records. I have an ADOQuery with this SQL statement:

SELECT * FROM streets

Connected to this ADOQuery I have a DataSetProvider and a ClientDataSet. When I open the ADOQuery it took exactly 1.5 seconds, but when I open the ClientDataSet it took 59 seconds!!! 59 seconds to open a query is completely out of question in a production environment.

Some people will say: "ADO didn't fetch all the records, so the difference". That's not true. You can open the ADOQuery and go to the LAST record (fetching all the 50,000 records), and the time is the same. 1.5 seconds to fetch 50,000 records! Nice number.

Other people will say: "You can't have a 50,000 records ClientDataSet!". Can't I? Why not? 15 years ago they told me to "fetch only a few records, not thousands!". Nowadays we have 8 Gb RAM application servers, 10 Mpbs internet and the same limits remain? ClientDataSets can be used as a cache mechanism, freeing the database server from returning the same resultsets over and over again, but I need to put more than a few hundred records in it! Besides that, there are briefcase model applications. How one can create a briefcase application using DataSnap if the DataSnap framework imposes such a low limit?

So I decided to find out why ADO takes only 1.5 seconds and DataSnap took 40 times more. Profiling Provider.pas I found out that most of that time is spent inside TDataSetProvider.InternalGetRecords method. TDataSetProvider.InternalGetRecords calls inherited TBaseProvider.InternalGetRecords, and it calls TDataSetProvider.CreateDataPacket. There is a really long chain of method calls, but in the end I discovered that the bottleneck is TDataPacketWriter.WriteDataSet. Looking at this method we may have a clue:

while (not DataSet.EOF) and (Result <>
As we can see, there is a while loop going through all records of the DataSet. Well, we know that this kind of construction can be very slow because each DataSet.Next call fires a long chain of events if DataSet.ControlsDisabled = False, that is, if we didn't call DataSet.DisableControls before entering the loop.

So, just for testing purposes, I did a little change in the code, like that:

ADOQuery1.DisableControls;
try
ClientDataSet.Open;
finally
ADOQuery1.EnableControls;
end;

The results were incredible: From 59 seconds, the time spent drop to 3.3 seconds, 95% better than before! The difference is bigger, the bigger is the number of rows in the DataSet.

There are two practical problems with this approach:
1) Call DataSet.DisableControls programatically in every point of the code where there is a ClientDataSet connected to a DataSet provider is totally out of the question;
2) DisableControls cannot be used when the DataSet is acting as a Master, in a Master/Detail relationship.

Another interesting think that I've found is that DBExpress doesn't suffer the same problem, that is, DisableControls makes little difference (I will benchmark it too).

I will write about the solution I've created in a new post.

Faça sua aplicação ADO + DataSnap VOAR!

Nota: Estes testes são válidos somente para aplicações que utilizam o mecanismo de acesso a dados ADO (ou dbGo).

Tenho usado o DataSnap de forma bem sucedida há muitos anos. Ultimamente venho otimizando os meus sistemas que usam DataSnap para a melhor performance possível, especialmente quando estou lidando com DataSets com muitos registros.

Andreas Hausladen fez um trabalho excelente em seu Midas SpeedFix, mas tem mais.

Eu tenho uma tabela simples "RUAS" contendo alguns campos: ID (Integer), NOME (varchar[50]) e mais alguns poucos campos. Bem, estou usando ADO para o acesso aos dados. A tabela tem 50.000 registros. Eu tenho um ADOQuery com este comando SQL:
SELECT * FROM ruas

Conectado a este ADOQuery eu tenho um DataSetProvider e a ele um ClientDataSet. Quando eu abro o ADOQuery leva 1,5 segundos, mas quando eu abro o ClientDataSet leva exatamente 59 segundos!!! 59 segundos para abrir uma query num ambiente de produção está completamente fora de questão.

Alguns vão dizer: "O ADO não fez o fetch de todos os registros, daí a diferença". Isto não é verdade. Você pode abrir o ADOQuery e ir para o último registro (efetivamente fazendo um fetch all) e o tempo será o mesmo. 1,5 segundoso para recuperar 50.000 registros! Um bom número.

Outros irão dizer: "Você não pode ter um ClientDataSet com 50.000 registros!". Não posso? Porquê não? 15 anos atrás me disseram "traga apenas alguns registros, não milhares". Atualmente temos servidores de aplicação com 8 Gb de RAM pelo menos, internet banda larga com 10 Mbps e os mesmos limites de 15 anos atrás se aplicam?

O ClientDataSet pode ser usado como mecanismo de cache, liberando o servidor de banco de dados de retornar o mesmo resultset repetidamente, mas eu preciso colocar mais do que umas poucas centenas de registros nele! Além disso, existem aplicações que seguem o modelo "Briefcase". Como alguém pode desenvolver uma aplicação com este modelo se o framework DataSnap impõe um limite tão baixo?

Então eu decidi descobrir porquê o ADO leva 1,5 segundos e o DataSnap leva 40 vezes mais. Fiz um profile da unit Provider.pas e descobri que a maior parte do tempo é gasta dentro do método TDataSetProvider.InternalGetRecords. O método TDataSetProvider.InternalGetRecords chama o TBaseProvider.InternalGetRecords herdado, que por sua vez chama TDataSetProvider.CreateDataPacket. Existe uma longa cadeia de chamadas de métodos, mas no final eu descobri que o gargalo é o método TDataPacketWriter.WriteDataSet. Analisando este método talvez tenhamos uma dica:

while (not DataSet.EOF) and (Result < RecsOut) do
begin
FIDSWriter.PutField(fldIsChanged, 1, @B);
for i := 0 to High(Info) do
Info[i].PutProc(@Info[i]);
Inc(Result);
if Result < RecsOut then
DataSet.Next;
end;

Como podemos ver, existe um loop while percorrendo todos os registros do DataSet. Bem, nós sabemos que este tipo de cenário pode ser bem lento pois cada chamada a DataSet.Next dispara uma longa sequência de eventos se DataSet.ControlsDisabled = False, ou seja, se não chamarmos o método DataSet.DisableControls antes do loop.

Então, só para testar o efeito de DisableControls na velocidade da obtenção dos dados pelo DataSetProvider, eu fiz uma pequena alteração no código onde eu o ClientDataSet era aberto, da seguinte forma:

ADOQuery1.DisableControls;
try
ClientDataSet.Open;
finally
ADOQuery1.EnableControls;
end;

O resultado que eu obtive foi incrível: Dos 59 segundos originais, o tempo caiu para 3,3 segundos, uma melhora de quase 95% no tempo de abertura do ClientDataSet!
Eu pude verificar que a diferença de performance é tanto maior quanto mais registros existem no ClientDataSet.

Existem dois problemas de ordem prática nesta abordagem:
1) Usar DisableControls programaticamente em todos os pontos do sistema onde se tem um ClientDataSet ligado a um DataSetProvider é totalmente inviável;
2) DisableControls não pode ser utilizado quando a tabela é master em uma relação Master/Detail.

Outra coisa interessante que eu descobri é que o DBExpress não sofre do mesmo problema, isto é, DisbleControls faz pouca diferença. De qualquer forma, irei medir o desempenho do DBExpress, com e sem DisableControls para comparar os resultados.

A solução que eu criei terá abordada num próximo post.

Friday, July 17, 2009

DataSnap Patch

Lendo o forum da Codegear encontrei um post com uma interessante listagem de patches para o DataSnap do Delphi versões 5, 6 e 7. Uma consulta aos patches mostra que vários deles são bem úteis e contornam erros relativamente comuns:

Unit Provider.pas:

1802
2338
2638
2792
4006
4014

Unit DBClient.pas:

430
1266
1381
1471
1520
1982
2333
4301
5707

Alguns destes patches, senão todos, foram publicados há bastante tempo pelo papa do DataSnap, Dan Miser no site www.distribucon.com. Infelizmente o código fonte dos bug fixes do DataSnap neste site não estão mais acessíveis devido a um erro no site (eventualmente consegue-se uma cópia dos fontes destes patches no cache do google).

Utilizando o WinMerge criei dois patches (arquivos diff) que podem ser utilizados com a ferramenta Patch for Windows. Aplicados aos arquivos Provider.pas e DBClient.pas originais da versão específica do Delphi, gerarão os arquivos fontes modificados em sua versão final, contendo todos os patches.
Atenção: Não utilize o arquivo patch em fontes originais de outra versão que não a especificada.

Provider_patch_D602.txt (Delphi 6.02)
DBClient_patch_D602.txt (Delphi 6.02)

Provider_patch_D71.txt (Delphi 7.1)
DBClient_patch_D71.txt (Delphi 7.1)

Um outro patch para o Provider.pas, contendo todos estes patches e ainda as alterações de um outro post meu sobre modificações no TDataSetProvider pode ser baixado aqui:

Provider_patch_enh_D602.txt
(Delphi 6.02 - Bug fixes + enhancements)
Provider_patch_enh_D71.txt
(Delphi 7.1 - Bug fixes + enhancements)

O download do executável patch.exe (zipado) pode ser obtido diretamente aqui.

Para quem nunca usou o patch.exe, a linha de comando para transformar o seu fonte original, digamos Provider.pas, no arquivo modificado será:

patch.exe -p1 -b Provider.pas < Provider_patch_D602.txt

Observação: Todos os update packs do Delphi 6 e 7 foram aplicados antes da geração do arquivo de Patch (Delphi 6 Update pack 1 e 2, Delphi 7 Update Pack 1). Logo, estes updates devem ser aplicados ANTES do patch.

Sunday, June 28, 2009

Erro no DataSnap: "LinkFields to detail must be unique"

Estive brigando com um erro "LinkFields to detail must be unique" do DataSnap, numa tela que continha 3 DataSets numa relação Mater-Detail típica. O mais interessante é que este tipo de relação é bem comum e eu já desenvolvi com DataSnap este tipo de construção incontáveis vezes, mas este erro resolveu aparecer para ficar.

Considere as seguintes tabelas numa relação Master-Detail:Os campos id_master (PK), id_detail (PK) e id_detail2 (PK) são inteiros e os desc_master, desc_detail e desc_detail2 são Varchar. O campo id_master é FK na tabela detalhe de primeiro nível, table_detail. O campo id_detail é FK na tabela detalhe de segundo nível, table_detail2. As PKs são todas auto-incremento com valor gerado no servidor (de aplicação).

Tenho um DataModule com 3 DataSets (no meu caso, usei TADOQuery) contendo os seguintes comandos SQL:

  • qryMaster:
SELECT * FROM table_master

  • qryDetail:
SELECT * FROM table_detail WHERE id_master = :id_master

  • qryDetail2:
SELECT * FROM table_detail2 WHERE id_detail = :id_detail


Existem ainda 2 componentes TDataSource ligando o qryDetail ao qryMaster (dsMaster) pela propriedade DataSource, e também um ligando o qryDetail2 ao qryDetail (dsDetail). Desta forma, tenho um DataSetProvider ligado ao DataSource dsMaster que irá prover os dados aos meus ClientDataSet. Eis meu DataModule:


O cdsMaster está ligado ao DataSetProvider, prvMaster, o cdsDetail ligado ao DataSetField cdsMasterqryDetail, e o cdsDetail2 está ligado ao DataSetField cdsDetailqryDetail2. Uma construção comum quando se trata de relação master-detail usando DataSnap.

O fato de estar tudo junto (apenas 2 camadas) facilita o entendimento e o debug. Mas poderia estar distribuído, com as queries e o provider num servidor de aplicação e os ClientDataSets na aplicação cliente.

ProviderFlags dos campos configurados corretamente (incluído pfInKey para id_master, id_detail e id_detail2 nas 3 queries), um form simples com 3 DBNavigators e 3 DBGrids ligados aos 3 ClientDataSets. Pois bem, era de se esperar que tudo funcionasse as mil maravilhas na primeira tentativa, certo? Errado!

Ao rodar o programa, inseri dados na tabela master e salvei, chamando então cdsMaster.ApplyUpdates. Tudo Ok. Inseri também registros no cdsDetail, tudo Ok. Ao rodar a aplicação e inserir registros no cdsDetail2 (o detalhe de segundo nível) e chamar o cdsMaster.ApplyUpdates, kabooommmm! Lá vem o erro "Link Fields to detail must be unique". Não fazia o menor sentido uma vez que os FK's são corretamente preenchidos automaticamente pelo DataSnap e os ID's são preenchidos com valores únicos negativos. Não havia nada errado!

O erro acontece ao aplicar os updates. Mas nem chega ao Provider. O erro acontece mesmo no ClientDataSet ANTES de enviar o Update ao Provider.
Aí fui debugar usando o DBClient.pas. O erro ocorre no método TCustomClientDataSet.InternalPost, na linha:


Check(FDSCursor.InsertRecord(ActiveBuffer));


Ou seja... Como isto ocorre dentro do Midas.dll (ou MidasLib.dcu), não há como debugar além deste ponto.

Usei o Google atrás de uma solução, fiz todas as alterações possíveis que pude imaginar para ver se o erro cessava, mas nada funcionava.

Depois de um tempo pensando, eu me lembrei que esta construção onde a PK da tabela master vira FK da tabela detalhe não me é usual. Eu sempre propago as chaves do mestre para compor a chave do detalhe, da forma:

table_detail:
  • id_master (PK)
  • id_detail (PK)
  • desc_detail

table_detail2:
  • id_master (PK)
  • id_detail (PK)
  • id_detail2 (PK)
  • desc_detail
Então resolvi mudar a ordem dos campos no ClientDataSet. No cdsDetail coloquei os campos na ordem (1) id_master, (2) id_detail e (3) desc_detail. No cdsDetail2 fiz o mesmo e coloquei na ordem (1) id_detail, (2) id_detail2 e (3) desc_detail2. Ou seja, o LinkField no detalhe deve ser o primeiro campo no DataSets!

Após isto o Update ocorre normalmente, sem erros. Ainda esta semana vou anexar o código fonte completo do projeto de exemplo.

Wednesday, March 4, 2009

Patching TDataSetProvider

Observação: Se aplica ao Delphi 6 e 7.

Depois de gastar muitas horas implementando alguma "mágica" e novas funcionalidades em um descendente direto do TDataSetProvider que uso em meus sistemas, usando o BDS 2006, parti para implementar as mesmas funcionalidades no TDataSetProvider do Delphi 6 (a empresa na qual trabalho possui sistemas em Delphi 6 cuja migração imediata para um compilador superior é inviável). Simplesmente abri minha unit no Delphi 6 e fui compilar em um projeto vazio e POWWWW!!!
Problema: Não existe o método DoBeforeUpdateRecord no TBaseProvider (ancestral do TDataSetProvider).
Toda a nova funcionalidade estava baseada em um novo DoBeforeUpdateRecord do TDataSetProvider. O que eu queria fazer é relativamente simples: Fazer algo que o TDataSetProvider padrão não faz, antes do update de cada registro, mais precisamente nos inserts.

O evento DoBeforeUpdateRecord do Provider é chamado pela classe Resolver durante o processo de update (no BDS 2006 em diante). Sem o método DoBeforeUpdateRecord virtual, eu teria que arrumar outra alternativa.
Tentei o InternalApplyUpdates, sem chance! Não tem como fazer o que queria por lá. ApplyUpdates então? Sem chance de novo! O método é estático e mesmo que fosse dinâmico eu teria que desviar a chamada para o evento BeforeUpdateRecord original, uma coisa que não me agradou.....
Tentei durante um bom tempo e sempre esbarrava em métodos estáticos que deveriam ser dinâmicos, protegidos que deveriam ser públicos, propriedades que deviam ser públicas e eram privadas...
Resultado: Não é viável fazer no Delphi 6!!! E então?

Solução: Bem, não gosto de modificar o fonte da VCL, mas neste caso é bem justificável e imprescindível. De quebra ainda corrigiria um bug antigo (http://www.distribucon.com/midasbug/index.aspx).

As modificações são simples, retiradas da própria unit Provider.pas porém da versão BDS 2006. Não têm absolutamente nenhum impacto no funcionamento e abrem grandes possibilidades de customização dos DataSetProviders. O mesmo pode ser feito no Delphi 7, e após o resultado que obtive, eu aconselho.

Segue a lista de modificações que fiz. As linhas adicionadas ou modificadas estão marcadas em azul:


TBaseProvider = class(TCustomProvider)
protected
procedure DoBeforeUpdateRecord(SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind; var Applied: Boolean); virtual;
procedure DoAfterUpdateRecord(SourceDS: TDataSet; DeltaDS: TCustomClientDataSet;
UpdateKind: TUpdateKind); virtual;
end;

TUpdateTree = class(TObject)
public
procedure Clear;
function DoUpdates: Boolean;
procedure RefreshData(Options: TFetchOptions);
procedure InitErrorPacket(E: EUpdateError; Response: TResolverResponse);
procedure InitData(ASource: TDataSet);
procedure InitDelta(const ADelta: OleVariant); overload;
procedure InitDelta(ADelta: TPacketDataSet); overload;
property Data: Pointer read FData write FData;
property Delta: TPacketDataSet read FDeltaDS;
property DetailCount: Integer read GetDetailCount;
property Details[Index: Integer]: TUpdateTree read GetDetail;
property ErrorDS: TPacketDataSet read GetErrorDS;
property HasErrors: Boolean read GetHasErrors;
property Name: string read FName write FName;
property Parent: TUpdateTree read FParent;
property Source: TDataSet read FSourceDS;
property IsNested: Boolean read GetIsNested;
end;

TCustomResolver = class(TComponent)
public
property Provider: TBaseProvider read FProvider;
property UpdateTree: TUpdateTree read FUpdateTree;
end;

// Implementation

procedure TBaseProvider.DoBeforeUpdateRecord(SourceDS: TDataSet;
DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind; var Applied: Boolean);
begin
if Assigned(FBeforeUpdateRecord) then
FBeforeUpdateRecord(Self, SourceDS, DeltaDS, UpdateKind, Applied);
end;

procedure TBaseProvider.DoAfterUpdateRecord(SourceDS: TDataSet;
DeltaDS: TCustomClientDataSet; UpdateKind: TUpdateKind);
begin
if Assigned(FAfterUpdateRecord) then
FAfterUpdateRecord(Self, SourceDS, DeltaDS, UpdateKind);
end;

procedure TDataSetProvider.SetDataSet(ADataSet: TDataSet);
begin
FDataSet := ADataSet;
if Assigned(FDataSet) then
FDataSet.FreeNotification(Self);
end;

function TCustomResolver.InternalUpdateRecord(Tree: TUpdateTree): Boolean;
var
RecNoSave: Integer;
Applied: Boolean;
UpdateKind: TUpdateKind;
E: Exception;
PrevErr, Err: EUpdateError;
begin
PrevErr := nil;
Err := nil;
Tree.Delta.UseCurValues := False;
while True do
try
UpdateKind := Tree.Delta.UpdateKind;
if ((UpdateKind = ukInsert) and (FPrevResponse in [rrMerge, rrApply])) or
((FPrevResponse = rrMerge) and Tree.Delta.HasMergeConflicts) then
DatabaseError(SInvalidResponse);
Applied := False;
RecNoSave := Tree.Delta.RecNo;
try
Provider.DoBeforeUpdateRecord(Tree.Source, Tree.Delta, UpdateKind, Applied); (* ACM patch *)
finally
if Tree.Delta.RecNo <> RecNoSave then
Tree.Delta.RecNo := RecNoSave;
end;
if not Applied then
case UpdateKind of
ukModify:
begin
if poDisableEdits in Provider.Options then
raise Exception.CreateRes(@SNoEditsAllowed);
DoUpdate(Tree);
end;
ukDelete:
begin
if poDisableDeletes in Provider.Options then
raise Exception.CreateRes(@SNoDeletesAllowed);
DoDelete(Tree);
end;
ukInsert:
begin
if poDisableInserts in Provider.Options then
raise Exception.CreateRes(@SNoInsertsAllowed);
DoInsert(Tree);
end;
end;
Provider.DoAfterUpdateRecord(Tree.Source, Tree.Delta, UpdateKind); (* ACM patch *)
if (poPropogateChanges in Provider.Options) and Tree.Delta.NewValuesModified then
LogUpdateRecord(Tree);
Break;
except
E := AcquireExceptionObject;
PrevErr.Free;
PrevErr := Err;
Err := IProviderSupport(Tree.Source).PSGetUpdateException(E, PrevErr);
if HandleUpdateError(Tree, Err, FMaxErrors, FErrorCount) then
begin
Tree.Delta.UseCurValues := True;
Continue;
end else
break;
end;
PrevErr.Free;
Err.Free;
FPrevResponse := rrSkip;
Result := FErrorCount <= FMaxErrors;
end;

Após a alteração no código fonte da VCL (você fez backup do original, certo?) basta salvá-lo, incluí-lo em um projeto e compilar o projeto.
A melhor forma de utilizar patches deste tipo para substituir o código original da DCU que geralmente é linkada ao executável é criar um diretório de patches para o seu Delphi, colocar lá os arquivos fontes modificados (neste caso Provider.pas) e incluir este caminho no LibraryPath do seu IDE.

Em um próximo post vou escrever sobre as modificações que fiz no TDataSetProvider, ou melhor, no descendente dele que uso.

Saturday, January 24, 2009

Midas Speed Fix 1.1 update

Andy did it again ;-)

The best Midas update EVER!

Well, go there and see for yourself: http://andy.jgknet.de/blog/?p=444

In my tests: At least 35% performance improvement inserting 30,000 records or less. 45% performance improvement inserting 50,000 records in a ClientDataSet. Very impressive.
But one thing you should know: MidasLib.dcu and FastMM MUST be linked to your module.





Warp speed in DataSnap, Mr. Sulu

Andreas Hausladen, o guru de patch em IDE e units Delphi arrumou mais uma...

Go here and see for yourself:

http://andy.jgknet.de/blog/?p=431

O patch por si só já é um fenômeno. O mais incrível foi ter feito isto em 2 horas SEM OS FONTES, sendo que a CodeGear não corrigiu em 10 anos.

Andreas, you are the man!

Os números de performance eu vou colocar num post em inglês para os que os usuários Delphi saibam como foi brabo...

Friday, November 21, 2008

Cache de DataSets em aplicações multi-camadas III

OK, você tem um mecanismo que faz o cache de seus dados localmente e não precisa trafegar os dados a todo momento, certo? Mas como descobre se o seu cache reflete fielmente os dados no banco de dados?
Bem, existem bancos de dados que permitem notificação de aplicações clientes. Mas meu objetivo é criar algo genérico que não esteja atrelado a uma implementação específica de SGBD, ou seja, não será possível usar mecanismos de notificação.
Então o que resta é fazer a consulta diretamente. Eu sempre usei, em todas as tabelas, um campo que me informa a data/hora de inclusão/alteração. Vamos supor então que este campo se chama dat_hor_edi, ou seja, "data/hora de edição". Sempre que eu incluo um novo registro na tabela eu atualizo este campo com a data/hora atual. Procedo da mesma forma quando eu altero o conteúdo do registro. Então uma consulta como

SELECT MAX(dat_hor_edi) FROM minha_tabela


sempre me fornecerá a data/hora da última inclusão e/ou edição que ocorreu nesta tabela. Assim, se eu executei a minha consulta na hora "H" e a consulta acima me retornou um valor para "dat_hor_edi" MAIOR que "H", eu sei que a tabela sofreu inclusão e/ou edição, e meu cache não reflete mais a situação atual do banco de dados, e então o cache precisa ser atualizado.
Mas aí tem um problema: Esta consulta não me garante as exclusões. Claro, se eu excluir um registro desta tabela a consulta acima não me mostrará isto.
Então precisamos de um outro dado para garantir a integridade do cache. A data de última edição/inclusão funcionará perfeitamente junto com o número de registros da consulta. Estes dois números juntos me permitem saber com certeza se meu cache tem ou não uma cópia fiel da tabela no banco de dados. Eu precisaria de uma consulta do tipo:


SELECT
COUNT(*) as TblRecCount,
MAX(dat_hor_edi) as TblTimeStamp
FROM minha_tabela


O número de registros de uma consulta é prontamente obtido, mas o campo "dat_hor_edi" precisa ser atualizado de uma das duas formas:
1) Pela aplicação, sempre que houver inclusão e ou edição do registro;
2) Por um trigger no banco de dados, criado específicamente para isto;

Eu já usei e continuo usando ambas as formas. A primeira pode parecer mais complicada de se conseguir, mas não o é de fato. Basta que sua aplicação seja construída de forma metódica. Obviamente a primeira forma não prevê "marretas" feitas diretamente no banco de dados.

Então, o cache em si é composto por 3 diferentes "dados": O DataSet, o número de registros deste DataSet, e a data/hora de última inclusão/edição neste conjunto de dados. Está aí uma classe desenhada, que num pseudo-código pascal seria:


TDataSetCache = class
public
TimeStamp: TDateTime;
RecordCount: Integer;
DataSet: TDataSet;
end;


Algumas pessoas já me perguntaram: "mas então para eu saber se preciso fazer uma consulta, eu precisarei fazer antes outra consulta?". A resposta é: Sim, precisará!
Mas você em geral troca uma consulta que retorna, digamos, 1000 registros e que leva 10 segundos e uma enormidade de processamento do seu SGBD por uma outra consulta que trafega meia dúzia de bytes e leva milésimos de segundos para ser executada.
O primeiro objetivo do cache é diminuir o tráfego de dados!

Na próxima vamos implementar a classe acima, o TDataSetCache! :)

Friday, October 17, 2008

Cache de DataSets em aplicações multi-camadas II

Identificando unicamente um DataSet.

Bem, quanto aos requisitos do post anterior:

1) O gerenciador do cache deverá ser capaz de identificar unicamente um DataSet

Como identificar unicamente um DataSet obtido a partir de uma consulta a um banco de dados? Bem, no meu caso os DataSets são resultado de uma consulta SQL. Independentemente do mecanismo de acesso a dados, quando se utiliza SQL em geral temos um objeto de query (TADOQuery, TSQLQuery, etc.) e o mesmo possui uma propriedade SQL do tipo TStrings. A string que contém o comando SQL é, com certeza, um identificador único de uma query certo? Mas a string não é ideal para um identificador único, pois pode ser gigante, difícil de comparar, etc. O melhor mesmo é gerar um hash a partir do SQL. SQL diferentes deverão gerar hashes únicos com uma probabilidade de colisão suficientemente baixa.
No meu caso, optei por usar não um hash propriamente dito, mas um CRC. Optei por usar o CRC64 que me possibilita ter uma probabilidade de colisão suficientemente baixa para uma aplicação de qualquer tamanho.
Quem tiver curiosidade sobre esta probabilidade, segue um teste com o algoritmo:

http://apollo.backplane.com/matt/crc64.html

Resumindo, a probabilidade de colisão é da ordem de 1 colisão para 2E12 mensagens, o que acredito que seja mais do que satisfatório.

Então, passamos nossa expressão SQL por um algoritmo de geração de CRC64 e obtemos um identificador único para nossa sentença SQL, como por exemplo CF247CF5C26FF896.

Mas temos ainda um problema: A mesma sentença de SQL executada contra dois bancos de dados diferentes (mas com estrutura compatível, como por exemplo um banco de dados de produção e outro de homologação) possuem identificadores idênticos porém o DataSet resultante é diferente.
Neste caso devemos incorporar ao identificador único do DataSet um identificador do banco de dados. O banco de dados geralmente é identificado pelo servidor, nome do banco ou schema, usuário, etc. Muitas formas são possíveis para se gerar um identificador do banco de dados. No meu caso, usando ADO como mecanismo de acesso a dados, optei por seguir a mesma linha de raciocínio e usar o CRC64 da string de conexão (propriedade ConnectionString do TADOConnection).

Então, com o CRC do SQL mais o CRC da string de conexão eu tenho um identificador único para meu DataSet que funcionará como índice. Mais sobre isto em breve.

Wednesday, April 23, 2008

Midas.dll and COM+ applications deployment issues

It is a known fact that Midas.dll needs to be registered before installing/registering a COM+ ActiveX Library under COM+, when deploying an ActiveX library written with Delphi, right?
Well... something interesting that I discovered this week: DON'T register a Midas.dll version 10.x (from BDS 2006 installation). Use a lower version - I'm using my D6 dll.
If you use version 10.x it will be registered without any errors, but when you try to install the ActiveX Library, boooom!!! You will get a "Error loading type library" error, as Midas.dll was not registered at all.
After ActiveX Library installation/registration, Midas.dll can be even deleted (If you are linking with MidasLib.dcu), as usual.

Monday, December 17, 2007

DataSnap.NET - Parte III

Recebi hoje os fontes dos projetos de Ondrej Kelle (http://tondrej.blogspot.com):

1) ASP.NET replacement of CodeGear's httpsrvr.dll

2) WinForms .NET replacement of CodeGear's Scktsrvr.exe

Ambos em C# (BDS 2006). Vou verificar e colocar para rodar em breve!

Obrigado Ondrej!