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 \(^.^)/
A solução 3 não resolveu meu problema. Mesmo com o Metadados = False, ao dar um POST em um registro com um código já existente no CDS ele continua retornando KEY VIOLATION. Existe alguma outra configuração que deve ser feita?
ResponderExcluiratt,
Muito bom Post
ResponderExcluirBoa tarde, estava com este problema de key vioçation. Pesquisei muito ate resolver o problema eu mesmo. descobri que para firebird atualize o generator é necessario que o a query seja fechada, então somente mudei o processo de inserção, pus no inicio o codigo para dar um "Open" no cds e no fim do processo um close. resolveu o meu problema. espero poder ajudar a outros que mesmo com os passos do autor do artigo nao tenham resolvido. Obrigado.
ResponderExcluirPara atualização de um registro a chave primária é obrigatória então a solução 2 não atenderia todos os casos. Uma resultado que consegui foi gerar o registro das chaves primárias do lado do cliente com valores descrescentes e negativos(o que não coincidiria) e atualizei as provider flags para que o campo não seja atualizado(removendo o pfInUpdate do campo chave). Desta forma o registro só é atualizado pela base de dados mas será exibido quando desejar uma listagem, por exemplo. Para isso utilizo outro componente com um número de campos reduzido para que não retorne todo o pacote de informações da tabela do banco e sobrecarregar a aplicação. Para este, não permito inserções ou atualizações de campos. Envio o ID para retornar todos os registros e atualizar com o mesmo componente(ClientDataset) que utilizo para inserção. É uma saída... Não centralizo todas as responsabilidades a um único componente mas cada um faz o seu papel perfeitamente e de forma eficiente. Gostei do post e me fez ter outros pontos de vista. Obrigado por compartilhar!
ResponderExcluir