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:
- It's a bit verbose
- 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....?
}
}
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.
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).
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.)