01 July 2011

Recursive queries from a custom validator

Just spent the morning tracking down a stack overflow while trying to implement a grails custom validator.

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.