quinta-feira, 30 de junho de 2011

Key Violation em nested datasets

Resolva de uma vez por todas o problema de "key violation" em datasets "detail" ligados a datasetfileds através de providers.

Cenário: você tem duas tabelas num relacionamento 1:n tipo mestre-detalhe no seu banco de dados, por exemplo Pedidos e ItensDoPedido.
A chave primária de ambas as tabelas é auto-numerada, por exemplo com ajuda de um generator do firebird. A Tabela itens tem uma chave estrangeira PedCodigo, que é o código do pedido, mas para controle interno também tem uma chave primária CodItem, autonumerada.
Você colocou no formulário dois SQLDatasets, um fazendo o select em Pedidos e outro fazendo o select em ItensDoPedido, com um "where CodPedido = :CodPedido" para fazer a "ligação". Você ligou o SQLDataset detalhe (ItensDoPedido) a um datasource ligado ao SQLDataset mestre. Você ligou também um DatasetProvider ao SQLDataset de Pedidos e ligou um ClientDataset cdsPedidos a esse provider, criando um dataset field (campo do tipo dataset), talvez chamado sqlItens, e ligando o ClientDataset cdsItem a este dataset field, criando assim o cdsDetahle.

Você talvêz tenha recebido alguma mensagem de que o campo CodItem é requerido e não pode ser deixado null, então tomou providências para deixar a propriedade required do campo = false ou atribuido algum valor randômico a ele. Se fez isso é sinal de um design fraco. Há maneiras mais elegantes de se resolver isso, veja as soluções 2 e 3.

Problema: Quando você insere o segundo item no ClientDataSet cdsItem recebe a exception "Key Violation".

Causa: O cdsItem (detalhe) também possui uma chave primária, CodItem. Quando você dá o post no segundo item ele é gravado com o mesmo valor do registro anterior: null ou zero se você definiu para atribuir zero no evento "onNewRecord" ou "beforepost". Como chave primária ele não pode se repetir no cdsItem, mesmo sabendo que os códigos serão criados no banco de dados automaticamente depois do applyupdates e do commit da transação. Antes disso não há valor definido para a chave primária do item.

Há 3 maneiras de solucionar:

1) Atribua um número randômico (pode até ser negativo) ou GetTickCount à chave primária no evento OnNewRecord.
Desvantagens: essa é a gambiarra mais feia e porca que existe. Além de poluir o código, caso o número randômico se repita, o que estatisticamente pode acontecer, ocorrerá o key violation. Caso use GetTickCount este pode trazer um número maior do que o suportado pelo integer field, já que retorna um cardinal sem sinal, podendo causar o erro de integer overflow. Portanto a solução 1 está aqui para fins "didáticos" (ou não). Nunca a use.

2) Para que você precisa trazer o campo da chave primária do item? Na maioria das vezes isso não é necessário. Você pode incluir todos os campos no select exceto a PK, já que ela ainda não está definida. E você não precisa exibi-la caso ela esteja null. Isso resolve o problema de uma maneira performática e elegante.

3) Metadados: Como o ClientDataset sabe que tal campo é chave primária se ele não é conectado diretamente ao banco de dados? Ele "sabe" isso porque provavelmente a propriedade GetMetadata do SQLDataset deve estar true. Colocando essa propriedade para false  o ClientDataset não "saberá" que esse campo não pode se repetir, muito menos que ele é requerido. Além disso o ClientDataset abre muito mais rápido caso essa propriedade esteja false, pois não tem que fuçar nos metadados da tabela. Essa técnica é ideal para quando você precisa mostrar o número da PK logo depois da inserção e persistência. Como por exemplo mostrar um número de comanda num sistema de restaurante ou mostrar o número da "carteirinha" de um cliente recém inserido.
Desvantagem: se por algum motivo, como campo requerido, not null ou qualquer outro você precisa que os metadados sejam trazidos para que sejam tratados no client e não no server esta técnica não deve ser usada, recorra à 2.

Mencionei o problema ocorrendo em nested datasets mas ele pode ocorrer em ClientDatasets "normais" ligados a uma única tabela.

Uma mistura das soluções 2 e 3 quando aplicável é a melhor e mais performática solução. Quando nenhuma das duas pode ser usada, ou seja, você precisa dos metadados por algum motivo e ainda precisa exibir a chave primária criada logo depois do registro salvo, considere usar um outro tipo de arquitetura, ou usar chaves primárias definidas via programação em vez de generators.

Happy Coding  \(^.^)/

0 comentários:

Postar um comentário

Postagens populares