Columnas únicas

En lecciones anteriores hemos tratado el tema de las claves primarias pero aún no habíamos explicado cómo validar que una columna es única. El JSR 303: Bean Validation no define ninguna anotación para validar que una columna es única. El motivo de ello es porque validar que una columna es única lo suele hacer la base de datos mediante una clave única en la tabla.

Para validar que una columna es única mediante el fichero de mapeo de Hibernate .hbm.xml simplemente deberemos añadir el atributo unique=“true” a la columna que queramos hacer única.

1|Usuario.hbm
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="ejemplo03.Usuario">
        <id column="IdUsuario" name="idUsuario" type="integer">
            <generator class="increment"/>
        </id>
        <property name="login" unique="true" />
        <property name="nombre"/>
        <property name="ape1"/>
        <property name="ape2"/>
        <property name="password"/>
 
    </class>
</hibernate-mapping>

En la línea 8 podemos ver el atributo unique con el valor true que define que la propiedad login es única.

Si queremos hacer ésto mismo usando notaciones es tan sencillo como añadir el atributo unique=true a la anotación @Column.

@Column(name="login",unique=true)
private String login;

Desgraciadamente ésto no es suficiente para que nuestra columna sea única. También deberemos hacer que la base de datos cree el índice con la columna única. Afortunadamente Hibernate posee una herramienta que nos genera todo el esquema de la base de datos como un Script de SQL para nuestra base de datos.

Si ejecutamos el siguiente código Java se generará en el raíz de nuestro proyecto el fichero script.sql con la creación de las tablas y su índices.

new org.hibernate.tool.hbm2ddl.SchemaExport(configuration).setOutputFile("script.sql").setDelimiter(";").create(true, false);
Es esta línea de Java la que crea el Script SQL con la columna única. Siendo entonces la base de datos la encargada de comprobar la unicidad de la columna y no siendo Hibernate el encargado de hacerlo.
Por supuesto posteriormente será necesario ejecutar el script manualmente contra la base de datos usando phpMyAdmin o cualquier otra herramienta.
También podemos hacer que Hibernate lance automáticamente el script contra la base de datos poniendo el segundo parámetro del método create al valor true. El problema de ésto es que el script que genera Hibernate borra las tablas con un drop y las vuelve a crear con lo que se pueden perder todos los datos. Así que es mejor revisar el script y lanzarlo manualmente.

org.hibernate.exception.ConstraintViolationException

El siguiente paso con las columnas únicas es tratar con la excepción que se lanzará cuando se intente insertar un valor que ya existe en la columna. La excepción que se lanza es org.hibernate.exception.ConstraintViolationException. Desgraciadamente esta excepción no hereda de la excepción javax.validation.ConstraintViolationException con lo que será necesario tratar las 2 excepciones.

El código ahora para persistir una clase quedará así:

1
Usuario usuario = new Usuario("lramirez","Luis","Ramirez","Cano","1234567","1234567");
 
try {
    session.beginTransaction();
 
    session.save(usuario);
 
    session.getTransaction().commit();
} catch (javax.validation.ConstraintViolationException cve) {
    session.getTransaction().rollback();
    System.out.println("No se ha podido insertar el profesor debido a los siguientes errores:");
    for (ConstraintViolation constraintViolation : cve.getConstraintViolations()) {
        System.out.println("En el campo '" + constraintViolation.getPropertyPath() + "':" + constraintViolation.getMessage());
    }                
} catch (org.hibernate.exception.ConstraintViolationException cve) {
    session.getTransaction().rollback();
    System.out.println("No se ha podido insertar el profesor debido al siguiente error:");
    System.out.println("El valor ya existe."+cve.getLocalizedMessage());
}

Vemos cómo se han añadido las líneas de las 15 a la 19 para tratar la nueva excepción org.hibernate.exception.ConstraintViolationException.

Debido a que las 2 excepciones tienen el mismo nombre de clase ,ConstraintViolationException, aunque en paquetes distintos, es necesario poner la FQCN de cada una de ellas para evitar ambigüedades. Por ello también se ha modificado la línea 9 para incluir la FQCN de la excepción.

El último problema que nos encontramos es que Hibernate no nos indica en la excepción cuál ha sido la propiedad que ha fallado, así que no nos queda más remedio que usar el mensaje de la excepción mediante getLocalizedMessage() para dar algo de información al usuario. Desgraciadamente el mensaje es muy poco adecuado para un usuario. En nuestro caso el resultado de llamar a getLocalizedMessage() es:

Duplicate entry 'lramirez' for key 'login'.
La última opción que nos queda es analizar el mensaje
Duplicate entry 'lramirez' for key 'login'

para obtener el nombre de la columna login y el valor duplicado lramirez. El problema es que sólo valdrá para MySQL y que si la propia base de datos decide cambiar el mensaje también podría dejar de funcionar.