iberck
9/22/2016 - 3:22 AM

Grails - Manejo de excepciones en los controladores [validation errors]

Grails - Manejo de excepciones en los controladores [validation errors]

Cuándo lanzar excepciones

Como regla general, si espera que el flujo natural del método devuelva un objeto, entonces si algo sucede lance una excepción. Por ejemplo si crateCard no puede crear una tarjeta, debería lanzar una excepción ya que siempre debería poder crear una tarjeta con un token que ya ha sido validado.

Si usted anticipa el nulo ocasional y quiere manejarlo de cierta manera, vaya con el nulo. Por ejemplo findUser podría devolver null si no encuentra el usuario.

Solo lance excepciones cuando suceda algo excepcional, hay un ejemplo en internet en donde el programador está basando la lógica del programa con excepciones: si encuentras la instancia ve hacia acá, si no la encuentras ve hacia allá. Otro problema es que lanza excepciones si no encuentra una instancia lo cual no es una condición excepcional, lo que debería estar haciendo es regresar boolean para determinar si se encuentra o no la instancia en la bd.

Errores excepcionales (500)

Por ejemplo no se pudo escribir en la bd, o el disco duro está lleno. Estos errores no se muestran al usuario porque no tiene sentido informarle este tipo de errores. Simplemente se dejan pasar para que sean mostrados en una página de error 500.

Errores que se notifican al usuario

Por ejemplo: no se pudo crear la tarjeta de crédito. Para poder notificar al usuario este tipo de errores hay tres alternativas:

  1. Utilizar el mecanismo de validación de Grails
  2. Retornar códigos al controlador
  3. Lanzar excepciones y cacharlas con try/catch
  4. Cachar las excepciones en un método especializado del controlador

1. Utilizar el mecanismo de validación de Grails

Utilice este mecanismo de static constraints para validar un command object/domain class, si necesita validaciones personalizadas invoque un servicio dentro del mismo código de validación.

También puede pasar el domain class a un servicio y agregar errores sobre su objeto errors, si hubiera errores de validación retornar true o false.

Utilice domain.errors.reject() para registrar errores globales y domain.errors.rejectValue() para registrar errores sobre una propiedad específica. Sin embargo tenga cuidado si se trata de un domain class porque el objeto errors es limpiado al terminar la transacción llamando así a save() el cual limpia el objeto errors. La manera de evitarlo es llamando el método discard() enseguida de agregar los errores, para más detalles consulte el gist grails-validation-errors.

2. Retornar códigos al controlador

Esta alternativa ahorra muchos try/catch en la aplicación y modela claramente la lógica que debe tomar un controlador cuando una operación retorne éxito o error.

Debe ser utilizada cuando la naturaleza del método indique que el retorno es parte de la lógica, por ejemplo findUser debe retornar un User si encuentra al usuario y null si no lo encuentra.

Con grails podremos validar si el usuario es null con:

User user = findUser()
if (!user) {
 ...
}

Hay que tener en cuenta que para métodos que modifican la base de datos, si se retorna un código de error habrá que hacer manualmente rollback por lo que retornar códigos no es muy recomendable en métodos que modifican la base de datos.

En conclusión, retorne códigos de error cuando se haga operaciones de lectura y cuando la naturaleza del método indique claramente que podría retornar éxito/error.

3. Cachar las excepciones en un método especializado del controlador

params.X->Se pueden obtener todos los valores que estaban en el request

params.controller->Controlador que lanzó la excepción

params.action->Acción que lanzó la excepción

def saveTC() {
    def tokenId = params.tid
    def membresia = Membresia.findById(params.mid)

    conektaService.createCard(membresia.conektaCustomerId, tokenId) // throws ConektaRuntimeException
    flash.success = "Se creo correctamente la tarjeta de crédito"
    redirect(action: "listTC", params: [id: params.mid])
}
    
def catchConektaRuntimeException(ConektaRuntimeException ex) {
    def membresia = Membresia.findById(params.mid)

    flash.error = ex.getMessage()
    render view: "/membresia/createTC", model: [membresiaInstance: membresia]
    return
}

Ventajas

Esta arquitectura es más limpia porque saca de la lógica del controlador los try/catch,

Desventajas

Se hace un poco complicado leer y entender a través de qué lugares se llega al exception handler. Además, la excepción debe incluir todos los datos para la renderizar el error dentro de la misma página, como el modelo o el lugar a dónde se tiene que redirigir.

Tal vez esta alternativa sea buena cuando se tengan controladores pequeños.

Referencia

http://stackoverflow.com/questions/99683/which-and-why-do-you-prefer-exceptions-or-return-codes

http://stackoverflow.com/questions/729379/why-not-use-exceptions-as-regular-flow-of-control

https://talldavedotnet.files.wordpress.com/2013/06/bestpracticespresentation_20130605_web.pdf