Abordar o fato de que as chaves primárias não fazem parte do seu domínio comercial

25

Em quase todas as circunstâncias, as chaves primárias não fazem parte do seu domínio comercial. Claro, você pode ter alguns objetos importantes voltados para o usuário com índices exclusivos ( UserNamepara usuários ou OrderNumberpedidos), mas na maioria dos casos, não há necessidade comercial de identificar abertamente objetos de domínio por um único valor ou conjunto de valores, para qualquer pessoa, exceto talvez um usuário administrativo. Mesmo nesses casos excepcionais, especialmente se você estiver usando identificadores exclusivos globais (GUID) , você gostará ou desejará empregar uma chave alternativa em vez de expor a própria chave primária.

Portanto, se minha compreensão do design orientado a domínio é precisa, as chaves primárias não precisam e, portanto , não devem ser expostas, e uma boa viagem. Eles são feios e cãibras no meu estilo. Mas se optarmos por não incluir chaves primárias no modelo de domínio, haverá consequências:

  1. Ingenuamente, objetos de transferência de dados (DTO) derivados exclusivamente de combinações de modelos de domínio não terão chaves primárias
  2. Os DTOs recebidos não terão uma chave primária

Portanto, é seguro dizer que, se você realmente permanecer puro e eliminar as chaves primárias no seu modelo de domínio, deverá estar preparado para lidar com todas as solicitações em termos de índices exclusivos nessa chave primária?

Em outras palavras, qual das seguintes soluções é a abordagem correta para lidar com a identificação de objetos específicos após remover a PK em modelos de domínio?

  1. Ser capaz de identificar os objetos com os quais você precisa lidar com outros atributos
  2. Recuperando a chave primária no DTO; ou seja, eliminando a PK ao mapear da persistência para o domínio e recombinando a PK ao mapear do domínio para o DTO?

EDIT: Vamos tornar isso concreto.

Diga meu modelo de domínio é VoIPProvidero que inclui campos como Name, Description, URL, bem como referências gosto ProviderType, PhysicalAddresse Transactions.

Agora, digamos que eu queira criar um serviço da Web que permita que usuários privilegiados gerenciem VoIPProviders.

Talvez um ID amigável ao usuário seja inútil nesse caso; afinal, os provedores de VoIP são empresas cujos nomes tendem a ser distintos no sentido do computador e até distintos o suficiente no sentido humano por razões comerciais. Portanto, basta dizer que um único VoIPProvideré completamente determinado por (Name, URL). Então agora vamos dizer que preciso de um método PUT api/providers/voippara que usuários privilegiados possam atualizar os VoIPprovedores. Eles enviam um VoIPProviderDTO, que inclui muitos, mas não todos os campos do VoIPProvider, incluindo alguns achatamentos potencialmente. No entanto, não consigo ler a mente deles, e eles ainda precisam me dizer de qual provedor estamos falando.

Parece que tenho 2 (talvez 3) opções:

  1. Inclua uma chave primária ou alternativa no meu modelo de domínio e envie-a para o DTO e vice-versa
  2. Identifique o provedor com o qual nos preocupamos por meio do índice exclusivo, como (Name, Url)
  3. Introduzir algum tipo de objeto intermediário que sempre possa mapear entre a camada de persistência, domínio e DTO de uma maneira que não exponha os detalhes da implementação sobre a camada de persistência - digamos, introduzindo um identificador temporário na memória ao passar do domínio para o DTO e vice-versa,
tacos_tacos_tacos
fonte
1
Ponto para refletir: muitas vezes a comunicação com especialistas em domínio fica empobrecida quando PK substituta é usada quando existe uma boa chave comercial. Parece que acabamos trabalhando para a estrutura ORM, e não o contrário.
Tulains Córdova
@ user61852 bem, independentemente do ORM, mesmo se você realmente tiver um nível baixo, ainda precisará de uma chave primária na implementação da camada do banco de dados. Portanto, concordo que a PK substituta oferece vantagens sobre a PK real usada por um mecanismo de persistência específico, mas se ela realmente representa um objeto de negócios que é significativo, é necessariamente único e, portanto, possui pelo menos uma propriedade exclusiva relacionada aos negócios que define isto não?
Tacos_tacos_tacos 27/08/14
1
Todas as vantagens dos substitutos são relacionadas ao computador e nenhuma relacionada ao ser humano.
Tulains Córdova
2
@ user61852: Concordo 100% (escrevi algo diferente?). Para meios de comunicação, use uma "chave comercial". Adicione restrições exclusivas para qualquer chave comercial também. Mas evite o uso de chaves de negócios para realmente implementar suas referências de banco de dados.
Doc Brown
2
as chaves comerciais são eternamente únicas - até que não sejam. Se você estiver usando as chaves de negócios como principais quando isso acontecer, a alteração nas regras de negócios quebrará mais coisas.
Psr

Respostas:

31

É assim que resolvemos isso (há mais de 15 anos, quando nem mesmo o termo "design controlado por domínio" foi inventado):

  • ao mapear o modelo de domínio para uma implementação de banco de dados ou modelo de classe em uma linguagem de programação específica, você possui uma regra simples e consistente como "para cada objeto de domínio mapeado para uma tabela relacional, a chave primária é" TablenameID ".
  • essa chave primária é completamente artificial, sempre tem o mesmo tipo e não tem significado comercial - apenas uma chave substituta
  • a "versão gráfica" do seu modelo de domínio (a que você usa para conversar com seus especialistas em domínio) não contém chaves primárias. Você não os expõe diretamente aos especialistas (mas os expõe a qualquer pessoa que esteja implementando o código do sistema).

Portanto, sempre que você precisar de uma chave primária para fins técnicos (como mapear relações com um banco de dados), você terá uma disponível, mas contanto que não queira "vê-la", altere seu nível de abstração para o "modelo de especialistas em domínio" " E você não precisa manter "dois modelos" (um com PKs e outro sem); em vez disso, mantenha apenas um modelo sem PKs e use um gerador de código para criar a DDL para seu DB, que adiciona a PK automaticamente de acordo com as regras de mapeamento.

Observe que isso não proíbe adicionar nenhuma "chave comercial" como um "OrderNumber" adicional, além do substituto OrderID. Tecnicamente, essas chaves de negócios se tornam chaves alternativas ao mapear para seu banco de dados. Apenas evite usá-las para criar referências a outras tabelas, sempre prefira usar as chaves substitutas, se possível, isso facilitará muito as coisas.

Para seu comentário: usar uma chave substituta para identificar registros não é uma operação relacionada a negócios, é uma operação puramente técnica. Para deixar isso claro, veja o seu exemplo: desde que você não defina restrições exclusivas adicionais, seria possível ter dois objetos VoIPProvider com a mesma combinação de (nome, url), mas VoIPProviderIDs diferentes.

Doc Brown
fonte
E a etapa de obter o objeto de domínio do objeto de persistência retornado (entidade ou modelo de linha da tabela ou qualquer outra coisa), acessar o DTO, recuperá-lo e retornar à persistência? Isso é feito apenas através de uma chave substituta (ou seja, uma definição de exclusividade orientada para os negócios) que requer uma resolução em cada operação de persistência?
tacos_tacos_tacos 27/08/14
1
@tacos_tacos_tacos: vamos nos ater ao seu exemplo de VoIPProvider. Na verdade, eu adicionaria um "VoIPProviderID" ao seu DTO, pelo menos no "lado da implementação" (se você também tiver uma versão gráfica para os especialistas em seu domínio, provavelmente não o mostrarei lá). Para fins de atualização, a maneira padrão de identificar um VoIPProvider específico deve ser o "VoIPProviderID" que você recuperou ao extrair os dados do banco de dados. Se os usuários da sua API preferirem a identificação por (nome, URL), forneça isso adicionalmente. ...
Doc Brown
... e se o desempenho parecer se tornar um problema real e mensurável, você também pode considerar o cache do mapeamento (nome, URL) para o VoIPProviderID em algum lugar. Mas eu não recomendaria implementar essa otimização antecipadamente, prematuramente.
Doc Brown
1
Às vezes, as chaves naturais parecem realmente atraentes, mas é muito fácil se queimar (por exemplo, "opa, agora eu tenho vários inquilinos e isso não é mais exclusivo").
Casey
1
@corsiKa: no contexto do que o OP pediu, recomendo fortemente que você tenha uma chave puramente gerada automaticamente "OrderID" (que não é impressa em nenhum recibo, mas usada apenas para tarefas internas, como referências a bancos de dados) e uma chave comercial separada "OrderNumber" (que pode, por exemplo, conter algo como o ano atual, que pode ser usado para classificação e filtragem, que pode ser alterado / corrigido posteriormente e que pode ser impresso nos recibos). O OP solicitou "Design Orientado a Domínio", o "Número da Ordem" faz parte do modelo de domínio, enquanto o "Código da Ordem" é apenas um detalhe da implementação.
Doc Brown
4

Você precisa ser capaz de identificar muitos objetos por algum índice exclusivo, e não é isso que é uma chave primária (ou pelo menos implica que uma esteja presente).

Índices exclusivos estão disponíveis para que você possa restringir ainda mais seu esquema de banco de dados, não como um substituto de atacado para PKs. Se você não está expondo PKs porque é feio, mas está expondo uma chave única ... você não está realmente fazendo algo diferente. (Presumo que você não esteja recebendo um PK e uma coluna de identidade aqui?)

gbjbaanb
fonte
Quando eu quero fazer uma operação que envolva uma instância específica de um objeto de domínio que seja persistente, preciso poder incluir no DTO explicitamente (por meio de algum tipo de chave, primário ou um ID de fácil utilização alternativo que seja exclusivo nessa tabela e também pode ser um índice na tabela) ou implicitamente (por meio de uma combinação de campos cujos valores identificam exclusivamente um registro específico) ... e além disso, no sentido prático, eu precisaria enviar no DTO alguma forma de rastreamento de alterações se eu fizesse o contrário (OriginalVal vs NewVal para todos os campos que identificam registro), não?
tacos_tacos_tacos 27/08/14
a pergunta explícita v implícita não é a mesma diferença? Você pode ter uma PK que abranja várias colunas, assim como um índice exclusivo. Não vejo diferença entre eles para seus propósitos.
Gbjbaanb
Claro, poderíamos ter um PK em várias colunas, por exemplo. Mas para mim está vazando algo sobre o banco de dados (o armazenamento) que não deve ter nada a ver com o coração e a alma, a entidade comercial. Se alguma tupla de campos de entidade comercial abranger a PK do banco de dados, ótimo. Mas não deve ser necessariamente o contrário, deveria?
Tacos_tacos_tacos 27/08/14
Você está pensando demais. Um índice exclusivo é tanto um artefato do esquema do banco de dados quanto um PK. Pense desta maneira - uma PK é apenas o primeiro (ou principal) índice exclusivo. é 'especial' porque geralmente você só precisa de um desses índices.
Gbjbaanb
É verdade, mas qualquer objeto de domínio significativo deve ser identificável estritamente em pelo menos um de seus campos relacionados a negócios, não? O fato de isso definir um índice no banco de dados é mais por razões de desempenho do que para ser usado para consultar o banco de dados facilmente ... Eu preferiria uma PK de uma coluna a um índice exclusivo de 6 colunas, e elas realmente servem a diferentes propósitos - um PK (ou índice com pequeno número de campos) também está disponível para conveniência do DBA / DBD, certo?
precisa saber é o seguinte
4

Sem chaves primárias no front-end, não há uma maneira fácil para o back-end saber o que você está enviando. Para corrigir isso, você precisaria de muito trabalho extra na análise dos dados, o que prejudicaria o desempenho e provavelmente levaria mais tempo e seria mais feio do que anexar uma chave a cada item.

Como exemplo, digamos que eu quero editar uma mensagem em um aplicativo; como o aplicativo saberia qual mensagem eu quero editar sem uma chave primária anexada? Editar objetos acontece o tempo todo e fazer isso sem chaves é quase impossível. Mas se você tiver objetos que não devem ser editados, pule a chave se achar que isso é perturbador, mas ter chaves primárias pode melhorar o desempenho aqui.

Johan
fonte
Bem, a mensagem é um exemplo extremo, mas mesmo assim nós saberíamos MessageSender, MessageRecipient, TimeSent- que deve ser único.
tacos_tacos_tacos
1
@tacos_tacos_tacos, como você cria FKs para as outras tabelas? Deve ser MessageSenderId, que provavelmente mapeia para uma tabela Usuários no UserId. Você não gostaria de usar o UserName como a chave entre as tabelas, pois isso pode mudar e se tornar um pesadelo de manutenção. É por isso que você geralmente une tabelas usando Chaves Primárias e não outra coluna (certamente há exceções). Essa estrutura de banco de dados ainda deve ser imposta. Agora você sempre pode acessar um modelo CQRS para seu aplicativo ... nesse caso, as regras mudam. Especialmente se você também usar o Event Sourcing.
CaffGeek
4

O motivo pelo qual usamos uma PK não relacionada a negócios é garantir que nosso sistema tenha um método fácil e consistente de determinar o que o usuário deseja.

Vejo que você respondeu com um comentário: MessageSender, MessageRecipient, TimeSent (para uma mensagem). Você ainda pode ter ambiguidade dessa maneira (por exemplo, com mensagens geradas pelo sistema disparando em algo que acontece com frequência). E como você validará o MessageSender e o MessageRecipient aqui? Suponha que você os valide usando Nome, Sobrenome, DateOfBirth e, eventualmente, você se deparará com uma situação em que duas pessoas nasceram no mesmo dia com o mesmo nome. Sem mencionar que você encontrará uma situação em que você tem uma mensagem nomeada tacostacostacos-America-1980-Doc Brown-France-1965-23/5/2014-11:43:54.003UTC+200. Isso é um monstro de nome, e você ainda não tem garantia de que terá apenas um deles.

o motivo pelo qual usamos uma chave primária é que sabemos que será exclusivo para a vida útil do software, independentemente dos dados inseridos, e sabemos que será um formato previsível (o que acontece se a chave acima tiver um traço em um nome de usuário? todo o seu sistema é uma merda).

Você não precisa mostrar seu ID ao seu usuário. Você pode ocultar isso (à vista, se necessário, através do URL).

Outro motivo pelo qual uma PK é tão útil é algo que você pode deduzir do exposto acima: uma PK faz com que você não precise forçar o computador a interpretar o código gerado pelo usuário. Se um usuário chinês usa seu código e digita vários caracteres chineses, de repente ele não precisa trabalhar internamente com eles, mas pode simplesmente usar o Guid que o sistema gerou. Se você tem um usuário árabe que digita a escrita árabe, seu sistema não precisa lidar com isso internamente, mas pode basicamente ignorar que está lá.

Como outros já disseram, um Guid é algo que pode ser armazenado internamente em um tamanho fixo. Você sabe com o que trabalha e é algo que pode ser usado universalmente. Você não precisa criar regras de design sobre como criar um determinado identificador e salvá-lo. Se o seu sistema usar apenas as 10 primeiras letras de um nome, ele não verá nenhuma diferença entre Michael Guggenheimer e Michael Gugstein e os confundirá 2. Se você o interromper de qualquer tamanho arbitrário, poderá ficar confuso. Se você limitar a entrada do usuário, poderá encontrar problemas com as limitações do usuário.

Quando olho para sistemas existentes como o Dynamics CRM, eles também usam a chave interna (a PK) para o usuário chamar um único registro. Se um usuário tiver uma consulta que não envolva o ID, ele retornará uma matriz de respostas possíveis e permitirá que o usuário escolha a opção. Se houver alguma chance de ambiguidade, eles darão a opção ao usuário.

Finalmente, também é um pouco de segurança através da obscuridade. Se você não souber o ID do registro, sua única opção é adivinhar. se é fácil adivinhar o ID (porque as informações de que é feito estão disponíveis ao público), qualquer pessoa pode alterá-lo. Você pode até instigar o usuário a alterá-lo através dos métodos clássicos CSRF ou XSS. Agora, obviamente, sua segurança já deveria ter sido responsável e atenuada antes de publicar a versão ao vivo, mas você ainda deve dificultar a ocorrência de possíveis abusos.

Nzall
fonte
1

Ao emitir um identificador para um sistema externo, você deve fornecer apenas URIs ou, alternativamente, uma chave ou um conjunto de chaves que possuam as mesmas propriedades que um URI, em vez de expor diretamente a chave primária do banco de dados (daqui em diante, consultarei URI ou uma chave ou um conjunto de chaves que possuam as mesmas propriedades que apenas um URI, ou seja, um URI abaixo não significa necessariamente o RFC 3986 URI).

Um URI pode ou não conter a chave primária do objeto e pode ou não ser realmente composto de chaves alternativas. Isso realmente não importa. O que importa é que apenas o sistema que gera o URI tem permissão para dividir ou combinar o URI para formar um entendimento do que é o objeto referido. Os sistemas externos sempre devem usar o URI como um identificador opaco. Não importa se um usuário humano pode identificar que uma parte do URI é na verdade uma chave substituta de banco de dados ou se consiste em várias chaves de negócios agrupadas ou se é realmente uma base64 desses valores. Estes são irrelevantes. O que importa é que o sistema externo não precisa ser necessário para entender o que o identificador significa usar o identificador. Os sistemas externos nunca devem ser obrigados a analisar os componentes no identificador ou combinar um identificador com outros identificadores para se referir a algo em seu sistema.

O uso de GUID atende a alguns desses critérios; no entanto, pode ser difícil desreferir identificadores como GUID de volta para o objeto, mesmo dentro do seu sistema; portanto, chaves opacas ao seu sistema como GUID só devem ser empregadas se o cliente analisar o URI / identificador realmente representa um risco de segurança.

De volta ao seu exemplo de VoIP, diga que um provedor de VoIP pode ser determinado exclusivamente por (VoIPProviderID) ou por (Nome, URL) ou por (GUID). Quando um sistema externo precisa atualizar o provedor de VoIP, ele pode simplesmente passar um PUT / provider / by-id / 1234 ou PUT /provider/foo-voip/bar-domain.comou PUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301e seu sistema entenderia que o sistema externo deseja atualizar o VoIPProvider. Esses URIs são gerados pelo seu sistema e somente ele precisa entender que tudo isso significa a mesma coisa. O sistema externo deve tratar apenas o que estiver no URI como basicamente a PUT <whatever>.

Suponha que você tenha os dados de diferentes provedores de VoIP armazenados em tabelas diferentes com esquemas diferentes (portanto, um conjunto de chaves completamente diferente identifica cada provedor de VoIP com base em qual tabela eles estão armazenados). Quando você tem um URI, eles podem ser acessados ​​uniformemente pelo sistema externo, sem levar em consideração como o sistema identifica o provedor de VoIP específico. Para o sistema externo, tudo é apenas um ponteiro opaco.

Quando seu sistema usa um URI para se referir a objetos dessa maneira, você não está vazando nada sobre como implementar seu sistema. Você gera o URI e o cliente simplesmente o passa de volta para você.

Lie Ryan
fonte
0

Vou ter que alvejar essa declaração imprecisa e ingênua:

Talvez um ID amigável ao usuário seja inútil nesse caso; afinal, os provedores de VoIP são empresas cujos nomes tendem a ser distintos no sentido do computador e até distintos o suficiente no sentido humano por razões comerciais.

Os nomes são terríveis como chaves, pois frequentemente mudam. Uma empresa pode ter muitos nomes durante sua vida útil, a empresa pode fundir, dividir, mesclar novamente, criar uma subsidiária distinta para fins fiscais específicos que possui 0 funcionários, mas todos os clientes que contratam funcionários de uma subsidiária completamente diferente.

Em seguida, entramos no fato de que os nomes das empresas não são nem remotamente exclusivos, como mostra o marco Apple vs. Apple .

Um bom mapeador ou estrutura relacional de objetos deve abstrair as chaves primárias e torná-las invisíveis, mas elas estão lá, e elas geralmente serão a única maneira de identificar exclusivamente um objeto em seu banco de dados.

Para referência, prefiro como o django lida com isso:

class VoipProvider(models.Model):
    name=fields.TextField()
    address=fields.TextField()

class Customer(models.Model):
    name=fields.TextField()
    address=fields.TextField()
    voipProvider=fields.ForeignKeyField(VoipProvider)

Dessa maneira, os detalhes de um provedor de um cliente podem ser acessados ​​no código usando:

myCustomer.voipProvider.name #returns the name of the customers VOIP Provider.

Embora as chaves Primárias / Estrangeiras não estejam visíveis, elas estão lá e podem ser usadas para acessar itens, mas são abstraídas.


fonte
Você está absolutamente certo, mas acho que quis dizer que "em alguns domínios, talvez haja uma tupla natural de valores sempre únicos". Se eles mudam, mas ainda são únicos, ainda identificam um único registro.
tacos_tacos_tacos
0

Eu acho que muitas vezes ainda olhamos incorretamente para esse problema da perspectiva do banco de dados: ah, não há chave natural, então precisamos criar uma chave substituta. Ah, não, não podemos expor a chave substituta de volta aos objetos do domínio, isso está vazando etc.

No entanto, às vezes uma atitude melhor é esta: se um objeto de negócios (domínio) não possui uma chave natural, talvez seja necessário atribuir uma. Esse é um problema duplo do domínio comercial: primeiro, as coisas precisam de uma identidade, mesmo na ausência de bancos de dados. Em segundo lugar, embora tentemos fingir que a persistência é uma idéia abstrata invisível para o domínio, a realidade é que a persistência ainda é um conceito de negócios. Obviamente, existem problemas em que a chave natural escolhida não é suportada pelo DB como chave primária (por exemplo, GUIDs em alguns sistemas) - nesse caso, você precisará adicionar uma chave substituta.

Assim, você acaba em um local muito semelhante, por exemplo, seu cliente tem um ID inteiro, mas, em vez de se sentir mal porque isso vazou do banco de dados para o domínio, você se sente feliz porque a empresa concordou que todos os clientes receberiam um ID e você persiste isso no banco de dados, conforme necessário. Você ainda pode introduzir um substituto, por exemplo, para apoiar a renomeação do ID do cliente.

Essa abordagem também significa que, se um objeto de domínio atinge a camada de persistência e não possui um ID, provavelmente é algum tipo de objeto de valor, portanto, não precisa de um ID.

Chalky
fonte