Nuestro siguiente tema a tratar con la arquitectura de Hibernate es el tratamiento de las excepciones.Antes de ver este tema es recomendable la lectura de Tratamiento de Excepciones.
Al realizar una operación con Hibernate se pueden lanzar cualquiera de las siguiente 4 excepciones:
Hemos indicado únicamente estas 4 excepciones ya que vamos a tratar cada una de ellas de forma distinta , no siendo necesario tratar ninguna otra ya que siempre caerán dentro de alguna de las cuatro.
Veamos cómo vamos a tratarlas. El siguiente código dentro del catch
deberá ponerse siempre que usemos Hibernate:
try { session.beginTransaction(); session.saveOrUpdate(entity); session.getTransaction().commit(); } catch (javax.validation.ConstraintViolationException cve) { try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); } throw new BussinessException(cve); } catch (org.hibernate.exception.ConstraintViolationException cve) { try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); } throw new BussinessException(cve); } catch (BussinessException ex) { try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); } throw ex; } catch (RuntimeException ex) { try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); } throw ex; } catch (Exception ex) { try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); } throw new RuntimeException(ex); }
Lo que debemos hacer siempre en las 4 excepciones es ver si hay una transacción activa y en ese caso hacer un rollback
. Si al hacer un rollback
se produce un error , simplemente generaremos un mensaje en el log.
try { if (session.getTransaction().isActive()) { session.getTransaction().rollback(); } } catch (Exception exc) { LOGGER.log(Level.WARNING,"Falló al hacer un rollback", exc); }
La propiedad estática LOGGER
la definiremos como una propiedad a nivel de clase usando Java Logging APIs de la siguiente forma:
private final static Logger LOGGER = Logger.getLogger(ProfesorController.class .getName());
Veamos ahora el tratamiento individualizado para cada una de ellas:
Si la excepción es de tipo RuntimeException
lo único que hacemos es volver a relanzar la misma excepción mediante:
throw ex;
Si la excepción es de tipo Exception
lo único que hacemos es relanzar la excepción envuelta en una RuntimeException
:
throw new RuntimeException(ex);
De esa forma evitamos la obligación de tratar las Checked Exceptions de Java cuando no sabemos que hacer con ella.
Esta excepción la vamos a reconvertir en una BussinessException
. En el siguiente apartado se explica en qué consiste BussinessException
.
throw new BussinessException(cve);
Esta excepción la vamos a reconvertir en una BussinessException
. En el siguiente apartado se explica en qué consiste BussinessException
.
throw new BussinessException(cve);
Si se produce ésta excepción simplemente la volveremos a relanzar sin modificar nada ya que queremos que se vea tal y como es.
throw ex;
catch
en un trozo de código de Hibernate, puede que el compilador dé un error ya que alguna de las excepciones no se lanza nunca. En ese caso simplemente eliminaremos el catch
que está dando el error.
En el apartado anterior hemos visto cómo tratamos las siguientes excepciones transformándolas en BussinessException
:
El significado de la nueva excepción es avisarnos de que los datos de las entidades contienen algún tipo de error y que el usuario debe modificarlo. Por ello la excepción hereda de java.lang.Exception ya que el error sí que es recuperable y el código debería estar preparado para avisar de ello al usuario y que éste corrija los datos.
Además de éso, el usar BussinessException
tiene las siguientes ventajas:
Desde otras clases de la aplicación , ahora al llamar a los métodos que tratan con Hibernate sólo deberemos preocuparnos de tratar la nueva excepción BussinessException
. Con ello simplificamos el resto del código de la aplicación. De lo contrario, habríamos tenido que arrastrar siempre el tratar con las 2 excepciones javax.validation.ConstraintViolationException y org.hibernate.exception.ConstraintViolationException.
BussinessException
sin necesidad de cambiar nada del manejo del excepciones del resto de la aplicación. Es decir que es mejor independizar de excepciones específicas del Hibernate.
El uso de javax.validation.ConstraintViolationException puede ser un poco raro de usar debido a las propiedades de la clase javax.validation.ConstraintViolation.
¿Qué hace el método getPropertyPath()? ¿Y getLeafBean()? Etc. Si la mayoría de estos métodos no los vamos a usar , mejor será crear una clase que contenga sólo la información importante ya que ayudará a los programadores a usarla mas rápidamente y sin errores.
Por ello BussinessException
contendrá una lista de objetos de BussinessMessage
. Esta estructura es similar a la que encontramos entre javax.validation.ConstraintViolationException y javax.validation.ConstraintViolation
En el siguiente diagrama UML podemos ver las similitudes y diferencias:
Como podemos ver , aunque hayamos perdido cierta funcionalidad que debe encontrase en javax.validation.ConstraintViolation, si ésta no pensábamos usarla, es mejor simplificar el código.
Al mostrar los mensajes de error que se incluyen en javax.validation.ConstraintViolation hay dos problemas principales:
Un ejemplo de mensaje de error es el siguiente:
ape1 - el tamaño tiene que estar entre 3 y 50
¿Qué es “ape1”? ¿No debería indicarse “1º Apellido”?. Y luego el artículo “el” del mensaje debería debería empezar en mayúsculas.
Así que el mensaje correcto debería ser:
1º Apellido - El tamaño tiene que estar entre 3 y 50
Ya hemos visto las ventajas y el diseño de BussinessException
, veamos ahora el código que los implementa.
public class BussinessException extends Exception { private Set<BussinessMessage> bussinessMessages = new TreeSet<>(); public BussinessException(List<BussinessMessage> bussinessMessages) { this.bussinessMessages.addAll(bussinessMessages); } public BussinessException(BussinessMessage bussinessMessage) { this.bussinessMessages.add(bussinessMessage); } public BussinessException(Exception ex) { bussinessMessages.add(new BussinessMessage(null, ex.toString())); } public BussinessException(javax.validation.ConstraintViolationException cve) { for (ConstraintViolation constraintViolation : cve.getConstraintViolations()) { String fieldName; String message; fieldName = getCaptions(constraintViolation.getRootBeanClass(), constraintViolation.getPropertyPath()); message = constraintViolation.getMessage(); bussinessMessages.add(new BussinessMessage(fieldName, message)); } } public BussinessException(org.hibernate.exception.ConstraintViolationException cve) { bussinessMessages.add(new BussinessMessage(null, cve.getLocalizedMessage())); } public Set<BussinessMessage> getBussinessMessages() { return bussinessMessages; } }
Podemos ver que el código es bastante sencillo. Consta principalmente de varios constructores y el método getBussinessMessages()
.
BussinessMessage
. Se usa un TreeSet para que los mensajes salgan ordenados por orden alfabético.BussinessMessage
.Ësto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción.BussinessMessage
.Esto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción.BussinessMessage
por cada uno de los javax.validation.ConstraintViolation.BussinessMessage
en función del mensaje de la excepción.BussinessMessage
.
El código de BussinessMessage
es el siguiente:
public class BussinessMessage implements Comparable<BussinessMessage> { private final String fieldName; private final String message; public BussinessMessage(String fieldName, String message) { if (message==null) { throw new IllegalArgumentException("message no puede ser null"); } if ((fieldName!=null) && (fieldName.trim().equals(""))) { this.fieldName =null; } else { this.fieldName = StringUtils.capitalize(fieldName); } this.message = StringUtils.capitalize(message); } @Override public String toString() { if (fieldName!=null) { return "'"+fieldName+ "'-"+message; } else { return message; } } /** * @return the fieldName */ public String getFieldName() { return fieldName; } /** * @return the message */ public String getMessage() { return message; } @Override public int compareTo(BussinessMessage o) { if ((getFieldName()==null) && (o.getFieldName()==null)) { return getMessage().compareTo(o.getMessage()); } else if ((getFieldName()==null) && (o.getFieldName()!=null)) { return 1; } else if ((getFieldName()!=null) && (o.getFieldName()==null)) { return -1; } else if ((getFieldName()!=null) && (o.getFieldName()!=null)) { if (getFieldName().equals(o.getFieldName())) { return getMessage().compareTo(o.getMessage()); } else { return getFieldName().compareTo(o.getFieldName()); } } else { throw new RuntimeException("Error de lógica"); } } }
La clase BussinessMessage
es también sencilla pero tiene la particularidad de implementar el interfaz java.lang.Comparable.
BussinessMessage
de forma que primero van los mensajes que incluyen el nombre del campo y luego los que no.
La última parte que nos queda por tratar es el tema del nombre de las columnas. En el texto que muestra BussinessMessage
se sigue viendo como nombre de una columna el nombre de la propiedad Java. Ya hemos explicado que esto debería ser solucionado.
Una sencilla solución es añadir una nueva anotación a cada propiedad que incluya un nombre amigable de dicha propiedad. La nueva anotación se llamará @Caption
. Su código Java es el siguiente:
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Caption { String value(); }
Ahora nuestras clase de dominio pueden quedar de la siguiente forma:
public class Profesor implements Serializable { private int id; @NotBlank @Caption("Nombre") private String nombre; @NotBlank @Caption("1º Apellido") private String ape1; @Caption("2º Apellido") private String ape2; public Profesor(){ } }
Vemos en las líneas 5, 9 y 12 como se ha añadido la nueva anotación @Caption
para incluir un nombre de la columna válido para el usuario.
Ahora debemos crear el código Java que en función de una clase de dominio y el nombre de la columna nos retorna el caption. El código Java es el siguiente:
private String getCaptions(Class clazz, Path path) { StringBuilder sb = new StringBuilder(); if (path != null) { Class currentClazz = clazz; for (Path.Node node : path) { ClassAndCaption clazzAndCaption = getSingleCaption(currentClazz, node.getName()); if (clazzAndCaption.caption != null) { if (sb.length() != 0) { sb.append("."); } if (node.isInIterable()) { if (node.getIndex() != null) { sb.append(node.getIndex()); sb.append("º "); sb.append(clazzAndCaption.caption); } else if (node.getKey() != null) { sb.append(clazzAndCaption.caption); sb.append(" de "); sb.append(node.getKey()); } else { sb.append(clazzAndCaption.caption); } } else { sb.append(clazzAndCaption.caption); } } else { sb.append(""); } currentClazz = clazzAndCaption.clazz; } return sb.toString(); } else { return null; } } private ClassAndCaption getSingleCaption(Class clazz, String fieldName) { ClassAndCaption clazzAndCaptionField; ClassAndCaption clazzAndCaptionMethod; if ((fieldName == null) || (fieldName.trim().equals(""))) { return new ClassAndCaption(clazz, null); } clazzAndCaptionField = getFieldCaption(clazz, fieldName); if ((clazzAndCaptionField != null) && (clazzAndCaptionField.caption != null)) { return clazzAndCaptionField; } clazzAndCaptionMethod = getMethodCaption(clazz, fieldName); if ((clazzAndCaptionMethod != null) && (clazzAndCaptionMethod.caption != null)) { return clazzAndCaptionMethod; } if (clazzAndCaptionField != null) { return new ClassAndCaption(clazzAndCaptionField.clazz,fieldName); } else if (clazzAndCaptionMethod != null) { return new ClassAndCaption(clazzAndCaptionMethod.clazz,fieldName); } else { return new ClassAndCaption(clazz, fieldName); } } private ClassAndCaption getFieldCaption(Class clazz, String fieldName) { Field field = ReflectionUtils.findField(clazz, fieldName); if (field == null) { return null; } Caption caption = field.getAnnotation(Caption.class); if (caption != null) { return new ClassAndCaption(field.getType(), caption.value()); } else { return new ClassAndCaption(field.getType(), null); } } private ClassAndCaption getMethodCaption(Class clazz, String methodName) { String suffixMethodName = StringUtils.capitalize(methodName); Method method = ReflectionUtils.findMethod(clazz, "get" + suffixMethodName); if (method == null) { method = ReflectionUtils.findMethod(clazz, "is" + suffixMethodName); if (method == null) { return null; } } Caption caption = method.getAnnotation(Caption.class); if (caption != null) { return new ClassAndCaption(method.getReturnType(), caption.value()); } else { return new ClassAndCaption(method.getReturnType(), null); } } private class ClassAndCaption { Class clazz; String caption; public ClassAndCaption(Class clazz, String caption) { this.clazz = clazz; this.caption = caption; } }
Este código lo deberemos incluir en la clase BussinessException.java
. No voy a entrar en detalles de como funciona dicho código ya que tiene cierta complejidad. Pero si que se va a explicar que hace cada función.
ClassAndCaption getMethodCaption(Class clazz, String methodName)
(Líneas 59-74):Si un método tiene la anotación @Caption
retorna el tipo y el valor del caption y sino retornará el tipo y null.ClassAndCaption getFieldCaption(Class clazz, String fieldName)
(Líneas 44-57): Si una propiedad tiene la anotación @Caption
retorna el tipo y el valor del caption y sino retornará el tipo y null.ClassAndCaption getSingleCaption(Class clazz, String fieldName)
(Líneas 24-42): Retorna el tipo y el caption de un campo tanto si dicha anotación está propiedad Java o en un método Java.String getCaptions(Class clazz, Path path)
(Líneas 1-22): Dado que el nombre de los campos está en un objeto javax.validation.Path que puede contener varios nombres de campos. Se obtienen todos ellos separados por puntos.
Por último debemos modificar la línea 22 de BussinessException
:
fieldName=constraintViolation.getPropertyPath().toString();
para que quede de la siguiente forma:
fieldName=getCaptions(constraintViolation.getRootBeanClass(), constraintViolation.getPropertyPath());
org.springframework.core-3.1.2.RELEASE.jar
. En la siguiente sesión trataremos en profundidad el framework de Spring y explicaremos como instalarlo adecuadamente.Por ahora simplemente recordar el incluir el jar org.springframework.core-3.1.2.RELEASE.jar
en los ejercicios que se realicen.