Houve algumas discussões aqui sobre entidades JPA e qual hashCode()
/ equals()
implementação deve ser usada para classes de entidade JPA. A maioria (se não todos) deles depende do Hibernate, mas eu gostaria de discuti-los de maneira neutra na implementação do JPA (a propósito, estou usando o EclipseLink).
Todas as implementações possíveis estão tendo suas próprias vantagens e desvantagens em relação a:
hashCode()
/equals()
conformidade do contrato (imutabilidade) paraList
/Set
operações- Se objetos idênticos (por exemplo, de sessões diferentes, proxies dinâmicos de estruturas de dados carregadas lentamente) podem ser detectados
- Se as entidades se comportam corretamente no estado desanexado (ou não persistente)
Tanto quanto posso ver, existem três opções :
- Não os substitua; confiar
Object.equals()
eObject.hashCode()
hashCode()
/equals()
trabalho- Não é possível identificar objetos idênticos, problemas com proxies dinâmicos
- sem problemas com entidades desanexadas
- Substitua-os, com base na chave primária
hashCode()
/equals()
está quebrado- identidade correta (para todas as entidades gerenciadas)
- problemas com entidades desanexadas
- Substitua-os, com base no Business-Id (campos de chave não primária; e as chaves estrangeiras?)
hashCode()
/equals()
está quebrado- identidade correta (para todas as entidades gerenciadas)
- sem problemas com entidades desanexadas
Minhas perguntas são:
- Perdi uma opção e / ou ponto pro / con?
- Qual opção você escolheu e por quê?
ATUALIZAÇÃO 1:
Por " hashCode()
/ equals()
estão quebrados", quero dizer que sucessivas hashCode()
invocações pode retornar valores diferentes, o que é (quando corretamente implementado) não quebrado no sentido da Object
documentação da API, mas o que causa problemas ao tentar recuperar uma entidade mudou a partir de um Map
, Set
ou outro baseado em hash Collection
. Consequentemente, as implementações de JPA (pelo menos EclipseLink) não funcionarão corretamente em alguns casos.
ATUALIZAÇÃO 2:
Obrigado por suas respostas - a maioria delas tem uma qualidade notável.
Infelizmente, ainda não tenho certeza de qual abordagem será a melhor para um aplicativo da vida real ou como determinar a melhor abordagem para o meu aplicativo. Então, vou manter a questão em aberto e esperar mais discussões e / ou opiniões.
hashcode()
na mesma instância de objeto deve retornar o mesmo valor, a menos que quaisquer campos usados naequals()
implementação sejam alterados. Em outras palavras, se você tiver três campos em sua classe e seuequals()
método usar apenas dois para determinar a igualdade de instâncias, poderá esperar que ohashcode()
valor de retorno mude se você alterar um desses valores - o que faz sentido quando você considera que essa instância do objeto não é mais "igual" ao valor que a instância antiga representava.Respostas:
Leia este artigo muito interessante sobre o assunto: Não deixe o hibernar roubar sua identidade .
A conclusão do artigo é assim:
fonte
suid.js
busca blocos de ID dossuid-server-java
quais você pode obter e usar no lado do cliente.Eu sempre substituo equals / hashcode e o implemento com base no ID do negócio. Parece a solução mais razoável para mim. Veja o seguinte link .
EDIT :
Para explicar por que isso funciona para mim:
fonte
Normalmente, temos dois IDs em nossas entidades:
equals()
ehashCode()
em particular)Dê uma olhada:
EDIT: para esclarecer o meu ponto sobre as chamadas ao
setUuid()
método. Aqui está um cenário típico:Quando executo meus testes e vejo a saída do log, corrijo o problema:
Como alternativa, pode-se fornecer um construtor separado:
Então, meu exemplo ficaria assim:
Eu uso um construtor padrão e um setter, mas você pode achar a abordagem de dois construtores mais adequada para você.
fonte
hashCode
/ padrãoequals
para igualdade de JVM e igualdadeid
de persistência? Isso não faz sentido para mim.Object
'sequals()
voltariafalse
neste caso.equals()
Retornos baseados em UUIDtrue
.Eu pessoalmente já usei todas essas três estratégias em diferentes projetos. E devo dizer que a opção 1 é, na minha opinião, a mais praticável em um aplicativo da vida real. Na minha experiência, quebrar a conformidade hashCode () / equals () leva a muitos erros malucos, pois você sempre acaba em situações em que o resultado da igualdade muda depois que uma entidade é adicionada a uma coleção.
Mas existem outras opções (também com seus prós e contras):
a) hashCode / equals com base em um conjunto de campos imutáveis , não nulos , designados pelo construtor
(+) todos os três critérios são garantidos
Os valores do campo (-) devem estar disponíveis para criar uma nova instância
(-) complica o manuseio se você precisar alterar um
b) hashCode / equals com base em uma chave primária designada pelo aplicativo (no construtor) em vez de JPA
(+) todos os três critérios são garantidos
(-) você não pode tirar proveito de estratégias simples e confiáveis de geração de ID, como sequências de banco de dados
(-) complicado se novas entidades forem criadas em um ambiente distribuído (cliente / servidor) ou cluster de servidor de aplicativos
c) hashCode / equals com base em um UUID atribuído pelo construtor da entidade
(+) todos os três critérios são garantidos
(-) overhead da geração de UUID
(-) pode haver um pequeno risco de que duas vezes o mesmo UUID seja usado, dependendo do algoritmo usado (pode ser detectado por um índice exclusivo no DB)
fonte
Collection
.Se você deseja usar
equals()/hashCode()
para seus Conjuntos, no sentido de que a mesma entidade só pode estar lá uma vez, existe apenas uma opção: Opção 2. Isso ocorre porque uma chave primária para uma entidade por definição nunca muda (se alguém realmente atualizar não é mais a mesma entidade)Você deve interpretar isso literalmente: Como você
equals()/hashCode()
é baseado na chave primária, não deve usar esses métodos até que a chave primária esteja definida. Portanto, você não deve colocar entidades no conjunto até que elas recebam uma chave primária. (Sim, UUIDs e conceitos semelhantes podem ajudar a atribuir chaves primárias antecipadamente.)Agora, teoricamente, também é possível conseguir isso com a opção 3, mesmo que as chamadas "chaves de negócios" tenham o inconveniente desagradável que elas podem mudar: "Tudo o que você precisará fazer é excluir as entidades já inseridas do conjunto ( s) e reinsira-os ". Isso é verdade - mas também significa que, em um sistema distribuído, você terá que garantir que isso seja feito absolutamente em todos os locais em que os dados foram inseridos (e você precisará garantir que a atualização seja realizada , antes que outras coisas ocorram). Você precisará de um sofisticado mecanismo de atualização, especialmente se alguns sistemas remotos não estiverem acessíveis no momento ...
A opção 1 pode ser usada apenas se todos os objetos em seus conjuntos forem da mesma sessão do Hibernate. A documentação do Hibernate deixa isso muito claro no capítulo 13.1.3. Considerando a identidade do objeto :
Continua a argumentar a favor da opção 3:
Isso é verdade, se você
Caso contrário, você poderá escolher a opção 2.
Em seguida, menciona a necessidade de uma relativa estabilidade:
Isto está certo. O problema prático que vejo com isso é: Se você não pode garantir estabilidade absoluta, como poderá garantir a estabilidade "enquanto os objetos estiverem no mesmo conjunto". Posso imaginar alguns casos especiais (como usar conjuntos apenas para uma conversa e depois jogá-los fora), mas questionaria a praticabilidade geral disso.
Versão curta:
fonte
equals
/hashCode
.Object
implementações padrão igual a e hashCode, porque isso não funciona após vocêmerge
e a entidade.Você pode usar o identificador de entidade, conforme sugerido nesta postagem . O único problema é que você precisa usar uma
hashCode
implementação que sempre retorna o mesmo valor, assim:fonte
Embora o uso de uma chave comercial (opção 3) seja a abordagem mais comumente recomendada ( wiki da comunidade Hibernate , "Java Persistence with Hibernate" p. 398), e é isso que costumamos usar, há um bug do Hibernate que interrompe isso para pessoas que buscam ansiosamente conjuntos: HHH-3799 . Nesse caso, o Hibernate pode adicionar uma entidade a um conjunto antes que seus campos sejam inicializados. Não sei por que esse bug não recebeu mais atenção, pois realmente torna problemática a abordagem de chave comercial recomendada.
Eu acho que o cerne da questão é que igual e hashCode devem ser baseados em estado imutável (referência Odersky et al. ), E uma entidade do Hibernate com chave primária gerenciada pelo Hibernate não tem esse estado imutável. A chave primária é modificada pelo Hibernate quando um objeto transitório se torna persistente. A chave comercial também é modificada pelo Hibernate, quando hidrata um objeto no processo de inicialização.
Isso deixa apenas a opção 1, herdando as implementações java.lang.Object com base na identidade do objeto ou usando uma chave primária gerenciada por aplicativo, conforme sugerido por James Brundege em "Não deixe o hibernar roubar sua identidade" (já mencionado pela resposta de Stijn Geukens ) e por Lance Arlaus em "Geração de objetos: uma melhor abordagem para a integração do Hibernate" .
O maior problema com a opção 1 é que instâncias desanexadas não podem ser comparadas com instâncias persistentes usando .equals (). Mas tudo bem; o contrato de iguais e hashCode deixa ao desenvolvedor decidir o que significa igualdade para cada classe. Então deixe apenas iguais e hashCode herdar de Object. Se você precisar comparar uma instância desanexada com uma instância persistente, poderá criar um novo método explicitamente para esse fim, talvez
boolean sameEntity
ouboolean dbEquivalent
ouboolean businessEquals
.fonte
Eu concordo com a resposta de Andrew. Fazemos a mesma coisa em nosso aplicativo, mas em vez de armazenar UUIDs como VARCHAR / CHAR, dividimos em dois valores longos. Consulte UUID.getLeastSignificantBits () e UUID.getMostSignificantBits ().
Mais uma coisa a considerar, é que as chamadas para UUID.randomUUID () são muito lentas, portanto, você pode querer gerar o UUID preguiçosamente apenas quando necessário, como durante persistência ou chamadas para equals () / hashCode ()
fonte
Como outras pessoas muito mais espertas do que eu já apontaram, há inúmeras estratégias por aí. Parece ser o caso, porém, de que a maioria dos padrões de design aplicados tenta abrir caminho para o sucesso. Eles limitam o acesso do construtor, se não impedem completamente as invocações do construtor com construtores especializados e métodos de fábrica. Na verdade, é sempre agradável com uma API clara. Mas se o único motivo é fazer com que as substituições iguais e hashcode sejam compatíveis com o aplicativo, então me pergunto se essas estratégias estão em conformidade com o KISS (Keep It Simple Stupid).
Para mim, gosto de substituir iguais e código de hash por meio da análise do ID. Nestes métodos, exijo que o ID não seja nulo e documente esse comportamento. Assim, se tornará o contrato dos desenvolvedores persistir em uma nova entidade antes de armazená-lo em outro lugar. Um aplicativo que não cumpra este contrato falhará dentro de um minuto (espero).
Atenção: se suas entidades estiverem armazenadas em tabelas diferentes e seu provedor usar uma estratégia de geração automática para a chave primária, você receberá chaves primárias duplicadas nos tipos de entidade. Nesse caso, compare também os tipos de tempo de execução com uma chamada ao Object # getClass (), o que obviamente tornará impossível que dois tipos diferentes sejam considerados iguais. Isso combina comigo na maior parte do tempo.
fonte
Object#getClass()
é ruim por causa de H. proxies. LigarHibernate.getClass(o)
ajuda, mas o problema da igualdade de entidades de diferentes tipos permanece. Existe uma solução usando o canEqual , um pouco complicado, mas utilizável. Concordou que geralmente não é necessário. +++ Jogar eq / hc em ID nulo viola o contrato, mas é muito pragmático.Obviamente já existem respostas muito informativas aqui, mas vou lhe dizer o que fazemos.
Não fazemos nada (ou seja, não substituimos).
Se precisarmos de equals / hashcode para trabalhar com coleções, usaremos UUIDs. Você acabou de criar o UUID no construtor. Usamos o http://wiki.fasterxml.com/JugHome para UUID. O UUID é um pouco mais caro em termos de CPU, mas é barato em comparação à serialização e acesso ao banco de dados.
fonte
Eu sempre usei a opção 1 no passado porque estava ciente dessas discussões e achava melhor não fazer nada até saber a coisa certa a fazer. Todos esses sistemas ainda estão sendo executados com êxito.
No entanto, da próxima vez, posso tentar a opção 2 - usando o ID gerado pelo banco de dados.
Hashcode e iguais lançarão IllegalStateException se o ID não estiver definido.
Isso impedirá que erros sutis envolvendo entidades não salvas apareçam inesperadamente.
O que as pessoas pensam dessa abordagem?
fonte
A abordagem das chaves comerciais não é adequada para nós. Usamos o ID gerado pelo banco de dados , tempId transitório temporário e substituímos equal () / hashcode () para resolver o dilema. Todas as entidades são descendentes de Entidade. Prós:
Contras:
Veja o nosso código:
fonte
Por favor, considere a seguinte abordagem com base no identificador de tipo predefinido e no ID.
As premissas específicas para a JPA:
A entidade abstrata:
Exemplo de entidade concreta:
Exemplo de teste:
Principais vantagens aqui:
Desvantagens:
super()
Notas:
class A
eclass B extends A
pode depender de detalhes concretos do aplicativo.Fico na expectativa dos seus comentários.
fonte
Esse é um problema comum em todos os sistemas de TI que usam Java e JPA. O ponto problemático vai além da implementação de equals () e hashCode (), afeta como uma organização se refere a uma entidade e como seus clientes se referem à mesma entidade. Já vi o bastante por não ter uma chave comercial a ponto de escrever meu próprio blog para expressar minha opinião.
Resumindo: use um ID sequencial, legível por humanos, curto, com prefixos significativos como chave comercial, gerados sem nenhuma dependência de outro armazenamento que não seja a RAM. Snowflake do Twitter é um exemplo muito bom.
fonte
Na IMO, você tem 3 opções para implementar equals / hashCode
Usar uma identidade gerada por aplicativo é a abordagem mais fácil, mas vem com algumas desvantagens
Se você pode trabalhar com essas desvantagens , use essa abordagem.
Para superar o problema de junção, pode-se usar o UUID como chave natural e um valor de sequência como chave primária, mas você ainda pode enfrentar os problemas de implementação equals / hashCode em entidades filhas composicionais que possuem IDs incorporados, desde que você deseja ingressar com base na chave primária. Usar a chave natural no ID de entidades filhas e a chave primária para se referir ao pai é um bom compromisso.
Na IMO, essa é a abordagem mais limpa, pois evitará todas as desvantagens e, ao mesmo tempo, fornecerá um valor (o UUID) que você pode compartilhar com sistemas externos sem expor os componentes internos do sistema.
Implemente-o com base em uma chave comercial, se você pode esperar que o usuário seja uma boa ideia, mas que também tenha algumas desvantagens
Na maioria das vezes, essa chave comercial é algum tipo de código que o usuário fornece e, com menos frequência, um composto de vários atributos.
Na IMO, você não deve implementar ou trabalhar exclusivamente com uma chave comercial. É um bom complemento, ou seja, os usuários podem pesquisar rapidamente por essa chave comercial, mas o sistema não deve confiar nela para operar.
Implementá-lo com base na chave primária tem seus problemas, mas talvez não seja tão importante
Se você precisar expor IDs ao sistema externo, use a abordagem UUID que sugeri. Caso contrário, você ainda pode usar a abordagem UUID, mas não precisa. O problema de usar um ID gerado pelo DBMS em equals / hashCode decorre do fato de que o objeto pode ter sido adicionado a coleções baseadas em hash antes de atribuir o ID.
A maneira óbvia de contornar isso é simplesmente não adicionar o objeto a coleções baseadas em hash antes de atribuir o ID. Entendo que isso nem sempre é possível, porque você pode querer deduplicação antes de atribuir o ID. Para ainda poder usar as coleções baseadas em hash, basta reconstruir as coleções depois de atribuir o ID.
Você poderia fazer algo assim:
Eu não testei a abordagem exata, então não tenho certeza de como funciona a alteração de coleções em eventos pré e pós-persistência, mas a idéia é:
Outra maneira de resolver isso é simplesmente reconstruir todos os seus modelos baseados em hash após uma atualização / persistência.
No final, cabe a você. Pessoalmente, uso a abordagem baseada em sequência na maior parte do tempo e só uso a abordagem UUID se precisar expor um identificador a sistemas externos.
fonte
Com o novo estilo
instanceof
do java 14, você pode implementar oequals
em uma linha.fonte
Se o UUID é a resposta para muitas pessoas, por que não usamos apenas métodos de fábrica da camada de negócios para criar as entidades e atribuir a chave primária no momento da criação?
por exemplo:
dessa maneira, obteríamos uma chave primária padrão para a entidade do provedor de persistência, e nossas funções hashCode () e equals () poderiam depender disso.
Também poderíamos declarar os construtores do carro protegidos e depois usar o reflexo em nosso método comercial para acessá-los. Dessa forma, os desenvolvedores não pretendem instanciar o Car com o novo, mas através do método de fábrica.
Que tal isso?
fonte
Tentei responder a essa pergunta pessoalmente e nunca fiquei totalmente satisfeito com as soluções encontradas até ler este post e, especialmente, o DREW. Gostei da maneira como ele preguiçosamente criou o UUID e o armazenou de maneira ideal.
Mas eu queria adicionar ainda mais flexibilidade, ou seja, criar UUID apenas preguiçosamente quando hashCode () / equals () for acessado antes da primeira persistência da entidade com as vantagens de cada solução:
Eu realmente aprecio o feedback da minha solução mista abaixo
fonte
Na prática, parece que a opção 2 (chave primária) é usada com mais frequência. As chaves de negócios naturais e IMUTAIS raramente são uma coisa, criar e dar suporte a chaves sintéticas são muito pesadas para resolver situações, o que provavelmente nunca aconteceu. Dê uma olhada na implementação AbstractPersistable do spring-data-jpa (a única coisa: para uso da implementação do Hibernate
Hibernate.getClass
).Apenas ciente de manipular novos objetos no HashSet / HashMap. Por outro lado, a opção 1 (continuação da
Object
implementação) é interrompida logo apósmerge
, situação muito comum.Se você não possui uma chave de negócios e precisa REAL de manipular uma nova entidade na estrutura de hash, substitua
hashCode
para constante, como abaixo foi recomendado a Vlad Mihalcea.fonte
Abaixo está uma solução simples (e testada) para Scala.
Observe que esta solução não se encaixa em nenhuma das três categorias fornecidas na pergunta.
Todas as minhas entidades são subclasses da UUIDEntity, por isso sigo o princípio de não repetir a si mesmo (DRY).
Se necessário, a geração de UUID pode ser mais precisa (usando mais números pseudo-aleatórios).
Código Scala:
fonte