Membro: use IDs únicos versus objeto de domínio

10

Depois de algumas respostas úteis sobre se devo usar objeto de domínio ou um ID exclusivo como parâmetro de método / função aqui Identificador x objeto de domínio como parâmetro de método , eu tenho uma pergunta semelhante re: members (a discussão das perguntas anteriores não conseguiu cobrir isso). Quais são os prós e os contras do uso de IDs exclusivos como membro versus objeto como membro. Estou perguntando em referência a linguagens fortemente tipadas, como Scala / C # / Java. Eu deveria ter (1)

User( id: Int, CurrentlyReadingBooksId: List[Int])
Book( id: Int, LoanedToId: Int )

ou (2), preferida a (1) Após analisar: Devemos definir tipos para tudo?

User( id: UserId, CurrentlyReadingBooksId: List[ BookId] )
Book( id: BookId, LoanedToId: UserId )

ou (3)

User( id: Int, CurrentlyReadingBooks: List[Book]) 
Book( id: Int, LoanedTo: User)

Embora eu não consiga pensar nos benefícios de ter o objeto (3), um dos benefícios de ter os IDs (2) e (1) é que, quando estou criando o objeto Usuário a partir do DB, não preciso criar o objeto Livro, que por sua vez, pode depender do próprio objeto Usuário, criando uma cadeia sem fim. Existe uma solução genérica para esse problema para RDBMS e No-SQL (se forem diferentes)?

Com base em algumas respostas até agora, reformulando minha pergunta: (com o uso de IDs supostamente em tipos agrupados) 1) Sempre usar IDs? 2) Sempre usar objetos? 3) Use IDs quando houver risco de recursão na serialização e desserialização, mas use objetos de outra forma? 4) Mais alguma coisa?

EDIT: Se você responder que os Objetos devem ser usados ​​sempre ou, em alguns casos, certifique-se de responder à maior preocupação que outros respondentes postaram => Como obter dados do DB

0fnt
fonte
1
Obrigado pela boa pergunta, esperamos seguir com interesse. Um pouco de uma pena que seu nome de usuário é "user18151", as pessoas com este tipo de nome de usuário são ignoradas por alguns :)
bjfletcher
@bjfletcher Obrigado. Eu mesmo tive essa percepção incômoda, mas nunca me ocorreu o porquê!
0fnt

Respostas:

7

Objetos de domínio como ids criam alguns problemas sutis / complexos:

Serialização / desserialização

Se você armazenar objetos como chaves, a serialização do gráfico será extremamente complicada. Você receberá stackoverflowerros ao fazer uma serialização ingênua para JSON ou XML por causa da recursão. Você precisará escrever um serializador personalizado que converta os objetos reais para usar seus IDs, em vez de serializar a instância do objeto e criar a recursão.

Passe objetos para segurança de tipo, mas apenas armazene IDs, e você poderá ter um método de acesso que carrega preguiçosamente a entidade relacionada quando é chamada. O cache de segundo nível cuidará das chamadas subseqüentes.

Vazamentos sutis de referência:

Se você usar objetos de domínio em construtores como você, criará referências circulares que serão muito difíceis de permitir que a memória seja recuperada para objetos que não estão sendo usados ​​ativamente.

Situação ideal:

IDs opacos vs int / long:

Um iddeve ser um identificador completamente opaco que não carrega informações sobre o que identifica. Mas deve oferecer alguma verificação de que é um identificador válido em seu sistema.

Os tipos brutos quebram isso:

int, longE Stringsão os tipos de matérias mais comumente usados para identificadores no sistema RDBMS. Há uma longa história de razões práticas que datam de décadas e todas elas são compromissos que se encaixam na economia spaceou na economia timeou em ambas.

Os IDs seqüenciais são os piores criminosos:

Ao usar um ID seqüencial, você está compactando informações semânticas temporais no ID por padrão. O que não é ruim até que seja usado. Quando as pessoas começam a escrever uma lógica comercial que classifica ou filtra a qualidade semântica do ID, elas estão criando um mundo de dor para futuros mantenedores.

String os campos são problemáticos porque designers ingênuos agrupam informações no conteúdo, geralmente semântica temporal também.

Isso torna impossível criar também um sistema de dados distribuídos, porque não12437379123 é único globalmente. As chances de outro nó em um sistema distribuído criar um registro com o mesmo número são praticamente garantidas quando você obtém dados suficientes em um sistema.

Em seguida, os hacks começam a contorná-lo e tudo se transforma em uma pilha de bagunça fumegante.

Ignorar grandes sistemas distribuídos ( clusters ) torna-se um pesadelo completo quando você começa a tentar compartilhar os dados com outros sistemas também. Especialmente quando o outro sistema não está sob seu controle.

Você acaba com o mesmo problema: como tornar sua identificação globalmente única.

O UUID foi criado e padronizado por um motivo:

UUIDpode sofrer de todos os problemas listados acima, dependendo de qual Versionvocê usar.

Version 1usa um endereço MAC e tempo para criar um ID exclusivo. Isso é ruim porque carrega informações semânticas sobre localização e hora. Isso não é, por si só, um problema, é quando desenvolvedores ingênuos começam a confiar nessas informações para a lógica de negócios. Isso também vaza informações que podem ser exploradas em qualquer tentativa de invasão.

Version 2O uso de usuários UIDou GIDdomian UIDou GUIno lugar a partir Version 1disso é tão ruim quanto Version 1para vazamento de dados e o risco de essas informações serem usadas na lógica de negócios.

Version 3é semelhante, mas substitui o endereço MAC e o tempo por um MD5hash de alguma matriz de byte[]algo que definitivamente tem significado semântico. Não há vazamento de dados para se preocupar, o byte[]não pode ser recuperado a partir do UUID. Isso fornece uma boa maneira de criar deterministicamente o UUIDformulário de instâncias e a chave externa de algum tipo.

Version 4 baseia-se apenas em números aleatórios, o que é uma boa solução, não carrega absolutamente nenhuma informação semântica, mas não é deterministicamente recriada.

Version 5é como Version 4mas usa em sha1vez de md5.

Chaves de domínio e chaves de dados transacionais

Minha preferência por IDs de objetos de domínio é usar Version 5ou, Version 3se houver restrição, Version 5por algum motivo técnico.

Version 3 é ótimo para dados de transações que podem estar espalhados por muitas máquinas.

A menos que você esteja limitado pelo espaço, use um UUID:

Eles têm garantia única, despejando dados de um banco de dados e recarregando em outro. Você nunca precisou se preocupar com IDs duplicados que realmente fazem referência a dados de domínio diferentes.

Version 3,4,5 são completamente opacos e é assim que deveriam ser.

Você pode ter uma única coluna como chave primária com a UUIDe, em seguida, pode ter índices exclusivos compostos para o que teria sido uma chave primária composta natural.

O armazenamento também não precisa ser CHAR(36). Você pode armazenar o campo UUIDem um byte / bit / número nativo para um determinado banco de dados, desde que ele ainda seja indexável.

Legado

Se você tem tipos brutos e não pode alterá-los, ainda pode abstraí-los no seu código.

Usar um Version 3/5de UUIDvocês pode passar o Class.getName()sinal de + String.valueOf(int)como a byte[]e ter uma chave de referência opaca que é recreativa e determinística.


fonte
Sinto muito por não ter sido claro na minha pergunta e me sinto muito pior (ou realmente boa), porque esta é uma resposta ótima e bem pensada e você claramente passou um tempo nela. Infelizmente, isso não se encaixa na minha pergunta, talvez mereça uma pergunta própria? "O que devo ter em mente ao criar um campo de identificação para o meu objeto de domínio"?
0fnt
Eu adicionei uma explicação explícita.
Tenho agora. Obrigado por dedicar um tempo à resposta.
0fnt
1
Aliás, os coletores de lixo geracionais da AFAIK (que eu acredito que é o sistema dominante de GC atualmente) não devem ter muita dificuldade em colocar em GC referências circulares.
0fnt
1
se C-> A -> B -> Ae Bé colocado em um Collectionentão Ae todos os seus filhos ainda são alcançáveis, essas coisas não são completamente óbvias e podem levar a vazamentos sutis . GCComo o menor dos problemas, serialização e desserialização do gráfico é um pesadelo de complexidade.
2

Sim, existem benefícios para ambos os lados e também há um compromisso.

List<int>:

  • Economize memória
  • Inicialização mais rápida do tipo User
  • Se seus dados vierem de um banco de dados relacional (SQL), você não precisará acessar duas tabelas para obter usuários, apenas a Userstabela

List<Book>:

  • O acesso a um livro é mais rápido do usuário, o livro foi pré-carregado na memória. Isso é bom se você puder ter uma inicialização mais longa para obter operações subseqüentes mais rápidas.
  • Se seus dados vierem de um banco de dados de repositório de documentos como HBase ou Cassandra, é provável que os valores dos livros lidos estejam no registro do Usuário, para que você possa facilmente ter obtido os livros "enquanto estava recebendo o usuário".

Se você não tiver problemas de memória ou CPU, eu continuaria com List<Book>o código que usa as Userinstâncias.

Compromisso:

Ao usar o Linq2SQL, o código gerado para o usuário da entidade terá um EntitySet<Book>carregamento lento quando você o acessar. Isso deve manter seu código limpo e a instância de Usuário pequena (tamanho da memória).

ytoledano
fonte
Supondo algum tipo de cache, o benefício de pré-carregamento seria nulo. Eu não usei o Cassandra / HBase, por isso não posso falar sobre eles, mas o Linq2SQL é um caso muito específico (embora não veja como o carregamento lento impedirá o caso de encadeamento infinito, mesmo neste caso específico e no caso geral)
precisa saber é
No exemplo do Linq2SQL, você realmente não obtém benefícios de desempenho, apenas um código mais limpo. Ao obter entidades um-para-muitos em um repositório de documentos como o Cassandra / HBase, a grande maioria do tempo de processamento é gasta na busca do registro, para que você possa obter todas as muitas entidades enquanto estiver lá (os livros, em este exemplo).
Ytoledano
Você tem certeza? Mesmo se eu armazenar Livro e Usuários normalizados separadamente? Para mim, parece que deve ser apenas um custo extra de latência de rede. De qualquer forma, como lidar com o caso RDBMS genericamente? (Eu editei a questão de mencionar que claramente)
0fnt
1

Regra curta e simples:

Os IDs são usados ​​nos DTOs .
As referências a objetos são geralmente usadas nos objetos de lógica de domínio / lógica de negócios e camada de interface do usuário.

Essa é a arquitetura comum em projetos maiores e empresariais o suficiente. Você terá mapeadores que se traduzem para esses dois tipos de objetos.

herzmeister
fonte
Obrigado por visitar e responder. Infelizmente, apesar de entender a distinção graças ao link do wiki, nunca vi isso na prática (desde que nunca trabalhei com grandes projetos de longo prazo). Você teria um exemplo em que o mesmo objeto fosse representado de duas maneiras para dois propósitos diferentes?
0fnt
aqui está uma pergunta real sobre o mapeamento: stackoverflow.com/questions/9770041/dto-to-entity-mapping-tool - e há artigos críticos como este: rogeralsing.com/2013/12/01/…
herzmeister
Realmente útil, obrigado. Infelizmente ainda não entendo como funcionaria o carregamento de dados com referências circulares? por exemplo, se um usuário indicar um livro e o livro indicar o mesmo usuário, como você criaria esse objeto?
0fnt
Olhe para o padrão de repositório . Você terá um BookRepositorye um UserRepository. Você sempre chamará myRepository.GetById(...)ou semelhante, e o repositório criará o objeto e carregará seus valores de um armazenamento de dados ou o obterá de um cache. Além disso, os objetos filhos geralmente são carregados preguiçosamente, o que também evita a necessidade de lidar com referências circulares diretas no momento da construção.
herzmeister