Monday, April 29, 2013

on save, validation & uuid in Grails

I want to to add a "uuid" field to my domain classes exposed to the world to prevent scanning with sequential ids. Using the Hibernate id generator works, but that means that the "id" field in the domain class has to be a String, and some plugins could have assumed that the "id" property is always a Long, causing grief and hours of debug. Also, while developing, it's a lot easier for us humans to deal with short and sequential ids.
So I added a "uid" field in my domain class, but I want to catch the case where the UUID is already used. Very very small probability, but I wanted to put in place some code that could catch exceptions while saving the domain instances even for other reasons. I also didn't want to interfere with the normal controller flow.

First, I generate an app with Grails 2.2.2 and create a "Country" domain class, its controller and views. I used "grails generate-all Country" for that. The scaffolded controller assumes that the "id" is a Long, explaining why I prefer to leave the "id" alone and use another property instead. That issue might be fixed in the future.
Here is the domain class code:
package app
class Country {
    // Long id is still generated by Grails
    String uid = null
    String name
    static constraints = {
        uid unique: true
        name size: 2..25
    }
    static mapping = {
        version false
    }
    // using beforeInsert doesn't work, since validation runs before.
    def beforeValidate() {
        if (uid == null) {
            uid = UUID.randomUUID().toString() // works
            //uid = 1
            println "Country beforeValidate uid: $uid"
        }
    }
}

The "uid" property is added and null assigned to it, then it is checked in the "beforeValidate" method and not in the "beforeInsert" method. The Grails documentation specifies that the validation is run *before* saving and since the "uid" cannot be null (by default), the validation would fail if not assigned a value prior validation. Be aware too that the validation can be run several times before saving, according to the Grails documentation. By checking if the "uid" is already set, we don't do the work twice. You will notice the line: "uid=1". This is to test the "duplicate uid" catching code in the controller, see below.

The controller "save" method, the only modified one:

    def save() {
        def countryInstance = new Country(params) // (1)
        // saves returns the instance if successful, null if failed
        def saveOK = countryInstance.save(flush: true)
        if (saveOK) {
            println "save OK"
            flash.message = message(code: 'default.created.message', args: [message(code: 'country.label', default: 'Country'), countryInstance.id])
            redirect(action: "show", id: countryInstance.id)
            return
        }
        else {
            def error = countryInstance.errors.getFieldError('uid')
            if (error) {
                println "error code: $error.code"
                if (error.code == 'unique') {
                    countryInstance.uid = UUID.randomUUID().toString()
                    println "CountryController RESAVE countryInstance.uid: $countryInstance.uid"
                    if (countryInstance.save(flush: true)) {
                        println "save OK"
                        flash.message = message(code: 'default.created.message', args: [message(code: 'country.label', default: 'Country'), countryInstance.id])
                        redirect(action: "show", id: countryInstance.id)
                        return
                    }
                    else {
                        println "save NOT OK"
                        // could be another validation error
                        def errors = countryInstance.errors // all the field errors with names & codes
                        println "errors: $errors"
                        render(view: "create", model: [countryInstance: countryInstance])
                        return
                    }
                }
            } else {
                println "no UID error"
                // show the error
                def codes = countryInstance.errors.getFieldErrors().code
                println "errors.code: $errors"
                render(view: "create", model: [countryInstance: countryInstance])
                return
            }
        }
   }
The flow is:
1) a "new Country(params)" instance is created , "save" calls the validation, the Country.beforeValidation method is called, the "uid" assigned, then the real "save" is performed.
2) if the "save" is successful, nothing to do
3) if the "save" fails, there is a check to see if the "uid" property has the "unique" error, in which case, we assign another UUID and retry the save. If there is another error, there is no retry, that must be something else. Chances to generate twice an existing id are probably nil.
4) if the "save" succeeds, then it means we had an existing UUID in the DB and generated another one successfully.

To test the flow, I assigned 1 to the "uid" in the Country domain class, and watched the controller assign a UUID to it after catching the exception.

Beyond the UUID scheme, in the controller code, we can see how to catch a validation exception.It shows how to check the field in error and the type of error.

Refer to the Grails javadocs and also these:

http://grails.org/doc/latest/api/grails/validation/ValidationException.html#errors
http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/validation/FieldError.html
http://static.springsource.org/spring/docs/3.0.x/javadoc-api/org/springframework/context/support/DefaultMessageSourceResolvable.html#getCode()

This was a quick hacking/testing session, let me know if I missed something or any improvement.

Happy Grailling.


No comments: