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

Saturday, November 29, 2008

SQL Injection

Tudo que você sempre quis saber sobre SQL Injection e nunca teve coragem de perguntar:

http://www.devarticles.com/c/a/MySQL/SQL-Injection-Attacks-Are-You-Safe/

Excelente texto que trata o assunto de forma objetiva, sem alarmismos exagerados (será? hehee) e contendo exemplos claros . Traz exemplos usando ASP e SQL Server, mas a vulnerabilidade e as técnicas de proteção são igualmente aplicáveis aos programas escritos em Delphi.
Recomendo a leitura!

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! :)