Tabla de Contenidos
Validaciones
En este tema vamos a ver cómo Hibernate puede validar los datos de nuestros objetos Java antes de que se persistan en la base de datos.
Hace unos años se creó una especificación estándar para validar los valores de los objetos Java mediante anotaciones. Esta especificación se define en el JSR 303: Bean Validation.
Instalación
Hibernate soporta estas anotaciones aunque el desarrollo del código que las soporta ha sido desarrollado como un subproyecto llamado Hibernate Validator. Ésto hace que sea necesario bajarse los JARs de Hibernate Validator e incluirlos en nuestro proyecto.
Desde la URL http://sourceforge.net/projects/hibernate/files/hibernate-validator/4.3.0.Final/ nos podemos bajar el fichero hibernate-validator-4.3.0.Final-dist.zip con la versión 4.3.0 de Hibernate Validator.
Deberemos descomprimir el fichero hibernate-validator-4.3.0.Final-dist.zip
y añadir los JAR que se encuentran en la carpeta dist
a nuestro proyecto tal y como hemos hecho con los JAR de Hibernate.
\dist\hibernate-validator-4.3.0.Final.jar
\dist\hibernate-validator-annotation-processor-4.3.0.Final.jar
\dist\lib\optional\joda-time-1.6.jar
\dist\lib\optional\jsoup-1.6.1.jar
\dist\lib\required\validation-api-1.0.0.GA.jar
\dist\lib\optional\hibernate-jpa-2.0-api-1.0.1.Final.jar
\dist\lib\required\jboss-logging-3.1.0.CR2.jar
\dist\lib\optional\log4j-1.2.16.jar
Introducción
Empezar a usar las validaciones mediate anotaciones es tan sencillo como añadir dichas anotaciones a las propiedades de las clases Java a validar.
Veamos un sencillo ejemplo con la clase Profesor
.
- 1
import java.io.Serializable; import java.util.Set; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Profesor implements Serializable { private int id; @NotNull @Size(min = 3, max = 50) private String nombre; @NotNull @Size(min = 3, max = 50) private String ape1; @Size(min = 3, max = 50) private String ape2; public Profesor(){ } public Profesor(String nombre, String ape1, String ape2) { this.nombre = nombre; this.ape1 = ape1; this.ape2 = ape2; } }
Al código fuente de la clase profesor se han añadido las siguientes nuevas anotaciones @NotNull
y @Size
.
@NotNull
: Esta anotación indicará que el valor de la propiedad a la que se le aplica no puede sernull
.@Size(min=n,max=m)
: Esta anotación indicará ,para una propiedad de tipo String que no sea null, que el tamaño mínimo deberá sern
caracteres y el máximo deberá ser dem
caracteres.
Expliquemos ahora cada una de las validaciones de la clase Profesor
.
- La propiedad
nombre
no puede sernull
(línea 10). - La propiedad
nombre
debe tener una longitud mínima de 3 caracteres y máximo de 50 (línea 11). - La propiedad
ape1
no puede sernull
(línea 13). - La propiedad
ape1
debe tener una longitud mínima de 3 caracteres y máximo de 50 (línea 14). - La propiedad
ape2
debe tener una longitud mínima de 3 caracteres y máximo de 50 (línea 16).
ape2
no pueda ser null ya que en otros países no existe el 2º apellido.
Ahora cuando vayamos a persistir la clase Profesor
se lanzará la excepción javax.validation.ConstraintViolationException en caso de que alguna de las validaciones no se cumpla.
Veamos cómo queda el código Java al persistir a Profesor
.
- 1
Profesor profesor = new Profesor("ca", null, "Gomez"); try { session.beginTransaction(); session.save(profesor); session.getTransaction().commit(); } catch (ConstraintViolationException cve) { session.getTransaction().rollback(); System.out.println("No se ha podido insertar el profesor"); }
Al ejecutar el código se mostrará al usuario el mensaje No se ha podido insertar el profesor ya que la propiedad ape1
tiene el valor null
.
Destacar que prácticamente no se ha modificado el código Java al persistir la clase Profesor
. Ahora simplemente tenemos que gestionar la excepción ConstraintViolationException pero no tenemos que llamar directamente a ningún código para que realice las validaciones. Por suerte Hibernate lo realiza automáticamente. Ësto último es de gran importancia ya que si tuviéramos que llamar directamente a un hipotético método validate() sería propenso a que lo olvidáramos en algún momento y podríamos acabar con datos erróneos en la base de datos.
Mensajes
En el ejemplo anterior se muestra un simple mensaje al usuario indicando que no ha podido insertarse el profesor pero sin indicar el motivo de ello. Como todos sabemos, se debe mostrar al usuario los motivos por lo que ha fallado dicha inserción 1).
La excepción ConstraintViolationException contiene la información con todas las validaciones que se han infringido, incluyendo el mensaje de error y la propiedad Java sobre la que se ha producido. Para ello llamaremos al método getConstraintViolations() que nos retorna un java.util.Set de objetos javax.validation.ConstraintViolation.
La clase ConstraintViolation contiene la información de una validación que se ha infringido. Los métodos más importantes que debemos usar son los siguientes:
- getMessage(): Obtiene el mensaje de error
- getPropertyPath(): Obtiene el nombre de la propiedad Java que ha infringido la validación.
Ahora el código Java debe quedar de la siguiente forma:
- 1
Profesor profesor = new Profesor("ca", null, "Gomez"); try { session.beginTransaction(); session.save(profesor); session.getTransaction().commit(); } catch (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()); } }
Como podemos ver, se han añadido las líneas 12, 13, y 14. Estas líneas contienen un bucle para recorrer cada una de las validaciones infringidas y mostrar el mensaje al usuario.
La salida por la consola en este caso será la siguiente:
No se ha podido insertar el profesor debido a los siguientes errores. En el campo 'ape1':no puede ser null En el campo 'nombre':el tamaño tiene que estar entre 3 y 50
Mensajes personalizados
En el ejemplo anterior podemos ver cómo el mensaje de la anotación @NotNull es “no puede ser null”. Personalmente considero que a un usuario nunca habría que nombrarle la palabra null
ya que sólo tiene sentido para los informáticos. Por suerte podemos cambiar los mensajes que se muestran al usuario.
Existen dos formas de cambiar los mensajes al usuario:
Reemplazo local
Para cada anotación de validación es posible usar el atributo message
para personalizar el mensaje que usa esa anotación concreta en ese atributo concreto.
Veamos cómo queda la clase Profesor
:
- 1
import java.io.Serializable; import java.util.Set; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class Profesor implements Serializable { private int id; @NotNull @Size(min = 3, max = 50) private String nombre; @NotNull(message="No puede estar vacío") @Size(min = 3, max = 50) private String ape1; @Size(min = 3, max = 50) private String ape2; public Profesor(){ } public Profesor(String nombre, String ape1, String ape2) { this.nombre = nombre; this.ape1 = ape1; this.ape2 = ape2; } }
Vemos cómo en la línea 13 se ha modificado la anotación @NotNull
para incluir el mensaje a mostrar al usuario.
Si volvemos a ejecutar el código la salida por consola será ahora:
No se ha podido insertar el profesor debido a los siguientes errores: En el campo 'nombre':el tamaño tiene que estar entre 3 y 50 En el campo 'ape1':No puede estar vacío
El problema de esta técnica es el nuevo mensaje.Sólo se aplica a ape1
con la validación @NotNull
, pero si el campo nombre
estuviera a null
seguiría saliendo el mensaje original.
Reemplazo global
Por suerte hay una forma de cambiar los mensajes para todas las veces que aparece la validación @NotNull
o cualquier otra anotación de validación.
Debemos crear el fichero ValidationMessages.properties
en el paquete raiz, insertar una línea con la FQCN de la anotación de la que queremos cambiar el mensaje , prefijar el texto “.messaje=
” y añadir a renglón seguido el muevo mensaje.
Veamos un ejemplo que quedará mucho más claro:
- | Fichero ValidationMessages.properties
javax.validation.constraints.NotNull.message=No puede estar vacio
Ahora con el siguiente código
- 1
Profesor profesor = new Profesor(null, null, "Gomez"); try { session.beginTransaction(); session.save(profesor); session.getTransaction().commit(); } catch (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()); } }
Como el objeto profesor
tiene los 2 campos nombre
y ape1
a null
(línea 1), al ejecutarse el código la salida por consola es:
No se ha podido insertar el profesor debido a los siguientes errores: En el campo 'nombre':No puede estar vacio En el campo 'ape1':No puede estar vacío
Es decir que a partir de ahora cualquier uso que hagamos de la anotación @NotNull
hará uso del nuevo mensaje.
Profesor
ya que no es necesario.
Validaciones simples
Hasta ahora sólo hemos usado las anotaciones @NotNull
y @Size
. Veamos una lista con más anotaciones para validar:
Anotación | Tipos 2) | Explicación |
---|---|---|
@AssertFalse | Boolean, boolean | El booleano deberá ser false |
@AssertTrue | Boolean, boolean | El booleano deberá ser true |
@Digits(integer=n, fraction=m) | Cualquier tipo numérico | Especifica el nº máximo de cifras enteras y decimales |
@Future | java.util.Date y java.util.Calendar | La fecha debe ser mayor que ahora |
@Past | java.util.Date y java.util.Calendar | La fecha debe ser menor que ahora |
@Max(n) | Cualquier tipo numérico | El valor deberá ser menor o igual a n |
@Min(n) | Cualquier tipo numérico | El valor deberá ser mayor o igual a n |
@NotNull | Object | El objeto no puede ser null |
@Null | Object | El objeto debe ser null |
@Pattern(regexp=“r”) | String | Comprueba que el valor se ajusta a la expresión regular r |
@Size(min=n, max=m) | String o colecciones | El tamaño del String o la colección debe estar entre n y m . |
@Email | String | El valor tiene el formato de una dirección de correo electrónico |
@NotBlank | String | Comprueba que el String no sea null y que al hacer un trim() aún haya algún caracter |
@Valid | Object | Si el campo es otro objeto que tiene sus propias validaciones, con esta anotación indicaremos que también debe validarse ese otro objeto |
Como podemos ver, las anotaciones son bastante sencillas de usar.
@NotNull
y @NotBlank
. En caso de que queramos que un String no esté vació lo normal es que queramos usar @NotBlank
ya que esta anotación nos garantiza que no hayan puesto únicamente espacios además de no ser null
.
@Valid
es obligatoria ponerla si queremos que se validen las anotaciones que hemos puesto en el otro objeto que está como propiedad del objeto que se está validando
Validaciones complejas
Hasta ahora hemos visto simples validaciones de campos pero , ¿qué ocurre cuando queremos validar más de un campo a la vez?. Hibernate Validator soporta 2 formas de hacerlo.
La anotación @ScriptAssert
La anotación @ScriptAssert
nos permitirá ejecutar una expresión en el lenguaje JavaScript 3) usando dentro del código cualquier propiedad de la clase.
@ScriptAssert
porque existe en el estándar pero recomiendo en vez esta anotación , que se use el truco que explico a continuación de esta anotación.
@ScriptAssert
debe colocarse a nivel de clase ya que no se refiere a ninguna propiedad concreta .
Un ejemplo es el siguiente:
@ScriptAssert(lang="javascript",script="(_this.password!=null)? _this.password.equals(_this.confirmPassword) : false",message="Los 2 passwords deben ser iguales") public class Usuario implements Serializable { private int idUsuario; @NotNull @Size(min = 3, max = 50) @Column(name="login",unique=true) private String login; @NotNull @Size(min = 3, max = 50) private String nombre; @NotNull @Size(min = 3, max = 50) private String ape1; @Size(min = 3, max = 50) private String ape2; @NotNull @Size(min = 7, max = 50) private String password; @NotNull @Size(min = 7, max = 50) private String confirmPassword; }
Vemos en la línea 1 cómo se ha añadido la anotación @ScriptAssert
con el código JavaScript:
(_this.password!=null)? _this.password.equals(_this.confirmPassword) : false
Se ha utilizado el operador ternario de JavaScript porque sólo es posible escribir una expresión con lo que no podemos usar un if
ni similares, lo que limita un poco el tipo de validaciones que podemos hacer.
_this
” con un subrayado delante de la palabra this
.
Si intentamos añadir más validaciones con @ScriptAssert
se producirá un error al compilar indicando que ya existe la anotación @ScriptAssert
. Para solucionarlo deberemos poner las anotaciones @ScriptAssert
dentro de una anotación @ScriptAssert.List
tal y como se ve en el siguiente ejemplo:
@ScriptAssert.List({ @ScriptAssert(lang="javascript",script="(_this.password!=null)? _this.password.equals(_this.confirmPassword) : false",message="Los 2 passwords deben ser iguales"), @ScriptAssert(lang="javascript",script="(_this.password=='123456')",message="La contraseña no puede ser tan tonta") })
Métodos Java de Validación
Podemos añadir también métodos Java que realicen las validaciones, usando un pequeño truco.
Veamos ahora un ejemplo de código:
@AssertTrue(message="El login y el password no pueden coincidir") private boolean isPasswordValido() { if ((login!=null) && (login.equalsIgnoreCase(password))) { return false; } else { return true; } }
Deberemos hacer un método Java que realice la validación que deseemos y que retorne true
si la validación ha tenido éxito y false
si ha fallado.
Entonces el truco consiste en añadir la anotación @AssertTrue
al método de forma que si éste retorna false
no se cumpla la validación de @AssertTrue
y nos avise de ello. Por último deberemos añadir un mensaje personalizado a la anotación @AssertTrue
.
is
o get
ya que en caso contrario no se llamará a dicho método en la validación
password
, no se debería llamar isPassword
o getPassword
pq sino Hibernate se confundirá con los set/get
de la propiedad password
. Por ello le añadimos el texto “Valido” o lo que queramos.
Esta forma de validar tiene varias ventajas respecto a @ScriptAssert
:
- El código fuente de la validación se comprueba en tiempo de compilación en vez de en tiempo de ejecución.
- Es posible usar toda la potencia de Java en vez de sólo expresiones en JavaScript.
- En caso de tener muchas validaciones queda todo mucho más ordenado mediante métodos que con multitud de anotaciones
@AssertTrue
al principio de la clase. - Al usar la anotación
@ScriptAssert
no se obtiene nada mediante el método ConstraintViolation.getPropertyPath() que obtiene el nombre de la propiedad que ha infringido la validación. Sin embargo, validando con métodos al usar ConstraintViolation.getPropertyPath() sí que se retorna el nombre del método sin la palabrais
, con lo que se ayuda al usuario a encontrar la causa del error.
Pero si lo hacemos público cualquiera podría ver las métodos y hacer uso de ellos cuando realmente no queremos que los llamen directamente.