Crie a entidade JPA perfeita [fechada]


422

Estou trabalhando com o JPA (implementação Hibernate) há algum tempo e, toda vez que preciso criar entidades, me deparei com problemas como AccessType, propriedades imutáveis, equals / hashCode, ....
Por isso, decidi tentar descobrir as melhores práticas gerais para cada problema e anotá-las para uso pessoal.
No entanto, eu não me importaria que alguém comentasse ou me dissesse onde estou errado.

Classe de entidade

  • implementar Serializable

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

Construtores

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

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

  • além deste construtor: tenha um construtor padrão privado do pacote

    Razão: O construtor padrão é necessário para o Hibernate inicializar a entidade; private é permitido, mas a visibilidade do pacote privado (ou público) é necessária para a geração do proxy de tempo de execução e recuperação eficiente de dados sem a 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 ao campo parece ser o favorito geral por causa do código mais claro, melhor encapsulamento e não há necessidade de criar setters para campos imutáveis

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

  • as propriedades podem ser privadas
    Motivo: Uma vez ouvi dizer que protegido é melhor para o desempenho (Hibernate), mas tudo o que posso encontrar na web é: O Hibernate pode acessar métodos de acessador públicos, privados e protegidos, bem como campos públicos, privados e protegidos diretamente . A escolha é sua e você pode combiná-la para se adequar ao design do aplicativo.

Equals / hashCode

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

Entidade de exemplo

@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 , adaptei minha maneira de implementar o eq / hC:

  • se uma chave comercial simples imutável estiver disponível: use esse
  • em todos os outros casos: use um uuid

6
Esta não é uma pergunta, é uma solicitação de revisão com uma solicitação de lista. Além disso, é muito aberto e vago ou, de maneira diferente: se uma entidade da JPA é perfeita depende do que será usada. Devemos listar todas as coisas que uma entidade pode precisar em todos os usos possíveis de uma entidade?
meriton

Eu sei que não é uma pergunta clara pela qual peço desculpas. Não é realmente um pedido de lista, mas um pedido de comentários / observações, embora outras sugestões sejam bem-vindas. Sinta-se à vontade para elaborar os possíveis usos de uma entidade JPA.
Stijn Geukens

Eu também gostaria que os campos fossem final(a julgar pela sua omissão de levantadores, acho que você também).
Sridhar Sarnobat 6/0318

Teria que tentar, mas não acho que o final funcionará, pois o Hibernate ainda precisa ser capaz de definir os valores nessas propriedades.
Stijn Geukens 8/03/19

De onde notNullvem?
bruno

Respostas:


73

Tentarei responder a vários pontos-chave: isso é da longa experiência em Hibernate / persistência, incluindo vários aplicativos principais.

Classe de entidade: implementar Serializable?

Chaves precisa implementar Serializable. O material que será enviado no HttpSession, ou será enviado por RPC / Java EE, precisará implementar o Serializable. Outras coisas: nem tanto. Gaste seu tempo com o que é importante.

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

Os construtores da lógica do aplicativo devem 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 geralmente são úteis.

OTOH, se as regras de aplicação (hoje) exigirem que um Cliente tenha um Endereço, deixe isso para um levantador. 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 se engane, deixe a possibilidade de dados desconhecidos, incompletos ou "parcialmente inseridos".

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

Sim, mas use 'protected' em vez de package private. Subclassificar coisas é uma verdadeira dor 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 do Hibernate, funcione.

Quanto aos campos 'imutáveis' para o aplicativo - o Hibernate ainda precisa ser capaz de carregá-los. Você pode tentar tornar esses métodos 'privados' e / ou colocar uma anotação neles, para impedir 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ê encontrará campos não inicializados / vazios em instâncias de proxy.

Protegido é melhor para o desempenho (Hibernate)?

Improvável.

Igual a / HashCode?

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

Um cliente pode mudar de endereço, mudar o nome de seus negócios, etc etc - não é comum, mas acontece. Também é possível fazer correções quando os dados não foram inseridos corretamente.

As poucas coisas que normalmente são mantidas imutáveis, são Parentalidade e talvez Tipo / Tipo - normalmente o usuário recria o registro, em vez de alterá-lo. Mas estes não identificam exclusivamente a entidade!

Portanto, longo e curto, 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 estabilidade e imutabilidade garantidas.

Você precisa planejar e considerar as fases de trabalho de comparação, hash 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) trabalhando com " dados não salvos ", se você comparar / hash no ID.

Igual a / HashCode - se uma Chave Comercial exclusiva não estiver disponível, use um UUID não transitório, criado quando a entidade é inicializada

Sim, esta é uma boa estratégia quando necessário. Esteja ciente de que os UUIDs não são gratuitos, porém em termos de desempenho - e o agrupamento complica as coisas.

Equals / HashCode - nunca se refere a entidades relacionadas

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

Parece um bom conselho.

Espero que isto ajude!


2
Re: construtores, muitas vezes vejo apenas zero arg (ou seja, nenhum) e o código de chamada tem uma grande lista longa de setters, o que me parece um pouco confuso. Existe realmente algum problema em ter alguns construtores que atendem às suas necessidades, tornando o código de chamada mais sucinto?
Furacão

totalmente opinativo, especialmente sobre ctor. o que é um código mais bonito? um monte de diferentes ctors que permitem que você saiba quais (combinações) de valores são necessários para criar um estado sadio do objeto ou um não-argumento que não dá nenhuma pista do que deve ser definido e em que ordem e o deixa sujeito a erros do usuário ?
mohamnag 02/02

1
@mohamnag Depends. Para dados gerados pelo sistema interno, os beans estritamente válidos são ótimos; no entanto, os aplicativos de negócios modernos consistem em um grande número de telas de assistente ou CRUD de entrada de dados do usuário. Os dados inseridos pelo usuário geralmente são formados parcial ou incorretamente, pelo menos durante a edição. Muitas vezes, existe até o valor comercial em poder registrar um estado incompleto para conclusão posterior - pense na captura de aplicativos de seguros, na inscrição de clientes etc. Manter as restrições ao mínimo (por exemplo, chave primária, chave comercial e estado) permite uma maior flexibilidade em termos reais. situações de negócios.
7278 Thomas W no dia

1
@ThomasW Primeiro, tenho de dizer que tenho uma forte opinião sobre o design orientado a domínio e o uso de nomes para nomes de classe e o significado de verbos completos para métodos. Nesse paradigma, a que você está se referindo, na verdade são DTOs e não as entidades de domínio que devem ser usadas para armazenamento temporário de dados. Ou você apenas entendeu / estruturou mal o seu domínio.
18720 mohamnag

@ ThomasW Quando eu filtrar todas as frases que você está tentando dizer que sou iniciante, não há mais informações em seu comentário, exceto com relação à entrada do usuário. Essa parte, como eu disse antes, deve ser feita nos DTOs e não diretamente na entidade. vamos falar daqui a 50 anos que você pode se tornar 5% da grande mente por trás do DDD como Fowler experimentou! Cheers: D
mohamnag

144

A especificação 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 da entidade deve ser uma classe de nível superior. Uma enumeração ou interface não deve ser designada como uma entidade.
  • A classe da 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, por meio de uma interface remota), a classe de entidade deve implementar a interface Serializable.
  • As classes abstrata e concreta podem ser entidades. As entidades podem estender classes não-entidade, bem como classes de entidade, e classes não-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.


13
Verdadeiro, igual a, código de hash ... não é um requisito da JPA, mas é claro que é recomendado e considerado uma boa prática.
Stijn Geukens

6
@TheStijn Bem, a menos que você planeje comparar entidades desanexadas pela igualdade, isso provavelmente é desnecessário. É garantido que o gerente da entidade retorne a mesma instância de uma determinada entidade toda vez que você a solicitar. Portanto, você pode se sair bem com comparações de identidade para entidades gerenciadas, tanto quanto eu entendo. Você poderia elaborar um pouco mais sobre os cenários em que você consideraria isso uma boa prática?
Edwin Dalorzo 17/05

2
Eu me esforço para sempre ter uma implementação correta de equals / hashCode. Não é necessário para a JPA, mas considero uma boa prática para quando entidades ou adicionadas a Conjuntos. Você pode decidir implementar apenas iguais quando as entidades serão adicionadas aos Conjuntos, mas você sempre sabe com antecedência?
Stijn Geukens

10
@TheStijn O provedor de JPA garantirá que, a qualquer momento, exista apenas uma instância de uma determinada entidade no contexto, portanto, mesmo seus conjuntos estarão seguros sem implementar equals / hascode, desde que você use apenas entidades gerenciadas. A implementação desses métodos para entidades não está livre de dificuldades, por exemplo, dê uma olhada neste artigo do Hibernate sobre o assunto. Meu argumento é que, se você trabalha apenas com entidades gerenciadas, é melhor sem elas; caso contrário, forneça uma implementação muito cuidadosa.
Edwin Dalorzo 18/05

2
@TheStijn Este é o bom cenário misto. Isso justifica a necessidade de implementar o eq / hC, como você sugeriu inicialmente, porque uma vez que as entidades abandonam a segurança da camada de persistência, você não pode mais confiar nas regras impostas pelo padrão JPA. No nosso caso, o padrão DTO foi aplicado arquitetonicamente desde o início. Por design, nossa API de persistência não oferece uma maneira pública de interagir com os objetos de negócios, apenas uma API para interagir com nossa camada de persistência usando DTOs.
Edwin Dalorzo 18/05

13

Minha adição de 2 centavos às respostas aqui são:

  1. Com referência ao acesso ao campo ou à propriedade (longe das considerações de desempenho), ambos são legitimamente acessados ​​por meio de getters e setters, portanto, a lógica do meu modelo pode configurá-los / obtê-los da mesma maneira. A diferença ocorre quando o provedor de tempo de execução de persistência (Hibernate, EclipseLink ou outro) precisa persistir / definir algum registro na Tabela A que possui uma chave estrangeira referente a alguma coluna na Tabela B. No caso de um tipo de acesso de propriedade, a persistência O sistema de tempo de execução usa meu método setter codificado para atribuir à célula na coluna da Tabela B um novo valor. No caso de um tipo de acesso ao Campo, o sistema de tempo de execução de persistência define a célula na coluna Tabela B diretamente. Essa diferença não é importante no contexto de um relacionamento unidirecional, no entanto, é DEVE usar meu próprio método de setter codificado (tipo de acesso de propriedade) para um relacionamento bidirecional, desde que o método de setter seja bem projetado para dar conta da consistência. A consistência é uma questão crítica para os relacionamentos bidirecionais.link para um exemplo simples de um setter bem projetado.

  2. Com referência a Equals / hashCode: É impossível usar os métodos Equals / hashCode gerados automaticamente pelo Eclipse para entidades que participam de um relacionamento bidirecional, caso contrário, elas terão uma referência circular resultando em uma exceção de estouro de pilha. Depois de tentar um relacionamento bidirecional (por exemplo, OneToOne) e gerar automaticamente Equals () ou hashCode () ou até toString (), você será pego nessa exceção do stackoverflow.


9

Interface da 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);
}

A implementação básica para todas as entidades simplifica as 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 da sala 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
}

Não vejo sentido em comparar a igualdade de entidades com base em campos de negócios em todos os casos de entidades JPA. Isso pode ser mais um caso se essas entidades JPA forem consideradas como ValueObjects controlados por domínio, em vez de entidades controladas por domínio (para as quais esses exemplos de código servem).


4
Embora seja uma boa abordagem usar uma classe de entidade pai para remover o código da placa da caldeira, não é uma boa idéia usar o ID definido no banco de dados em seu método equals. No seu caso, comparar duas novas entidades geraria um NPE. Mesmo se você tornar nulo seguro, duas novas entidades sempre serão iguais, até que sejam mantidas. Eq / hC deve ser imutável.
Stijn Geukens

2
Equals () não lançará NPE, pois verifica se o ID do banco de dados é nulo ou não e, caso o ID do banco de dados seja nulo, a igualdade seria falsa.
ahaaman

3
Na verdade, não vejo como perdi o fato de o código ser seguro para nulos. Mas IMO usando o id ainda é uma prática ruim. Argumentos: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens

No livro 'Implementando DDD' de Vaughn Vernon, argumenta-se que você pode usar id para iguais se usar "geração PK inicial" (gere um id primeiro e passe-o para o construtor da entidade, em vez de permitir que o banco de dados gere o id quando você persistir a entidade).
Wim Deblauwe

ou se você não planeja igual comparar entidades não persistentes? Por que você deveria ...
Enerccio
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.