Search code examples
hibernategrailsgrails-controller

Grails controller saves the domain item anyways


Using Grails 2.3.9

In a controller I do some checks and when failing returning with a custom error response. Here is a code snippet:

def update() {
    def instance = Group.get(params.groupId)
    instance.properies = params

    if (instance.hasErrors()) {
        // going through here: instance is not saved
        respond instance.errors, view:'edit'
        return
    }

    // do custom checks
    if(checksFailed) {
        // instance is saved anyways :(
        instance.discard()
        response.sendError(response.SC_CONFLICT)
        return
    }

    instance.save flush:true
    // ... respond success
}

When the check fails, my error response is returned as I want it. However, the instance gets saved anyways (the instance.save is not reached). How can I make Grails/Hibernate know to not save this instance?

Another approach might be to disconnect the instance from the Hibernate session. How would I have to do this in Grails?

Update: in a controller where the domain instance is scaffolded, the instance.discard() trick works:

def update(Group groupInstance) {
    if (groupInstance.hasErrors()) {
        // going through here: instance is not saved
        respond instance.errors, view:'edit'
        return
    }

    // do custom checks
    if(checksFailed) {
        // instance is not saved
        groupInstance.discard()
        response.sendError(response.SC_CONFLICT)
        return
    }

    groupInstance.save flush:true
    // ... respond success
}

I find it difficult to understand why it works in one case and not in another.


Solution

  • The best way to do this is not to use discard() but instead to use transactions and the native transaction features of your database. I would recommend moving the logic into a service (separating your controller flow code and the logic of your application. Example:

    class GroupService {
       @Transactional
       void updateBook(params) {
           ....
           // do custom checks
           if(checksFailed) {
              status.setRollbackOnly() // rollback transaction        
           }
           else {
               instance.save flush:true
           }
       }
    }
    

    You can either call setRollbackOnly on the transaction status or throw a runtime exception.

    Note if you use exceptions, they can be expensive so it is best to create a custom exception type say GroupUpdateException and override the fillInStackTrace to not fill the stack trace which reduces the cost of exception. See Why is Throwable.fillInStackTrace() method public? Why would someone use it?