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 9, 2009

O "miserável" Windows XP

Estava lendo o blog do Erick Sasse, mais especificamente o post sobre as novidades do Delphi 2010, onde ele se refere ao Windows XP como miserável :).

Eu já penso 200% diferente do Erick. Não posso me dar ao luxo de usar em minha máquina de desenvolvimento algo que reconhecidamente tem bugs, ou as "features" do Vista, que irão impactar negativamente na minha produtividade.

Imagine eu estar desenvolvendo algo com cronograma apertado, como de hábito, e de repente descobrir que no Vista não consigo debugar a aplicação como fazia no XP? Isto parece lenda, mas aconteceu na mudança do Windows 2000 para o XP e o Delphi 5: Na época, debugar aplicações web sob o IIS ou objetos COM usando a dobradinha D5/Win2000 era incrivelmente fácil. Fazer o mesmo no XP, ainda mais antes do SP1 se tornou algo bem chato que de vez em quando simplesmente teima em não funcionar, sabe-se lá porquê. Aí você pára de desenvolver, ou seja, fazer a atividade fim do seu trabalho que paga seu salário, para passar horas no google procurando COMO fazer aquilo que você já fazia há anos, e agora não pode mais por causa da definição de "evolução" de Redmond.

Um outro exemplo prático do Vista: Até o SP1 do Vista, a cópia de arquivos na rede era tão mais lenta do que no XP que todo mundo pensava que a rede estava com problemas. Este problema por si só foi suficiente impactante para me deixar longe do Vista.

É claro que tenho máquinas com Vista e Windows 7, basicamente para testes mas não as usaria em produção. O Vista porquê não me acrescenta nada e me subtrai muito. A reconhecida melhoria de segurança do Vista não é bem vinda no meu ambiente de trabalho. É impossível, ou no mínimo extremamente chato e contraproducente, desenvolver aplicativos dos mais diversos e trabalhar sem as prerrogativas de segurança de administrador local. E a mania da Microsoft em perguntar toda hora para o usuário se tem certeza que deseja fazer alguma coisa ou pedir para você confirmar que você é você mesmo chegou a níveis intoleráveis no Vista. Não é à toa que 9 entre 10 usuários "satisfeitos" com o Vista que eu conheço desligaram o UAC e rodam em níveis de segurança próximos aos que experimentavam no "miserável" XP.
Penso que o UAC é ótimo para donas de casa que vivem recebendo links no email ou no MSN do tipo "veja as fotos do seu marido com outra" e clicam nele sem pestanejar! Mas para profissionais de TI?
O Vista é tão ruim, mas tão ruim, que a maioria do pessoal que o adotou e "gostou" dele não o usa mais, porquê instalou o Windows 7 no primeiro dia de lançamento da versão beta, travando os servidores da Microsoft com milhões de downloads simultâneos... O "amor" pelo Vista é tão grande que correram atrás do seu substituto, usando-o em suas máquinas principais mesmo estando na mais crua versão beta!

Já o Windows 7, eu não o uso em produção simplesmente porquê ainda está em release candidate, ou seja, se nem a Microsoft sabe se o Windows 7 está bom o suficiente para ser usado em PRODUÇÃO, muito menos eu, pobre mortal! Certamente irei usá-lo assim que ele estiver pronto.

A única coisa realmente boa do Vista foi o seu insucesso, se tornando o maior fracasso da história da Microsoft (mesmo a MS tendo forçado o Vista OEM goela abaixo de seus parceiros HP, Dell, etc.): Sem este fracasso, o cronograma do Windows 7 não teria sido muito adiantado, me permitindo by-passar completamente um sistema operacional da Microsoft pela primeira vez desde o mais miserável de todos, o famigerado Windows 3.11!

Em minha opinião, as palavras "inferior" ou "pior" para o XP em relação ao Vista simplesmente não se aplicam. O que é realmente importante é que eu faço mais (e tudo que preciso) numa máquina XP do que eu faria se estivesse usando o reluzente Vista Ultimate.

O reconhecimento da solidez da posição do XP vem da própria Microsoft que lançou o "Modo XP" no Windows 7, sem o qual eles mesmos desconfiam que muita gente não irá migrar para o 7. Mesmo assim, a adoção do novo SO parece ainda estar comprometida pelo fiasco do Vista.

PS: Hoje estava passeando pelo forum da Embarcadero e olha a pérola que eu encontrei:

"I intend to slowly incinerate the Vista box on my Weber barbecue
as soon as Windows 7 comes out, enjoying a good glass of
Prosecco whilst watching it burn. Vengeance is mine!"

https://forums.codegear.com/thread.jspa?threadID=22604&tstart=0

Este é mais um que "não compreende os benefícios do Vista" ;-)

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.

Thursday, July 23, 2009

Update Packs do Delphi 5

Tenho recebido um grande número de pedidos para enviar os updates Delphi 5 Enterprise Update Pack #1 e Delphi 5 ADO Update Pack #2, aparentemente porquê os arquivos não se encontram mais disponíveis no site da CodeGear. Vai entender...

Então aqui estão eles (arquivos zipados com 7zip):

Delphi 5 Enterprise Update Pack #1

Delphi 5 ADO Update Pack #2

Delph 5 Update Packs

I am receiving a large number of requests to send Delphi 5 Enterprise Update Pack #1 and Delphi 5 ADO Update Pack #2, apparently because they are not available from Embarcadero website anymore. Go figure it out...

So here they are (7zip compressed files):

Delphi 5 Enterprise Update Pack #1

Delphi 5 ADO Update Pack #2


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.

Friday, April 17, 2009

Hierarquia de Classes do Intraweb

Toda vez que preciso determinar a hierarquia de controles Intraweb ou bibliotecas de terceiros, como a da ArcanaTech, preciso fazer uma de duas coisas:

1) Obter a hierarquia via código

2) Procurar por muito tempo usando a documentação, ou o Google ou no código fonte (o IW não vem com código fonte completo, logo não é muito eficaz).

Entãou vou documentar a hierarquia de algumas classes aqui. À medida que for tendo tempo, pretendo incluir a maioria das classes do Intraweb no diagrama de classes.




Monday, April 13, 2009

Configurações "secretas" do BDS 2006

Ontem fui tentar criar um controle ActiveX no meu BDS 2006 e para minha surpresa não havia mais o respectivo Wizard. Abri o Delphi 6 e lá estava ele, então onde está o do BDS 2006? Como não tinha alternativa, crei o controle usando o Delphi 6, e depois recompilei-o no BDS 2006. Mas queria saber porquê esta opção não estava mais diponível. Pesquisando encontrei as chaves responsáveis pela habilitação ou não de várias opções de criação de novos itens. Segue abaixo o arquivo .REG contendo as entradas que estão faltando.


Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\Software\Borland\BDS\4.0\Type Library] "ActiveXWizard"="True"
"TransactionalWizards"="True"
"AxRegMenuCheckFile"="True"
"EmbeddedTypeLibraryEditor"="True"
"InteropCheck"="True"
"DefaultPersonality"="Delphi.Personality"

Delphi Projects -> ActiveX (ANTES)

Delphi Projects -> ActiveX (DEPOIS)


Três novos itens estão agora disponíveis, entre eles o ActiveX Control que eu precisava.
Além disso, nos itens do tipo Multitier, haverá o Transactional Data Module, que também não vem habilitado por default no BDS 2006.

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...