Grails controllers are not very DRY. It’s easy to find duplicated code fragments in default generated controller. Take a look at code sample below. It is duplicated four times in show
, edit
, update
and delete
actions:
class BookController { def show() { def bookInstance = Book.get(params.id) if (!bookInstance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), params.id]) redirect(action: "list") return } [bookInstance: bookInstance] } }
Why is it duplicated?
There is a reason for that duplication, though. If you move this snippet to a method, it can redirect to "list"
action, but it can’t prevent controller from further execution. After you call redirect
, response status changes to 302, but after method exits, controller still runs subsequent code.
Solution
At TouK we’ve implemented a simple trick to resolve that situation:
- wrap everything with a simple
withStoppingOnRender
method, - whenever you want to render or redirect AND stop controller execution – throw
EndRenderingException
.
We call it Big Return – return from a method and return from a controller at once. Here is how it works:
class BookController { def show(Long id) { withStoppingOnRender { Book bookInstance = Book.get(id) validateInstanceExists(bookInstance)[bookInstance: bookInstance] } } private void validateInstanceExists(Book instance) { if (!instance) { flash.message = message(code: 'default.not.found.message', args: [message(code: 'book.label', default: 'Book'), params.id]) redirect(action: "list") throw new EndRenderingException() } } } class EndRenderingException extends RuntimeException {}
Example usage
For simple CRUD controllers, you can use this solution and create some BaseController
class for your controllers. We use withStoppingOnRender
in every controller so code doesn’t look like a spaghetti, we follow DRY principle and code is self-documented. Win-win-win! Here is a more complex example:
class DealerController { @Transactional def update() { withStoppingOnRender { Dealer dealerInstance = Dealer.get(params.id) validateInstanceExists(dealerInstance) validateAccountInExternalService(dealerInstance) checkIfInstanceWasConcurrentlyModified(dealerInstance, params.version) dealerInstance.properties = params saveUpdatedInstance(dealerInstance) redirectToAfterUpdate(dealerInstance) } } }