Handling Grails transactions programmatically

2019-04-06 04:33发布

问题:

When I need to save a list of objects, and each object should be saved in it's own transaction (so that if one fails they don't all fail), I do it like this:

List<Book> books = createSomeBooks()
books.each { book ->
  Book.withNewSession {
    Book.withTransaction {TransactionStatus status ->
      try {
        book.save(failOnError: true)
      } catch (ex) {
        status.setRollbackOnly()
      }
    }
  }
} 

I use Book.withNewSession because if one book fails to save and the transaction is rolled back, the session will be invalid which will prevent subsequent books from saving. However, there are a couple of problems with this approach:

  1. It's a bit verbose
  2. A new session will always be created for each book, even if the previous book succeeded

Is there a better way? One possibility that occurred to me is to dependency-inject the Hibernate SessionFactory and do this instead

List<Book> books = createSomeBooks()
books.each { book ->
  try {
    Book.withTransaction {
      book.save(failOnError: true)
    }
  } catch (ex) {
    // use the sessionFactory to create a new session, but how....?
  }
}

回答1:

This should do it:

List<Book> books = createSomeBooks()
books.each { book ->
  Book.withNewTransaction {TransactionStatus status ->
    try {
      book.save(failOnError: true)
    } catch (ex) {
      status.setRollbackOnly()
    }
  }
} 

The session isn't invalid if you rollback, it is just cleared. So any attempts to access entities read from the DB would fail, but writes of not-yet-persisted entities will be just fine. But, you do need to use separate transactions to keep one failure from rolling back everything, hence the withNewTransaction.



回答2:

Could you try validating them first, and then saving all the ones that passed? I'm not sure if it's any more performant, but it may be a little cleaner. Something like:

List<Book> books = createSomeBooks()
List<Book> validatedBooks = books.findAll { it.validate() }
validatedBooks*.save()

Although I'm not sure if .validate() promises the save won't fail for other reasons, and if the data is independent (ie a unique constraint passes until the next book tries to save as well).



回答3:

Maybe you could use groovy meta-programming & grails dynamic domain methods?

In Bootstrap:

    def grailsApplication

    def init = {

    List.metaClass.saveCollection = {
        ApplicationContext context = (ApplicationContext) ServletContextHolder.getServletContext().getAttribute(GrailsApplicationAttributes.APPLICATION_CONTEXT);
        SessionFactory sf = context.getBean('sessionFactory')
        Session hsession = sf.openSession()
        def notSaved = []
        delegate.each {
            if(!it.trySave()) {
                notSaved << it
                hsession.close()
                hsession = sf.openSession()
            }
        }
        hsession.close()
        return notSaved
    }

    grailsApplication.getArtefacts("Domain")*.clazz.each { clazz ->
        def meta = clazz.metaClass
        meta.trySave = {
            def instance = delegate
            def success = false
            clazz.withTransaction { TransactionStatus status ->
                try {
                    instance.save(failOnError: true) // ', flush: true' ?
                    success = true
                } catch (ex) {
                    status.setRollbackOnly()
                }
            }
            return success
        }
    }
    }

And then:

class TheController {
    def index() {
        List<Book> books = createSomeBooks()

        def notSaved = books.saveCollection()
        books.retainAll { !notSaved.contains(it) }

        println "SAVED: " + books
        println "NOT SAVED: " + notSaved
    }
}

Of course there must be some checks performed (e.g. list contains domain classes, etc.). You can also pass to closures specific params to make this more flexible (e.g. flush, failOnError, deepValidate, etc.)