iberck
6/20/2016 - 9:34 PM

Cambios automáticos en la edición

Cambios peligrosos (automáticos) en la edición

Cambios automáticos en la edición

Por defecto, Hibernate hace persistentes automáticamente los cambios de las instancias persistentes durante un flush(). El interceptor OSIV hace flush de la sesión de hibernate al final del HTTP request, por lo tanto cualquier instancia con cambios pendientes (dirty) es persistida hacia la base de datos aún si no llama explicitamente el método save().

Hay que tener en cuenta que las modificaciones serán persistidas automáticamente solo si el domain class entra a una transacción, ya sea que el controlador esté marcado con @Transactional o que el domain object sea pasado a un método @Transactional.

Cuando la instancia tiene errores de validación, no será guardada NINGUNA de sus propiedades. Es decir, no se guardarán las propiedades que tuvieron errores de validación ni las que tienen valores temporales sin errores de validación. Los errores de validación pueden ser agregados porque alguna propiedad no cumple con sus constraints o porque explicitamente se agregó un error al objeto errors del domain class a través de domain.errors.reject o domain.errors.rejectValue.

En el código de este gist se agrega el código que demuestra cómo se modifica automáticamente la instancia sin ser guardada explicitamente. Para validar que se modifica el domain class si invocar explicitamente save() ingrese una edad válida y vuelva a ingresar a la página, verá que la matrícula cambió su valor.

Para validar que no se modifica automáticamente ninguna de las propiedades del domain class cuando tiene errores de validación, ingrese una edad menor a 0 (saldrá un error de validación) y vuelva a introducir en la url text/index y verá que la matrícula no fué persistida en la bd.

La manera de evitar que las propiedades sean autopersistidas es invocando read() en vez de obtener el domain class con get() (un domain class como argumento de acción invoca a get()). Con read() se obtiene una instancia y evita los cambios autopersistentes, sin embargo si invoca explicitamente save() sí se guardarán los cambios a la base de datos.

Cuando se hace databinding de una propiedad en la acción a través de id, obtiene el domain object con el método get del domain class. Esto significa que si no encuentra el id del domain object, el objeto valdrá null y que si no tenemos cuidado con las reglas anteriores, el objeto podría ser modificado automáticamente:

void action1(Usuario usuario) { // Usuario.get(id)
}

Otra manera para que no se guarde automáticamente el estado de un objeto es invocando el método discard(), el cual saca al objeto de la sesión y se evitan cambios autopersistentes.

Un lugar donde puede caer en el error de modificar un domain class sin darte cuenta es en el siguiente escenario:

Se valida que la edad de un alumno sea menor a 80 dentro de un método de algún servicio, si es mayor a 80, lanzará una Exception (la cual no hace rollback de la transacción) entonces se estará introduciendo la edad temporalmente dentro del alumno pero al no ser un error de validación de sus constraints, el valor de la edad aunque sea capturado incorrectamente, se quedará guardado dentro del objeto alumno:

@Transactional
Alumno specialValidation(Alumno alumno) {
    if (alumno.edad > 80) {
        throw new MyValidationException("error al validar la edad del alumno")
    }

    return alumno;
}

La manera de resolverlo es introduciendo la validación dentro de los static constraints del domain class o que MyValidationException extienda de RuntimeException para que la transacción haga rollback al lanzarce la excepción.

http://stackoverflow.com/questions/32767450/grails-validation-errors-disappear-from-service-to-controller

Nota importante

Al parecer este comportamiento ya no se da gracias a que en el archivo DataSource.groovy tiene la propiedad

hibernate {
    // ...
    flush.mode = 'manual' // OSIV session flush mode outside of transactional context
}

https://stackoverflow.com/questions/24309084/flush-mode-changed-in-grails-from-auto-to-manual

package us.incorpora.sigrem

import org.springframework.security.access.annotation.Secured

@Secured(['permitAll'])
class TestController {

    AlumnoService alumnoService

    def index() {
        def alumno = Alumno.get(1)
        render view: "index", model: [alumno: alumno]
    }

    def updateAlumno(Alumno alumno) {
        alumno.matricula = "111111" // campo temporal que pudo ser asignado en un método o en la vista

        if (alumno.hasErrors()) {
            render view: "index", model: [alumno: alumno]
            return
        }

        alumnoService.updateAlumno(alumno)
        flash.success = "alumno actualizado"
        redirect action: "index"
    }

}
<%@ page defaultCodec="html" pageEncoding="UTF-8" contentType="text/html; charset=UTF-8" %>

<html>
<head xmlns="http://www.w3.org/1999/html" xmlns="http://www.w3.org/1999/html">
    <meta name="layout" content="adminlte">
</head>

<body>

<g:form action="updateAlumno">
    <g:hiddenField name="id" value="${alumno.id}" />

    <g:hasErrors bean="${alumno}">
        <div class="alert alert-danger">
            <g:renderErrors bean="${alumno}" as="list"/>
        </div>
    </g:hasErrors>

    Matricula: ${alumno.matricula}<br/>
    Nombre: <g:textField name="nombre" value="${fieldValue(bean: alumno, field: "nombre")}" /> <br/>
    Edad: <g:textField name="edad" value="${fieldValue(bean: alumno, field: "edad")}"/> <br/>

    <g:submitButton name="enviar"/>
</g:form>

</body>

</html>
package us.incorpora.sigrem

import grails.transaction.Transactional

/**
 * Created by iberck on 06/03/2017.
 */
@Transactional
class AlumnoService {

    @Transactional
    void updateAlumno(Alumno alumno) {
        // aquí alumno no es guardado pero entra a una transacción
    }

}
package us.incorpora.sigrem

class Alumno {

    String nombre
    String matricula
    int edad

    static constraints = {
        edad min: 0
        matricula nullable: true
    }

}