Buenas prácticas en la construcción de la capa de persistencia

LIBP-0048 (Libro de pautas)

Se deben tener en cuenta las siguientes pautas al construir una capa de persistencia mediante JPA.

A continuación se ofrece un resumen de recomendaciones para mejorar la construcción de una capa de persistencia basada en la especificación JPA

Pautas

TítuloCarácter
Usar el modelo de grado fino y las posibilidades de vinculación en JPAObligatoria
Implementar Serializable en las clases EntityObligatoria
Proteger el constructor por defectoObligatoria
Acceder mediante campos, preferentemente a hacerlo sobre propiedadesObligatoria
Configurar la caché de segundo nivelObligatoria
Configurar el modo de carga de entidadesObligatoria
Buena definición de las clases con claves primariasObligatoria

Usar el modelo de grado fino y las posibilidades de vinculación en JPA

En JPA disponemos de las anotaciones @Embeddable y @Embedded haciendo mas fino el modelado del diseño, lo que lo convierte en un diseño más expresivo. Esta anotación sirve para designar objetos persistentes que no tienen la necesidad de ser una entidad por sí mismas. Esto es porque los objetos Embeddable son identificados por los objetos entidad, estos objetos nunca serán mantenidos o accedidos por sí mismos, sino como parte del objeto entidad al que pertenecen.

Un @Embeddable es un objeto de valor y como tal, deberá ser inmutable. Para ello, sólo pueden existir métodos getters y no setters en su clase. La identidad de un objeto de valor se basa en su estado más que su objeto de identificación. Esto significa que el @Embeddable no tendrá ninguna anotación @Id.

Como ejemplo tenemos:

@Embeddable
public class Address implements Serializable {
   ...
   private String houseNumber;
   private String street;

   @Transient
   public String getHouseNumber() { return houseNumber; }

   @Transient
   public String getStreet() { return street; }

   @Basic
   public String getAddress() { return street + " " + houseNumber; }

   // setter needed by JPA, but protected as value object is immutable to domain
   protected void setAddress(String address) {
       // do all the parsing and rule enforcement here
   }
}

@Entity
public class Person implements Serializable {
   ...
   private Address address;

   @Embedded
   public Address getAddress() { return address; }
   public void setAddress(Address address) { this.address = address; }
}

Se utiliza esta anotación @Embedded, para indicar que se está usando un objeto integrado. Las propiedades de este objeto se vincularán con la tabla de la entidad donde esta siendo utilizado.

Implementar Serializable en las clases Entity

La especificación JPA dice que debe de hacerse pero algunos proveedores de JPA no lo hacen. Hibernate como proveedor de JPA no lo hace, lo que puede provocar que aparezcan errores dentro del código que lancen la excepción ClassCastException si Serializable no ha sido implementado

Proteger el constructor por defecto

Los especificación JPA determina un constructor por defecto de las clases vinculadas, pero un constructor predeterminado rara vez tiene sentido en términos de modelo. Con él, se podría construir una instancia de entidad sin estado. Un constructor siempre debe salir de la instancia creada en su estado normal. El requisito del constructor por defecto sólo para la creación dinámica de instancias de la clase es posible por el proveedor de la JPA.

Es recomendable definir el constructor por defecto como protegido. Hibernate, incluso se lo acepta como privado.

Acceder mediante campos, preferentemente a hacerlo sobre propiedades

Es preferible especificar vinculación objeto-relacional anotando campos entidad directamente, en lugar de anotar métodos get / set (propiedades), por varias razones. Los beneficios combinados de la persistencia especificada por campos provocan que sea el enfoque más atractivo, sin tener una única razón determinante.

Debido a que la persistencia tiene que ver con el almacenamiento, actualización y recuperación de los datos en sí, parece más limpia denotar la persistencia directamente en los datos más que indirectamente a través de los métodos get y set. También es más claro marcar un campo como transitorio (transient) para indicar que no hay que mantenerlo al marcar un método get () como transitorio.

El orden de la lógica de negocio y persistencia en los métodos get / set no está garantizada. Los desarrolladores pueden dejar la lógica de negocio fuera de estos métodos, pero si los campos están anotados en su lugar, no importa si la lógica de negocios se agrega al obtener o establecer los métodos más tarde.

Un desarrollador puede querer métodos que manipulan o recuperar más de un atributo a la vez o que no tienen "get" o "set" en sus nombres. Con anotaciones de campo, el desarrollador tiene la libertad de escribir el nombre de estos métodos como se desee sin la necesidad de hacer la anotación @transient

Configurar la caché de segundo nivel

JPA dispone de dos tipos de memoria caché llamadas caché de nivel 1 y caché de nivel 2. Es necesario definir de forma correcta los parámetros de configuración de la caché:

  • La caché de nivel 1 está siempre disponible y es la zona de memoria que utiliza JPA para guardar los datos que vamos recuperando de base de datos y vamos modificando. Cuando queremos persistir en base de datos (haciendo un commit()), JPA realizará las operaciones oportunas en base de datos para que los datos de este nivel de caché acaben en base de datos. La única forma de vaciar esta caché es con un clear() o si se consume el ciclo de vida del contexto de JPA.
  • La caché de nivel 2 hay que habilitarla en la configuración, y es la memoria que JPA pone a disposición de la aplicación para guardar objetos de uso frecuente en dicha aplicación y conseguir una mejora en el rendimiento. Estos objetos que se pueden almacenar en caché son Entidades (Datos) y Querys (Consultas). Cada implementación de JPA tiene una forma de configurar este tipo de caché, pero lo que tienen más o menos en común todas las implementaciones es indicar el tamaño de la caché, el tiempo de vida en caché de las entidades (de los datos almacenados) y si la caché esta en un entorno distribuido o en un sola máquina (que siempre va a ser una sola máquina).

Un fallo muy común es pensar que al activar esta caché es JPA la que memoriza las Entidades y las Querys más usadas pero NO ES ASÍ. Cada implementación de JPA tiene sus propios objetos y mecanismos para que con el código de la aplicación que se ha programado se guarde en esta caché lo que se quiera.

Configurar el modo de carga de entidades

El modo de carga de entidades puede ser “LAZILY” o “EAGERLY”. Para explicar la diferencia supongamos que tenemos la siguiente entidad A:

@Entity
public class A{
  @Id
  private String id;
  @Column(name="B)
  private B b; // B es una entidad
  @Column(name="C)
  private C c; // C es una entidad
}

La diferencia entre los dos modos de carga consiste en que al traer una entidad “A” al contexto JPA (al hacer una consulta) el modo “LAZILY” solo carga los campos de esa entidad “A” y no los campos de las entidades “B” y “C” a los que se hace referencia desde “A”. Mientras que el modo “EAGERLY” trae al contexto de JPA todo. Por lo general, parece mejor el modo “LAZILY” pero el modo “EAGERLY” es ideal cuando la aplicación que utiliza JPA es una aplicación web (siempre y cuando el tamaño de las tablas no sea muy grande con respecto a la memoria y velocidad de acceso del servidor donde este desplegada la aplicación web).

Buena definición de las clases con claves primarias

Una clase con clave primaria debe cumplir los siguientes requerimientos:

  • El modificador de control de acceso de la clase debe ser público.
  • Las propiedades de la clave primaria deben ser públicas o protegidas si se utiliza el acceso a la base de la propiedad.
  • La clase debe tener un constructor público por defecto.
  • La clase debe implementar los métodos hashCode() y equals(Object other).
  • La clase debe ser serializable.
  • Una clave primaria debe representarse y vincularse por campos múltiples o propiedades de la clase de la entidad, o debe representarse y vincularse como una clase embebida.
  • Si la clave primaria está compuesta por varios campos o propiedades, los nombres y tipos de campos de la clave primaria o propiedades en la clave primaria debe coincidir con las de la entidad.
public final class LineItemKey implements Serializable {
    public Integer orderId;
    public int itemId;

    public LineItemKey() {}

    public LineItemKey(Integer orderId, int itemId) {
        this.orderId = orderId;
        this.itemId = itemId;
    }

    public boolean equals(Object otherOb) {
        if (this == otherOb) {
            return true;
        }
        if (!(otherOb instanceof LineItemKey)) {
            return false;
        }
        LineItemKey other = (LineItemKey) otherOb;
        return (
                    (orderId==null?other.orderId==null:orderId.equals
                    (other.orderId)
                    )
                    &&
                    (itemId == other.itemId)
                );
    }

    public int hashCode() {
        return (
                    (orderId==null?0:orderId.hashCode())
                    ^
                    ((int) itemId)
                );
    }

    public String toString() {
        return "" + orderId + "-" + itemId;
    }
}