5.7. Otros aspectos#
5.7.1. Patrón DAO#
El patrón visto en la unidad sobre conectores para abstraer de la persistencia a la lógica de negocio, sigue siendo perfectamente válido. De este modo, podríamos tomar la interfaz Crud definida entonces e implementar clases DAO a partir de ella. Esas implementaciones en vez de basarse en JDBC se basarían en JPA.
5.7.2. Excepciones#
JPA define su propio conjunto de excepciones derivadas todas de PersistenceException, que a su vez deriva de RuntimeException. Hay que tener en cuenta dos particularidades:
Que todas deriven en última instancia de RuntimeException, supone que las excepciones de este ORM, a diferencia de las de JBDC, no son comprobadas; y, por tanto, no es necesario capturarlas o declararlas a través de
throws
.Cuando se genera una excepción, la sesión suele quedar en estado inconsistente, por lo que conviene cerrarla y comenzar una nueva.
Excepciones comunes
Una lista no exhaustiva de excepciones es esta:
- NoResultException, NonUniqueResultException
Se producen cuando se espera un único resultado (véase .getSingleResult()) y no se obtiene eso.
- OptimisticLockException
Esta relacionada con problemas de concurrencia: se lanza cuando se detecta que una entidad se modificó por otra transacción después de haber sido recuperada por la actual.
- RollbackException
Se produce cuando una transacción no puede confirmarse, por lo general por violarse restricciones en los valores de los campos.
- TransactionRequiredException
Se lanza cuando se intenta hacer una operación de escritura sin haber iniciado una transacción.
Dependiendo de qué proveedor se use, además de las genéricas, pueden lanzarse otras, como estas específicas de Hibernate:
- JDBCConnectionException
Producida por problemas en la conexión a la base de datos como que ésta no exista o no haya conectividad o las credenciales sean inválidas.
- LazyInitializationException
Se produce cuando se intenta resolver una relación perezosa sin que el objeto esté asociado a la sesión activa.
5.7.3. Mapeo avanzado#
Incluimos bajo este epígrafe otros aspectos más específicos que se reflejan al anotar las clases.
5.7.3.1. Cascada de operaciones#
Cuando dos entidades se relaciones entre sí, los cambios en uno de los extremos pueden afectar al otro.
SQL
En SQL, al definir la relación con una clave foránea puede especificarse qué
ocurre con la entidad hijo[1] cuando la entidad padre se elimina (ON
DELETE
) o cuando el identificador de la entidad padre se actualiza (ON
UPDATE
)[2]. Esto efectos son:
CASCADE
aplicar también la operación a la entidad hijo, lo que se traduce en borrar la entidad hijo si la entidad padre se borró; o actualizar el valor de la clave foránea, si se actualizó el identificador del padre.
SET NULL
poner a nulo la clave foránea.
SET DEFAULT
poner un valor predeterminado (el que se especifique a continuación) como valor de la clave foránea.
RESTRICT
impedir la operación.
JPA, tanto si la relación es unidireccional como bidireccional, permite
añadir la anotación @OnDelete
en el lado propietario (el que tiene la clave
foránea) para notar el comportamiento ON DELETE
, aunque sólo se soporta el
efecto CASCADE
:
@ManyToOne
@JoinColumn(name = "id_centro")
@OnDelete(action = OnDeleteAction.CASCADE)
private Centro centro;
De este modo, se añadirá en el esquema ON DELETE CASCADE
y la base de datos
se encargará de propagar la operación en cascada.
Prudencia
En SQLite la integridad referencial está deshabilitada por defecto.
Hibernate
Prudencia
Por lo general, las operaciones en cascada se definen de padre a hijo cuando las relaciones son bidireccionales. Cuando las relaciones son unidireccionales, las operaciones en cascada pueden definirse también y se aplican de hijo a padre, pero no suelen ser buena idea por provocar inconsistencias y, por lo general, es mejor evitarlas.
JPA permite desencadenar efectos adicionales en el nivel de aplicación
sobre las entidades hijo cuando se realiza una operación en la entidad
padre. Para aclararnos recordemos que para la relación bidireccional entre
Centro
(entidad padre) y Estudiante
(entidad hijo) hemos definido lo
siguiente:
// Estudiante
@ManyToOne
@JoinColumn(name = "id_centro")
private Centro centro
// Centro
@OneToMany(mappedBy = "centro")
private List<Estudiante> estudiantes;
Pueden definirse los siguientes tipos de cascada:
CascadeType.PERSIST
Provoca que guardar la entidad padre provoque que se guarden también las entidades hijo. En nuestro ejemplo, guardar un objeto
Centro
implicará también que se guarden los nuevos objetosEstudiante
que se hayan añadido a su lista de matriculados.CascadeType.MERGE
Provoca que la actualización de la entidad padre provoca una actualización de todas las entidades hijo con la que está relacionada. Entiéndase como si internamente se recorriera la lista de entidades hijo y se le aplicara a todas ellas el método
.merge
.CascadeType.REMOVE
Provoca que la eliminación de la entidad padre, provoque la eliminación de todas las entidades hijo. Es, por tanto, el equivalente a
ON DELETE CASCADE
, pero hecho en el nivel de aplicación y no desencadenado por la propia base de datos.CascadeType.REFRESH
Provoca que refrescar la entidad padre, refresque también todas las entidades hijo.
CascadeType.DETACH
Propaga el desvinculamiento de la entidad padre a todas las entidades hijo.
CascadeType.ALL
Equivale a haber configurado todas las anteriores.
A todas estos desencadenamientos debe añadirse la opción:
orphanRemoval = true
Provoca la eliminación de la entidad hijo en la base de datos al eliminarla de la lista en la entidad padre, es decir, desvincularla de ésta.
Estas anotaciones deben incluirse en el extremo de la entidad padre de las relaciones bidireccionales:
@OneToMany(mappedBy = "centro", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
private List<Estudiante> estudiantes;
5.7.3.2. Mapeo de colecciones#
La anotación @ElementCollection
permite mapear colecciones de tipos básicos
(como String
, Integer
o LocalDate
) sin que haya necesidad de crear
una entidad aparte. Supongamos que quisiéramos añadir un listado de números de
teléfono a cada centro:
@ElementCollection
@CollectionTable(name = "Telefono", joinColumns = @JoinColum(name = "id_estudiante"))
@Column(name = "numero")
private List<Integer> telefonos = new ArrayList<>();
Si no se permiten elementos repetidos, puedo optarse por un java.util.HashSet en vez de un java.util.ArrayList.
Notas al pie
5.7.4. Concurrencia#
JPA permite también controlar el acceso concurrente a la base de datos. Nótese que, cuando hay dos o más accesos a la base de datos, uno de ellos puede obtener datos y el otro, alterarlos después de aquel acceso. La consecuencia esa que los datos obtenidos en esa primera sesión no serán exactamente iguales a los que hay en la base de datos y eso puede dar lugar a situaciones de inconsistencia. Para paliar estos problemas, hay dos estrategias de bloqueo.
5.7.4.1. Bloqueo optimista#
Este bloqueo se basa en el uso de un atributo anotado con @Version
que
signifique la versión del objeto, de modo que cada vez que se cambian sus
valores, el ORM se encarga de aumentar su versión. No tenemos que
preocuparnos por dotarlo de valor, por lo que podemos establecer su getter
como protected
. Por ejemplo, si quisiéramos que Centro
tuviera un
atributo de este tipo podríamos añadir a su definición lo
siguiente:
@Version
private int version;
public int getVersion() {
return version;
}
private void setVersion(int version) {
this.version = version;
}
Ante un atributo de este tipo, cada vez que hagamos una operación de actualización o borrado, el ORM comprobará que el valor almacenado en la base de datos para este campo coincide con el del objeto que pretendemos borrar o actualizar, y, en caso contrario, lanza una excepción OptimisticLockException.
Esta solución es ideal para situaciones en que las colisiones por concurrencia no son frecuentes.
5.7.4.2. Bloqueo pesimista#
Este bloqueo es más adecuado cuando las colisiones son probables. Hay tres tipos de bloqueo:
PESSIMISTIC_READ
,que bloquea sólo las lecturas.
PESSIMISTIC_WRITE
que bloquea tanto escrituras como lecturas.
PESSIMISTIC_FORCE_INCREMENT
,que equivale al anterior, pero, además, aumenta la versión en caso de que la entidad sea versionada.
Para practicarlo sólo hay que obtener el objeto, indicando que queremos bloquearlo:
try(EntityManager em = emf.createEntityManager()) {
EntityTransaction tx = em.getTransaction();
try {
tx.begin();
Centro castillo = sesion.find(Centro.class, 11004866L, LockModeType.PESSIMISTIC_WRITE);
// Acabamos de bloquear ese registro, así
// que ninguna otra sesión concurrente podrá
// leer o escribir este registro durante la transacción.
// ...
tx.commit();
}
catch(Exception e) {
if(tx != null && tx.isActive()) tx.rollback();
e.printStackTrace();
}
// Registro desbloqueado.
}
El bloqueo sobre el registro dura hasta que se cierra la transacción en la que se llevó a cabo.
Los bloqueos se pueden definir también sobre consultas y, en ese caso, se aplicarán a todos los registros devueltos por la consulta:
// También podríamos haber usado Criteria API
TypedQuery<Estudiante> query = session.createQuery("FROM Estudiante", Estudiante.class);
query.setLockMode(LockModeType.PESSIMISTIC_WRITE);
List<Estudiante> estudiantes = query.setResultList(); // Todos están bloquedos
Nota
En caso de obtener columnas sueltas, no registros enteros, el bloqueo podría establecerse sólo por columnas si el SGBD lo soporta.
Prudencia
En SQLite los bloques se hacen para toda la base de datos, no se pueden bloquer registros individuales.
5.7.5. Optimización#
Así tratar JPQL y Criteria API indicamos que fetch permitía forzar una carga inmediata de las entidades relacionadas. Ahora bien, ¿cuál es el comportamiento de JPA ante las relaciones?
Si al anotar una entidad no se especifica nada, las relaciones @ManyToOne
y
@OneToOne
cargan inmediatamente la entidad relacionada, mientras que las
relaciones @OneToMany
y @ManyToMany
lo hacen de forma perezosa:
@ManyToOne
JoinColum(name = "id_centro", nullable = "false")
private Centro centro; // Carga inmediata (eager).
@ManyToMany(mappedBy = "estudiantes")
private List<Curso> cursos; // Carga perezosa (lazy).
Si quiere modificarse este comportamiento predeterminado, pueden añadirse
FetchType.EAGER
o FetchType.LAZY
, según convenga:
@ManyToOne(fetch = FetchType.LAZY)
JoinColum(name = "id_centro", nullable = "false")
private Centro centro; // Carga perezosa.
@ManyToMany(mappedBy = "estudiantes", fetch = FetchType.EAGER)
private List<Curso> cursos; // Carga inmediata.
Tanto en JPQL como en Criteria API, los JOIN a secas respetan este comportamiento derivado de las anotaciones, mientras que los FETCH JOIN fuerzan siempre la carga inmediata.