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
}
}
})