Saturday, January 24, 2009

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

Grupo Intraweb no Yahoo Groups

Está aí o link:

http://br.groups.yahoo.com/group/iwbr/

quem usa Intraweb para desenvolvimento web deveria se associar e ajudar a disseminar o conhecimento sobre esta excelente ferramenta!

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, October 15, 2008

Cache de DataSets em aplicações multi-camadas

Há algum tempo utilizo mecanismos diversos de cache para melhorar a performance das aplicações que desenvolvo, principalmente com relação a acesso a bancos de dados. De uma forma mais aplicada venho desenvolvendo um mecanismo genérico de cache, para ser utilizado em aplicações multi-camadas. Problemas simples como o do post anterior (campos de lookup) podem ser muito beneficiados com mecanismo deste tipo.

É interessante ver que a maioria das aplicações mais "avançadas" ou elaboradas possuem algum mecanismo de cache mas no "mundo real" das aplicações comerciais que possuem acesso pesado a bancos de dados, o uso de um mecanismo de cache, por mais precário que seja, é raro ou inexistente.
O Delphi tem, out of the box, tudo que é necessário para fazer um cache nem tão rudimentar assim. Na verdade, depois de pronto, acho que poderia bater em muita coisa comercial que tem por aí... O ClientDataSet é um candidato perfeito para ser um DataSet genérico utilizado em um cache: é um DataSet em memória, pode ser gravado em disco em formato nativo (binário) e está presente na maioria das aplicações multi-camadas escritas em Delphi.

De forma geral e bem simplificada, o cache deveria funcionar da seguinte forma:
- Um DataSet é requerido à aplicação servidora (middle-tier) pela aplicação cliente;
- O mecanismo de cache (iremos chamá-lo de "gerenciador do cache") verifica se este DataSet já está no cache. Caso não esteja, a consulta normal ao banco de dados é efetuada. Caso o DataSet exista no cache, o gerenciador do cache deverá determinar se o cache está atualizado. Se estiver, o DataSet no cache é retornado à aplicação cliente sem necessidade de acesso ao banco de dados. Se não estiver, a consulta normal ao banco de dados é efetuada;
- Caso a consulta normal ao banco seja efetuada (o DataSet não está no cache ou o mesmo se encontra desatualizado) o gerenciador do cache se encarrega de incluir/atualizar o cache com este DataSet recém obtido do banco de dados;
- O gerenciador do cache deve prever acessos simultâneos de diferentes threads;
- O gerenciador do cache deve persistir e recuperar o cache em um repositório local (disco).

Simples não? Bem... não é tão simples mas também não é ciência de foguetes. Veremos que existem vários requisitos que deverão ser satisfeitos para que o mecanismo de cache seja viável. Entre eles, podemos citar:

1) O gerenciador do cache deverá ser capaz de identificar unicamente um DataSet;
2) O gerenciador do cache deverá ser capaz de identificar de forma inequívoca se o cache reflete a situação atual do banco de dados, ou seja, se o cache está atualizado no que tange àquele DataSet específico.

O requisito número 1 exige que o banco de dados se adapte à aplicação, ou seja, ao uso do cache. Os sistemas que desenvolvo já possuíam uma estrutura adequada, mesmo antes de um mecanismo de cache ser pensado. Então neste caso foi fácil.

Irei detalhar como satisfazer os requisitos 1 e 2 no próximo post.

Sunday, September 21, 2008

Campos Lookup - Uma abordagem racional

Ando lendo em sites e publicações especializadas em Delphi muita coisa sobre campos de lookup e sua utilização em sistemas, como por exemplo as famosas LookupComboBoxes. Geralmente fala-se mal, recomenda-se não utilizar campos lookup. Ou então que sua utilização é muito restrita, e só serve para tabelas contendo poucos registros (exatamente quantos, ninguém fala, certo?).. Coisas que eu ouvia há 10 ou 15 anos, quando quase todo mundo desenvolvia usando os velhos e bons conceitos Cliente/Servidor.

De lá prá cá muita coisa mudou. A banda de internet que a alguns têm em casa é mais rápida que algumas redes que usavam cabos coaxiais em 1995.
Mesmo assim, alguns teimam em usar o mesmo conceito antigo que "não se pode trafegar pela rede meia dúzia de registros que possivelmente não serão usados"... AFFFFF!!!!
Então vejamos... O que é mesmo AJAX? Quanta informação desnecessária é trafegada para dar a uma aplicação web AJAX o seu comportamento?
De qualquer forma, nada melhor que fatos e dados para basear o meu ponto de vista.

Suponha uma tabela "cliente", com a seguinte estrutura:

codigo_cliente - Integer
nome_cliente - varchar(50)
codigo_profissao - Integer

e uma tabela "profissao" com a seguinte estrutura:

codigo_profissao - Integer
descricao_profissao - varchar(30)

Em 99% dos casos eu ouço que não se deveria fazer lookup na tabela de profissões, e sim um JOIN.
Imagine uma consulta típica para um relatório, onde irei listar os clientes e suas profissões. O SQL típico seria:

SELECT c.codigo_cliente, c.nome_cliente, p.descricao_profissao
FROM cliente c
INNER JOIN profissao p
ON c.codigo_profissao = p.codigo_profissao
ORDER BY c.codigo_cliente ASC

Com alguma simplificação podemos assumir um tamanho de registro de dados igual a 84 bytes. Se eu tivesse 2000 clientes, teríamos um pacote de dados de aproximadamente 165 kbytes.

Agora, vejamos por outro lado. Imagine que eu tivesse 2 consultas separadas:
SELECT codigo_cliente, nome_cliente, codigo_profissao
FROM cliente
ORDER BY codigo_cliente ASC

SELECT codigo_profissao, descricao_profissao
FROM profissao

Vamos supor ainda que minha tabela de profissão contivesse 500 registros. O total de dados trafegados seria 58*2000 (clientes) + 34*500 (profissões) = 130 kbytes.

Oras! Eu fiz duas consultas, trouxe toda a tabela de profissões para o lado cliente e ainda tive um tráfego menor???? Hum... Agora, imagine que este relatório é chamado por várias vezes durante a execução do programa?

Quantas vezes por dia a tabela de profissões muda? Talvez sejam adicionadas novas profissões, de vez em quando, mas em sistemas do mundo real isto ocorre muito no início do ciclo de vida do sistema e tende a diminuir - e muito - com o tempo. Não poderíamos carregar a tabela de profissões, digamos na inicialização do sistema, e usá-la como Lookup durante o ciclo de vida da aplicação?

Um mecanismo mais inteligente poderia checar as alterações da tabela de profissões antes de sua utilização (como lookup no relatório, por exemplo) e fazer um refresh sempre que necessário. O ganho de performance tanto de banco de dados quanto da aplicação é significativo.

Alguns conceitos de C/S estão certamente ultrapassados. O pessoal que os usa sem parar para pensar (e principalmente MEDIR) o que acontece de fato em sistemas do mundo real deveria revê-los.