Hooking into Grails Domain object save()
Asked Answered
V

6

7

I'm writing a grails plugin and I need to hook into the domain save() method to do some logic after the save. I need to do this across multiple domain classes. I'm trying to avoid hibernate events in the cases where a plugin user is not using hibernate with GORM.

I've tried many thing but below is what I think should have had the best chance at working. In all cases grailsSave is null. How can I do this?

def doWithDynamicMethods = { ctx ->
    application.domainClasses.each { dc ->
        def grailsSave = dc.metaClass.pickMethod('save', [Map] as Class[])

        domainClass.metaClass.save = { Map params ->
        grailsSave.invoke(delegate, [params] as Object[])
        println "Saved object, now do my thing"
        //...
        }
    }
}

I have the following set in my *Plugin.groovy class:

def dependsOn = [domainClass: '1.1 > *', hibernate: '1.1 > *']
def loadAfter = ['hibernate']
Vaasta answered 24/12, 2009 at 0:33 Comment(0)
V
6

I was unable to successfully get a reference to the save() methods during plugin/app initialization; I don't know why. Instead, I decided to create a listener for the hibernate events after insert, update, and deletes. This post by Sean Hartsock regarding the Audit Logging plugin was a perfect primer for doing that.

Here's the gist of the Listener:

class MyListener implements PostInsertEventListener, PostUpdateEventListener, PostDeleteEventListener, Initializable {

        public void onPostInsert(final PostInsertEvent event) {
            // logic after insert
            return
        }

        public void onPostUpdate(final PostUpdateEvent event) {
            // logic after update
            return
        }

        public void onPostDelete(final PostDeleteEvent event) {
            // logic after delete
            return
        }


        public void initialize(final Configuration config) {
            return
        }   
    }

Then in the *GrailsPlugin.groovy:

def doWithApplicationContext = { applicationContext ->

    // add the event listeners for reindexing on change
    def listeners = applicationContext.sessionFactory.eventListeners
    def listener = new MyListener()

    ['postInsert', 'postUpdate', 'postDelete'].each({
       addEventTypeListener(listeners, listener, it)
    })

}


// copied from http://hartsock.blogspot.com/2008/04/inside-hibernate-events-and-audit.html
private addEventTypeListener(listeners, listener, type) {
    def typeProperty = "${type}EventListeners"
    def typeListeners = listeners."${typeProperty}"

    def expandedTypeListeners = new Object[typeListeners.length + 1]
    System.arraycopy(typeListeners, 0, expandedTypeListeners, 0, typeListeners.length)
    expandedTypeListeners[-1] = listener

    listeners."${typeProperty}" = expandedTypeListeners
}

Fairly simple at the end of the day...

Vaasta answered 27/12, 2009 at 0:0 Comment(2)
Shawn and his Audit Logging plugin rocks.Zillah
Thanks for sharing! I've been to lazy to look this up my self.Palpate
V
2

There are three different version of save added to the metaClass,

save(Map)
save(Boolean)
save()

Which one are you calling in your testing? You'll need to add you code to each one.

Another thing to check is whether your plugin is running after the hibernate plugin which adds the three methods to the metaClass

cheers

Lee

Vocation answered 24/12, 2009 at 3:21 Comment(6)
Thanks Lee, I think I'm having problem forcing plugin to load after hibernate. If I run in doWithDynamicMethods all attempts to call pickMethod are null. If I run in grails console they are not null. I have this in my plugin but no luck: def dependsOn = [domainClass: '1.1 > *', hibernate: '1.1 > *'] def loadAfter = ['hibernate']Vaasta
Hmmm loadAfter is the one you want - does it need to be static loadAfter = ['hibernate']Vocation
static didn't work either. Strange I just noticed Robert Fischer had tried to do similar things with save() in his GORM Labs plugin but had commented the code out in previous versions and doesn't exist in the most recent. Any ideas for how to approach this differently?Vaasta
What about some kind of Hibernate interceptor? I don't know the API that well but it might provide the ability to plugin in a global interceptor/listener?Vocation
That's what I ended up doing, creating a listener for the PostInsertEventListener, PostUpdateEventListener and PostDeleteEventListener. I tried with everything I had to wrap the save() methods and tap into the afterUpdate() type events but then after digging deeper found that all of that logic was dependent upon hibernate anyway so I might as well go the hibernate listener route. Thanks Lee.Vaasta
+1 — you do need to handle all three map implementations. Which is kinda annoying. The reason the save stuff is commented out in GORM Labs is because it got into Grails 1.2, so it's not necessary. I should just wipe it at this point. And provide some better hooks into extending GORM.Zillah
P
2

Have a look at the Falcone Util plugin. This plugin allows you to hook into Hibernate events (see documentation at the bottom of the page). I don't know if this is exactly what you want, but you might get some hints.

Ps! I don't think the plugin works with Grails 1.2 yet.

Palpate answered 25/12, 2009 at 16:53 Comment(1)
Thanks mate, that plugin looks excellent. Would be awesome if this type of event capability was available in the core.Vaasta
Z
2

This is an issue of premature optimization: older versions of Groovy seriously penalized MetaClass mangling, and so GORM does not add all of its magic until it detects the need to.

Easiest solution is to have your plugin dependOn GORM Labs (I work around it there). The alternative solution is to trigger methodMissing manually (which would be duplicating the work I did). See the GORM Labs documentation for details on how I accomplished that.

Zillah answered 28/12, 2009 at 16:33 Comment(0)
P
1

Wouldn't this best be added to the service class that owns the unit of work? That's where the usual Spring/Grails idiom would have such logic. You needn't modify the save at all.

Panoptic answered 24/12, 2009 at 1:30 Comment(5)
Though I'm building a pluging to be used by others. My logic may very well be in a service but the point it needs to fire is on the save. ThanksVaasta
The idea of delegating a bunch of logic directly relating to the domain to another tier is one of the biggest annoyances in Spring, and it's why Java has a reputation for fetishisizing complexity.Zillah
How is an an individual domain object to know when it's part of a larger unit of work and when it's not?Panoptic
If it's actually part of a conceptually larger unit of work and if there is no "owning" domain object, then that's fine to drop that into a service. But to answer "I want to extend what 'save' means for domain objects" with "create a service!" is a hack, not an answer.Zillah
"Owning" domain object? What does that mean? Hack? Your opinion, nothing more.Panoptic
G
1

Additional GORM methods are lazily initialized on first call to any of them. To initialize them in doWithDynamicMethods simply call one of the static methods on your domain class(es):

def doWithDynamicMethods = { ctx ->

    application.domainClasses.each { dc -> 

        // call any static method to initialize dynamic gorm methods
        dc.clazz.count()

        def grailsSave = dc.metaClass.pickMethod('save', [Map] as Class[])
        //...
    }
}

Your save() method will be available now. As this is called at start up a single count shouldn't be to much of a problem.

Grisby answered 4/8, 2010 at 20:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.