Grails: Rollback associations done by data binding in a service - grails

Given the following two domain classes:
class Book {
String title
static hasMany = [authors: Author]
static belongsTo = Author
static constraints = {
title(nullable: false)
}
}
class Author {
static hasMany = [books: Books]
}
We create and persist domain objects in services and make use of the data binding feature of Grails. Such a method looks like the following one:
def createAndPersistBook(params) throws ValidationException {
log.debug("Attempt to create and persist book")
Book book = new Book(params)
book.save(flush: true, failOnError: true)
log.debug("Created: ${book}")
book
}
When we pass the params map
params = ["authors": "[2]"]
to the service method (there is no title defined thus validation will fail) the association from the newly created book to the already existing author (and vice-versa) is done by data binding. But since the title is nullable: false and not defined a ValidationException is thrown and the transaction is rolled back.
What we expected now is that the book is not being saved, but Book.list().isEmpty() returns false. We think that this is because of the dirty-check by hibernate, meaning the books collection of the existing author has changed and will be persisted and this save gets cascaded to the book instance.
What is the best way to prevent grails from saving the book in this scenario? Or why is the association done by data binding not properly rolled back when validation fails?

If you've specified that your service is transactional, any uncaught exception will cause a transaction to rollback within a service method. The only thing that might stand in your way is if your RDBMS does not support true transactions/rollback.
Have you specified whether the service is transactional? You should have a statement such as below to declare the service is transactional.
def transactional = true

Related

Grails4 GORM Domains - join tables empty

I was away from Grails for some time so I tried to create demo rest-api app with some basic domain relationship (one-to-many, many-to-many) and faced some wierd issues.
In short, I have 4 domain classes as follows:
class Publisher {
String name
static hasMany = [books:Book]
}
class Author {
String name
static hasMany = [books: Book]
}
class Category {
String name
static belongsTo = Book
static hasMany = [books:Book]
}
class Book {
String title
Publisher publisher
static hasMany = [categories: Category]
}
I'm trying to insert some demo data within bootrap.groovy (with multiple different approaches) but, data that should go into joint tables is not persisted (empty). For example, even when using cascade-create, 'edge' records are persisted (for example categories created from boook) but, there is no data in join table between two of them):
Bootstrap.groovy with different approaches to insert records:
// Stand-alone category
def science = new Category(name: 'science').save(failOnError: true)
// publishers
def manning = new Publisher(name: 'manning').save(failOnError: true)
def amazon = new Publisher(name: 'amazon').save(failOnError: true)
// Create single author
def johnDoe = new Author(name: 'John Doe').save(failOnError: true)
// Create-cascade from Author-Book-Category with explicid 'new' and 'save' Book
def jackDanields = new Author(name: 'Jack Daniels')
.addToBooks(new Book(title: 'Hate book', publisher: manning).addToCategories(name: 'love').save())
.addToBooks(new Book(title: 'Fiction book', publisher: manning).addToCategories(name: 'fiction').save())
.save(failOnError: true)
// Create-cascade from Author-Book without explicit save of book
def zoran = new Author(name: 'Zoran')
.addToBooks(title: 'First book', publisher: manning)
.addToBooks(title: 'Second book', publisher: manning)
.addToBooks(title: 'Third book', publisher: manning)
.save(failOnError: true)
I tried with both H2 and MariaDb and result is the same.
Full project available at github: https://github.com/zbubric/grails4-rest-sample
So, it there any catch that I missed or it is some known issue/feature?
zbubric
Answering to your question
So, it there any catch that I missed or it is some known
issue/feature?
I totally agree with Olav's comment that you need to wrap all inserts in BootStrap.groovy into a new transaction to fix the issue.
Let me explain why transaction will enable saving of data in join tables.
In BootStrap.groovy by default there is an open hibernate session (with COMMIT flush mode) but there is no active hibernate transaction.
COMMIT flush mode is important here because with this mode the flush of session is only done on commit of transaction. But in your case no session flush will be done afterwards because there is no active transaction in BootStrap.groovy as I sad before. And no session flush means no saving of entities associations that were added by cascade (also checked it on practice with raw sql queries inside a transaction to the database - had data in join tables only after session flush).
To check if there is a session in BootStrap.groovy (and get more details about it) you can reference
Holders.applicationContext.getBean("sessionFactory").getCurrentSession()
in your code.
To check if there is an active transaction in BootStrap.groovy you can add flush: true to any of the save method calls.
For example:
def science = new Category(name: 'science').save(failOnError: true, flush: true)
In this case you should get an exception:
javax.persistence.TransactionRequiredException: no transaction is in progress
It behaves like that because by default in grails 4.0.0 sessions are configured to not allow flushes without an active transaction.
I believe you have your Category>Book relationship wrong as well.
Book has one Category
Category can have oneorMany Books
Thus Book can either have a 'Category cat' field or 'belongsTo'; belongsTo should not be in the same place as the 'hasMany'

Why does Grails appear to be saving my invalid domain object?

I am using Grails 2.4.2. I'm saving an instance of a domain class that violates the constraints, so I would expect the save to fail. But it appears to succeed. The Grails documentation states:
Another thing to bear in mind is that Grails validates a domain instance every time you save it. If that validation fails the domain instance will not be persisted to the database.
The domain class:
class Book {
String name
static constraints = {
name unique: true
}
String toString() { "[Book: id=${id} name=${name}]" }
}
The following code modifies a second instance so that it has the same name as the first and tries to save it (book3.save). I expect this to fail, and it does return null. But then I load up the same object by Id (Book.get(book2.id)) and find that it has the new name. Finally, it prints the entire list of books, and we see that they both have the same name.
def book1 = new Book(name: "Cats").save()
def book2 = new Book(name: "Dogs").save()
log.info "The books: ${Book.list()}"
def book3 = Book.get(book2.id)
book3.name = "Cats"
def book3save = book3.save() // Should fail?
def book4 = Book.get(book2.id)
log.info "The result of saving book ${book2.id} with name 'Cats' is ${book3save}"
log.info "The book with ID ${book2.id} is now named ${book4.name}"
log.info "The books are: ${Book.list()}"
The output:
The books: [[Book: id=1 name=Cats], [Book: id=2 name=Dogs]]
The result of saving book 2 with name 'Cats' is null
The book with ID 2 is now named Cats
The books are: [[Book: id=1 name=Cats], [Book: id=2 name=Cats]]
Grails will not throw exceptions on validation errors by default. You can do
book3.save(failOnError: true)
which will throw an exception.
or you need to check the return value of save(). A null means the validation failed. You can also do a book3.validate() to see if the instance is valid before attempting a save.
I think what you are seeing in the appearance of the book being updated is due to the hibernate session. Your save failed but a call to Book.get() loads the instance from the current hibernate session not the database. If you check the dirty flag you can confirm this checking isDirty() or call refresh() to refetch the instance from the database.

Operation save in Grails on a POGO

I'm beginner in Grails and I have a problem when I try to save a POGO
I have created 1 domain class
class Book {
String title
}
Then, I have generated the controller and view automatically.
Now, I want to be able to create a book with the code by clicking "create" (I know it is possible directly with the code generated but for my example I want to do it by the code). To do this, I have modified the method 'save(Book bookInstance)' in the controller like this
#Transactional
def save(Book bookInstance) {
def book = new Book(title:"New Grails Book").save()
But, when I go to the URL localhost:8080/myApp/book/create and then I click "Create", I have the error
message -> /myApp/WEB-INF/grails-app/views/book/save.jsp
description -> The requested resource is not available.
When I put this code in bootStrap, it is OK, so I don't understand why it is not in the controller
When you have a hasMany property in a domain class, Grails adds a Set property to the domain class with an AST transformation (so it's actually there in the bytecode, and it's visiable to Java) to represent the collection, and when you add a belongsTo a field of that type is added. So it's as if you had this code:
class Author {
Set<Book> books
static hasMany = [books: Book]
String name
}
and
class Book {
Author author
static belongsTo = [author: Author]
String title
}
The AST xform uses the map key as the field name, so you can use any valid field name, but the convention is to do what you did.
Properties are nullable:false by default, so your code doesn't save the Book instance because you didn't set the author property. When doing this explicitly you typically don't create the Book directly, but instead add it to the Author's collection using the dynamic addToBooks method. This sets the author field back-reference and when you save the author, the book is transitively validated and saved. This is all handled for you when you have code like new Book(params).save(), and you can do it directly, e.g.
Author author = ...
def book = new Book(title:"New Grails Book", author: author).save()
If you're using a generated controller and GSPs, there should be an author id in the params map, it'll likely be author.id, so that first line would be
Author author = Author.get(params['author.id'])
but you can add
println params
at the top of the action method to see all of the submitted params.
In general you don't want to look at the return value of the save call, since it will be null if there's a validation error and there's no way to retrieve the errors. So change
def book = new Book(...).save()
to
def book = new Book(...)
book.save()
and now you can call book.hasErrors(), book.getErrors(), book.errors, etc. to see if it was successful and if not, what went wrong.
But that's not the exact problem you're seeing, just one you will when you fix your problem. There's no save.gsp, and Grails also looks for save.jsp and confusingly includes that name in the not-found message. The save method is accessed via a POST request, typically from the form generated by the create action, and it either re-displays create.gsp with the submitted data and error messages when validation fails, or redirects to the view action when the save succeeds. There's no need for a save.gsp when using the generated code.

grails validate method on domain object

I wrote a very simple test case and found that Grails does only a shallow validation when i call validate on a domain object. Is it possible for me to do a deep validation in grails? Can anybody help me?
class Person {
Address address
}
class Address {
String city
}
When i do new Address().validate() it returns false but when i do new Person(address: new Address()).validate it returns true.
While "deep validation" currently isn't documented for the validate() and save() methods, it will be in future (the document states that the documentation has been missing, while being relevant for the complete 1.3.x tree). The documentation on these methods' deepValidate parameter then will state:
#deepValidate# (optional) - Determines
whether associations of the domain
instance should also be validated,
i.e. whether validation cascades or
not. This is #true# by default - set
to #false# to disable cascading
validation.
Tests, however, show that "deep validation" is not performed in any of these cases:
one-to-one associations
one-to-many associations
associated objects assigned using the matching setter
associated objects assigned using the matching addTo*(..) method, e.g., person.addToAddresses(..)
using both the validate() and save() methods,
and also, using both methods with an explicit deepValidate: true parameter
Similar findings have been published at another place, categorizing the "non-behavior" as a "known issue". My own, comprehensive, test cases can be downloaded from here.
The solution, finally, is to manually invoke validation on the child object:
class Person {
Address primaryAddress
static hasMany = [secondaryAddresses: Address]
static constraints = {
primaryAddress validator: {
it?.validate()
}
secondaryAddresses validator: {
it?.every { it?.validate() }
}
}
}

Grails save() doesn't save if called after relationship is added

I have two domain classes - Person has many Books. If I create a
Person, call save(), then create+add Books to the Person, everything
is persisted to the database. If I create a Person, then create+add
Books, followed by save(), nothing get persisted.
Example code:
class Person {
...
static hasMany = [books: Book]
}
class Book {
...
static belongsTo = [person: Person]
}
Works, saves to database:
def person = new Person(...).save()
def book = new Book(...)
person.addToBooks(book)
Doesn't not work, doesn't save to database:
def person = new Person(...)
def book = new Book(...)
person.addToBooks(book)
person.save()
Why is this?
I'm running this from the Groovy console. I've tried calling ctx.sessionFactory.currentSession.clear() which doesn't help.
Resolved: The Book I was attaching to the Person had errors in it. By calling person.hasErrors() and getErrors() I was able to see that my Book was failing validation. This was being run from the Grails console so there wasn't any validation error messages.
You need to manually flush the hibernate session like this:
person.save(flush:true)
I don't know for sure but I presume it's because save() sets the ID of the Person which would be required to be able to add a Book to it's book collection.
I'd suggest doing some googling on Hibernate collection behaviour, it could be the way Hibernate works or maybe it's a bug in the version of Grails you're using.
Another thing to try is forcing a flush with person.save(flush:true)

Resources