15
votes

Grails offers the ability to automatically create and bind domain objects to a hasMany List, as described in the grails user guide.

So, for example, if my domain object "Author" has a List of many "Book" objects, I could create and bind these using the following markup (from the user guide):

<g:textField name="books[0].title" value="the Stand" /> 
<g:textField name="books[1].title" value="the Shining" /> 
<g:textField name="books[2].title" value="Red Madder" /> 

In this case, if any of the books specified don't already exist, Grails will create them and set their titles appropriately. If there are already books in the specified indices, their titles will be updated and they will be saved. My question is: is there some easy way to tell Grails to remove one of those books from the 'books' association on data bind?

The most obvious way to do this would be to omit the form element that corresponds to the domain instance you want to delete; unfortunately, this does not work, as per the user guide:

Then Grails will automatically create a new instance for you at the defined position. If you "skipped" a few elements in the middle ... Then Grails will automatically create instances in between.

I realize that a specific solution could be engineered as part of a command object, or as part of a particular controller- however, the need for this functionality appears repeatedly throughout my application, across multiple domain objects and for associations of many different types of objects. A general solution, therefore, would be ideal. Does anyone know if there is something like this included in Grails?

6

6 Answers

12
votes

removeFrom*

Opposite of the addTo method in that it removes instances from an association.

Examples

def author = Author.findByName("Stephen King")

def book = author.books.find { it.title = 'The Stand' }

author.removeFromBooks(book)
12
votes

Just ran into this issue myself. It's easy to solve. Grails uses java.util.Set to represent lists. You can just use the clear() method to wipe the data, and then add in the ones you want.

//clear all documents
bidRequest.documents.clear()

//add the selected ones back
params.documentId.each() {
    def Document document = Document.get(it)
    bidRequest.documents.add(document)
    log.debug("in associateDocuments: added " + document)
};

//try to save the changes
if (!bidRequest.save(flush: true)) {
    return error()
} else {
    flash.message = "Successfully associated documents"
}

I bet you can do the same thing by using the "remove()" method in the case that you don't want to "clear()" all the data.

4
votes

For a good explanation of deleting a collection of child objects with GORM have a look at the Deleting Children section of this blog post - GORM gotchas part 2

It's recommended reading, as are parts 1 and 3 of the series.

0
votes

I am just starting to learn Grails myself and saw your question as an interesting research exercise for me. I do not think you can use the conventional data binding mechanism - as it fills in the blanks using some kind of Lazy map behind the scenes. So for you to achieve your goal your "save" method (or is it a function?) is unlikely to contain anything like:

def Book = new Book(params)

You need a mechanism to modify your controller's "save" method.

After some research, I understand you can modify your scaffolding template which is responsible for generating your controller code or runtime methods. You can get a copy of all the templates used by Grails by running "grails install-templates" and the template file you would need to modify is called "Controller.groovy".

So in theory, you could modify the "save" method for your whole application this way.

Great! You would think that all you need to do now is modify your save method in the template so that it iterates through the object entries (e.g. books) in the params map, saving and deleting as you go.


However, I think your required solution could still be quite problematic to achieve. My instinct tells me that there are many reasons why the mechanism you suggest is a bad idea.

For one reason, off the top of my head, imagine you had a paginated list of books. Could that mean your "save" could delete the entire database table except the currently visible page? Okay, let us say you manage to work out how many items are displayed on each page, what if the list was sorted so it was no longer in numerical order - what do you delete now?

Maybe multiple submit buttons in your form would be a better approach (e.g. save changes, add, delete). I have not tried this kind of thing in Grails but understand actionSubmit should help you achieve multiple submit buttons. I certainly used to do this kind of thing in Struts!

HTH

0
votes

I'm just running into this same issue.

My application's domain is quite simple: it has Stub objects which have a hasMany relationship with Header objects. Since the Header objects have no life of their own, they're entirely managed by the Stub controller and views.

The domain class definitions:

class Stub {
List headers = new ArrayList(); 
static hasMany = [headers:Header]
static mapping = {headers lazy: false}
}

class Header {
String value
static belongsTo = Stub     
}

I've tried the "clear and bind" method but the end result is that the "cleared" objects are left over in the database and grails will just create new instances for the ones that were not removed from the relationship. It does seem to work from an user's perspective, but it will leave lots of garbage objects in the database.

The code in the controller's update() method is:

stubInstance.headers.clear()
stubInstance.properties = params

An example: while editing the -many side of this relationship I have (for a given Stub with id=1):

<g:textField name="headers[0].value" value="zero" id=1 />
<g:textField name="headers[1].value" value="one" id=2 />
<g:textField name="headers[2].value" value="two" id=3 />

in the database there are 3 Header instances:

id=1;value="zero"
id=2;value="one"
id=3;value"two"

after removing header "one" and saving the Stub object the database will have headers:

id=1;value="zero"
id=2;value="one"
id=3;value"two"
id=4;value="zero"
id=5;value="two"

and the Stub object will now have an association with Headers with id=4 and id=5...

Furthermore, without the clearing of the list, if an index is not present in the submitted request.headers list, on data binding grails will keep the existing object at that location unchanged.

The solution that occurs to me is to bind the data, then check the Stub's headers for elements that are not present in the submitted list and remove them.

This looks like a pretty simple scenario, isn't there any built-in functionality to address it? It's a bit overkill to have to write your own synchronization logic for maintaining relationships, especially when the quirks that make it non-trivial are caused by grails itself.

What about deletion, shouldn't the clear()'ed elements be gone from the database? Am I missing something in the relationship or domain object definitions?

0
votes
class Stub {
  List headers = new ArrayList(); 
  static hasMany = [headers:Header]
  static mapping = {
  headers lazy: false
  **headers cascade: "all-delete-orphan"**
   }
  }

class Header {
    String value
    static belongsTo = Stub     
 }

I have added the cascade property on the owning side of relationship and Now if you try to save the stub, it will take care of removing deleted items from the collection and delete them from the DataBase.