Pma
Pma

Reputation: 1093

Grails command objects - is there a common pattern?

I a wondering is there any preffered pattern in using command objects in grails application. Particularry if i should define another method for saving objects or use the same controller method for presenting form and saving?

Let me show an example with separate methods for presenting form and saving an object

def user(int id) {} // shows the edit user form - user.gsp. Submit takes us to saveUser method
def saveUser(UserCommand cmd) {} // actually saves the user, then redirects somewehre else

Everything should work but:

If there is a validation error the saveUser method will have to perform the whole logic that the user method did. If before displaying user form we have to load additional objects from database, perform some calculations etc... it would have to be done another time because we have to display the form again and include validation errors. Which leads to unnecessary code duplication. I cannot redirect to user method when validation fails because i would loose the command object and any errors associated with it. therefore i would not be able to display validation errors.

Another example using the same method for saving and presenting form

def user(UserCommand cmd, Integer id) {
   def u=User.load(id)
   if (request.method=="POST"&&cmd.validate()) {
      // populate u with command object values and save in database
      // then redirect somewhere
   }
}

This example removes the need for code duplication. Saving user is only done if there was a POST request, in other case only the form is presented. In case of validation errors they are accessible and can be shown on the gsp page.

The main problem is that even when there is a GET request to the user page (which means that the user.gsp form is shown) an empty command object instance is created and validated (because it was defined in the same controller). Therefore every time the form is shown there are validation errors that provided values are empty (because command object is empty)

Both scenarios can be easily modified to work correctly (for example: saving command object before redirect in session in scenario 1 and only displaying validation errors if there was a POST request in scenario 2) but it takes additional code and does not seem to be very elegant.

Is there a simpler - more "grails" solution to this problem?

Upvotes: 3

Views: 1512

Answers (3)

Gregor Petrin
Gregor Petrin

Reputation: 2931

I started adding an isBlank getter to my command objects:

class UserCommand {
    String username
    String password

    boolean isBlank() { !username && !password }
}

Then in my controller I recreate blank commands so no validation is run:

def createUser(UserCommand cmd) {
    if (cmd.blank) cmd = new UserCommand()
    //...render form, there will be no errors on inital view
}

def saveUser(UserCommand cmd) {
    if (cmd.hasErrors()) {
        return createUser(cmd)
    } else {
    //...and so on
}

The only downside is that if users submit a blank form, there will be no validation errors, but on the other hand it should be obvious that a blank form must be filled with something, why would it be there otherwise?

Upvotes: 0

raaputin
raaputin

Reputation: 105

Problem: Duplicating model population logic in controller actions. E.g. inserting a list of authors to the model in multiple actions.

Solution I sometimes find elegant:

def afterInterceptor = { model ->
    model.authors = authorService.list()
}

Check the Grails documentation for more: http://grails.org/doc/latest/ref/Controllers/afterInterceptor.html

Upvotes: 1

Manuel Vio
Manuel Vio

Reputation: 506

Here's a sample of what I usually do in this case:

def create(MeetingCommand cmdMtn) {
    switch (request.method) {
    case 'GET':
        cmdMtn = new MeetingCommand() 
        bindData cmdMtn, params, [include: boundProperties]
        createEditModel(cmdMtn: cmdMtn)
        break
    case 'POST':
            def meetingInstance
            Meeting.withTransaction { status ->
                try{
                    Boolean okValidated = (cmdMtn.validate() && !cmdMtn.hasErrors())
                    if (okValidated) {
                        meetingInstance = meetingService.bindInstance(cmdMtn)
                        flashMessage 'meeting.created', FLASH_CLASS_SUCCESS, [meetingInstance.id], 'Meeting creato'  
                        redirect action: ACTION_SHOW, id: meetingInstance.id
                    }
                    else {
                        log.error cmdMtn
                        log.error cmdMtn.errors
                        status.setRollbackOnly()
                        createEditModel(cmdMtn: cmdMtn)
                    }
                } catch (e){
                    status.setRollbackOnly()
                    log.error e
                    createEditModel(cmdMtn: cmdMtn)
                }
            }
        break
    }
}
protected createEditModel(mdl = [:]) {
    mdl
}

I've taken inspiration from http://blog.freeside.co/post/41774629876/semi-restful-scaffolded-controllers and the domain persistence logic has been moved into the service layer.

Upvotes: 1

Related Questions