Grails validación y manejo de errores
El método validate()
valida que los valores de las propiedades del objeto coincidan con los constraints definidos en dicho objeto, si no coinciden agrega los respectivos errores en el objeto errors
.
Primero, al hacer el binding de los valores del request hacia el domain object se valida que los tipos de datos sean correctos (por ejemplo no se puede asignar "ad" a un int).
Después, el método validate()
o save()
(internamente invoca a validate()
) validan los valores de las propiedades contra los constraints definidos en el domain/command object.
Cuando se envía un domain/command object como argumento en una acción de un controlador, por automático realiza las validaciones de tipo y de constraints, es decir invoca al método validate()
.
${persona.nombre}
: Cuando hay errores de validación es imposible mostrar los valores incorrectos capturados por el usuario.
${fieldValue(bean: persona, field: 'nombre')}
: Cuando hay errores de validación, muestra el valor incorrecto capturado por el usuario, si no los hay obtiene el valor del field.
El siguiente pedazo de código muestra una manera genérica de utilizar un formulario con controles y manejo de errores.
El databinding lo hace en base al name
del control, no tiene nada que ver el atributo value
.
El atributo value sirve para asignar el valor del control, el value del control se obtiene a través del taglib fieldValue
El taglib fieldValue
sirve para poder mostrar el valor erroneamente capturado (cuando se captura y tiene algún error), si no se obtuviera el valor a través del tablig fieldValue
se obtendría el último valor guardado en el bean.
index.gsp:
<div class="container">
<div class="row">
<div class="col-xs-12">
<g:hasErrors bean="${cmd}">
<div class="alert alert-danger">
<g:renderErrors bean="${cmd}" as="list"/>
</div>
</g:hasErrors>
</div>
</div>
<form class="form-horizontal">
<div class="form-group">
<label for="nombre" class="col-sm-3 control-label">Nombre:</label>
<div class="col-sm-3">
<g:textField name="nombre" value="${fieldValue(bean: cmd, field: 'nombre')}"/>
</div>
</div>
<g:actionSubmit action="send" value="Enviar"/>
</form>
</div>
TestController.groovy:
class TestController {
def index() {
render view: "index", model:[cmd: new MyCommand()] // index.gsp
}
def send(MyCommand cmd) {
log.info("nombre==>${cmd.nombre}")
render view: "index", model: [cmd:cmd]
}
private static class MyCommand {
String nombre
static constraints = {
nombre minSize: 5
}
}
}
Todo domain class o command object tiene un objeto errors. El objeto errors almacena los mensajes de error y los valores erroneamente capturados.
La propiedad errors
de cada domain class es una instancia de la interfaz Errors
de Spring. La interfaz proporciona métodos para navegar por los errores de navegación y también para obtener los valores originales.
errors.reject
: Sirve para registrar un error global
errors.reject(String errorCode)
errors.reject(String errorCode, Object[] errorArgs, String defaultMessage)
errors.reject(String errorCode, String defaultMessage)
errors.rejectValue
: Sirve para registrar un error a nivel de field
Con el siguiente código se puede utilizar hasErrors como operador ternario: ${hasErrors(bean: membresiaInstance, field: "ptosxanio", "1") ? "danger" : "primary"}
Renombrar un field name:
<full packagePath>.<domain name>.<propertyName>.<label>=<message>
Un field no cumple la validación nullable:
<full packagePath>.<domain name>.<propertyName>.nullable=<message>
Es un error querer lanzar una grails.validation.ValidationException
ya que esta excepción es utilizada dentro por el propio Grails para agregar errores a un domain class/command object. En vez de eso nosotros tenemos el mecanismo de alto nivel static constraints
del domain class/command object.
Lo mejor es que un command object realice las validaciones y se ayude de servicios.
Si un servicio necesita lanzar excepciones, cree sus propias excepciones RuntimeException
para descartar los valores introducidos temporalmente en el domain class, ya que al no ser errores constraints, éstos no serán descartados y persistidos automáticamente hacia la base de datos a causa de OSIV en Grails. Recuerde lanzar excepciones para situaciones excepcionales.
A menudo los errores se muestran con un render
ya que se envía de vuelta a la vista el command object/doman class con los errores de validación. Cuando sea así, utilice request.error = message
para introducir el mensaje de error porque si lo introduce como flash.error = message
dicho error será mostrado 2 veces.
Cuando utilice redirect
introduzca el mensaje de error con flash.error
para que el mensaje viva durante el doble request.
Esto sucede porque cuando termina un método @Transactional
, invoca el método save()
sobre el domain class el cual a su vez limpia el objeto errors
.
Hay que tener mucho cuidado con agregar errores al objeto errors
de un domain class en un método de servicio ya que se pierden por arte de magia. La manera de evitar que se pierdan es lanzando una RuntimeException y cachándola en el controlador, de esta forma se hace rollback de la transacción y se evita sea invocado el método save()
.
Otra manera de evitarlo es invocando el método discard() sobre la instancia para que la saque de la sesión y no se ejecute sobre ella automáticamente el método save()
:
@Transactional
class AlumnoService {
@Transactional
void updateAlumno(Alumno alumno) {
if (alumno.edad > 80) {
alumno.errors.reject("alumno.error.edad")
alumno.discard()
return
}
alumno.save(failOnError: true)
}
}
// ---------------------------------------
// Errors reject
// ---------------------------------------
errors.reject("key_codigo_error")
// ---------------------------------------
// Errors reject con argumentos
// ---------------------------------------
errors.reject("key_codigo_error", ["arg1", "arg2"] as Object[], "defaultMessage(optional)")