Técnicas de validação de dados em Delphi

Este artigo apresenta diversas técnicas para validação de dados em aplicações Delphi e analisa as vantagens e desvantagens de cada uma, estimulando a adoção de práticas mais eficientes.

Dados validados, informações úteis

A comprovação da validade dos dados introduzidos em um sistema de informações é um aspecto tão importante de um projeto que muitas vezes pode determinar seu sucesso ou fracasso. Não é raro sistemas serem abandonados pelos usuários por não terem um tratamento mais completo de validação e consistência de dados, sendo posteriormente substituídos por outros mais atentos a esse requerimento.

Acontece que um sistema só pode fornecer informações úteis se for alimentado com dados coerentes e completos. Se você deseja que seu sistema apresente bons resultados, não pode permitir que seja alimentado com lixo: “trash in, trash out” (se entra lixo, sai lixo), diz o ditado.

Validação em um mundo cliente/servidor

Com o surgimento da arquitetura cliente/servidor, especialmente em sistemas multicamadas, surgiram novos questionamentos. Agora não é apenas um único programa atuando sobre os arquivos de dados. São duas, três ou mais camadas funcionais trabalhando em conjunto para rodar uma aplicação, geralmente em máquinas diferentes. Qual dessas camadas deve ser a responsável pela validação dos dados? E se a tarefa for dividida (ou repetida) entre as camadas lógicas, como deve ser essa distribuição?

Considere, por exemplo, uma aplicação web comum que tem como cliente um browser, acessando uma aplicação ASP (Active Server Pages) em um servidor web, que se liga a componentes COM+, contendo regras de negócio, os quais finalmente são conectados a um servidor SQL. Temos aí, realmente, uma aplicação de quatro camadas. E aí? Onde deve ficar a validação de dados? No navegador, em JavaScript? Ou no código VBScript do servidor ASP? Talvez concentrar tudo nas regras de negócio do Servidor de aplicações? Mas e quanto às regras de integridade no servidor de banco de dados?

Certamente não há uma única resposta para todas essas questões. Sobretudo, não podemos esquecer que cada caso é um caso, e que as singularidades de cada sistema devem ser consideradas cuidadosamente. Por isso mesmo, vamos analisar diversas abordagens de validação de dados em Delphi, e seus prós e contras.

Classificação dos Métodos

Podemos classificar as diversas técnicas de validação, primeiramente pela camada lógica da aplicação onde atuam. Assim, temos técnicas que agem diretamente na interface com o usuário (1), outras que são aplicadas nos módulos de dados locais ou remotos (2) e, finalmente, aquelas que são reforçadas pelo sistema de gerenciamento de banco de dados (3).

Existem métodos de validação corretivos e preventivos. Os corretivos são aqueles que exibem uma mensagem de erro para o usuário quando um valor inválido é inserido. Os preventivos têm uma outra abordagem: só permitem que o usuário insira valores válidos em um campo.

Outro critério importante é considerar se a validade dos dados é testada por campo, por registro ou por lote de alterações. A validação por campo ocorre logo no momento em que o usuário muda o valor de um campo. Já a validação de um registro acontece após todos os campos terem sido editados, mas antes do registro ser gravado. As validações por lote de alterações ocorrem somente quando um conjunto de registros inseridos, alterados ou excluídos é enviado para o servidor para ser efetivado.

É preciso notar também que certos métodos dão resposta imediata ao usuário durante a edição dos dados, enquanto outros dependem do processamento de uma transação ou da aplicação de um lote de alterações.

Todas essas características devem ser consideradas na hora de escolher o melhor método de validação para determinada função. Por exemplo, a verificação da duplicidade de um nome de cliente deve ser avisada tão logo o campo seja preenchido. Caso contrário corre-se o risco de desperdiçar o valioso tempo do usuário reescrevendo dados de um cliente que já constava no seu cadastro. Por outro lado, uma aplicação Internet pode permitir que clientes façam pedidos de compra de um produto que está em falta no estoque. O saldo negativo resultante pode gerar um aviso de demora de entrega para o cliente, mas não impede que a compra seja realizada.

Enfim, vamos examinar algumas técnicas de validação de dados.

TWinControl.OnExit

Um método de validação é verificar o valor informado pelo usuário em controles visuais assim que o foco de edição sair do controle. O evento OnExit, herdado da classe TWinControl, ocorre quando o foco de edição muda de um controle para outro.

figura1
Figura 1. Validando Edit1.Text

Por exemplo, considere um formulário com um controle TEdit e um botão “OK” (Figura 1). O controle TEdit tem o foco enquanto o usuário está editando o seu texto. Ao clicar em um botão, o TEdit recebe um evento OnExit e em seguida o botão recebe um evento OnEnter. Querendo assegurar que o valor do campo texto seja um número positivo, o evento OnExit pode ser manipulado desta forma:

procedure TForm1.Edit1Exit(Sender: TObject);

            var
            
              Valor: Integer;
            
            begin
            
              try
            
                Valor:= StrToInt(Edit1.Text);
            
              except
            
                Edit1.SetFocus;
            
                MessageDlg(Format('''%s'' não é um número inteiro', [Edit1.Text]),
            
                  mtError, [mbOk], 0);
            
                Exit;
            
              end;
            
              if not (Valor > 0) then
            
              begin
            
                Edit1.SetFocus;
            
                MessageDlg(Format('''%s'' não é um número positivo',
            
                  [Edit1.Text]), mtError, [mbOk], 0);
            
                Exit;
            
              end;
            
            end;

A primeira coisa que se nota, com este método, é que é escrito código demais, apenas para validar se o valor é um número positivo. Em um formulário com muitos campos isso se tornaria bastante inconveniente ao desenvolvedor.

No código, o primeiro teste verifica se o texto corresponde a um número inteiro, tratando a exceção EConvertError que é levantada quando o argumento da função StrToInt não corresponder a um inteiro. O segundo teste verifica se o valor numérico entrado é maior que zero. Em qualquer caso, é retornado o foco para o TEdit, indicando ao usuário o campo que causou a mensagem de erro.

Este é um método intimamente ligado ao código de interface e dá respostas imediatas ao usuário, campo-a-campo. Exige muito trabalho de codificação e é difícil de manter, pois uma simples mudança nas regras de negócio implica em alterações em todo o código do programa.

Esse mecanismo traz também o inconveniente de bloquear a movimentação do usuário entre os controles visuais até que um valor válido qualquer seja informado, mesmo que o usuário queira sair da janela.

Um outro problema – mais sutil, mas não menos sério – ocorre quando o formulário inclui um botão com a propriedade Default definida como True, o que o faz responder automaticamente à tecla ENTER como se o botão tivesse sido pressionado pelo usuário. Nesse caso, o evento OnClick do botão é chamado, mas o foco de edição não sai do controle ativo para o botão. Assim, a rotina de validação que era chamada no evento OnExit será ignorada e dados incorretos poderão vir a ser gravados.

TButton.OnClick

Uma versão mais simples e eficiente da técnica anterior é aplicar todos os testes de validação de uma só vez, no momento em que o registro estiver para ser gravado. No evento OnClick de um botão “OK” ou “Inserir”, antes de salvar o registro no banco de dados é verificada a validade dos campos de dados (veja um exemplo na Figura 2). Caso uma inconsistência seja encontrada, uma mensagem de erro é exibida e o fluxo do programa é interrompido.

Aqui a verificação dos dados ocorre registro a registro. É permitido ao usuário manter valores incorretos ou incompletos nos campos até que o registro precise ser gravado. Isso dá mais liberdade de ação para o usuário, mas pode ser inconveniente em certas aplicações.

figura2
Figura 2. Validando no OnClick do botão “Inserir”

Veja um exemplo dessa abordagem:

procedure TForm1.BtnInserirClick(Sender: TObject);

            var
            
              Valor, Desconto: Double;
            
            begin
            
              try
            
                if StrToFloat(ValorEdit.Text) <= 0.0 then
            
                  raise Exception.CreateFmt(
            
                    '''%s'' não é um valor numérico positivo', [ValorEdit.Text]);
            
              except
            
                ValorEdit.SetFocus;
            
                raise;
            
              end;
            
              try
            
                if DescontoCheckBox.Checked and (DescontoEdit.Text = '') then
            
                  raise Exception.Create('O valor do desconto está em branco');
            
              except
            
                DescontoEdit.SetFocus;
            
                raise;
            
              end;
            
              { Gravar o registro no banco de dados aqui }
            
            end;

O primeiro teste verifica se o texto de ValorEdit é um numero positivo. O segundo requer que o valor do desconto seja preenchido caso o usuário tenha marcado o Checkbox “Desconto”. Se o fluxo do programa continuar após todas as verificações, o registro é gravado.

Para ganhar produtividade nesse tipo de validação, é possível codificar pequenas rotinas que recebem como parâmetro um controle visual e disparam exceções, conforme um determinado critério de validação. Por exemplo, você pode codificar procedimentos utilitários como CheckEditNotNull, CheckEditIsDate etc. Veja um exemplo:

procedure CheckEditNotNull(Edit: TEdit);

            begin
            
              if Trim(Edit.Text) = '' then
            
              begin
            
                Edit.SetFocus;
            
                Edit.SelectAll;
            
                raise Exception.Create('Este campo precisa ser preenchido');
            
              end;
            
            end;

TDataSet.BeforePost

As técnicas anteriores têm a virtude de funcionar para qualquer tipo de controle visual. No entanto, quando usamos controles data-aware (TDBEdit, TDBGrid etc.) é mais conveniente codificar os testes de validade nos eventos dos componentes de acesso a dados.

O jeito mais comum de fazer isso é manipular o evento BeforePost (Figura 3). Qualquer descendente de vTDataSet publica esse evento, que é chamado quando os dados editados estão prestes a ser gravados no buffer de registros. Caso uma exceção seja disparada durante o processamento desse evento, o método Post é interrompido e os dados não são gravados, permitindo que o usuário faça as devidas correções.

figura3
Figura 3. Evento BeforePost em um TIBDataSet

Ao invés de acessar os controles visuais de edição de dados, a validação é feita diretamente com os campos do dataset. Para selecionar o controle em caso de erro é usado o método FocusControl de TField.

procedure TForm3.IBDataSet1BeforePost(DataSet: TDataSet);

            begin
            
              if DataSet.FieldByName('Codigo').AsInteger < 1 then
            
              begin
            
                DataSet.FieldByName('Codigo').FocusControl;
            
                DatabaseError('Código deve ser um valor positivo');
            
              end;
            
              if (DataSet.FieldByName('DataInicial').AsDateTime >
            
                DataSet.FieldByName('DataFinal').AsDateTime) then
            
              begin
            
                DataSet.FieldByName('DataInicial').FocusControl;
            
                DatabaseError('Data inicial é posterior à data final');
            
              end;
            
            end;
            
            

Por fazer a verificação somente no final da edição do registro, esse tipo de método de validação é mais amigável com o usuário ao validar campos interdependentes. No exemplo anterior, o valor do campo “DataInicial” deve ser menor ou igual que o do campo “DataFinal”. Se esses valores fossem validados campo-a-campo, o usuário teria muito mais dificuldade de corrigir um período inválido.

No último exemplo, usamos a função DatabaseError para acusar o erro. É uma forma mais elegante de disparar uma exceção ao validar dados de um registro. A função DatabaseError(Mensagem) é equivalente à instrução raise EDatabaseError.Create(Mensagem).

Propriedades de TField

Limitar os valores possíveis de um campo é uma tarefa tão comum que o Delphi já traz recursos prontos para vários tipos de validação. Basta definir alguma propriedades do objeto TField instanciado, e o Delphi se encarrega da validação do campo automaticamente. Podemos dizer que esse é um tipo declarativo de validação, enquanto os anteriores são do tipo procedural.

As restrições para aceitação de um valor são declaradas nas propriedades EditMask, MaxValue, MinValue e Required do campo. Preferencialmente, são usados campos persistentes para definir essas propriedades em tempo de projeto (Figura 4). Nada impede, no entanto, que sejam alteradas por código as propriedades de um objeto TField obtido em tempo de execução – pelo método FieldByName, por exemplo.

figura4
Figura 4. Campos persistentes (TField) simplificam o processo de validação

A propriedade Required é, sem dúvida, a mais freqüentemente utilizada. Sempre que você quiser forçar que determinado campo seja preenchido pelo usuário, defina Required como True. Se for igual a False, o campo é considerado opcional.

Use MinValue e MaxValue para definir a faixa de valores numéricos permitida para um campo. Se ambas propriedades tiverem valor zero, serão ignoradas e o campo aceitará qualquer valor. Como era de se esperar, essas duas propriedades só estão disponíveis para campos numéricos.

A propriedade EditMask impõe uma máscara de edição para o campo. O conteúdo de EditMask é uma string que consiste de três campos separados por ponto-e-vírgula. A primeira parte da máscara é a máscara em si. A segunda parte é um caractere que determina se textos literais da máscara (com hífens em CEPs, ou pontos e CPFs) devem ser salvos como parte dos dados. A terceira parte é usada para representar caracteres não preenchidos da máscara.

Confira na Tabela 1 os caracteres especiais usados neste primeiro campo da máscara

O 9 permite um caractere numérico nesta posição, mas não o requer.
Caractere Significado na máscara
! Se um! aparecer na máscara, caracteres opcionais são representados no texto com espaços em branco no início. Caso contrário, caracteres opcionais são representados no texto como espaços em branco no final.
> Se um > aparecer na máscara, todos os caracteres que seguem ficam em maiúsculas até o fim da máscara ou até que um < seja encontrado.
< Se um < aparecer na máscara, todos os caracteres que seguem ficam em minúsculas até o fim da máscara ou ate que um > seja encontrado.
<> Se esses dois caracteres aparecerem juntos na máscara, nenhuma checagem de maiúsculas ou minúsculas é feita e os dados são formatados do jeito que o usuário digitar.
\ O caractere que segue uma \ é um literal. Use esta barra para usar qualquer dos caracteres especiais de máscara como um literal nos dados (para mostrar um sinal de maior > no campo, por exemplo.
L O L requer um caractere alfabético somente nesta posição.
L O l permite somente um caractere alfabético nesta posição, mas não o requer.
A O A requer um caractere alfanumérico somente nesta posição (A-Z, a-z ou 0-9).
A O a permite um caractere alfanumérico nesta posição, mas não o requer.
C O C requer um caractere qualquer nesta posição.
c O c permite um caractere qualquer nesta posição, mas não o requer.
0 O 0 requer um caractere numérico somente nesta posição.
0
# O # permite um caractere numérico ou um sinal de mais ou de menos nesta posição, mas não o requer.
: O: é usado para separar horas, minutos e segundos em campos do tipo hora. Se o caractere que separa horas, minutos e segundos for diferente nas opções regionais do Painel de Controle no seu sistema, esse caractere é que será usado.
/ O / é usado indicar o separador de dias, meses e anos em datas. Se o caractere separador for diferente nas opções regionais do Painel de Controle no seu sistema, esse caractere é que será usado.
; O ; é usado para separar os três campos da máscara
_ O _ (sublinhado) automaticamente insere espaços no texto. Quando o usuário entra caracteres no campo, o cursor salta o _ (sublinhado).
Tabela 1. Caracteres de máscaras

Na Tabela 2 veja alguns exemplos de máscaras EditMask:

EditMask Significado
(00)_0000-0000;0;* Número de telefone com o código DDD
9990;0;_ Número inteiro de 1 a 4 caracterees.
>LL Estado (UF), duas letras maiúsculas
Tabela 2. Exemplos de máscaras

Uma vez que essas propriedades são definidas nos campos de um DataSet, ao invés de em controles visuais, observamos que elas são aplicadas em nível de módulo de dados (Data Module), não de interface com o usuário. Isto é um avanço significativo que facilita a separação lógica entre camadas de interface e regras de negócio.

DICA - Traduzindo DBConsts.pas Todos os testes de validação desempenhados pelas propriedades EditMask, MaxValue, MinValue e Required podem gerar exceções que mostram mensagens de erro ao usuário. Essas mensagens são constantes declaradas na unit DBConsts como resource strings. Se você quiser ter as mensagens em português é simples: salve uma cópia de DBConsts.pas na pasta do seu projeto, edite as strings livremente e adicione esse módulo ao seu projeto. Depois de compilado, todas as mensagens do sistema passam a usar a sua versão em português.

Lookup Fields

Uma abordagem diferente para assegurar que o usuário só entre com valores válidos em um campo é limitar as possibilidades de escolha para os valores existentes em uma tabela relacionada. Com esse propósito, existem os campos lookup.

Campos lookup são editados com os componentes TDBLookupComboBox ou TDBLookupListBox, disponíveis na paleta Data Controls. Também os grids TDBGrid suportam campos lookup, abrindo uma lista drop-down com os valores existentes na tabela de lookup para o usuário escolher.

figura5
Figura 5. Criando um novo campo tipo lookup

Para criar um campo tipo lookup, abra o editor de campos do DataSet e selecione New Field... do menu popup (Figura 4). Será aberta a caixa de diálogo New Field (Figura 5). Depois de selecionar o tipo de campo lookup, é preciso ainda preencher os campos KeyFields, LookupDataSet LookupKeyFields e LookupResultField. Você pode obter mais informações sobre campos lookup na documentação do Delphi.

Campos lookup são uma forma muito interessante de validação de campo, porque têm um caráter preventivo e não corretivo como os demais. Quando associado à propriedade Required, este tipo de campo impõe ao usuário a escolha de um valor existente em uma lista.

TField.OnValidate

Outro método bastante eficiente e claro para codificar a validação de um campo é usar o evento OnValidate de TField (Figura 6). De fato, como o nome indica, este evento serve justamente para este fim: verificar a validade um campo antes que seu valor seja armazenado no buffer do registro.

Este método é semelhante em funcionalidade à técnica de manipular eventos OnExit dos controles visuais, só que é mais abrangente e é independente da implementação da interface. Usando OnValidate você pode (e deve) realmente separar a lógica de negócios da interface com o usuário.

figura6
Figura 6. O evento OnValidate

Para exemplificar, digamos que um determinado campo de data não possa ser posterior à data corrente do sistema. Tendo criado campos persistentes no DataSet, selecionamos o campo “Data” e criamos um manipulador para o seu evento OnValidate:


            procedure TForm1.CadastroDataValidate(Sender: TField);

begin

  if Sender.AsDateTime > SysUtils.Date then

    DatabaseError('Data é posterior à data do sistema');

end;
            

A exceção disparada no código vai suspender o processo de gravação do valor do campo no buffer do registro e manter o foco no controle correspondente ao campo. Simples assim!

Constraints em Delphi

Constraints (restrições) são regras de validação em sintaxe SQL declaradas em um DataSet ou em um campo. Os constraints são validados pela aplicação cliente, provendo um retorno imediato ao usuário, sem que o registro precise ser enviado ao servidor.

Em aplicações DataSnap, os constraints podem ser definidos no DataSet remoto do servidor de aplicações, sendo automaticamente transferidos para o ClientDataSet da aplicação cliente. Para isso ocorrer, basta que o seu componente DataSetProvider tenha a propriedade Constraints definida como True (valor default). Isso é muito prático, porque basta alterar as regras de validação no servidor de aplicações e todos os clientes passarão automaticamente adotar os novos constraints, sem que precisem ser recompilados e redistribuídos.

Para definir um constraint em um campo, usamos a propriedade CustomConstraint de TField (Figura 7). Por exemplo, em um campo do tipo numérico, pode-se definir CustomConstraint como “x > 0 and x <= 100”, onde x (ou qualquer outro nome de identificador: “Value”, “Campo”, “Col” etc.) é interpretado como o valor corrente daquele campo. Se o resultado da expressão lógica for falso, uma exceção é disparada exibindo uma mensagem de erro genérica que faz referência àquele constraint (Figura 8). Você pode também personalizar a mensagem de erro exibida simplesmente definindo a propriedade ConstraintErrorMessage (Figura 9)

figura7
Figura 7. Propriedades CustomConstraint e ConstraintErrorMessage de TField
figura8
Figura 8. Mensagem de erro padrão de falha de constraint
figura9
Figura 9. Mensagem de erro personalizada, alterando ConstraintErrorMessage.

Além dos constraints de cada campo, um DataSet pode ter várias regras de validação na sua propriedade Constraints (Figura 9), que é uma coleção de objetos TCheckConstraint (Figura 10). Regras declaradas nesta coleção podem fazer referência a várias colunas. Também podem ser definidas em Constraints mais de uma regra de validação para o mesmo campo, com mensagens de erro distintas.

figura10
Figura 10. Editando Constraints do DataSet “Pessoas”
figura11
Figura 11. Editando o primeiro item de Pessoas.Constraints”

Uma diferença importante entre TField.CustomConstraint e TDataSet.Constraints é que o primeiro realiza a validação campo-a-campo, enquanto o segundo somente verifica as regras de validação quando o registro estiver para ser gravado. Em outras palavras, um constraint de campo é equivalente ao evento OnValidate de um campo; um constraint de DataSet é equivalente ao evento BeforePost de um DataSet.

Os vários operadores e funções que podem ser usados em constrainsts são relacionados na Tabela 3.

Operador / Função Exemplo
Comparações
= Estado = 'RJ'
<> Estado <> 'SP'
>= DataInformada >= '1/1/2002'
<= Total <= 100,000
> Total <= 100,000
< Campo1 < Campo2
BLANK Estado <> 'BA' or Estado =
BLANK
IS NULL Campo1 IS NULL
IS NOT NULL Campo1 IS NOT NULL
Operadores lógicos
And Estado = 'RJ' and Pais = 'BR'
Or Estado = 'SP' or Estado = 'BA'
NOT not (Estado = 'SC')
Operadores aritméticos
+ Total + 5 > 100
- Campo1 – 7 <> 10
* Desconto * 100 > 20
/ Desconto > Total / 5
Funções para string
Upper Upper(Campo1) = 'ALWAYS'
Lower Lower(Campo1 + Campo2) = 'abcd'
Substring Substring(CampoData,8) = '2002' Substring(CampoData,1,3) = 'JAN'
Trim Trim(Campo1 + Campo2) Trim(Campo1, '-')
TrimLeft TrimLeft(CompoString) TrimLeft(Campo1, '$') <> ''
TrimRight TrimRight(CompoString) TrimRight(Campo1, '.') <> ''
Funções para data e hora
Year Year(CampoData) = 2002
Month Month(CampoData) <> 12
Day Day(CampoData) = 1
Hour Hour(CampoData) < 16
Minute Minute(CampoData) = 0
Second Second(CampoData) = 30
GetDate GetDate – CampoData > 7
Date CampoData – 1 = Date(GetDate)
Time CampoHora > Time(GetDate)
Diversos
LIKE Memo LIKE '%palavra%'
In Day(CampoData) in (1,7)
* Estado = 'S*'
Tabela 3. Funções e operadores para constraints

TDataSetProvider.ResolveToDataSet

Servidores de aplicação DataSnap utilizam componentes TDataSetProvider para publicar DataSets através da interface IAppServer, enviando pacotes de dados (Data) e recebendo pacotes de alterações (Delta).

Normalmente, quando recebe um comando ApplyUpdates o provedor manda o DataSet remoto acessar diretamente o banco de dados através de instruções SQL de manipulação de dados INSERT, UPDATE ou DELETE.

Uma alternativa é definir a propriedade ResolveToDataSet do DataSetProvider como True (Figura 11). Desse modo é adotado o método tradicional de edição de DataSets, usando os métodos Insert, Edit e Post. Podemos então usar técnicas convencionais de validação de DataSets no servidor, tratando os eventos BeforePost e OnValidate. As exceções geradas nesse momento são automaticamente retransmitidas à aplicação cliente.

figura12
Figura 12. TDataSetProvider.ResolveToDataSet

Quando ResolveToDataSet está ativo, durante o ApplyUpdates o DataSet remoto é aberto e os dados da consulta são todos carregados em memória. Cada registro a ser alterado é localizado internamente usando o método Locate. Evidentemente isso não é muito eficiente. Por isso, de forma geral o uso da propriedade ResolveToDataSet não é recomendado.

Reconciliação de Erros

As alterações feitas em um DataSet cliente são mantidas em memória até que ao sistema faça a aplicação dos dados. O método ApplyUpdates envia o pacote com todas as inserções, alterações e exclusões de registros para resolução no DataSetProvider (que pode ser local ou remoto, em um servidor de aplicações DataSnap).

Eventuais erros de validação e conflitos de concorrência de acesso à base de dados serão também trazidos em lote para o DataSet cliente decidir o que fazer. Esses erros devem então ser reconciliados no evento OnReconcileError do componente TClientDataSet. Veja a sintaxe:


                TReconcileAction = (raSkip, raAbort, raMerge,

     raCorrect, raCancel, raRefresh);

 

  TReconcileErrorEvent = procedure(

     DataSet: TCustomClientDataSet; E: EReconcileError;

     UpdateKind: TUpdateKind; var Action: TReconcileAction) of object;
                

A forma mais simples de manipular esse evento é usar o formulário de resposta padrão da Borland: Reconcile Error Dialog (Figura 13). Para usar o formulário, entre no menu File|New>Other..., página Dialogs, e selecione o ícone correspondente.

figura13
Figura 13. Criando um Reconcile Error Dialog

O seguinte código ilustra a manipulação do evento OnReconcileError usando o formulário padrão


                TreconcileErrorForm:

 

implementation

 

uses recerror;

 

procedure TData Module1.ClientDataSet1ReconcileError(

  DataSet: TCustomClientDataSet; E: EReconcileError;

  UpdateKind: TUpdateKind; var Action: TReconcileAction);

begin

  Action:= HandleReconcileError(DataSet, UpdateKind, E)

end;

 
                

A função HandleReconcileError está na unit RecError e simplesmente exibe um TReconcileErrorForm (Figura 14), retornando a opção de ação escolhida pelo usuário. As possíveis ações definidas pelo tipo TReconcileAction que o evento retorna são: raSkip, raAbort, raMerge, raCorrect, raCancel e raRefresh.

figura14
Figura 14. Formulário padrão de reconciliação de erros

Database Constraints

Paralelamente a todos os recursos de validação de dados nas camadas cliente e do servidor de aplicações, o sistema de gerenciamento de banco de dados também verifica suas próprias regras de integridade de dados.

Quase todos os bancos de dados compatíveis com o padrão ANSI SQL-92 dão suporte a um conjunto mínimo de constraints de validação de integridade de dados: cláusulas CHECK de colunas e de tabelas; cláusulas FOREIGN KEY; chaves primárias (PRIMARY KEY) e índices UNIQUE (que podem ser considerados regras de validação, já que impedem a duplicidade de valores nas colunas) e TRIGGERS, que são blocos de código SQL disparados automaticamente quando uma linha da tabela é inserida, alterada ou excluída.

Mesmo que passem por todas as validações presentes na camada (ou nas camadas) de aplicação Delphi, podem surgir erros de validação provenientes do servidor SQL, gerando exceções que podem ser tratadas internamente por código ou exibidas diretamente para o usuário.

Conclusões

Há muitas técnicas para resolver o mesmo problema de formas diferentes. O desenvolvedor deve se conscientizar das possibilidades e conseqüências de suas escolhas.

Além da integridade de dados, um outro critério que precisa ser levado em conta é o da usabilidade. Ninguém gosta de ser chamado atenção a todo instante, muito menos por um computador. Técnicas como usar o evento OnExit de controles visuais são muito restritivas para o usuário.

Em algumas condições, usar o recurso de cached updates de um DataSet cliente não é recomendado, porque o usuário realmente precisa que os dados sejam gravados na hora e não faz sentido resolver erros tardiamente. Esses tipos de situações por vezes deixam os usuários bastante irritados com os processos de validação. É preciso usar o bom senso.

De forma geral, devem ser preferidas técnicas que conservem a integridade dos dados de maneira preventiva, evitando mensagens de erros desnecessárias. Assim, devem ser considerados recursos como máscaras de edição e campos lookup sempre que possível.

Entre as questões mais técnicas, é recomendável adotar sempre métodos declarativos no lugar de códigos procedurais para tratar validações corretivas. São mais eficientes e fáceis de manter. Constraints em Delphi são os principais recursos de validação declarativa.

Finalmente, é muito importante isolar a lógica de negócios da interface com o usuário. Em aplicações cliente/servidor tradicionais, de duas camadas, isso é feito separando funcionalmente os formulários dos módulos de dados e usando os eventos BeforePost e OnValidate, e as propriedades EditMask, Min, Max e Required. Em sistemas multicamadas, a concentração das regras de negócio deve ser ainda mais forte. A replicação automática de constraints para os clientes resolve a questão da ineficiência de validar dados no servidor.

revista
Esse artigo faz parte da revista Clube Delphi edição 31. Clique aqui para ler todos os artigos desta edição