Crear la entidad JPA perfecta [cerrada]

422

He estado trabajando con JPA (implementación de Hibernate) durante algún tiempo y cada vez que necesito crear entidades me encuentro luchando con problemas como AccessType, propiedades inmutables, equals / hashCode, ....
Así que decidí intentar encontrar las mejores prácticas generales para cada problema y escribir esto para uso personal.
Sin embargo, no me importaría que nadie lo comentara o me dijera dónde me equivoco.

Clase de entidad

  • implementar serializable

    Motivo: La especificación dice que tiene que hacerlo, pero algunos proveedores de JPA no lo hacen cumplir. Hibernate como proveedor de JPA no aplica esto, pero puede fallar en algún lugar profundo de su estómago con ClassCastException, si no se ha implementado Serializable.

Constructores

  • crear un constructor con todos los campos obligatorios de la entidad

    Motivo: Un constructor siempre debe dejar la instancia creada en un estado sano.

  • además de este constructor: tener un paquete constructor privado predeterminado

    Motivo: el constructor predeterminado es necesario para que Hibernate inicialice la entidad; se permite la privacidad, pero se requiere la visibilidad del paquete privado (o público) para la generación de proxy en tiempo de ejecución y la recuperación eficiente de datos sin instrumentación de código de bytes.

Campos / Propiedades

  • Use el acceso de campo en general y el acceso a la propiedad cuando sea necesario

    Motivo: este es probablemente el tema más discutible ya que no hay argumentos claros y convincentes para uno u otro (acceso a la propiedad frente al acceso al campo); sin embargo, el acceso a los campos parece ser el favorito general debido a un código más claro, una mejor encapsulación y sin necesidad de crear setters para campos inmutables

  • Omitir configuradores para campos inmutables (no es necesario para el campo de tipo de acceso)

  • las propiedades pueden ser privadas
    Motivo: una vez escuché que protegido es mejor para el rendimiento (Hibernate) pero todo lo que puedo encontrar en la web es: Hibernate puede acceder a métodos de acceso públicos, privados y protegidos, así como a campos públicos, privados y protegidos directamente . La elección depende de usted y puede combinarla para adaptarse al diseño de su aplicación.

Igual / hashCode

  • Nunca use una identificación generada si esta identificación solo se establece cuando persiste la entidad
  • Por preferencia: use valores inmutables para formar una clave comercial única y úsela para probar la igualdad
  • si no hay una Clave comercial única disponible, use un UUID no transitorio que se crea cuando se inicializa la entidad; Vea este gran artículo para más información.
  • nunca se refiera a entidades relacionadas (ManyToOne); Si esta entidad (como una entidad matriz) necesita ser parte de la clave comercial, compare solo los ID. Llamar a getId () en un proxy no activará la carga de la entidad, siempre que esté utilizando el tipo de acceso a la propiedad .

Entidad de ejemplo

@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

}

Otras sugerencias para agregar a esta lista son más que bienvenidas ...

ACTUALIZAR

Desde que leí este artículo, adapté mi forma de implementar eq / hC:

  • si hay disponible una clave comercial simple inmutable: utilícela
  • en todos los demás casos: use un líquido
Stijn Geukens
fuente
66
Esta no es una pregunta, es una solicitud de revisión con una solicitud de una lista. Además, es muy abierto y vago, o dicho de otra manera: si una entidad JPA es perfecta depende de para qué se vaya a utilizar. ¿Deberíamos enumerar todas las cosas que una entidad podría necesitar en todos los usos posibles de una entidad?
meriton
Sé que no es una pregunta clara por la que me disculpo. No es realmente una solicitud de una lista, sino una solicitud de comentarios / observaciones, aunque se aceptan otras sugerencias. Siéntase libre de elaborar sobre los posibles usos de una entidad JPA.
Stijn Geukens
También me gustaría que los campos estuvieran final(a juzgar por su omisión de establecedores, supongo que usted también).
Sridhar Sarnobat
Tendría que intentarlo, pero no creo que final funcione ya que Hibernate aún necesita poder establecer los valores en esas propiedades.
Stijn Geukens
De donde notNullviene
bruno

Respuestas:

73

Trataré de responder varios puntos clave: esto se debe a la larga experiencia de Hibernación / persistencia, incluidas varias aplicaciones importantes.

Clase de entidad: implementar Serializable?

Keys necesita implementar Serializable. Las cosas que van a ir en la HttpSession, o que RPC / Java EE enviará por cable, deben implementar Serializable. Otras cosas: no tanto. Dedica tu tiempo a lo que es importante.

Constructores: ¿crear un constructor con todos los campos obligatorios de la entidad?

Los constructores para la lógica de la aplicación deben tener solo unos pocos campos críticos de "clave externa" o "tipo / tipo" que siempre se conocerán al crear la entidad. El resto debe establecerse llamando a los métodos de establecimiento; para eso están.

Evite poner demasiados campos en constructores. Los constructores deben ser convenientes y dar cordura básica al objeto. Nombre, tipo y / o padres son típicamente útiles.

OTOH si las reglas de la aplicación (hoy) requieren que un Cliente tenga una Dirección, déjela a un establecedor. Ese es un ejemplo de una "regla débil". Tal vez la próxima semana, ¿desea crear un objeto Cliente antes de ir a la pantalla Introducir detalles? No se tropiece, deje la posibilidad de datos desconocidos, incompletos o "parcialmente ingresados".

Constructores: también, paquete constructor privado predeterminado?

Sí, pero use 'protegido' en lugar de paquete privado. Subclasificar cosas es un verdadero dolor cuando las partes internas necesarias no son visibles.

Campos / Propiedades

Utilice el acceso al campo 'propiedad' para Hibernate y desde fuera de la instancia. Dentro de la instancia, use los campos directamente. Motivo: permite que funcione la reflexión estándar, el método más simple y básico para Hibernate.

En cuanto a los campos 'inmutables' para la aplicación, Hibernate todavía necesita poder cargarlos. Puede intentar hacer que estos métodos sean 'privados' y / o poner una anotación en ellos, para evitar que el código de la aplicación haga accesos no deseados.

Nota: cuando escriba una función equals (), ¡use getters para valores en la 'otra' instancia! De lo contrario, presionará campos no inicializados / vacíos en instancias de proxy.

¿Protegido es mejor para el rendimiento (Hibernate)?

Improbable.

Igual / HashCode?

Esto es relevante para trabajar con entidades, antes de que se hayan guardado, lo cual es un problema espinoso. Hashing / comparación de valores inmutables? En la mayoría de las aplicaciones comerciales, no hay ninguna.

Un cliente puede cambiar la dirección, cambiar el nombre de su negocio, etc., etc., no es común, pero sucede. También es necesario realizar correcciones cuando los datos no se ingresaron correctamente.

Las pocas cosas que normalmente se mantienen inmutables son Parenting y quizás Type / Kind, normalmente el usuario recrea el registro, en lugar de cambiarlo. ¡Pero estos no identifican únicamente a la entidad!

Entonces, a corto y largo plazo, los datos "inmutables" afirmados no son realmente. Los campos de clave principal / ID se generan con el propósito preciso, de proporcionar tal estabilidad garantizada e inmutabilidad.

Debe planificar y considerar su necesidad de comparar y procesar las fases de trabajo de procesamiento de solicitudes cuando A) trabaja con "datos modificados / enlazados" de la interfaz de usuario si compara / hash en "campos modificados con poca frecuencia", o B) trabaja con " datos no guardados ", si compara / hash en ID.

Equals / HashCode: si una clave comercial única no está disponible, use un UUID no transitorio que se crea cuando se inicializa la entidad

Sí, esta es una buena estrategia cuando se requiere. Tenga en cuenta que los UUID no son gratuitos, sin embargo, en cuanto al rendimiento, y la agrupación complica las cosas.

Equals / HashCode: nunca haga referencia a entidades relacionadas

"Si la entidad relacionada (como una entidad principal) necesita ser parte de la clave comercial, agregue un campo no insertable y no actualizable para almacenar la identificación principal (con el mismo nombre que ManytoOne JoinColumn) y use esta identificación en la verificación de igualdad "

Suena como un buen consejo.

¡Espero que esto ayude!

Thomas W
fuente
2
Re: constructores, a menudo veo solo cero arg (es decir, ninguno) y el código de llamada tiene una gran lista de setters que me parece un poco desordenado. ¿Existe realmente algún problema con tener un par de constructores que se adapten a sus necesidades, haciendo que el código de llamada sea más sucinto?
Huracán
totalmente obstinado, especialmente sobre ctor. ¿Qué es el código más hermoso? un conjunto de diferentes factores que le permiten saber qué (combinación de) valores son necesarios para crear un estado sensato del obj o un argumento sin argumentos que no da idea de lo que se establecerá y en qué orden, y lo deja propenso a los errores del usuario ?
mohamnag
1
@mohamnag depende. Para los datos internos generados por el sistema, los beans estrictamente válidos son excelentes; sin embargo, las aplicaciones comerciales modernas consisten en un gran número de CRUD de entrada de datos de usuario o pantallas de asistente. Los datos ingresados ​​por el usuario a menudo tienen una forma parcial o deficiente, al menos durante la edición. Muy a menudo, incluso existe un valor comercial para poder registrar un estado incompleto para su finalización posterior: piense en la captura de la aplicación de seguro, las suscripciones de clientes, etc. Mantener las restricciones al mínimo (por ejemplo, clave principal, clave comercial y estado) permite una mayor flexibilidad real situaciones de negocios
Thomas W
1
@ThomasW primero tengo que decir que tengo una gran opinión sobre el diseño impulsado por dominios y el uso de nombres para los nombres de clase y el significado de verbos completos para los métodos. En este paradigma, a lo que se refiere es en realidad DTO y no las entidades de dominio que deberían usarse para el almacenamiento temporal de datos. O simplemente ha malinterpretado / estructurado su dominio.
mohamnag
@ThomasW cuando filtro todas las oraciones que intentas decir que soy un novato, no queda información en tu comentario, excepto en lo que respecta a la entrada del usuario. Esa parte, como dije antes, se hará en DTO y no directamente en la entidad. ¡hablemos en otros 50 años que puede convertirse en el 5% de la gran mente detrás de DDD como Fowler ha experimentado! aplausos: D
mohamnag
144

La especificación JPA 2.0 establece que:

  • La clase de entidad debe tener un constructor sin argumentos. Puede tener otros constructores también. El constructor sin argumentos debe ser público o protegido.
  • La clase de entidad debe ser una clase de nivel superior. Una enumeración o interfaz no debe designarse como una entidad.
  • La clase de entidad no debe ser final. Ningún método o variable de instancia persistente de la clase de entidad puede ser final.
  • Si una instancia de entidad se va a pasar por valor como un objeto separado (por ejemplo, a través de una interfaz remota), la clase de entidad debe implementar la interfaz serializable.
  • Ambas clases abstractas y concretas pueden ser entidades. Las entidades pueden ampliar las clases que no son de entidad, así como las clases de entidad, y las clases que no son de entidad pueden ampliar las clases de entidad.

La especificación no contiene requisitos sobre la implementación de métodos igual y hashCode para entidades, solo hasta donde sé, hasta donde yo sé.

Edwin Dalorzo
fuente
13
Es cierto, igual, hashcode, ... no son un requisito de JPA pero, por supuesto, se recomiendan y se consideran buenas prácticas.
Stijn Geukens
66
@TheStijn Bueno, a menos que planee comparar entidades separadas para la igualdad, esto probablemente no sea necesario. Se garantiza que el administrador de la entidad devolverá la misma instancia de una entidad determinada cada vez que la solicite. Por lo tanto, puede hacerlo bien con las comparaciones de identidad para las entidades administradas, por lo que yo entiendo. ¿Podría por favor explicar un poco más sobre esos escenarios en los que consideraría esto una buena práctica?
Edwin Dalorzo
2
Me esfuerzo por tener siempre una implementación correcta de equals / hashCode. No es necesario para JPA, pero lo considero una buena práctica para cuando se agregan entidades o conjuntos. Podría decidir implementar solo iguales cuando se agregarán entidades a los Conjuntos, pero ¿sabe siempre de antemano?
Stijn Geukens
10
@TheStijn El proveedor de JPA se asegurará de que en un momento dado solo haya una instancia de una entidad determinada en el contexto, por lo tanto, incluso sus conjuntos están seguros sin implementar equals / hascode, siempre que solo use entidades administradas. La implementación de estos métodos para entidades no está exenta de dificultades, por ejemplo, eche un vistazo a este artículo de Hibernate sobre el tema. Mi punto es que si solo trabaja con entidades administradas, es mejor que no las tenga, de lo contrario, proporcionará una implementación muy cuidadosa.
Edwin Dalorzo
2
@TheStijn Este es el buen escenario mixto. Justifica la necesidad de implementar eq / hC como sugirió inicialmente porque una vez que las entidades abandonan la seguridad de la capa de persistencia, ya no puede confiar en las reglas impuestas por el estándar JPA. En nuestro caso, el patrón DTO se hizo cumplir arquitectónicamente desde el principio. Por diseño, nuestra API de persistencia no ofrece una forma pública de interactuar con los objetos comerciales, solo una API para interactuar con nuestra capa de persistencia utilizando DTO.
Edwin Dalorzo
13

Mis 2 centavos adicionales a las respuestas aquí son:

  1. Con referencia al acceso de campo o propiedad (lejos de las consideraciones de rendimiento), a ambos se accede legítimamente mediante getters y setters, por lo tanto, la lógica de mi modelo puede establecerlos / obtenerlos de la misma manera. La diferencia viene a jugar cuando el proveedor de tiempo de ejecución de persistencia (Hibernate, EclipseLink u otro) necesita persistir / establecer algún registro en la Tabla A que tiene una clave foránea que hace referencia a alguna columna en la Tabla B. En caso de un tipo de acceso a la Propiedad, la persistencia El sistema de tiempo de ejecución utiliza mi método de establecimiento codificado para asignar un nuevo valor a la celda de la columna de la Tabla B. En el caso de un tipo de acceso de campo, el sistema de tiempo de ejecución de persistencia establece la celda en la columna Tabla B directamente. Esta diferencia no es importante en el contexto de una relación unidireccional, Sin embargo, es NECESARIO usar mi propio método de establecimiento codificado (Tipo de acceso de propiedad) para una relación bidireccional, siempre que el método de establecimiento esté bien diseñado para tener en cuenta la coherencia. La consistencia es un tema crítico para las relaciones bidireccionales.enlace para un ejemplo simple para un setter bien diseñado.

  2. Con referencia a Equals / hashCode: es imposible utilizar los métodos Equals / hashCode autogenerados de Eclipse para las entidades que participan en una relación bidireccional, de lo contrario tendrán una referencia circular que dará como resultado una excepción de stackoverflow. Una vez que intente una relación bidireccional (digamos OneToOne) y genere automáticamente Equals () o hashCode () o incluso toString (), quedará atrapado en esta excepción de stackoverflow.

Sym-Sym
fuente
9

Interfaz de la entidad

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

Implementación básica para todas las entidades, simplifica las implementaciones de 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);
}
}

Entidad de habitación 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
}

No veo el punto de comparar la igualdad de entidades basadas en campos de negocios en todos los casos de Entidades JPA. Eso podría ser más un caso si estas entidades JPA se consideran Objetos de valor controlados por dominio, en lugar de Entidades controladas por dominio (para lo que son estos ejemplos de código).

ahaaman
fuente
44
Aunque es un buen enfoque utilizar una clase de entidad principal para extraer el código de placa de caldera, no es una buena idea usar el ID definido de DB en su método de igualdad. En su caso, comparar 2 nuevas entidades incluso arrojaría un NPE. Incluso si lo hace nulo seguro, 2 nuevas entidades siempre serían iguales, hasta que persistan. La ecuación / hC debe ser inmutable.
Stijn Geukens
2
Equals () no arrojará NPE ya que se verifica si el ID de DB es nulo o no y en caso de que el ID de DB sea nulo, la igualdad sería falsa.
ahaaman
3
De hecho, no veo cómo me perdí que el código sea nulo-seguro. Pero IMO usando la identificación sigue siendo una mala práctica. Argumentos: onjava.com/pub/a/onjava/2006/09/13/…
Stijn Geukens
En el libro 'Implementando DDD' de Vaughn Vernon, se argumenta que puedes usar id para iguales si usas "generación PK temprana" (Genera un id primero y pásalo al constructor de la entidad en lugar de permitir que la base de datos genere la identificación cuando persiste la entidad.)
Wim Deblauwe
o si no planea en igualdad de condiciones comparar entidades no persistentes? ¿Por qué deberías ...
Enerccio