Thursday, February 4, 2010

Use Exceptions To Control Validation Flow.

When using default grails scaffolding a controller action, for example save, will look some thing like this


def save = {
        def fooInstance = new Foo(params)
        if(!fooInstance.hasErrors() && fooInstance.save()) {
            flash.message = "Foo ${fooInstance.id} created"
            redirect(action:show,id:fooInstance.id)
        }
        else {
            render(view:'create',model:[fooInstance:fooInstance])
        }
    }

This has a number of problems.

First it is difficult to move this code into a service, because you cannot call render from a service.  Not moving this code to a service decreases reuse, and means that the database code is not transactional.

There are many reasons why you should use exceptions instead of return code logic to handle this tpe of error, some of them are detailed here.  The nested if else statements are hard to read, and it is easy for code to continue executing as if nothing had happened.

Runtime Exceptions in a service cause the transaction to be rolled back, which is the desired behavior when a validation error has occurred.

So what we want to do is move the code that saves foo to a service, and throw an exception if the input is not valid.  We will then catch the exception in the controller and render the create view.


Listing 2 FooService
class FooService {
  boolean transactional = true

  Foo saveFoo(Map params) {
    def fooInstance = new Foo(params)
    if (!fooInstance.hasErrors() && fooInstance.save()) {
      return fooInstance
    } else {
      throw new ValidationException("Foo is not Valid", fooInstance)
    }
  }
}

Tip: If you are using grails 1.2 support for this is built into the framework  You can now call save(failOnError:true)  and and exception will be thrown.  Or better yet you can set the configuration parameter grails.gorm.save.failOnError = true to enable in your entire application


Listing 3 FooController
class FooController {
  def fooService

  def save = {
    try {
      def fooInstance = fooService.saveFoo(params)
      redirect(action: show, id: fooInstance.id)
    } catch (ValidationException e) {
      log.warn("Validation Failed on Foo ${e.message}" )
      render(view: 'create', model: [fooInstance: e.invalidObject])
    }
  }
}


Listing 4 Validation Exception
class ValidationException extends RuntimeException {
  Object invalidObject
  
  ValidationException(String message, Object invalidObject){
    super(message)
    this.invalidObject = invalidObject
  }
}


If you fly and have an iphone you should check out this awesome holder for watching videos in flight. I use mine all the time!!