O dilema JPA hashCode () / equals ()


311

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) para List/ Setoperaçõ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 :

  1. 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
  2. Substitua-os, com base na chave primária
    • hashCode()/ equals()está quebrado
    • identidade correta (para todas as entidades gerenciadas)
    • problemas com entidades desanexadas
  3. 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:

  1. Perdi uma opção e / ou ponto pro / con?
  2. 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 Objectdocumentação da API, mas o que causa problemas ao tentar recuperar uma entidade mudou a partir de um Map, Setou 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.


4
Não entendo o que você quer dizer com "hashCode () / equals () broken")
nanda

4
Eles não seriam "quebrados" nesse sentido, pois, nas opções 2 e 3, você implementaria equals () e hashCode () usando a mesma estratégia.
mate b

11
Isso não se aplica à opção 3. hashCode () e equals () devem estar usando o mesmo critério; portanto, se um de seus campos for alterado, sim, o método hashcode () retornará um valor diferente para a mesma instância do que anteriormente, mas assim será igual a (). Você parou a segunda parte da frase do javadoc hashcode (): sempre que é invocado no mesmo objeto mais de uma vez durante a execução de um aplicativo Java, o método hashCode deve retornar consistentemente o mesmo número inteiro, desde que não haja informações usado em comparações iguais no objeto é modificado .
mate b

1
Na verdade, essa parte da sentença significa que a chamada oposta hashcode()na mesma instância de objeto deve retornar o mesmo valor, a menos que quaisquer campos usados ​​na equals()implementação sejam alterados. Em outras palavras, se você tiver três campos em sua classe e seu equals()método usar apenas dois para determinar a igualdade de instâncias, poderá esperar que o hashcode()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.
mate b

2
"problemas ao tentar recuperar uma entidade alterada de um mapa, conjunto ou outras coleções baseadas em hash" ... isso deve ser "problemas ao tentar recuperar uma entidade alterada de um HashMap, HashSet ou outras coleções baseadas em hash"
nanda

Respostas:


122

Leia este artigo muito interessante sobre o assunto: Não deixe o hibernar roubar sua identidade .

A conclusão do artigo é assim:

A identidade do objeto é enganosamente difícil de implementar corretamente quando os objetos são mantidos em um banco de dados. No entanto, os problemas decorrem inteiramente de permitir que objetos existam sem um ID antes de serem salvos. Podemos resolver esses problemas assumindo a responsabilidade de atribuir IDs de objetos das estruturas de mapeamento objeto-relacional, como o Hibernate. Em vez disso, os IDs do objeto podem ser atribuídos assim que o objeto é instanciado. Isso torna a identidade do objeto simples e sem erros e reduz a quantidade de código necessária no modelo de domínio.


21
Não, esse não é um bom artigo. Esse é um ótimo artigo sobre o assunto e deve ser uma leitura obrigatória para todos os programadores da JPA! +1!
Tom Anderson

2
Sim, estou usando a mesma solução. Não permitir que o banco de dados gere o ID também tem outras vantagens, como poder criar um objeto e já criar outros objetos que o referenciam antes de persistir. Isso pode remover a latência e vários ciclos de solicitação / resposta nos aplicativos cliente-servidor. Se você precisar de inspiração para essa solução, consulte meus projetos: suid.js e suid-server-java . Basicamente, suid.jsbusca blocos de ID dos suid-server-javaquais você pode obter e usar no lado do cliente.
Stijn de Witt

2
Isso é simplesmente insano. Eu sou novo em hibernar trabalhos sob o capô, estava escrevendo testes de unidade e descobri que não posso excluir um objeto de um conjunto depois de modificá-lo, concluí que é por causa da alteração do código de hash, mas não consegui entender como resolver. O artigo é simples lindo!
XMight 26/04/16

É um ótimo artigo. No entanto, para as pessoas que veem o link pela primeira vez, sugiro que possa ser um exagero para a maioria dos aplicativos. As outras três opções listadas nesta página devem resolver o problema mais ou menos de várias maneiras.
HopeKing

1
O Hibernate / JPA usa o método equals e hashcode de uma entidade para verificar se o registro já existe no banco de dados?
Tushar Banne

64

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 .

Para resumir tudo isso, aqui está uma lista do que funcionará ou não das diferentes maneiras de lidar com equals / hashCode: insira a descrição da imagem aqui

EDIT :

Para explicar por que isso funciona para mim:

  1. Normalmente, não uso coleção baseada em hash (HashMap / HashSet) no meu aplicativo JPA. Se for necessário, prefiro criar a solução UniqueList.
  2. Acho que alterar a identificação de negócios no tempo de execução não é uma prática recomendada para nenhum aplicativo de banco de dados. Em casos raros em que não há outra solução, eu faria um tratamento especial como remover o elemento e colocá-lo de volta na coleção baseada em hash.
  3. Para o meu modelo, defino o ID do negócio no construtor e não fornece setters para ele. Deixei a implementação do JPA alterar o campo em vez da propriedade.
  4. A solução UUID parece ser um exagero. Por que UUID se você tem uma identificação comercial natural? Afinal, eu definiria a exclusividade da identificação comercial no banco de dados. Por que ter TRÊS índices para cada tabela no banco de dados?

1
Mas nesta tabela falta uma quinta linha "funciona com Lista / Conjuntos" (se você pensa em remover uma entidade que faz parte de um Conjunto de um mapeamento OneToMany) que seria respondida "Não" nas duas últimas opções porque seu hashCode ( ) alterações que violem seu contrato.
MRalwasser

Veja o comentário sobre a pergunta. Você parece não entender a iguais / contrato hashcode
nanda

1
@ Ralwasser: Eu acho que você quer dizer a coisa certa, não é apenas o contrato igual / hashCode () que é violado. Mas um mutável igual a / hashCode cria problemas com o contrato Set .
Chris Lercher 17/02

3
@MRalwasser: o código de hash pode mudar apenas se o ID da empresa mudar, e o ponto é que a ID da empresa não muda. Portanto, o código de hash não muda, e isso funciona perfeitamente com coleções de hash.
Tom Anderson

1
E se você não tiver uma chave comercial natural? Por exemplo, no caso de um ponto bidimensional, Ponto (X, Y), em um aplicativo de desenho gráfico? Como você armazenaria esse ponto como uma entidade?
21414 jhegedus

35

Normalmente, temos dois IDs em nossas entidades:

  1. É apenas para a camada de persistência (para que o provedor de persistência e o banco de dados possam descobrir relacionamentos entre objetos).
  2. É para as nossas necessidades de aplicação ( equals()e hashCode()em particular)

Dê uma olhada:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    // assuming all fields are subject to change
    // If we forbid users change their email or screenName we can use these
    // fields for business ID instead, but generally that's not the case
    private String screenName;
    private String email;

    // I don't put UUID generation in constructor for performance reasons. 
    // I call setUuid() when I create a new entity
    public User() {
    }

    // This method is only called when a brand new entity is added to 
    // persistence context - I add it as a safety net only but it might work 
    // for you. In some cases (say, when I add this entity to some set before 
    // calling em.persist()) setting a UUID might be too late. If I get a log 
    // output it means that I forgot to call setUuid() somewhere.
    @PrePersist
    public void ensureUuid() {
        if (getUuid() == null) {
            log.warn(format("User's UUID wasn't set on time. " 
                + "uuid: %s, name: %s, email: %s",
                getUuid(), getScreenName(), getEmail()));
            setUuid(UUID.randomUUID());
        }
    }

    // equals() and hashCode() rely on non-changing data only. Thus we 
    // guarantee that no matter how field values are changed we won't 
    // lose our entity in hash-based Sets.
    @Override
    public int hashCode() {
        return getUuid().hashCode();
    }

    // Note that I don't use direct field access inside my entity classes and
    // call getters instead. That's because Persistence provider (PP) might
    // want to load entity data lazily. And I don't use 
    //    this.getClass() == other.getClass() 
    // for the same reason. In order to support laziness PP might need to wrap
    // my entity object in some kind of proxy, i.e. subclassing it.
    @Override
    public boolean equals(final Object obj) {
        if (this == obj)
            return true;
        if (!(obj instanceof User))
            return false;
        return getUuid().equals(((User) obj).getUuid());
    }

    // Getters and setters follow
}

EDIT: para esclarecer o meu ponto sobre as chamadas ao setUuid()método. Aqui está um cenário típico:

User user = new User();
// user.setUuid(UUID.randomUUID()); // I should have called it here
user.setName("Master Yoda");
user.setEmail("yoda@jedicouncil.org");

jediSet.add(user); // here's bug - we forgot to set UUID and 
                   //we won't find Yoda in Jedi set

em.persist(user); // ensureUuid() was called and printed the log for me.

jediCouncilSet.add(user); // Ok, we got a UUID now

Quando executo meus testes e vejo a saída do log, corrijo o problema:

User user = new User();
user.setUuid(UUID.randomUUID());

Como alternativa, pode-se fornecer um construtor separado:

@Entity
public class User {

    @Id
    private int id;  // Persistence ID
    private UUID uuid; // Business ID

    ... // fields

    // Constructor for Persistence provider to use
    public User() {
    }

    // Constructor I use when creating new entities
    public User(UUID uuid) {
        setUuid(uuid);
    }

    ... // rest of the entity.
}

Então, meu exemplo ficaria assim:

User user = new User(UUID.randomUUID());
...
jediSet.add(user); // no bug this time

em.persist(user); // and no log output

Eu uso um construtor padrão e um setter, mas você pode achar a abordagem de dois construtores mais adequada para você.


2
Eu acredito que esta é uma solução correta e boa. Também pode ter uma pequena vantagem de desempenho, porque os números inteiros geralmente apresentam melhor desempenho nos índices do banco de dados do que os uuids. Mas, além disso, você provavelmente poderia eliminar a propriedade atual do ID inteiro e substituí-la pelo uuid (atribuído ao aplicativo)?
Chris Lercher

4
Qual é a diferença de usar os métodos hashCode/ padrão equalspara igualdade de JVM e igualdade idde persistência? Isso não faz sentido para mim.
Behrang Saeedzadeh

2
Funciona nos casos em que você possui vários objetos de entidade apontando para a mesma linha em um banco de dados. Object's equals()voltaria falseneste caso. equals()Retornos baseados em UUID true.
Andrew Андрей Листочкин

4
-1 - não vejo motivo para ter dois IDs e, portanto, dois tipos de identidade. Isso parece completamente inútil e potencialmente prejudicial para mim.
Tom Anderson

1
Desculpe por criticar sua solução sem apontar para uma que eu preferiria. Em resumo, eu daria aos objetos um único campo de ID, implementaria iguais e hashCode com base nele e geraria seu valor na criação de objetos, em vez de salvá-los no banco de dados. Dessa forma, todas as formas do objeto funcionam da mesma maneira: não persistentes, persistentes e desanexadas. Os proxies do Hibernate (ou similares) também devem funcionar corretamente, e acho que nem precisariam ser hidratados para lidar com chamadas iguais e hashCode.
Tom Anderson

31

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)


Sou fã da opção 1 e da abordagem C também. Não faça nada até que você absolutamente precise, é a abordagem mais ágil.
Adam Gent

2
+1 para a opção (b). IMHO, se uma entidade tiver um ID comercial natural, essa também deve ser sua chave primária do banco de dados. É um design de banco de dados simples, direto e bom. Se não tiver esse ID, será necessária uma chave substituta. Se você definir isso na criação do objeto, tudo o resto será simples. É quando as pessoas não usam uma chave natural e não geram uma chave substituta mais cedo que entram em problemas. Quanto à complexidade na implementação - sim, há algumas. Mas realmente não muito, e isso pode ser feito de uma maneira muito genérica que o resolve uma vez por todas as entidades.
Tom Anderson

Também prefiro a opção 1, mas como escrever um teste de unidade para afirmar a igualdade total é um grande problema, porque temos que implementar o método equals para Collection.
OOD Waterball

Apenas não faça isso. Veja Não mexa com o Hibernate
alonana 27/02

29

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 :

Dentro de uma sessão, o aplicativo pode usar com segurança == para comparar objetos.

No entanto, um aplicativo que use == fora de uma sessão pode produzir resultados inesperados. Isso pode ocorrer mesmo em alguns lugares inesperados. Por exemplo, se você colocar duas instâncias desanexadas no mesmo conjunto, ambas poderão ter a mesma identidade de banco de dados (ou seja, elas representam a mesma linha). A identidade da JVM, no entanto, por definição não é garantida para instâncias em um estado desanexado. O desenvolvedor deve substituir os métodos equals () e hashCode () em classes persistentes e implementar sua própria noção de igualdade de objetos.

Continua a argumentar a favor da opção 3:

Há uma ressalva: nunca use o identificador de banco de dados para implementar a igualdade. Use uma chave comercial que seja uma combinação de atributos exclusivos, geralmente imutáveis. O identificador do banco de dados será alterado se um objeto transitório for persistente. Se a instância transitória (geralmente juntamente com instâncias desanexadas) for mantida em um conjunto, a alteração do código hash quebra o contrato do conjunto.

Isso é verdade, se você

  • não pode atribuir a identificação antecipadamente (por exemplo, usando UUIDs)
  • e, no entanto, você deseja absolutamente colocar seus objetos em conjuntos enquanto estiverem em estado transitório.

Caso contrário, você poderá escolher a opção 2.

Em seguida, menciona a necessidade de uma relativa estabilidade:

Os atributos para chaves de negócios não precisam ser tão estáveis ​​quanto as chaves primárias do banco de dados; você só precisa garantir a estabilidade, desde que os objetos estejam no mesmo conjunto.

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:

  • A opção 1 pode ser usada apenas com objetos em uma única sessão.
  • Se puder, use a Opção 2. (Atribua PK o mais cedo possível, porque você não pode usar os objetos em conjuntos até que a PK seja atribuída.)
  • Se você pode garantir uma relativa estabilidade, pode usar a opção 3. Mas tenha cuidado com isso.

Sua suposição de que a chave primária nunca muda é falsa. Por exemplo, o Hibernate apenas aloca a chave primária quando a sessão é salva. Portanto, se você usar a chave primária como seu hashCode, o resultado de hashCode () antes de salvar o objeto pela primeira vez e depois de salvar o objeto pela primeira vez será diferente. Pior, antes de salvar a sessão, dois objetos recém-criados terão o mesmo hashCode e poderão se substituir quando adicionados às coleções. Você pode ter que forçar um salvamento / liberação imediatamente na criação do objeto para usar essa abordagem.
William Billingsley

2
@ William: A chave primária de uma entidade não muda. A propriedade id do objeto mapeado pode mudar. Isso ocorre, como você explicou, especialmente quando um objeto transitório se torna persistente . Por favor, leia a parte da minha resposta com atenção, onde eu disse sobre os métodos equals / hashCode: "você não deve usar esses métodos até que a chave primária esteja definida".
Chris Lercher 25/02

Concordo plenamente. Com a opção 2, você também pode fatorar equals / hashcode em uma super classe e reutilizá-lo por todas as suas entidades.
Theo

+1 Sou novo na JPA, mas alguns dos comentários e respostas aqui sugerem que as pessoas não entendem o significado do termo "chave primária".
Raedwald 31/01

16
  1. Se você tiver uma chave comercial , use-a para equals/ hashCode.
  2. Se você não possui uma chave comercial, não deve deixá-la com as Objectimplementações padrão igual a e hashCode, porque isso não funciona após você mergee a entidade.
  3. Você pode usar o identificador de entidade, conforme sugerido nesta postagem . O único problema é que você precisa usar uma hashCodeimplementação que sempre retorna o mesmo valor, assim:

    @Entity
    public class Book implements Identifiable<Long> {
    
        @Id
        @GeneratedValue
        private Long id;
    
        private String title;
    
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Book)) return false;
            Book book = (Book) o;
            return getId() != null && Objects.equals(getId(), book.getId());
        }
    
        @Override
        public int hashCode() {
            return 31;
        }
    
        //Getters and setters omitted for brevity
    }

Qual é o melhor: (1) onjava.com/pub/a/onjava/2006/09/13/… ou (2) vladmihalcea.com/… ? A solução (2) é mais fácil que (1). Então, por que devo usar (1). Os efeitos de ambos são os mesmos? Os dois garantem a mesma solução?
N172

E com sua solução: "o valor hashCode não muda" entre as mesmas instâncias. Isso tem o mesmo comportamento como se fosse o "mesmo" uuid (da solução (1)) sendo comparado. Estou certo?
N172

1
E armazene o UUID no banco de dados e aumente a área de cobertura do registro e no buffer pool? Acho que isso pode levar a mais problemas de desempenho a longo prazo do que o hashCode exclusivo. Quanto à outra solução, você pode conferir se ela fornece consistência em todas as transições de estado da entidade. Você pode encontrar o teste que verifica isso no GitHub .
Vlad Mihalcea

1
Se você tiver uma chave comercial imutável, o hashCode poderá usá-la e se beneficiará de vários buckets; portanto, vale a pena usá-lo se você tiver um. Caso contrário, basta usar o identificador de entidade, conforme explicado no meu artigo.
Vlad Mihalcea 22/01

1
Estou feliz que você gostou. Tenho centenas de outros artigos sobre JPA e Hibernate.
precisa saber é o seguinte

10

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 sameEntityou boolean dbEquivalentou boolean businessEquals.


5

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 ()

@MappedSuperclass
public abstract class AbstractJpaEntity extends AbstractMutable implements Identifiable, Modifiable {

    private static final long   serialVersionUID    = 1L;

    @Version
    @Column(name = "version", nullable = false)
    private int                 version             = 0;

    @Column(name = "uuid_least_sig_bits")
    private long                uuidLeastSigBits    = 0;

    @Column(name = "uuid_most_sig_bits")
    private long                uuidMostSigBits     = 0;

    private transient int       hashCode            = 0;

    public AbstractJpaEntity() {
        //
    }

    public abstract Integer getId();

    public abstract void setId(final Integer id);

    public boolean isPersisted() {
        return getId() != null;
    }

    public int getVersion() {
        return version;
    }

    //calling UUID.randomUUID() is pretty expensive, 
    //so this is to lazily initialize uuid bits.
    private void initUUID() {
        final UUID uuid = UUID.randomUUID();
        uuidLeastSigBits = uuid.getLeastSignificantBits();
        uuidMostSigBits = uuid.getMostSignificantBits();
    }

    public long getUuidLeastSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidLeastSigBits;
    }

    public long getUuidMostSigBits() {
        //its safe to assume uuidMostSigBits of a valid UUID is never zero
        if (uuidMostSigBits == 0) {
            initUUID();
        }
        return uuidMostSigBits;
    }

    public UUID getUuid() {
        return new UUID(getUuidMostSigBits(), getUuidLeastSigBits());
    }

    @Override
    public int hashCode() {
        if (hashCode == 0) {
            hashCode = (int) (getUuidMostSigBits() >> 32 ^ getUuidMostSigBits() ^ getUuidLeastSigBits() >> 32 ^ getUuidLeastSigBits());
        }
        return hashCode;
    }

    @Override
    public boolean equals(final Object obj) {
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractJpaEntity)) {
            return false;
        }
        //UUID guarantees a pretty good uniqueness factor across distributed systems, so we can safely
        //dismiss getClass().equals(obj.getClass()) here since the chance of two different objects (even 
        //if they have different types) having the same UUID is astronomical
        final AbstractJpaEntity entity = (AbstractJpaEntity) obj;
        return getUuidMostSigBits() == entity.getUuidMostSigBits() && getUuidLeastSigBits() == entity.getUuidLeastSigBits();
    }

    @PrePersist
    public void prePersist() {
        // make sure the uuid is set before persisting
        getUuidLeastSigBits();
    }

}

Bem, na verdade, se você substituir equals () / hashCode (), precisará gerar UUID para cada entidade de qualquer maneira (presumo que você queira persistir em todas as entidades criadas no seu código). Você faz isso apenas uma vez - antes de armazená-lo em um banco de dados pela primeira vez. Depois disso, o UUID é carregado apenas pelo Provedor de persistência. Portanto, não vejo o ponto de fazê-lo preguiçosamente.
Andrew Андрей Листочкин

Votei sua resposta com um voto positivo porque realmente gosto de suas outras idéias: armazenar o UUID como um par de números no banco de dados e não converter para um tipo específico dentro do método equals () - esse é realmente legal! Definitivamente vou usar esses dois truques no futuro.
Andrew Андрей Листочкин

1
Obrigado pela votação. O motivo da inicialização lenta do UUID foi em nosso aplicativo que criamos muitas entidades que nunca são colocadas em um HashMap ou persistidas. Então, vimos uma queda de 100x no desempenho quando estávamos criando o objeto (100.000 deles). Portanto, apenas iniciamos o UUID se necessário. Eu só queria que houvesse um bom suporte no MySql para números de 128 bits, para que pudéssemos usar o UUID também para identificação e não nos importássemos com o auto_increment.
Drew

Ah eu vejo. No meu caso, nem declaramos o campo UUID se a entidade correspondente não for colocada em coleções. A desvantagem é que, às vezes, precisamos adicioná-lo, porque mais tarde acontece que realmente precisamos colocá-los em coleções. Isso acontece algumas vezes durante o desenvolvimento, mas felizmente nunca aconteceu conosco após a implantação inicial de um cliente, portanto não foi um grande problema. Se isso acontecer depois que o sistema for lançado, precisaremos de uma migração de banco de dados. UUID preguiçoso são muito úteis em tais situações.
Andrew Андрей Листочкин 27/02

Talvez você também deva tentar o gerador UUID mais rápido que Adam sugeriu em sua resposta, se o desempenho for um problema crítico em sua situação.
Andrew Андрей Листочкин 27/02

3

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.


Mesmo com um banco de dados faltando seqüências (como Mysql), é possível simulá-las (por exemplo, tabela hibernate_sequence). Portanto, você sempre pode obter um ID exclusivo nas tabelas. +++ Mas você não precisa disso. Ligar Object#getClass() é ruim por causa de H. proxies. Ligar Hibernate.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.
maaartinus 24/05

2

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.


1

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?


1

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:

  1. Nenhum campo extra no DB
  2. Nenhuma codificação extra nas entidades descendentes, uma abordagem para todos
  3. Sem problemas de desempenho (como no UUID), geração de ID do banco de dados
  4. Não há problema com os hashmaps (não é preciso ter em mente o uso de igual & etc.)
  5. O código de hash da nova entidade não mudou com o tempo, mesmo depois de persistir

Contras:

  1. Pode haver problemas com a serialização e desserialização de entidades não persistentes
  2. O código de hash da entidade salva pode mudar após o recarregamento do DB
  3. Objetos não persistentes considerados sempre diferentes (talvez isso esteja correto?)
  4. O quê mais?

Veja o nosso código:

@MappedSuperclass
abstract public class Entity implements Serializable {

    @Id
    @GeneratedValue
    @Column(nullable = false, updatable = false)
    protected Long id;

    @Transient
    private Long tempId;

    public void setId(Long id) {
        this.id = id;
    }

    public Long getId() {
        return id;
    }

    private void setTempId(Long tempId) {
        this.tempId = tempId;
    }

    // Fix Id on first call from equal() or hashCode()
    private Long getTempId() {
        if (tempId == null)
            // if we have id already, use it, else use 0
            setTempId(getId() == null ? 0 : getId());
        return tempId;
    }

    @Override
    public boolean equals(Object obj) {
        if (super.equals(obj))
            return true;
        // take proxied object into account
        if (obj == null || !Hibernate.getClass(obj).equals(this.getClass()))
            return false;
        Entity o = (Entity) obj;
        return getTempId() != 0 && o.getTempId() != 0 && getTempId().equals(o.getTempId());
    }

    // hash doesn't change in time
    @Override
    public int hashCode() {
        return getTempId() == 0 ? super.hashCode() : getTempId().hashCode();
    }
}

1

Por favor, considere a seguinte abordagem com base no identificador de tipo predefinido e no ID.

As premissas específicas para a JPA:

  • entidades do mesmo "tipo" e o mesmo ID não nulo são consideradas iguais
  • entidades não persistentes (assumindo que não há ID) nunca são iguais a outras entidades

A entidade abstrata:

@MappedSuperclass
public abstract class AbstractPersistable<K extends Serializable> {

  @Id @GeneratedValue
  private K id;

  @Transient
  private final String kind;

  public AbstractPersistable(final String kind) {
    this.kind = requireNonNull(kind, "Entity kind cannot be null");
  }

  @Override
  public final boolean equals(final Object obj) {
    if (this == obj) return true;
    if (!(obj instanceof AbstractPersistable)) return false;
    final AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null != this.id
        && Objects.equals(this.id, that.id)
        && Objects.equals(this.kind, that.kind);
  }

  @Override
  public final int hashCode() {
    return Objects.hash(kind, id);
  }

  public K getId() {
    return id;
  }

  protected void setId(final K id) {
    this.id = id;
  }
}

Exemplo de entidade concreta:

static class Foo extends AbstractPersistable<Long> {
  public Foo() {
    super("Foo");
  }
}

Exemplo de teste:

@Test
public void test_EqualsAndHashcode_GivenSubclass() {
  // Check contract
  EqualsVerifier.forClass(Foo.class)
    .suppress(Warning.NONFINAL_FIELDS, Warning.TRANSIENT_FIELDS)
    .withOnlyTheseFields("id", "kind")
    .withNonnullFields("id", "kind")
    .verify();
  // Ensure new objects are not equal
  assertNotEquals(new Foo(), new Foo());
}

Principais vantagens aqui:

  • simplicidade
  • garante que as subclasses forneçam a identidade do tipo
  • comportamento previsto com classes proxy

Desvantagens:

  • Requer que cada entidade chame super()

Notas:

  • Precisa de atenção ao usar herança. Por exemplo, igualdade de instância class Ae class B extends Apode depender de detalhes concretos do aplicativo.
  • Idealmente, use uma chave comercial como o ID

Fico na expectativa dos seus comentários.


0

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.


0

Na IMO, você tem 3 opções para implementar equals / hashCode

  • Use uma identidade gerada por aplicativo, ou seja, um UUID
  • Implementá-lo com base em uma chave comercial
  • Implementá-lo com base na chave primária

Usar uma identidade gerada por aplicativo é a abordagem mais fácil, mas vem com algumas desvantagens

  • As junções são mais lentas quando usadas como PK, porque 128 bits é simplesmente maior que 32 ou 64 bits
  • "Depurar é mais difícil" porque verificar com seus próprios olhos se alguns dados estão corretos é bem difícil

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.

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @NaturalId UUID uuid;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on uuid
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @Embeddable class ChildId {
    UUID parentUuid;
    UUID childUuid;
    // equals/hashCode based on parentUuid and childUuid
  }
  // equals/hashCode based on id
}

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.

  • As junções são mais lentas porque a junção com base no texto de tamanho variável é simplesmente lenta. Alguns DBMS podem até ter problemas para criar um índice se a chave exceder um determinado comprimento.
  • Na minha experiência, as chaves de negócios tendem a mudar, o que exigirá atualizações em cascata dos objetos referentes a ele. Isso é impossível se sistemas externos fizerem referência a ele

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:

@Entity class Parent {
  @Id @GeneratedValue Long id;
  @OneToMany(mappedBy = "parent") Set<Child> children;
  // equals/hashCode based on id
}

@Entity class Child {
  @EmbeddedId ChildId id;
  @ManyToOne Parent parent;

  @PrePersist void postPersist() {
    parent.children.remove(this);
  }
  @PostPersist void postPersist() {
    parent.children.add(this);
  }

  @Embeddable class ChildId {
    Long parentId;
    @GeneratedValue Long childId;
    // equals/hashCode based on parentId and childId
  }
  // equals/hashCode based on id
}

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 é:

  • Remover temporariamente o objeto de coleções baseadas em hash
  • Persistir
  • Adicionar novamente o objeto às coleções baseadas em hash

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.


0

Com o novo estilo instanceofdo java 14, você pode implementar o equalsem uma linha.

@Override
public boolean equals(Object obj) {
    return this == obj || id != null && obj instanceof User otherUser && id.equals(otherUser.id);
}

@Override
public int hashCode() {
    return 31;
}

-1

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:

@ManagedBean
public class MyCarFacade {
  public Car createCar(){
    Car car = new Car();
    em.persist(car);
    return car;
  }
}

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?


Uma abordagem que funciona muito bem se você deseja aceitar o desempenho, ao gerar o guid ao fazer uma pesquisa no banco de dados.
Michael Wiles

1
E quanto ao teste de carro? Nesse caso, você precisa de uma conexão com o banco de dados para testar? Além disso, seus objetos de domínio não devem depender da persistência.
21414 jhegedus

-1

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:

  • equals () significa "objeto refere-se à mesma entidade lógica"
  • use o ID do banco de dados o máximo possível, porque por que eu faria o trabalho duas vezes (preocupação com o desempenho)
  • evitar problemas ao acessar hashCode () / equals () em entidade ainda não persistente e manter o mesmo comportamento depois que ele realmente persistir

Eu realmente aprecio o feedback da minha solução mista abaixo

public class MyEntity { 

    @Id()
    @Column(name = "ID", length = 20, nullable = false, unique = true)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

    @Transient private UUID uuid = null;

    @Column(name = "UUID_MOST", nullable = true, unique = false, updatable = false)
    private Long uuidMostSignificantBits = null;
    @Column(name = "UUID_LEAST", nullable = true, unique = false, updatable = false)
    private Long uuidLeastSignificantBits = null;

    @Override
    public final int hashCode() {
        return this.getUuid().hashCode();
    }

    @Override
    public final boolean equals(Object toBeCompared) {
        if(this == toBeCompared) {
            return true;
        }
        if(toBeCompared == null) {
            return false;
        }
        if(!this.getClass().isInstance(toBeCompared)) {
            return false;
        }
        return this.getUuid().equals(((MyEntity)toBeCompared).getUuid());
    }

    public final UUID getUuid() {
        // UUID already accessed on this physical object
        if(this.uuid != null) {
            return this.uuid;
        }
        // UUID one day generated on this entity before it was persisted
        if(this.uuidMostSignificantBits != null) {
            this.uuid = new UUID(this.uuidMostSignificantBits, this.uuidLeastSignificantBits);
        // UUID never generated on this entity before it was persisted
        } else if(this.getId() != null) {
            this.uuid = new UUID(this.getId(), this.getId());
        // UUID never accessed on this not yet persisted entity
        } else {
            this.setUuid(UUID.randomUUID());
        }
        return this.uuid; 
    }

    private void setUuid(UUID uuid) {
        if(uuid == null) {
            return;
        }
        // For the one hypothetical case where generated UUID could colude with UUID build from IDs
        if(uuid.getMostSignificantBits() == uuid.getLeastSignificantBits()) {
            throw new Exception("UUID: " + this.getUuid() + " format is only for internal use");
        }
        this.uuidMostSignificantBits = uuid.getMostSignificantBits();
        this.uuidLeastSignificantBits = uuid.getLeastSignificantBits();
        this.uuid = uuid;
    }

o que você quer dizer com "UUID um dia gerado nesta entidade antes de eu persistir"? você poderia dar um exemplo para este caso?
Jhegedus

você poderia usar o tipo de geração atribuído? por que o tipo de geração de identidade é necessário? tem alguma vantagem sobre o atribuído?
Jevedus #

o que acontece se você 1) criar um novo MyEntity, 2) colocá-lo em uma lista, 3) salvar no banco de dados e 4) carregar a entidade de volta do banco de dados e 5) tentar ver se a instância carregada está na lista . Meu palpite é que não será o que deveria ser.
Jevinhods #

Obrigado pelos seus primeiros comentários que me mostraram que eu não estava tão claro quanto deveria. Em primeiro lugar, "UUID um dia gerado nesta entidade antes de persistir" foi um erro de digitação ... "antes de persistir a TI" deveria ter sido lido. Para os outros comentários, editarei meu post em breve para tentar explicar melhor minha solução.
user2083808

-1

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 HibernateHibernate.getClass ).

public boolean equals(Object obj) {
    if (null == obj) {
        return false;
    }
    if (this == obj) {
        return true;
    }
    if (!getClass().equals(ClassUtils.getUserClass(obj))) {
        return false;
    }
    AbstractPersistable<?> that = (AbstractPersistable<?>) obj;
    return null == this.getId() ? false : this.getId().equals(that.getId());
}

@Override
public int hashCode() {
    int hashCode = 17;
    hashCode += null == getId() ? 0 : getId().hashCode() * 31;
    return hashCode;
}

Apenas ciente de manipular novos objetos no HashSet / HashMap. Por outro lado, a opção 1 (continuação da Objectimplementação) é interrompida logo após merge, 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 hashCodepara constante, como abaixo foi recomendado a Vlad Mihalcea.


-2

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:

import javax.persistence._
import scala.util.Random

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
abstract class UUIDEntity {
  @Id  @GeneratedValue(strategy = GenerationType.TABLE)
  var id:java.lang.Long=null
  var uuid:java.lang.Long=Random.nextLong()
  override def equals(o:Any):Boolean= 
    o match{
      case o : UUIDEntity => o.uuid==uuid
      case _ => false
    }
  override def hashCode() = uuid.hashCode()
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.