views:

1403

answers:

3

Summary A parent can have many children. How do you write a service such that, if after adding a parent there is an error when adding a child, the entire transaction is rolled back. For example, add parent p1, successfully add child c1, then when adding child c2 an error occurs, both p1 and c1 should be rolled back.

Detailed Problem

In the following code, there is a unique constraint on the name property of the child. So if you try to add the same name twice with a different parent, then the child record should not be added and the parent record should be rolled back.

My problem is that the parent record is not being rolled back.

I am using MySQL w/ InnoDB with Grails 1.2-M2 and Tomcat 6.018.

Data Source

import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration
dataSource {
    configClass = GrailsAnnotationConfiguration.class
    pooled = true
    driverClassName = "com.mysql.jdbc.Driver"
    dialect = org.hibernate.dialect.MySQLInnoDBDialect
    zeroDateTimeBehavior="convertToNull" //Java can't convert ''0000-00-00 00:00:00' to TIMESTAMP
    username = "root"
    password = "12345"
    loggingSql=false
}

hibernate {
    cache.use_second_level_cache=true
    cache.use_query_cache=true
    cache.provider_class='com.opensymphony.oscache.hibernate.OSCacheProvider'
}
// environment specific settings
environments {
    development {
     dataSource {
      dbCreate = "create-drop" // one of 'create', 'create-drop','update'
       url = "jdbc:mysql://localhost:3306/transtest?zeroDateTimeBehavior=convertToNull"

     }
    }
    test {
     dataSource {
      dbCreate = "update"
      url = "jdbc:mysql://localhost:3306/transtest?zeroDateTimeBehavior=convertToNull"

     }
    }
    production {
     dataSource {
      dbCreate = "update"
      url = "jdbc:mysql://localhost:3306/transtest?zeroDateTimeBehavior=convertToNull"
     }
    }
}

I have the following simple domain classes:

Parent:

class Parent {

    static hasMany = [ children : Child ]

    String  name

    static constraints = {
        name(blank:false,unique:true)
    }
}

Child

class Child {

    static belongsTo = Parent

    String name

    Parent parent

    static constraints = {
        name(blank:false,unique:true)
    }
}

Simple Data Entry GSP

<%@ page contentType="text/html;charset=UTF-8" %>

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>Sample title</title>
  </head>
  <body>
    <h1>Add A Record</h1>
  <g:form action="add" name="doAdd">
    <table>
      <tr>
        <td>
          Parent Name
        </td>
        <td>
          Child Name
        </td>
      </tr>
      <tr>
        <td>
          <g:textField name="parentName"  />
        </td>
        <td>
          <g:textField name="childName" />
        </td>
      </tr>
      <tr><td><g:submitButton name="update" value="Update" /></td></tr>
    </table>
  </g:form>
</body>
</html>

Controller

class AddrecordController {

    def addRecordsService

    def index = {
        redirect action:"show", params:params
    }

    def add = {
        println "do add"


        addRecordsService.addAll(params)
        redirect action:"show", params:params

    }

    def show = {}

}

Service

class AddRecordsService {

   // boolean transactional = true //shouldn't this be all I need?
      static transactional = true // this should work but still doesn't nor does it work if the line is left out completely
    def addAll(params) {
        println "add all"
        println params
        def Parent theParent =  addParent(params.parentName)
        def Child theChild  = addChild(params.childName,theParent)
        println theParent
        println theChild
    }

    def addParent(pName) {
        println "add parent: ${pName}"
        def theParent = new Parent(name:pName)
        theParent.save()
        return theParent
    }

    def addChild(cName,Parent theParent) {
        println "add child: ${cName}"
        def theChild = new Child(name:cName,parent:theParent)
        theChild.save()
        return theChild
    }

}
+1  A: 

I believe it should be:

class AddRecordsService {
    static transactional = true;// note *static* not boolean
}
Gareth Davis
Thanks!Yes, it definitely is supposed to be static. However, it still doesn't work. I think it's actually supposed to default to true if not specified. But removing the line all together doesn't work either.Perhaps this a grails bug?I corrected my code above.
Brad Rhoads
Turns out that boolean actually does work, but should be static. The real problem was not throwing an exception as explained in the next answer.
Brad Rhoads
+2  A: 

You also need to make sure a RuntimeException is thrown inside the service in order for the transaction to be automatically rolled back.

So I'd do this:

def addParent(pName) {
        println "add parent: ${pName}"
        def theParent = new Parent(name:pName)
        if(!theParent.save()){
            throw new RuntimeException('unable to save parent')
        }
        return theParent
    }

def addChild(cName,Parent theParent) {
    println "add child: ${cName}"
    def theChild = new Child(name:cName,parent:theParent)
    theChild.save()
    if(!child.save()){
        throw new RuntimeException('unable to save child')
    }
    return theChild
}

and then catch exceptions in the controller and render the errors.

The other way is to turn of automatic transactions and use Parent.withTransaction and manually mark the transaction for rollback if there is a validation error.

leebutts
Thanks for adding those important details.
Brad Rhoads
>You also need to make sure a RuntimeException is thrown inside the >service in order for the transaction to be automatically rolled >back.That was my problem! Seems like grails should do this by convention.
Brad Rhoads
I think there is a configuration option coming version 1.2 to make save() throw exceptions instead of returning null if validation fails
leebutts
I have a follow up question here:http://stackoverflow.com/questions/1640666/how-to-know-the-cause-of-a-validation-error
Brad Rhoads
+1  A: 

Alternatively you could use the failOnError property when saving your domain objects - if the save fails for a validation error, then it will throw an exception.

 def addChild(cName,Parent theParent) {
    println "add child: ${cName}"
    def theChild = new Child(name:cName,parent:theParent)
    theChild.save(failOnError:true)
    return theChild
}

This behavior can also be enabled globally with by setting the grails.gorm.failOnError property in grails-app/conf/Config.groovy to true

For more information, see the User Guide docs for 'save': http://grails.org/doc/latest/ref/Domain%20Classes/save.html

Mike Hugo
It appears that theChild.save(failOnError:true) works, but setting the property file in Config.groovy doesn't.BTW - I have a follow up question here:http://stackoverflow.com/questions/1640666/how-to-know-the-cause-of-a-validation-error
Brad Rhoads