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 ( UserName
para usuários ou OrderNumber
pedidos), 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:
- 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
- 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?
- Ser capaz de identificar os objetos com os quais você precisa lidar com outros atributos
- 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 é VoIPProvider
o que inclui campos como Name
, Description
, URL
, bem como referências gosto ProviderType
, PhysicalAddress
e Transactions
.
Agora, digamos que eu queira criar um serviço da Web que permita que usuários privilegiados gerenciem VoIPProvider
s.
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/voip
para que usuários privilegiados possam atualizar os VoIP
provedores. 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:
- Inclua uma chave primária ou alternativa no meu modelo de domínio e envie-a para o DTO e vice-versa
- Identifique o provedor com o qual nos preocupamos por meio do índice exclusivo, como
(Name, Url)
- 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,
fonte
Respostas:
É assim que resolvemos isso (há mais de 15 anos, quando nem mesmo o termo "design controlado por domínio" foi inventado):
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.
fonte
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?)
fonte
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.
fonte
MessageSender
,MessageRecipient
,TimeSent
- que deve ser único.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.
fonte
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.com
ouPUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301
e 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 aPUT <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ê.
fonte
Vou ter que alvejar essa declaração imprecisa e ingênua:
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:
Dessa maneira, os detalhes de um provedor de um cliente podem ser acessados no código usando:
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
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.
fonte