De los Tilos sabiduría

bases += patrones += relaciones += datos

Archive for the ‘Grails’ Category

Grails with Envers

without comments

I needed some form of  revision system for a Grails project I’m working on. So I searched the Internet hoping to find a nice solution for it and found an entry on stackoverflow: How to manage object revisions in Grails?. The options mentioned:

  1. the Grails Envers plugin
  2. the Gvers plugin
  3. build yourself
  4. use a version control system

Grails is build on top of Hibernate so the Envers option should be the logical choice. Sadly the Grails Envers plugin does not work :(

The author of the stackoverflow post chooses the Gvers plugin. I’ve tried it and it works, but it does not use a central revision number like subversion does.

Searching on Grails and Envers gave me an extra link: Grails 1.1.1 + Envers 1.2.1.GA. They have tried, but it only works for JPA annotated classes.

Let’s try it myself and see what happens. So I installed the latest Grails version 1.3.7. It uses hibernate 3.3 under the covers. So I went to the Envers site and downloaded the 1.2.2.GA release. Put the envers-1.2.2.ga-hibernate-3.3.jar in the Grails library folder. Envers uses hibernate event listeners. Let’s find out how to add them. Luckily there is an easy way to do this as of version 1.3.4 (Allow plugins to easily insert hibernate event listeners). This is what I’ve put in grails-app > conf > spring > resources.groovy:

beans = {
    auditListener(org.hibernate.envers.event.AuditEventListener)
    hibernateEventListeners(org.codehaus.groovy.grails.orm.hibernate.HibernateEventListeners) {
        listenerMap = ['post-insert':auditListener,
                       'post-update':auditListener,
                       'post-delete':auditListener,
                       'pre-collection-update':auditListener,
                       'pre-collection-remove':auditListener,
                       'post-collection-recreate':auditListener]
    }
}

Know let’s test it and create a domain class.

package nl.delostilos

import org.hibernate.envers.Audited

@Audited
class Entity {

 String name

}

Create a scaffolded controller:

package nl.delostilos

class EntityController {

 def scaffold = true
}

Ok, ready to go. Started the grails application and check what’s in the database. Cool, grails made the table ‘entity’ and envers added the tables ‘entity_aud’ and ‘revinfo’. Now let’s insert some data via the scaffolded interface and see what happens.The dat is inserted in the grails table but not in the envers tables :(
What’s happening? I found the answer in this post on the envers forum Spring and Envers. It mentions that you have to do everything transactional, so that envers can record it.

Ok, let’s make the scaffolded controller actions transactional. First install the templates:

grails install-templates

In the folder src > templates > scaffolding You’ll find the Controller.groovy template. We’ll make the save, update and the delete transactional.
My code snippet of the save action:

   def save = {
        def ${propertyName} = new ${className}(params)
        ${className}.withTransaction{
            if (${propertyName}.save(flush: true)) {
                flash.message = "\${message(code: 'default.created.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])}"
                redirect(action: "show", id: ${propertyName}.id)
            }
            else {
                render(view: "create", model: [${propertyName}: ${propertyName}])
            }
        }
    }

The update action code snippet:

    def update = {
        def ${propertyName} = ${className}.get(params.id)
        if (${propertyName}) {
            if (params.version) {
                def version = params.version.toLong()
                if (${propertyName}.version > version) {
                    <% def lowerCaseName = grails.util.GrailsNameUtils.getPropertyName(className) %>
                    ${propertyName}.errors.rejectValue("version", "default.optimistic.locking.failure", [message(code: '${domainClass.propertyName}.label', default: '${className}')] as Object[], "Another user has updated this ${className} while you were editing")
                    render(view: "edit", model: [${propertyName}: ${propertyName}])
                    return
                }
            }
            ${propertyName}.properties = params
            ${className}.withTransaction{
                if (!${propertyName}.hasErrors() && ${propertyName}.save(flush: true)) {
                    flash.message = "\${message(code: 'default.updated.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), ${propertyName}.id])}"
                    redirect(action: "show", id: ${propertyName}.id)
                }
                else {
                    render(view: "edit", model: [${propertyName}: ${propertyName}])
                }
            }
        }
        else {
            flash.message = "\${message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])}"
            redirect(action: "list")
        }
    }

And finally the delete action code snippet:

    def delete = {
        def ${propertyName} = ${className}.get(params.id)
        if (${propertyName}) {
            try {
                ${className}.withTransaction{
                    ${propertyName}.delete(flush: true)
                }
                flash.message = "\${message(code: 'default.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])}"
                redirect(action: "list")

            }
            catch (org.springframework.dao.DataIntegrityViolationException e) {
                flash.message = "\${message(code: 'default.not.deleted.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])}"
                redirect(action: "show", id: params.id)
            }
        }
        else {
            flash.message = "\${message(code: 'default.not.found.message', args: [message(code: '${domainClass.propertyName}.label', default: '${className}'), params.id])}"
            redirect(action: "list")
        }
    }

OK, know let’s try again. Added some data via the scaffolded controller and … Yes, it works :) the data is audited in the ‘entity_aud’ table and the ‘revinfo’ table has a revision added. Cool. Now I’ll have to find out how to change some envers properties/behavior and make the default GORM actions transactional.

Update: Lucas Ward wrote a new Grails-Envers-plugin:)
Now you won’t have to add the Spring stuff in the resource.groovy file anymore. It’s added by the plugin, and it add’s some handy methods on the GORM classes. It also adds the envers library. You still have to make every action to the database transactional to have it recorded in the envers tables.

Written by delostilos

March 15th, 2011 at 9:04 am

Posted in Grails

Tagged with ,