Herramientas de usuario

Herramientas del sitio


unidades:06_objetos_validaciones:02_validaciones

Diferencias

Muestra las diferencias entre dos versiones de la página.


unidades:06_objetos_validaciones:02_validaciones [2023/04/07 21:26] (actual) – creado - editor externo 127.0.0.1
Línea 1: Línea 1:
 +====== 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 [[http://jcp.org/en/jsr/detail?id=303|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 [[http://www.hibernate.org/subprojects/validator.html|Hibernate Validator]]. Ésto hace que sea necesario bajarse los JARs  de [[http://www.hibernate.org/subprojects/validator.html|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 [[http://sourceforge.net/projects/hibernate/files/hibernate-validator/4.3.0.Final/hibernate-validator-4.3.0.Final-dist.zip/download|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''
 +
 +<note tip>
 +Se deben copiar todos los ficheros excepto los siguientes:
 +  * ''\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'' 
 +</note>
 +
 +<note warning>
 +**No** bajaros la versión Hibernate Validator 5.0.0.Final. Esta se hizo estable solo 2 semanas antes de empezar el curso.Así que no la usaremos.
 +</note>
 +===== 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''.
 +
 +<code java 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;
 +    }
 +}
 +</code>
 +
 +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 ser ''null''.
 +  * **''@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á ser ''n'' caracteres y el máximo deberá ser de ''m'' caracteres.
 +
 +Expliquemos ahora cada una de las validaciones de la clase ''Profesor''.
 +  * La propiedad ''nombre'' no puede ser ''null'' (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 ser ''null'' (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).
 +
 +<note tip>
 +No se ha validado que ''ape2'' **no** pueda ser null ya que en otros países no existe el 2º apellido.
 +</note>
 +
 +Ahora cuando vayamos a persistir la clase ''Profesor'' se lanzará la excepción <javadoc jee6>javax.validation.ConstraintViolationException</javadoc> en caso de que alguna de las validaciones no se cumpla.
 +
 +Veamos cómo queda el código Java al persistir a ''Profesor''.
 +
 +<code java 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");
 +}
 +</code>
 +
 +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 <javadoc jee6>javax.validation.ConstraintViolationException|ConstraintViolationException</javadoc> 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 (( o actualización , borrado, etc )).
 +
 +La excepción <javadoc jee6>javax.validation.ConstraintViolationException|ConstraintViolationException</javadoc> 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 <javadoc jee6>javax.validation.ConstraintViolationException#getConstraintViolations()|getConstraintViolations()</javadoc> que nos retorna un <javadoc jdk7>java.util.Set</javadoc> de objetos <javadoc jee6>javax.validation.ConstraintViolation</javadoc>.
 +
 +La clase <javadoc jee6>javax.validation.ConstraintViolation|ConstraintViolation</javadoc> contiene la información de una validación que se ha infringido. Los métodos más importantes que debemos usar son los siguientes:
 +  * <javadoc>javax.validation.ConstraintViolation#getMessage()|getMessage()</javadoc>: Obtiene el mensaje de error
 +  * <javadoc>javax.validation.ConstraintViolation#getPropertyPath()|getPropertyPath()</javadoc>: Obtiene el nombre de la propiedad Java que ha infringido la validación.
 +
 +Ahora el código Java debe quedar de la siguiente forma:
 +<code java 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());
 +    }                
 +}
 +</code>
 +
 +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]]
 +  * [[#Reemplazo global]]
 +
 +=== 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'':
 +<code java 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;
 +    }
 +}
 +</code>
 +
 +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:
 +
 +<code java | Fichero ValidationMessages.properties>
 +javax.validation.constraints.NotNull.message=No puede estar vacio
 +</code>
 +
 +Ahora con el siguiente código
 +<code java 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());
 +    }                
 +}
 +</code>
 +
 +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.
 +
 +<note tip>Y ahora ya podemos quitar el mensaje personalizado de la línea 13 de la clase ''Profesor'' ya que no es necesario.</note>
 +===== 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 (( Tipo de la propiedad a la que se puede aplicar)) ^ 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.
 +
 +<note tip>Debemos destacar el uso de ''@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''.</note>
 +
 +<note important>
 +Recuerda que la anotación ''@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
 +</note>
 +===== 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]]
 +  * [[#Métodos Java de Validación]]
 +
 +==== La anotación @ScriptAssert ====
 +La anotación ''@ScriptAssert'' nos permitirá ejecutar una expresión en el lenguaje JavaScript ((O cualquier otro  lenguaje de Script como Googvy siempre y cuando se instale )) usando dentro del código cualquier propiedad de la clase. 
 +
 +
 +<note important>
 +Explico la anotación ''@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.
 +</note>
 +
 +''@ScriptAssert'' debe colocarse a nivel de clase ya que no se refiere a ninguna propiedad concreta .
 +
 +Un ejemplo es el siguiente:
 +<code java>
 +@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;    
 +}
 +</code>
 +
 +Vemos en la línea 1 cómo se ha añadido la anotación ''@ScriptAssert'' con el código JavaScript:
 +<code javascript>
 +(_this.password!=null)?  _this.password.equals(_this.confirmPassword) : false
 +</code>
 +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.
 +
 +<note tip>
 +Para acceder a los valores nótese que se usa "''_this''" con un subrayado delante de la palabra ''this''.
 +</note>
 +
 +
 +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:
 +<code java>
 +@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")
 +})
 +</code> 
 +==== Métodos Java de Validación ====
 +Podemos añadir también métodos Java que realicen las validaciones, usando un pequeño truco. 
 +
 +<note important>
 +Este truco que vamos a explicar ahora mismo , es de una sencillez pasmosa pero muy util para colocar las validaciones que debamos hacer en código Java
 +</note>
 +
 +Veamos ahora un ejemplo de código:
 +
 +<code java>
 +@AssertTrue(message="El login y el password no pueden coincidir")
 +private boolean isPasswordValido() {
 +    if ((login!=null) && (login.equalsIgnoreCase(password))) {
 +        return false;
 +    } else {
 +        return true;
 +    }
 +}
 +</code>
 +
 +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''.
 +
 +<note tip>
 +El nombre del método deberá empezar por ''is'' o ''get'' ya que en caso contrario no se llamará a dicho método en la validación
 +</note>
 +
 +<note important>
 +El nombre del método no debe llamarse como el de una propiedad. Es decir si está la propiedad ''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.
 +</note>
 +
 +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 <javadoc>javax.validation.ConstraintViolation#getPropertyPath()|ConstraintViolation.getPropertyPath()</javadoc> que obtiene el nombre de la propiedad que ha infringido la validación. Sin embargo,  validando con métodos al usar <javadoc>javax.validation.ConstraintViolation#getPropertyPath()|ConstraintViolation.getPropertyPath()</javadoc> sí que se retorna el nombre del método sin la palabra ''is'', con lo que se ayuda al usuario a encontrar la causa del error.
 +
 +<note tip>
 +El único problema de esta técnica de validación es que al hacer el método privado y que aparentemente nadie lo llame , si usamos herramientas de chequeo de código como [[http://www.sonarsource.org/|Sonar]], ésta nos indicará que tenemos un método privado que no usamos.
 +
 +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.
 +</note>