java - jpa equals hashcode example




Crie a entidade JPA perfeita (3)

A especificação do JPA 2.0 afirma que:

  • A classe de entidade deve ter um construtor no-arg. Pode ter outros construtores também. O construtor no-arg deve ser público ou protegido.
  • A classe de entidade deve ser uma classe de nível superior. Um enum ou interface não deve ser designado como uma entidade.
  • A classe de entidade não deve ser final. Nenhum método ou variável de instância persistente da classe de entidade pode ser final.
  • Se uma instância de entidade deve ser passada por valor como um objeto desanexado (por exemplo, através de uma interface remota), a classe de entidade deve implementar a interface serializável.
  • Ambas as classes abstrata e concreta podem ser entidades. Entidades podem estender classes que não sejam de entidade, assim como classes de entidade, e classes que não sejam de entidade podem estender classes de entidade.

A especificação não contém requisitos sobre a implementação de métodos equals e hashCode para entidades, apenas para classes de chave primária e chaves de mapa, tanto quanto eu sei.

Eu tenho trabalhado com o JPA (implementação do Hibernate) já há algum tempo e toda vez que eu preciso criar entidades eu me encontro lutando com problemas como AccessType, propriedades imutáveis, equals / hashCode, ....
Então, decidi tentar descobrir as melhores práticas gerais para cada problema e escrevê-las para uso pessoal.
Não me importaria, no entanto, que alguém comentasse ou me dissesse onde estou errado.

Classe de Entidade

  • implementar Serializable

    Razão: A especificação diz que você precisa, mas alguns provedores JPA não impõem isso. O Hibernate como provedor de JPA não impõe isso, mas pode falhar em algum lugar no seu estômago com ClassCastException, se Serializable não tiver sido implementado.

Construtores

  • criar um construtor com todos os campos obrigatórios da entidade

    Razão: Um construtor deve sempre deixar a instância criada em um estado sã.

  • além desse construtor: ter um construtor padrão privado de pacote

    Razão: O construtor padrão é necessário para que o Hibernate inicialize a entidade; private é permitido, mas a visibilidade do pacote privado (ou público) é necessária para a geração de proxy de tempo de execução e recuperação de dados eficiente sem instrumentação de bytecode.

Campos / Propriedades

  • Use o acesso ao campo em geral e acesso à propriedade quando necessário

    Razão: esta é provavelmente a questão mais discutível, pois não há argumentos claros e convincentes para um ou outro (acesso à propriedade versus acesso ao campo); no entanto, o acesso de campo parece ser o favorito geral por causa de um código mais claro, melhor encapsulamento e sem necessidade de criar setters para campos imutáveis.

  • Omitir setters para campos imutáveis ​​(não é necessário para o campo de tipo de acesso)

  • propriedades podem ser privadas
    Razão: Ouvi dizer que proteção é melhor para desempenho (Hibernate), mas tudo que posso encontrar na Web é: O Hibernate pode acessar métodos de acesso público, privado e protegido, assim como campos públicos, privados e protegidos diretamente. A escolha é sua e você pode combiná-la para se adequar ao design do seu aplicativo.

Equals / hashCode

  • Nunca use um ID gerado se esse id for definido apenas quando persistir a entidade
  • Por preferência: use valores imutáveis ​​para formar uma chave de negócios exclusiva e use isso para testar a igualdade
  • se uma chave comercial exclusiva não estiver disponível, use um UUID não transitório que é criado quando a entidade é inicializada; Veja este ótimo artigo para mais informações.
  • nunca se referem a entidades relacionadas (ManyToOne); se essa entidade (como uma entidade pai) precisar fazer parte da Chave comercial, compare os IDs apenas. Chamar getId () em um proxy não acionará o carregamento da entidade, desde que você esteja usando o tipo de acesso à propriedade .

Exemplo de entidade

@Entity
@Table(name = "ROOM")
public class Room implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue
    @Column(name = "room_id")
    private Integer id;

    @Column(name = "number") 
    private String number; //immutable

    @Column(name = "capacity")
    private Integer capacity;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "building_id")
    private Building building; //immutable

    Room() {
        // default constructor
    }

    public Room(Building building, String number) {
        // constructor with required field
        notNull(building, "Method called with null parameter (application)");
        notNull(number, "Method called with null parameter (name)");

        this.building = building;
        this.number = number;
    }

    @Override
    public boolean equals(final Object otherObj) {
        if ((otherObj == null) || !(otherObj instanceof Room)) {
            return false;
        }
        // a room can be uniquely identified by it's number and the building it belongs to; normally I would use a UUID in any case but this is just to illustrate the usage of getId()
        final Room other = (Room) otherObj;
        return new EqualsBuilder().append(getNumber(), other.getNumber())
                .append(getBuilding().getId(), other.getBuilding().getId())
                .isEquals();
        //this assumes that Building.id is annotated with @Access(value = AccessType.PROPERTY) 
    }

    public Building getBuilding() {
        return building;
    }


    public Integer getId() {
        return id;
    }

    public String getNumber() {
        return number;
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder().append(getNumber()).append(getBuilding().getId()).toHashCode();
    }

    public void setCapacity(Integer capacity) {
        this.capacity = capacity;
    }

    //no setters for number, building nor id

}

Outras sugestões para adicionar a esta lista são mais que bem-vindas ...

ATUALIZAR

Desde a leitura deste artigo eu adaptei minha maneira de implementar o eq / hC:

  • se uma chave de negócio simples e imutável estiver disponível: use isso
  • em todos os outros casos: use um uuid

Interface de entidade

public interface Entity<I> extends Serializable {

/**
 * @return entity identity
 */
I getId();

/**
 * @return HashCode of entity identity
 */
int identityHashCode();

/**
 * @param other
 *            Other entity
 * @return true if identities of entities are equal
 */
boolean identityEquals(Entity<?> other);
}

Implementação básica para todas as Entidades, simplifica implementações Equals / Hashcode:

public abstract class AbstractEntity<I> implements Entity<I> {

@Override
public final boolean identityEquals(Entity<?> other) {
    if (getId() == null) {
        return false;
    }
    return getId().equals(other.getId());
}

@Override
public final int identityHashCode() {
    return new HashCodeBuilder().append(this.getId()).toHashCode();
}

@Override
public final int hashCode() {
    return identityHashCode();
}

@Override
public final boolean equals(final Object o) {
    if (this == o) {
        return true;
    }
    if ((o == null) || (getClass() != o.getClass())) {
        return false;
    }

    return identityEquals((Entity<?>) o);
}

@Override
public String toString() {
    return getClass().getSimpleName() + ": " + identity();
    // OR 
    // return ReflectionToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
}
}

Entidade do quarto impl:

@Entity
@Table(name = "ROOM")
public class Room extends AbstractEntity<Integer> {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "room_id")
private Integer id;

@Column(name = "number") 
private String number; //immutable

@Column(name = "capacity")
private Integer capacity;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "building_id")
private Building building; //immutable

Room() {
    // default constructor
}

public Room(Building building, String number) {
    // constructor with required field
    notNull(building, "Method called with null parameter (application)");
    notNull(number, "Method called with null parameter (name)");

    this.building = building;
    this.number = number;
}

public Integer getId(){
    return id;
}

public Building getBuilding() {
    return building;
}

public String getNumber() {
    return number;
}


public void setCapacity(Integer capacity) {
    this.capacity = capacity;
}

//no setters for number, building nor id
}

Eu não vejo um ponto de comparar a igualdade de entidades com base nos campos de negócios em todos os casos de Entidades JPA. Isso pode ser mais comum se essas entidades JPA forem consideradas como ValueObjects acionados por domínio, em vez de Entidades orientadas a domínio (para as quais esses exemplos de código servem).


Tentarei responder a vários pontos-chave: isso é a partir da longa experiência de persistência / hibernação, incluindo vários aplicativos importantes.

Classe de Entidade: implementar Serializable?

Chaves precisa implementar Serializable. Coisas que vão no HttpSession, ou serem enviadas pelo RPC / Java EE, precisam implementar o Serializable. Outras coisas: não tanto. Gaste seu tempo com o que é importante.

Construtores: cria um construtor com todos os campos obrigatórios da entidade?

Construtor (es) para lógica de aplicativo, deve ter apenas alguns campos críticos de "chave estrangeira" ou "tipo / tipo" que sempre serão conhecidos ao criar a entidade. O resto deve ser definido chamando os métodos setter - é para isso que servem.

Evite colocar muitos campos em construtores. Os construtores devem ser convenientes e dar sanidade básica ao objeto. Nome, tipo e / ou pais são todos tipicamente úteis.

OTOH se as regras de aplicação (hoje) exigem que um cliente tenha um endereço, deixe isso para um setter. Esse é um exemplo de uma "regra fraca". Talvez na próxima semana, você queira criar um objeto Customer antes de ir para a tela Enter Details? Não desista, deixe a possibilidade de dados desconhecidos, incompletos ou "parcialmente inseridos".

Construtores: também, construtor de padrão privado de pacote?

Sim, mas use "protegido" em vez de pacote privado. Material de subclasses é uma dor real quando os internos necessários não são visíveis.

Campos / Propriedades

Use o acesso ao campo 'property' para o Hibernate e de fora da instância. Dentro da instância, use os campos diretamente. Razão: permite que a reflexão padrão, o método mais simples e básico para o Hibernate, funcione.

Quanto aos campos 'imutáveis' para a aplicação - o Hibernate ainda precisa ser capaz de carregá-los. Você poderia tentar tornar esses métodos 'privados' e / ou colocar uma anotação neles, para evitar que o código do aplicativo faça acesso indesejado.

Nota: ao escrever uma função equals (), use getters para valores na instância 'other'! Caso contrário, você atingirá campos não inicializados / vazios em instâncias de proxy.

Protegido é melhor para o desempenho (Hibernate)?

Improvável.

Equals / HashCode?

Isso é relevante para trabalhar com entidades antes que elas sejam salvas - o que é um problema espinhoso. Hashing / comparando em valores imutáveis? Na maioria dos aplicativos de negócios, não há nenhum.

Um cliente pode alterar o endereço, alterar o nome do seu negócio, etc etc - não é comum, mas acontece. Correções também precisam ser possíveis de serem feitas quando os dados não foram digitados corretamente.

As poucas coisas que normalmente são imutáveis ​​são Parenting e talvez Type / Kind - normalmente o usuário recria o registro, em vez de modificá-lo. Mas estes não identificam unicamente a entidade!

Então, longos e curtos, os dados "imutáveis" alegados não são realmente. Os campos Chave Primária / ID são gerados com a finalidade precisa de fornecer essa estabilidade e imutabilidade garantida.

Você precisa planejar e considerar sua necessidade de comparação e hashing e processamento de solicitações quando A) trabalhar com "dados alterados / vinculados" da interface do usuário se comparar / hash em "campos alterados com pouca frequência" ou B) trabalhar com " dados não salvos ", se você comparar / hash no ID.

Equals / HashCode - se uma chave comercial exclusiva não estiver disponível, use um UUID não transitório que é criado quando a entidade é inicializada

Sim, esta é uma boa estratégia quando necessário. Porém, esteja ciente de que os UUIDs não são livres, e o desempenho em cluster complica as coisas.

Equals / HashCode - nunca se refere a entidades relacionadas

"Se entidade relacionada (como uma entidade pai) precisar fazer parte da chave comercial, adicione um campo não inserível e não atualizável para armazenar o ID pai (com o mesmo nome de ManytoOne JoinColumn) e use esse id na verificação de igualdade "

Soa como um bom conselho.

Espero que isto ajude!





equals