Referenciando valores do banco de dados na lógica de negócios

43

Acho que essa é outra pergunta sobre codificação embutida e práticas recomendadas. Digamos que eu tenha uma lista de valores, digamos frutas, armazenados no banco de dados (ele precisa estar no banco de dados, pois a tabela é usada para outros fins, como relatórios do SSRS), com um ID:

1 Apple 
2 Banana 
3 Grapes

Eu posso apresentá-los ao usuário, ele seleciona um, ele é armazenado em seu perfil como FavouriteFruit e o ID armazenado em seu registro no banco de dados.

Quando se trata de regras de negócios / lógica de domínio, quais são as recomendações para atribuir lógica a valores específicos. Digamos que se o usuário selecionou o Grapes, quero executar alguma tarefa extra, qual é a melhor maneira de referenciar o valor do Grapes:

// Hard coded name
if (user.FavouriteFruit.Name == "Grapes")

// Hard coded ID
if (user.FavoriteFruit.ID == 3) // Grapes

// Duplicate the list of fruits in an enum
if (user.FavouriteFruit.ID == (int)Fruits.Grapes)

ou alguma outra coisa?

Como é claro que o FavouriteFruit será usado em todo o aplicativo, a lista poderá ser adicionada ou editada.

Alguém pode decidir que deseja que 'Grapes' seja renomeado para 'Grape' e isso, obviamente, quebraria a opção de string codificada.

O código codificado não é totalmente claro, porém, como mostrado, você pode adicionar um comentário para identificar rapidamente qual item é.

A opção enum envolve a duplicação de dados do banco de dados, o que parece errado, pois pode ficar fora de sincronia.

De qualquer forma, agradeço antecipadamente por quaisquer comentários ou sugestões.

Kate
fonte
1
Obrigado a todos: as sugestões e conselhos gerais são realmente úteis. @RemcoGerlich sua ideia de separar as preocupações de uma string usada para fins de exibição e outra como um código de pesquisa para um código mais legível é muito boa.
Kate
1
Vou dar a @Mike Nakis sua idéia de objetos pré-carregados, pois isso parece ser o melhor dos dois mundos.
Kate
1
Eu sugeriria uma variação na sua primeira solução. Faça com que sua tabela contenha uma terceira coluna de como será processada e você usa esse campo para determinar qual código executar. Não é um campo de exibição e pode ser compartilhado entre vários frutos.
Kickstart
1
A opção enum envolve a duplicação de dados do banco de dados, o que parece errado, pois pode ficar fora de sincronia. Eu gosto disso, na verdade. É como a contabilidade de dupla entrada. Se os dois lados do livro não se equilibrarem, você saberá que algo está errado. Isso torna as coisas de mudança mais deliberadas.
Radarbob 04/12/2015
1
Hmmm ... Se houver uma relação de 1: 1 de ID para uma String, isso será redundante e ter os dois será inútil. Uma String pode servir como uma chave de banco de dados tão bem quanto como um número inteiro. MyApplication.Grape.IDé gago, por assim dizer. Uma "Apple" não é uma "Red_Apple", assim como o ID 3 também é 4. Portanto, o potencial de renomear "Apple" para "Red_Apple" não faz mais sentido do que declarar que 3 é 4 (e talvez até 3). O objetivo de um enum é abstrair seu DNA numérico. Talvez seja hora de realmente desacoplar chaves de banco de dados relacionais arbitrárias que literalmente não têm significado nos modelos de negócios de alguém.
radarbob

Respostas:

31

Evite seqüências de caracteres e constantes mágicas a todo custo. Eles estão completamente fora de questão, nem devem ser considerados opções. Isso parece deixar você com apenas uma opção viável: identificadores, ou seja, enumerações. No entanto, há também mais uma opção, que na minha opinião é a melhor. Vamos chamar esta opção "Objetos pré-carregados". Com objetos pré-carregados, você pode fazer o seguinte:

if( user.FavouriteFruit.ID == MyApplication.Grape.ID )

O que acabou de acontecer aqui é que eu obviamente carreguei toda a linha Grapena memória, por isso tenho seu ID pronto para uso em comparações. Se você estiver usando ORM (Object-Relational Mapping), ficará ainda melhor:

if( user.FavouriteFruit == MyApplication.Grape )

(É por isso que eu chamo de "Objetos pré-carregados".)

Portanto, o que faço é que, durante a inicialização, carrego todas as minhas tabelas de "enumeração" (tabelas pequenas, como dias da semana, meses do ano, sexos etc.) na classe de domínio do aplicativo principal. Eu os carrego pelo nome, porque, obviamente, MyApplication.Grapedeve receber a linha chamada "Grape", e afirmo que todos e cada um deles foram encontrados. Caso contrário, temos um erro de tempo de execução garantido durante a inicialização, que é o menos maligno de todos os erros de tempo de execução.

Mike Nakis
fonte
17
Não discordo da resposta, mas acho que o imperativo de "Evitar seqüências de caracteres e constantes mágicas a todo custo" discorda do restante da resposta, que na verdade exige que você tenha pelo menos um lugar onde constantes ou cadeias mágicas sejam usadas ao preencher seus "objetos pré-carregados". Isso é notável, penso eu, porque não são maneiras de evitar "cordas e constantes mágicas" inteiramente, mas é geralmente mais ofuscando do que vale a pena ...
svidgen
2
@svidgen, você não concorda que existe uma diferença fundamental entre espalhar a ligação por nome em todo o lugar x vincular por nome apenas uma vez, para carregar o conteúdo de um registro com o mesmo nome e fazê-lo apenas na inicialização, onde erros de tempo de execução são quase tão benignos quanto erros de compilação? De qualquer forma, maneiras de evitar até a menor ligação pelo nome são sempre interessantes, apesar da ofuscação que você mencionou, então eu ficaria curioso para saber o que você tem em mente.
quer
Oh, eu concordo completamente. E, dada a natureza do OP, eu apenas sugeriria que essa resposta poderia se beneficiar da alteração "a todo custo" para "sempre que possível e possível" ou algo semelhante. ... Se eu tivesse mais tempo, apenas para completar, escreveria uma resposta que lida com algum tipo de absurdo metaprogramador ... mas, não é disso que o OP (ou alguém na maioria dos casos) provavelmente precisa . Mas, uma solução de metaprogamming se alinharia mais com sua primeira declaração como está.
Svidgen
1
@ user469104 a diferença é que os IDs podem mudar, e o aplicativo ainda carregará todas as linhas corretamente e executará todas as comparações corretamente. Além disso, você é livre para refatorar o código e renomear linhas da maneira que desejar, e o único lugar onde você precisa procurar coisas para corrigir é na inicialização do aplicativo, e isso tende a ser muito óbvio: Grape = fetchRow( Fruit.class, NameColumn, "Grape" ); e se você faça algo incorretamente, AssertionErrore você será informado.
Mike Nakis
1
@grahamparks não mais do que um enumteria sido uma sequência mágica. O ponto é a concentração de todas as ligações por nome em apenas um local , a validação de todas durante a inicialização e a segurança do tipo .
precisa
7

A verificação na cadeia de caracteres é a mais legível, mas tem dupla função: é usada como identificador e como descrição (que pode mudar por razões não relacionadas).

Normalmente, divido as duas tarefas em campos separados:

id  code    description
 1  grape   Grapes
 2  apple   Apple

Onde a descrição pode mudar (mas não "Grapes" para "Banana"), mas o código não pode mudar, nunca.

Embora isso ocorra principalmente porque nossos IDs são quase sempre gerados automaticamente e, portanto, não são adequados. Se você pode escolher IDs livremente, talvez possa garantir que eles estejam sempre corretos e usá-los.

Além disso, com que frequência alguém realmente edita "Grapes" para "Grape"? Talvez nada disso seja necessário.

RemcoGerlich
fonte
8
Eu não acho que ainda mais redundância é a resposta ...
Robbie Dee
4
Também considerei essa opção e tentei, mas foi o que acabou acontecendo: em algum momento, a "maçã" teve que ser diferenciada em "maçã verde" e "maçã vermelha". Mas como "apple" já era usado em uma infinidade de lugares no código, não era possível renomeá-lo, portanto era necessário ter "apple" e "green_apple". E, como resultado, o Sheldon em mim me impediu de dormir por várias noites até eu entrar lá e refatorar tudo para "Objetos pré-carregados". (ver minha resposta.)
Mike Nakis
1
Eu definitivamente gosto dos seus objetos pré-carregados, mas se a sua "maçã" é diferenciada, você não precisa repassar tudo de qualquer maneira, seja qual for o método escolhido?
RemcoGerlich
Você pode até ter uma tabela separada para o nome da descrição, em apoio à internacionalização.
Erik Eidt
1
O @MikeNakis e a refatoração são essencialmente uma pesquisa e substituição em toda a sua base de código, substituindo Fruit.Apple por Fruit.GreenApple. Se eu usar valores de String codificados, eu faria uma Pesquisa e substituição em toda a base de código para substituir "apple" por "green_apple", que é quase a mesma coisa. - A refatoração parece melhor, porque o IDE está substituindo.
Falco
4

O que você espera aqui é que a lógica de programação seja automaticamente adaptável à alteração de dados. Opções estáticas simples como o Enum não funcionam aqui porque você não pode adicionar enumerações extras no tempo de execução.

Alguns padrões que eu já vi:

  • Enums + padrão para proteger contra uma nova entrada no banco de dados que arruina o dia do seu programa.
  • Codificação de ações a serem executadas (lógica de negócios) no próprio banco de dados. Em muitos casos, isso é muito possível porque muitas lógicas são reutilizadas. A implementação da lógica deve estar no programa.
  • Atributos / colunas extras no banco de dados para marcar o novo valor como 'a ser ignorado' no programa até que o programa seja implantado corretamente.
  • Falha nos mecanismos rápidos em torno do caminho do código que carrega / recarrega os valores do banco de dados. (Se a ação correspondente não estiver no programa E não estiver marcada para ser ignorada, não faça a atualização).

Em geral, eu gosto que os dados sejam completos em termos de referência às ações que elas implicam - mesmo que as próprias ações possam ser implementadas em outro lugar. Qualquer código que determine ações independentes dos dados apenas fragmentou sua representação de dados, o que provavelmente irá divergir e levar a erros.

Subu Sankara Subramanian
fonte
4

Armazená-los nos dois lugares (em uma tabela e em um ENUM) não é tão ruim assim. O raciocínio é o seguinte:

Armazenando-os em uma tabela de banco de dados, podemos impor integridade referencial no banco de dados por meio de chaves estrangeiras. Portanto, quando você associa uma pessoa ou qualquer entidade a uma fruta, é apenas uma fruta que existe na tabela do banco de dados.

Armazená-los como um ENUM também faz sentido, porque podemos escrever código sem seqüências de caracteres mágicas e isso torna o código mais legível. Sim, eles precisam se manter sincronizados, mas realmente seria difícil adicionar uma linha ao ENUM e uma nova instrução de inserção ao banco de dados.

Uma coisa, uma vez que um ENUM é definido, não altere seu valor. Por exemplo, se você tivesse:

  • maçã
  • Uva

NÃO renomeie Grape para Grapes. Basta adicionar um novo ENUM.

  • maçã
  • Uva
  • Uvas

Se você precisar migrar dados, aplique uma atualização para mover toda a Grape para Grapes.

Jon Raynor
fonte
Como etapa adicional, trabalhei em lojas onde os valores de metadados têm um sinalizador de exclusão na tabela para indicar que eles não devem ser usados ​​(eles foram descontinuados ou existe uma versão mais recente).
Robbie Dee
1

Você está certo em fazer esta pergunta; na verdade, é uma boa pergunta, enquanto você tenta se defender contra a avaliação de condições imprecisas.

Dito isto, a avaliação (suas ifcondições) não precisa necessariamente ser o foco de como você a contorna. Em vez disso, preste atenção à maneira como você propaga as alterações que causariam um problema 'fora de sincronia'.

Abordagem de String

Se você deve usar strings, por que não expor a funcionalidade de alterar a lista por meio da interface do usuário? Projete o sistema para que, ao mudar Grapepara Grapes, por exemplo, você atualize todos os registros atualmente em referência Grape.

Abordagem de identificação

Eu sempre prefiro fazer referência a um ID, apesar do comprometimento de alguma legibilidade. The list may be added tonovamente pode ser algo de que você seria notificado se expusesse esse recurso de interface do usuário. Se você estiver preocupado com a reordenação de itens alterando o ID, propague essa alteração para todos os registros dependentes novamente. Da mesma forma que acima. Outra opção (seguindo a convenção de normalização adequada, seria ter uma coluna enum / id - e fazer referência a uma FruitDetailtabela mais detalhada , que possui uma coluna 'Order' que você pode procurar).

De qualquer forma, você pode ver que estou sugerindo controlar a alteração ou atualização da sua lista. Se você faz isso através do uso de um ORM ou de algum outro acesso a dados, é determinado pelas especificidades de sua tecnologia. O que você está fazendo, essencialmente, é exigir que as pessoas que se afastam do DB para essas mudanças - o que eu acho que está bem. A maioria dos principais CRMs fará o mesmo requisito.

JᴀʏMᴇᴇ
fonte
1
No banco de dados , o ID numérico está sendo armazenado para registros filho, especificamente para evitar esse problema. Esta pergunta é sobre como fazer interface com uma linguagem de programação.
Clockwork-Muse
1
@ Clockwork-Muse - para evitar que problema? Isso não faz sentido.
31515
Eu uso bastante a abordagem de identificação, mas a identificação está bloqueada e não pode ser alterada. A cadeia anexada, é claro, pode porque as pessoas geralmente gostam de renomear coisas como "caminhão" se tornam "caminhão" etc., enquanto a coisa em si (representada por ID) não muda.
Brian Knoblauch
Se você seguir a abordagem de identificação, como você lida com os bancos de dados de desenvolvimento versus produção? Com os IDs incrementados automaticamente, adicionar itens aos dois DBs em ordem diferente resultará em IDs diferentes.
Protector one
Não precisa ser incrementado automaticamente? Nesse caso, não deve ser, especialmente se for o valor inteiro da enumeração subjacente que estamos usando.
JᴀʏMᴇᴇ 18/07
0

Um problema muito comum. Embora a duplicação do lado do cliente de dados pareça violar os princípios DRY , isso se deve realmente à diferença de paradigma entre as camadas.

Ter a enumeração (ou qualquer outra coisa) fora de sintonia com o banco de dados também não é tão incomum. Você pode ter enviado outro valor para uma tabela de metadados para oferecer suporte a um novo recurso de relatórios que ainda não foi usado no código do lado do cliente.

Às vezes acontece o contrário também. Um novo valor de enumeração é adicionado ao lado do cliente, mas a atualização do banco de dados não pode ocorrer até que o DBA possa aplicar as alterações.

Robbie Dee
fonte
Sim, você descreveu o problema. Qual é a sua solução?
Protector one
1
@Protectorone Você supor que não é uma solução bala de prata que é uma suposição errônea na minha experiência. O melhor que você pode esperar é que alguma entidade comercial possua o domínio do problema, pelo menos você poderá ver qual lado está atrasado - lado do cliente ou lado do banco de dados. Banca e finanças são tipicamente muito eficientes a este respeito com o setor de varejo estar visivelmente menos assim ...
Robbie Dee
0

Supondo que estamos falando sobre o que é essencialmente uma pesquisa estática, a terceira opção - o enum - é basicamente a única opção sensata. É o que você faria se o banco de dados não estivesse envolvido, por isso faz sentido.

A pergunta então é sobre como manter enumerações e tabelas estáticas / de pesquisa no banco de dados sincronizadas e, infelizmente, isso não é um problema para o qual ainda tenho uma resposta completa.

Por opção, faço toda a manutenção do esquema no código e, portanto, posso manter um relacionamento entre a compilação do aplicativo e a versão esperada do esquema, por isso é fácil manter a pesquisa e a enumeração sincronizadas, mas é algo que é preciso lembrar. Faz. Seria melhor se fosse mais automatizado (e também um teste de integração automatizado para garantir que as enumerações e as pesquisas correspondessem), mas isso não é algo que eu já implementei.

Murph
fonte
1
Não acredito que sejam apenas pesquisas estáticas, caso contrário, poderiam ser extraídas do banco de dados e consumidas como estão. O problema que eu entendo é quando a lógica de negócios deve ser aplicada, dependendo do valor de pesquisa usado. Mas, além disso, sim - enums são geralmente empregados para esse fim.
Robbie Dee #
Ok, preciso de um termo melhor "para pesquisa estática", o contexto que você descreve é ​​o que eu quis dizer :) A chave é "estática" - esses são valores que não alteram o problema, estão adicionando novos valores e alterando o "rótulo" ( mas não a intenção) para valores existentes.
Murph