Tabla de Contenidos

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 Tratamiento de Excepciones.

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:

1
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:

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:

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

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 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 BussinessExceptioncontendrá 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:

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.

1 | BussinessException.java
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().

El código de BussinessMessage es el siguiente:

1 | BussinessMessage.java
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.

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:

1 | Caption.java
@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:

1 | Usuario.java
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:

1
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.

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.
1)
Realmente Hibernate lanza excepciones del tipo org.hibernate.HibernateException pero como hereda de RuntimeException podemos tratar únicamente ésta última