I have a domain class that has a parent/child relationship with itself (instance form a tree). When I update instances, I need to make sure that I haven't created a cycle (the instance has itself as its own parent/ancestor)
To do this, I created a custom validator to check that the object doesnt have itself as its own parent and that its parent is not also a child/descendent
class Afsc { String id String code Afsc parent def afscService def sessionFactory ... static constraints = { ... parent (nullable: true, validator: { val, obj -> if (obj.id && (val?.id == obj.id) || obj.afscService.getChild(obj, val.code)) { return 'AFSC.cannot.be.cyclic' } }) } ... }
The fun part is that getChild() does a recursive search checking to see that the instance doesnt have its parent as a child:
def getChild(parent, targetCode) { def children = Afsc.findAllByParent(parent) def child = children.find { it.code == targetCode } if (!child) { children.each { child = child ?: getChild(it,targetCode) } } return child }
When I tried this, I started getting stack overflow exceptions. Long story short, it turns out that using findAllByParent() causes hibernate to flush changes before executing the query. Well, guess what happens when changes are flushed? That's right - the validator gets called. Which calls getChild(). Which calls findAllByParent(). Which causes a flush. Which gives us infinite recursion and a stack overflow.
Once I figured out what was going on, it wasnt too hard to find the solution - tell grails/hibernate not to automatically flush changes:
parent (nullable: true, validator: { val, obj -> if (obj.id && val) { if (val.id == obj.id) { return 'AFSC.cannot.be.cyclic' } def originalFlushMode = obj.sessionFactory.currentSession.flushMode obj.sessionFactory.currentSession.flushMode = org.hibernate.FlushMode.MANUAL try { if (obj.afscService.getChild(obj, val.code)) { return 'AFSC.cannot.be.cyclic' } } catch(Exception e) { return e.getMessage() } finally { obj.sessionFactory.currentSession.flushMode = originalFlushMode } } })
Update 04-Apr-2014:
Today I was writing a custom validator and something was telling me that querying the database from a validator might cause problems, so I decided to google a bit to see if I was right. I guess it was my subconscious reminding me that I'd already solved this problem once before.
Having seen a couple of other solutions for this problem, it seems like a better solution might be
to use the withNewSession() domain class dynamic function so that the query can be performed in a new session. See http://adhockery.blogspot.com/2010/01/upgrading-grails-11-12.html for an example.