Dicho esto, mi recomendación es usar JPA por defecto en cualquier proyecto que trabaje con bases de datos y plantearse usar SQL nativo o funciones almacenadas exclusivamente en los casos estrictamente necesarios en los que el rendimiento, la mantenibilidad, legibilidad o simpleza así lo aconsejen.
En mi experiencia usando JPA 1.0 (Toplink Essentials) y JPA 2.0 (EclipseLink) en diversos proyectos he recopilado los siguientes
Aunque la especificación JPA enfatiza el uso de anotaciones, puedes usar el fichero de mapeo JPA orm.xml para almacenar los metadatos. Aunque el uso de anotaciones o descriptores XML es una cuestión de gustos, considero especialmente útil usar descriptores XML cuando nos apoyamos en wizards y herramientas de generación de código (como
), ya que éstas suelen sobreescribir nuestros ficheros de entidad perdiendo así todas nuestras NamedQuerys. Almacenar las NamedQuery en el fichero orm.xml nos permitirá no perderlas cuando Dali o cualquier otro generador de código sobreescriba nuestras entidades.
La caché de entidades de JPA es manejada por el framework internamente. Es posible que un entity de tu aplicación, que recuperaste vía em.find() o JPQL, ya no esté en dicha caché porque haya sido reciclado (aunque en tu código apenas "hayan pasado" un par de llamadas de métodos o un par de líneas de código ;-). En estos casos es cuando te encuentras con el primer error, por ejemplo, si llamas a EntityManager.remove() de un objeto "detached".
Una solución que se te puede ocurrir es usar EntityManager.refresh() para "refrescar" el objeto y sincronizarlo con la base de datos, es decir: para hacerlo "manejado" nuevamente... Pero te puedes encontrar con otro error:
refresh() cannot be called on a detached entity
Para asegurarte que tienes un objeto "manejado" y, en cualquier caso, para estar seguro que puedes modificar un objeto sincronizado con la base de datos, una la combinación de find() y refresh() siguiente:
Usuario e = em.find(Usuario.class, id);
try {
em.refresh(e);
} catch( EntityNotFoundException ex ) {
e = null;
}
3.- Uso de callbacks. El caso especial de la anotación @PreUpdate
Las anotaciones @Pre* y @Post* para
callbacks methods tienen una utilidad ciertamente limitada. De hecho, no hay más que leer la sección 3.5 de la especificación JPA:
“In general, the lifecycle method of a portable application should not invoke EntityManager or Query operations, access other entity instances, or modify relationships within the same persistence context. A lifecycle callback method may modify the non-relationship state of the entity on which it is invoked.”
Existen razones técnicas de peso para lo anterior (por ejemplo, evitar bucles infinitos que pueden dar al traste con una aplicación), pero desde el punto de vista del desarrollo está muy lejos de constituir un equivalente a los disparadores en la base de datos, ya que sólo podemos trabajar con la propia entidad, e incluso, con restricciones, como ahora veremos.
La anotación @PrePersist nos permite establecer un estado en una entidad antes de que ésta sea persistida. ¿Funciona igual @PreUpdate? Vamos a verlo.
Imaginemos que tenemos una entidad con un campo campoDato que, cuando se modifica, debe tener su fecha correspondiente de modificación en su campo aparejado fechaCampoDato. Para olvidarnos de éste último, podríamos tener la tentación de hacer algo como esto:
@PrePersist
@PreUpdate
public void preUpdate() {
if ( campoDato != null) {
// Si tiene campoDato, hay que garantizar su fecha también
if ( fechaCampoDato == null) {
fechaCampoDato = new Date();
}
}
}
Sin embargo,
no funciona (al menos en EclipseLink) ¿Por qué? Porque para la verificación de modificación ("
dirty check"), los campos modificados son detectados
antes de llamar al método @PreUpdate y, por tanto, los cambios realizados en el método @PreUpdate no son detectados. Nuevamente, esto está realizado así por razones de rendimiento debido a que las verificaciones de modificación son muy costosas.
En definitiva, los métodos
callback no son la panacea en lo referente a la gestión del ciclo de vida de nuestras entidades.
4.- ¿JPQL demasiado lenta?
Cuando tenemos un modelo complejo con muchas relaciones, algunas ejecuciones de JPQL en el que muchas entidades se vean involucradas (muchos JOIN) pueden resultar inaceptablemente lentas. Especialmente cuando nos percatamos que la consulta nativa SQL equivalente apenas lleva unos pocos milisegundos. En estos casos, optamos por realizar una consulta nativa SQL (
createNativeQuery(java.lang.String sqlString,java.lang.Class resultClass) ) obteniendo también un rendimiento lamentable, cuando la misma consulta SQL se ejecuta centenas de veces más rápido en nuestro cliente SQL. ¿Qué está pasando? ¿Cómo solucionarlo?
En estos casos el problema no es la consulta: es el propio JPA, o más concretamente, la resolución de entidades de JPA. Cuando se usan NamedQuery o entity-mapped-nativequery (es decir, nativas usando mapping "automático" a la entidad de retorno), JPA realiza la consulta y mapeo de las entidades devueltas, resolviendo en éstas las relaciones involucradas en la consulta. Es decir, que si tengo una entidad con varias relaciones, resuelve las listas de estas relaciones, generando una cantidad de consultas enorme y consumiendo, por tanto, muchísimo tiempo.
La solución para estos casos es simple: realizar una consulta nativa que devuelva los identificadores de las entidades en lugar de las entidades, para luego resolver las entidades manualmente. Por ejemplo:
public List<User> getUserByProfile(Integer profileId) {
List<Vector> rows = null;
List<User> users = new ArrayList<User>();
String query;
try {
query = "select distinct t1.id " +
" from t1,t2,t3,t4,t5" +
" where t1.f1_id = t2.id" +
" and t2.f2_id = t3.id" +
" and t4.f4_id = t5.id" +
" and t5.id = " +
" and t2.end_date is null" +
" and exists (select * from a " +
" where date is null and h = t5.id)" +
" and t5.id = "+ profileId;
Query q = em.createNativeQuery(query);
log.debug("query {}", query);
rows = q.getResultList();
for ( Vector row : rows ) {
users.add(em.find(User.class, (User)row.get(0)) );
}
return users;
} catch (Exception e) {
log.error("Excepcion ", e);
}
}
Podrás comprobar que la solución anterior, aún siendo poco "
bonita" y más tediosa, es centenares de veces más rápida que dejar a JPA que resuelva las entidades. La razón es muy sencilla: con esta forma, no se resuelven innecesariamente las relaciones de las tablas en la consulta.
4.- @PrivateOwned o tratamiento de huérfanos
Es frecuente confundir el atributo cascade y pensar que éste se encarga de todo. Pero no es así. Si tenemos una relación @OneToMany(cascade=ALL) y borramos el padre, sus hijos también se eliminarán. Pero, ¿y si queremos borrar alguna entidad hija? Si tenemos un objeto A que tiene una lista de objetos B hijos y borramos el segundo, de la lista (usando remove()), una llamada a merge() no eliminará el objeto dereferenciado. ¿Por qué? Porque el objeto sigue siendo una entidad que no ha sido explícitamente eliminada. Hemos borrado una referencia a él, pero no la única y, en todo caso, el objeto sigue manteniendo la referencia a su padre.
Muchas veces nos encontramos la definición del atributo @PrivateOwned así: "Use @PrivateOwned to specify that a relationship is privately owned"... lo cual obviamente, no aclara mucho. Lo voy a explicar aquí un poco más claro: @PrivateOwned implica que los hijos pertenecientes a la lista no deben existir sin el padre, con lo que, al quedar dereferenciados, deberán ser eliminados. Podrás usar este atributo para buena parte de las relaciones @OneToMany cuyos hijos tengan una dependencia funcional absoluta del padre y en las que normalmente trabajarás con el objeto padre como una unidad única y no con los hijos de forma independiente. Es decir, en aquellas donde un objeto hijo "huérfano" no tiene sentido.
@OneToMany(mappedBy = "padre", cascade={CascadeType.ALL})
@PrivateOwned
private List<Hijo> hijos;
Con JPA 1.0, los objetos hijos "huérfanos" deben eliminarse explícitamente con em.remove() ya que este atributo sólo está disponible para JPA 2.0.
5.- Logging
Durante el ciclo de desarrollo, es una buena idea activar el logging de las sentencias SQL que nuestro proveedor de JPA realiza en tiempo de ejecución, para tener visibilidad de qué está ocurriendo en cada momento. En EclipseLink, se configura a través de una propiedad en el fichero persistence.xml.
<properties>
<property name="eclipselink.logging.level" value="FINE"/>
</properties>
6.- JPA Caching
La caché de nivel 2,
L2 o s
hared cache, es una caché más allá del
EntityManager: es una caché global para toda la unidad de persistencia (
PersistenceUnit). En general, la caché es un elemento importante y complejo cuya correcta configuración puede tener un significativo impacto en nuestra aplicación. Explicar estos detalles excede el alcance de este artículo y hay mucha información ya publicada al respecto. No obstante, expondré aquí mi experiencia con TopLink (JPA 1.0) y EclipseLink (JPA 2.0)
JPA 1.0
Mi experiencia con Toplink Essentials es que es conveniente desactivar la shared cache, salvo que la base de datos no tenga modificaciones externas simultáneamente a nuestra aplicación (otras aplicaciones, etc), lo cual no suele ocurrir (en mi caso, nunca). Así que, como norma general, la opción habitual es configurarlo en el persistence.xml:
<properties>
<property name="toplink.cache.shared.default" value="false"/>
</properties>
JPA 2.0
EclipseLink ofrece mayores niveles de configuración y ajuste que permiten una configuración más fina y granulada, aunque aún me quedan por probar el comportamiento en producción de muchas opciones. De momento, una buena opción es desactivar la shared cache para todas las entidades y permitir sobreescribir esta configuración para entidades concretas vía orm.xml o anotación @Cacheable en función de nuestra aplicación.
<properties>
<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
</properties>
Referencias y más información: