====== Excepciones ====== 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 [[patrones:excepciones]]. ===== Tratamiento de Excepciones ===== Al realizar una operación con Hibernate se pueden lanzar cualquiera de las siguiente 4 excepciones: * javax.validation.ConstraintViolationException * org.hibernate.exception.ConstraintViolationException * java.lang.RuntimeException|RuntimeException (( Realmente Hibernate lanza excepciones del tipo org.hibernate.HibernateException pero como hereda de java.lang.RuntimeException|RuntimeException podemos tratar únicamente ésta última )) * java.lang.Exception|Exception 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 [[http://docs.oracle.com/javase/6/docs/technotes/guides/logging/|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: ==== RuntimeException ==== Si la excepción es de tipo ''RuntimeException'' lo único que hacemos es volver a relanzar la misma excepción mediante: throw ex; ==== Exception ==== 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. ==== javax.validation.ConstraintViolationException ==== Esta excepción la vamos a reconvertir en una ''BussinessException''. En el siguiente apartado se explica en qué consiste ''BussinessException''. throw new BussinessException(cve); ==== org.hibernate.exception.ConstraintViolationException ==== Esta excepción la vamos a reconvertir en una ''BussinessException''. En el siguiente apartado se explica en qué consiste ''BussinessException''. throw new BussinessException(cve); ==== BussinessException ==== 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; Al poner todos los ''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. ===== Mejoras con BusinessException ===== En el apartado anterior hemos visto cómo tratamos las siguientes excepciones transformándolas en ''BussinessException'': * javax.validation.ConstraintViolationException * org.hibernate.exception.ConstraintViolationException 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: * [[#Unificar el tratamiento de las excepciones]] * [[#Simplificar el uso de javax.validation.ConstraintViolationException]] * [[#Mejorar los mensajes de error]] ==== Unificar el tratamiento de las excepciones ==== 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. Otra ventaja de la unificación de excepciones es que si nuestra aplicación dejará de usar Hibernate para volver a JDBC podríamos seguir usando la nueva ''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. ==== Simplificar el uso de javax.validation.ConstraintViolationException ==== 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 javax.validation.ConstraintViolation#getPropertyPath()|getPropertyPath()? ¿Y javax.validation.ConstraintViolation#getLeafBean()|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: class BussinessException BussinessException : Set getBussinessMessages() class BussinessMessage BussinessMessage : getFieldName() BussinessMessage : getMessage() BussinessException "1" -- "1..n" BussinessMessage : bussinessMessages class ConstraintViolationException ConstraintViolationException : Set getConstraintViolations() class ConstraintViolation ConstraintViolation: getPropertyPath() ConstraintViolation: getMessage() ConstraintViolation: getMessageTemplate() ConstraintViolation: getRootBean() ConstraintViolation: getRootBeanClass() ConstraintViolation: getLeafBean() ConstraintViolation: getInvalidValue() ConstraintViolation: getConstraintDescriptor() ConstraintViolationException "1" -- "1..n" ConstraintViolation : constraintViolations 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. ==== Mejorar los mensajes de error ==== Al mostrar los mensajes de error que se incluyen en javax.validation.ConstraintViolation hay dos problemas principales: * El nombre de los campos es el de las propiedades Java y no un nombre amigable para el usuario. * Los mensajes empiezan en minúscula. 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 Se podría argumentar que esos dos problemas deben ser de la capa de presentación y no de la capa de negocio. Pero ya que javax.validation.ConstraintViolation nos está ofreciendo los mensajes de error como un texto, deberíamos hacer todo lo posible por mejorar la calidad de los mismos y no hacer que cada presentación que realicemos tenga que preocuparse de estos mismos problemas. ===== El código de BussinessException ===== 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 bussinessMessages = new TreeSet<>(); public BussinessException(List 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 getBussinessMessages() { return bussinessMessages; } } Podemos ver que el código es bastante sencillo. Consta principalmente de varios constructores y el método ''getBussinessMessages()''. * Línea 3: Un java.util.TreeSet donde guardar todos los ''BussinessMessage''. Se usa un java.util.TreeSet|TreeSet para que los mensajes salgan ordenados por orden alfabético. * Líneas 5-7: Constructor al que directamente se le pasa una lista de ''BussinessMessage''.Ësto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción. * Líneas 9-11: Constructor al que directamente se le pasa un único ''BussinessMessage''.Esto permitirá generar mensajes al usuario aunque no haya habido ninguna excepción. * Líneas 13-15: Constructor al que se le pasa una java.lang.Exception|Exception. Eso permite mostrar al usuario un mensaje aunque sea un java.lang.Exception|Exception. * Líneas 13-15: Constructor al que se le pasa una javax.validation.ConstraintViolationException.Este constructor creará un ''BussinessMessage'' por cada uno de los javax.validation.ConstraintViolation. * Líneas 29-31: Constructor al que se le pasa una org.hibernate.exception.ConstraintViolationException. Creará un único ''BussinessMessage'' en función del mensaje de la excepción. * Línea 33-35: Retorna la lista de todos los ''BussinessMessage''. El código de ''BussinessMessage'' es el siguiente: public class BussinessMessage implements Comparable { 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. * Línea 1: Implementar el interfaz java.lang.Comparable|Comparable. Se implementa este interfaz para que se pueda usar dentro de la clase java.util.TreeSet y que los mensajes salga ordenados. * Líneas 13 y 15 : Se pone en mayúsculas el primer carácter del campo y el mensaje para que quede mas amigable al usuario. * Línea de la 43 a la 59: Método java.lang.Comparable#compareTo(T)|compareTo(T) del interfaz java.lang.Comparable|Comparable. Este método ordena las ''BussinessMessage'' de forma que primero van los mensajes que incluyen el nombre del campo y luego los que no. ===== Nombres de las columnas ===== 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. Vuelvo a insistir que ciertos frameworks de la capa de presentación hacen este mismo trabajo y se puede pensar que dicha anotación no debería estar en la capa de negocio. 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()); Para la realización de éste código se han usado las clases org.springframework.util.ReflectionUtils y org.springframework.util.StringUtils del framework Spring. Estas clases son simple clases de utilidad con funciones sencillas. Para poder usarlas se ha incluido el jar ''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.