Как заставить транзакции работать в Grails

Сводка У родителя может быть много детей. Как написать такой сервис, чтобы, если после добавления родителя возникала ошибка при добавлении дочернего, откатывалась вся транзакция. Например, добавить родительский p1, успешно добавить дочерний c1, затем при добавлении дочернего c2 возникает ошибка, следует откатить и p1, и c1.

Подробное описание проблемы

В следующем коде есть уникальное ограничение на свойство name дочернего элемента. Поэтому, если вы попытаетесь дважды добавить одно и то же имя с другим родителем, дочернюю запись не следует добавлять, а родительскую запись следует откатить.

Моя проблема в том, что родительская запись не откатывается.

Я использую MySQL с InnoDB с Grails 1.2-M2 и Tomcat 6.018.

Источник данных

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

У меня есть следующие простые классы домена:

Родительский:

class Parent {

    static hasMany = [ children : Child ]

    String  name

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

Ребенок

class Child {

    static belongsTo = Parent

    String name

    Parent parent

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

Простой GSP для ввода данных

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

Контроллер

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 = {}

}

Сервис

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
    }

}

person Brad Rhoads    schedule 27.10.2009    source источник


Ответы (3)


Вы также должны убедиться, что внутри сервиса выброшено исключение RuntimeException, чтобы транзакция автоматически откатилась.

Так что я бы сделал это:

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
}

а затем ловить исключения в контроллере и отображать ошибки.

Другой способ — отключить автоматические транзакции и использовать Parent.withTransaction и вручную пометить транзакцию для отката, если есть ошибка проверки.

person leebutts    schedule 27.10.2009
comment
Спасибо за добавление этих важных деталей. - person Brad Rhoads; 27.10.2009
comment
›Вам также необходимо убедиться, что RuntimeException вызывается внутри ›сервиса, чтобы ›транзакция автоматически откатилась. Это была моя проблема! Похоже, что Grails должен делать это по соглашению. - person Brad Rhoads; 27.10.2009
comment
Я думаю, что в версии 1.2 есть опция конфигурации, позволяющая save() вызывать исключения вместо возврата null, если проверка не удалась. - person leebutts; 28.10.2009
comment
У меня есть дополнительный вопрос: stackoverflow.com/questions/1640666/ - person Brad Rhoads; 29.10.2009
comment
выполнит ли это .save(failOnError:true)? - person Mikey; 03.03.2012
comment
@ Майки, да, я не уверен, что это было, хотя, когда я ответил на это :) - person leebutts; 03.03.2012
comment
Вот чего мне не хватало! Автоматизированные транзакции = сбой и откат при исключении, ручные транзакции = сбой и откат только при ручном запросе на откат. - person Sergey Orshanskiy; 10.10.2013
comment
Кстати, вы бы использовали Parent.withTransaction или Child.withTransaction или оба? - person Sergey Orshanskiy; 10.10.2013
comment
@SergeyOrshanskiy вы можете использовать любой из них, так как он начнет транзакцию с использованием того же источника данных. - person leebutts; 11.10.2013

Я считаю, что это должно быть:

class AddRecordsService {
    static transactional = true;// note *static* not boolean
}
person Gareth Davis    schedule 27.10.2009
comment
Спасибо! Да, он определенно должен быть статичным. Однако это все еще не работает. Я думаю, что на самом деле предполагается, что по умолчанию установлено значение true, если не указано иное. Но удалить всю строку тоже не получится. Возможно, это ошибка Grails? Я исправил свой код выше. - person Brad Rhoads; 27.10.2009
comment
Оказывается, логическое значение действительно работает, но должно быть статическим. Настоящая проблема заключалась не в том, чтобы генерировать исключение, как объяснено в следующем ответе. - person Brad Rhoads; 27.10.2009

В качестве альтернативы вы можете использовать свойство failOnError при сохранении объектов вашего домена — если сохранение не удается из-за ошибки проверки, тогда будет выдано исключение.

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

Это поведение также можно включить глобально, установив для свойства grails.gorm.failOnError в файле grails-app/conf/Config.groovy значение true.

Для получения дополнительной информации см. документы руководства пользователя для сохранения: http://grails.org/doc/latest/ref/Domain%20Classes/save.html

person Mike Hugo    schedule 27.10.2009
comment
Похоже, что theChild.save(failOnError:true) работает, но настройка файла свойств в Config.groovy не работает. Кстати, у меня есть дополнительный вопрос: stackoverflow.com/questions/1640666/ - person Brad Rhoads; 29.10.2009