terça-feira, 11 de novembro de 2008

POG ColdFusion para identificar registros que nao podem ser excluidos de uma tabela

Odeio POGs. Em primeiro lugar porque a exigência comigo mesmo faz com que eu não descanse até encontrar uma solução verdadeiramente elegante.

A situação desta vez foi a seguinte: Se vocês repararem, todos os sistemas que a gente encontra por ai, no CRUD apresentam a possibilidade de excluir qualquer registro, verificando no momento da transação (server-side) se aquela ação é realmente possível ou não. Se possível, simplesmente efetiva, senão retorna uma mensagem para o usuário. A minha intenção é já desabilitar a opção de excluir na interface, prevendo tal condição. Para isso, preciso que no momento de mandar os registros de uma tabela para serem apresentados na interface, eu verifique se estes não são referenciados de alguma forma.

Conversando com meu amigo Jefferson Petilo, primeiro definimos o que era óbvio. Precisaríamos no banco de dados que fosse, utilizar as restrições de operação entre registros das tabelas através de chaves estrangeiras. Considere:

Tabela1 (TiposDeImagens):

Campo Tipo Função
id (uuid, int, etc) Chave Primaria
label (varchar) Descrição do tipo

Tabela2 (Imagens):

Campo Tipo Função
id (uuid, int, etc) Chave Primaria
label (var char) Descrição da imagem
tipo (uuid, int, etc) Chave Estrangeira
OnDelete = restrito


Logo depois, por não encontrar nada universal no SQL ou que pudessemos facilmente replicar para vários banco de dados, decidimos que a melhor maneira de verificar registro por registro (no exemplo, da Tabela1, TiposDeImagens) quanto a possibilidade do mesmo ser excluído, seria:

POG_cf_img0_11nov08_17h53m

Porém isso, claro, na ocasião de ser disparado contra um registro que não possui ligações, ocasionaria na exclusão do registro em sí.

Ai entra o POG:

POG_cf_img1_11nov08_17h54m

O <cftransaction> com o ROLLBACK logo após o teste de deleção do registro, garante que o mesmo não seja efetivamente excluído.

Continuando a conversa com o Jefferson verificamos ainda a implementação desta solução via “trigger/actions” no banco de dados. Qual não foi a nossa surpresa ao reparar que o “POG” acabou sendo uma solução bem mais simples de se implementar.

Outra coisa em relação ao uso de “triggers/actions” é o fato de tornar a solução dependente de banco de dados e implicar em se ter várias versões de um código relativo a um mesmo e único recurso. Mas essa é definitivamente outra discussão de prós e contras.

O que fica para mim agora, é o desejo de estudar alguma solução, que seja ao mesmo tempo “elegante” (antônimo de POG, embora eu e o Jefferson tenhamos considerado este POG como “elegante”) e simples de ser implementada.

13 comentários:

Fernando S. Trevisan disse...

Oi Vicente,

Concordo que POG é terrível e confesso que fiquei um pouco horrorizado com a solução que vocês encontraram - sempre acho esses processamentos caso-a-caso complicados.

Existem algumas alternativas, mas todas soam extremamente POG:

1. Manter uma memory table, ou VIEW, algo do tipo, que retorne os registros já marcando aqueles que não podem ser excluídos - isso pode ser feito inclusive em uma query só;

2. Você pode executar essa query também diretamente no código, mas aí você teria um problema de performance, dependendo da quantidade de registros;

3. Outra forma é manter em cache uma lista dos ids de tipos que contêm imagens, por meio de uma query distinct na foreign key da tabela imagens. Na hora de "loopar" pelos tipos, você verifica se consta da lista e aí toma a decisão.

Enfim, são todos POGs, uma vez que o correto mesmo era ter um modo de retornar isso automaticamente do banco - talvez isso seja uma falha na implementação SQL? Ter um "estado do registro", tipo, se é atualizável e/ou "deletável" seria interessante. :)

Abs!

Vicente Maciel Junior disse...

Pois é cara, concordo com você. Enfim, tudo o que for arranjado implicará num POG dos brabos para esse tipo de caso.

Das soluções, essa foi a mais simples de implementar, manter e em termos de performance não gerou um impacto significativo. Mas não estou querendo dizer que outras opções, em maior escala de uso, resultariam em desempenho bem maior. Porém, com o alto custo para manutenibilidade.

Eu e o Jefferson concluímos exatamente a mesma coisa... Há um "GAP" neste sentido nos RDBMS. Trata-se de algo que certamente deferia fazer parte dos recursos disponibilizados em suas respectivas APIs. E sinceramente, não posso acreditar que não exista pelo menos uma tecnologia que já tenha implementado isso.

Valeu pelas várias opções indicadas. Eu vou experimentar sim implementar todas assim que puder. Tks!

jpetilo disse...

No Cenário, Fernando, o que determina se o registro pode ou não ser removido é existência ou não de dependências (que é um processo extremamente dinâmico). Eu acho que nesse caso uma view não se aplicaria, sem contar que a SQL ficasse extremamente complexa.

Inicialmente, o óbvio seria fazer uso da integridade referencial para obter essa resposta (a partir de um try/catch) no momento em que fosse relizado o processo de exclusão. Ou seja, o usuário clica no botão e ele recebe uma mensagem que operação não pode ser concluida, por causa das dependências dos registros (que é uma regra de negócio do sistema).

Só que como o Vicente pretende implementar em benefício da usabilidade algo que se antevenha a essa validação de força bruta, enumeramos diversas possibilidaes, e nas que não tornaram a solução totalmente dependende de banco de dados causou a sensação de que era muito código para pouca coisa.

A mais simples dela seria produzir uma trigger de rastreabilidade que chamasse. Mas essa deveria ser replicada para cada tabela dependente, ou buscar uma solução mais dinâmica, fazendo uso dos metadados.

Em nenhuma das situações foi alcançado um resultado tão simples e objetivo como o que 'poguiamos'.

Quem me conhece e também conhece o Vicente sabe que somos mega perfeccionistas e a prova disso é que ele não se deu satisfeito com meu incentivo moral ahahah e decidiu compartilhar nossa obra prima para buscar uma solução mais elegante. Eu disse a ele que nos divertiríamos muito com isso!

abraços

Vicente Maciel Junior disse...

PROTESTO!

(risos)

Como assim não me dei por satisfeito? Fiquei até orgulhoso (argh)!

A questão é simplesmente não me conformar com as várias situações com as quais a gente se depara e temos que desenvolver POGs simplesmente porque ninguém pensou nisso antes.

Como assim ninguém pensou antes na necessidade de um objeto de dados reportar as suas dependências individuais. Isso soa absurdo para mim.

Mas é isso ai Jeff! Por isso que trabalhar soluções com você além de produtivo, é muito divertido!

;)

Fernando S. Trevisan disse...

Vicente e Jefferson (especialmente respondendo ao comentário do Jefferson :)

Entendi desde o princípio o problema que vocês tiveram :D Quando houver imagens, o tipo não pode ser excluído, correto?

Se você fizer um select distinct na foreign key da tabela imagens, terá uma lista com os IDs de tipos que não podem ser excluídos.

Na hora de exibir a listagem de tipos, basta verificar contra a lista. É uma query só (neste POG, em oposição a uma query para cada tipo, do POG de vocês) e um IF na hora de exibir.

Para fazer de forma mais fácil de manter, dá pra ter uma função ou um método em um objeto que retorne se ele consta da lista; dá para armazenar em cache e dar flush conforme atualiza-se a tabela de imagens, enfim, tem diversas formas de fazer.

Não acho que a solução de vocês foi ruim, mas definitivamente me dá arrepios ver uma solução que testa diretamente a deleção. Acho difícil algo dar errado, mas... ... ...

Pois é :D

Abs!

Vicente Maciel Junior disse...

Entendi perfeitamente sua proposta de solução Fernando. O problema para mim seria encaixá-la na arquitetura que já tenho implementada.

Trata-se de uma aplicação com Front-end Flex e que no Back-end consome o Transfer (framework). Na arquitetura que desenvolvi no server-side consumindo o Transfer utilizo-o como DAO, tendo obviamente 1 para cada tipo/dado (entidade) bem como um Service que será consumido pelo Flex.

Não uso certas convenções de nomemclatura, mas então, neste esquema tenho:

- ImageTypeService (DAO*)
- ImageTypeRemoteService
- ImageService (DAO*)
- ImageRemoteService

* Como citei, simples implementação do Transfer em sí.

Claro, estou excluindo citar VOs e outras partes da arquitetura por não fazerem parte do contexto da discussão.

Desta forma, tenho nos objetos total independência quanto a relacionarem-se com outros tipos já que utilizando o Transfer posso deixar a relação de uma dado com outro dado totalmente por conta dele.

Exemplo: Embora eu tenha o dado Image relacionando-se com ImageType em nenhum momento dentro de Image eu tenho ligação com o objeto ImageType a não ser um valor "imagetype" que consta no VO ImageVO. Enfim, a abstração do Transfer me permite tratar isso muito bem.

Então, foi muito mais simples, dentro do ImageTypeRemoteService eu ter um método "isActive" com o POG, sem depender de analisar a tabela relacionada a "Image".

Concordo plenamente que é assustador testar um DELETE. Principalmente valer-se de um ROLLBACK por segurança. Mas qual outra forma mais simples de testar um CONSTRAINT do que essa? Concordo que deveria existir, mas até agora não achei.

Eu vou dar uma olhada no Transfer depois para ver se não há como eu me valer dos recursos internos dele para fazer algo parecido com o que você sugeriu sem que eu tenha que quebrar a regra de manter o isolamento dos meus objetos, ou seja, ter que instanciar o DAO/RemoteService de Image dentro do RemoteService de ImageType.

Estou quase certo de que dá pra fazer algo do tipo com o objeto "com.transfer.object.Object" que posso obter através do "transferFactory.getTransfer().getTransferMetaData('tipo.Tipo')".

PS: Sei que este inter-instanciamento de objetos pode ser inevitável. Mas estou tentando ao máximo evitar objetivando o melhor que eu puder de "loose coupling" na arquitetura. A gente entraria ai em outro grau de discussão, eu sei. Do tipo "loose/tight coupling x performance", mas por enquanto, o máximo de modularidade possível é o meu maior objetivo, mas estou de olho na performance. Quando começar a ter a performance prejudicada pelo "loose coupling" que estou forçando, extendo os objetos e torno-os "tight coupling" para combater tais problemas.

Espero que eu tenha conseguido justificar melhor meu contexto.

CARAMBA! Conversar de programação com vocês dois exige um grau de preparo enorme! Affff! Vamos ver até onde eu "guento". ;)

jpetilo disse...

Entendi a solução do Fernando, eu já fiz coisas assim também e é bastante útil.

Quanto a deleção, ela não funciona se existir dependências já que ela se apoia naquele princípio inicial da chave estrangeira, que não possui deleção em cascata. Algo que é 100% confiável.

No entanto, envolvendo frameworks, para evitar workarround eu optaria por internalizar essas regras por meio de uma trigger. Dessa forma, o dao já traria uma coluna com um flag, informado se o registro pode ou não ser deletado (possui ou não dependências) e seria transparente na interface.

Jeff

Fernando S. Trevisan disse...

Vicente, agora eu entendi, não sabia em que framework (sempre há um, não é mesmo? :) a solução tinha que se encaixar.

Nunca trabalhei com Transfer mas entendi o conceito e o problema que vocês enfrentaram... realmente.

E Jefferson, realmente, a solução via BD é bem melhor que qualquer coisa "improvisada" no CF. :)

Abs e valeu pela "discussão"!

Vicente Maciel Junior disse...

Essa questão do BD remete a discussão novamente para a questão de ter uma solução dependente do banco de dados a ser utilizado e o gerenciamento de várias versões (code) de uma mesma feature.

Exatamente o que discutimos inicialmente Jefferson e que culminou no POG para simplificar, excluir qualquer dependência e tornar a manutenção simples.

Eu quem agradeço o nivel da discussão Fernando!

jpetilo disse...

Raciocinando melhor... a sugestão inicial do Fernando, em criar uma view (ou um meétodo sql no componente do Service) pode resolver a questão de forma muito melhor.. e a sql não ficaria complexa como eu inicialmente disse.. seria algo assim : perfeitamente implementável em qualquer framework.


SELECT it.imagem_tipo_id,it.imagem_tipo,
( select count(1)
from imagem as im
where im.imagem_tipo_id = it.imagem_tipo_id) as active
FROM imagem_tipo as it


Abaixo o POG!!!

Fernando S. Trevisan disse...

Jeff, ah-ha! Eu sabia desde o começo, hahahaha :D

Abs!

Vicente Maciel Junior disse...

Realmente! Agora sim elegante. Não tinha enxergado dessa maneira quando o Fernando sugeriu.

Melhor seria se o Transfer suportasse Views. Algo a verificar.

Mas se eu não implementar Views, para manter o Loose Coumpling estou pensando em ter em criar um objeto na arquitetura para tratar essa view.

Assim que implementar relato aqui.

Super thanks galera!

Vicente Maciel Junior disse...

TB ABAIXO O POG!

Só para ficar documentado de certa forma, eu resolvi o problema, na minha opinião, da forma mais elegante possível e que também poderia causar menos impacto.

Em resumo, simplesmente explorando os recursos do Transfer-ORM.

Ao definirmos as configurações dos objetos que representarão as tabelas do nosso banco de dados, referenciamos também os seus relacionamentos nas 3 formas possíveis em qualquer banco: ONE2MANY, MANY2ONE e MANY2MANY. A escolha de um ou outro determina a funcionalidade/estrutura dos objetos que representarão dados no framework.

Resumindo o máximo possível a solução, eu simplesmente mudei a relação entre os objetos que representam as tabelas Imagem e TipoDeImagem, de:

Imagem > ManyToOne > TipoDeImagem

para:

TipoDeImagem > OneToMany > Imagem

A difereça é que o framework na relação ManyToOne a coleção Imagem trazia para cada item uma referência para o respectivo item da coleção TipoDeImagem e na relação OneToMany a coleção TipoDeImagem traz para cada item uma Array de referência aos elementos da coleção Imagem que apontam para ele.

Assim ficou muito fácil, rápido, pratico, dentro das convenções do framework, mantendo meu "loose coupling" ao máximo, determinar o valor da propriedade "ativo" para cada item, simplesmente verificanto o "lenght" desta array.

DETALHE: Pesquisei nas "entranhas" do Transfer e percebi que ele se vale de um cache, baseado no pattern "Memento" (http://en.wikipedia.org/wiki/Memento), para não implicar esses recursos em problemas de performance, de uma forma que me pareceu fantástica.

Depois vou fazer um post colocando a solução com seus devidos códigos.

Muito obrigado pelo incentivo ao NÃO POG! ;)