Tuesday, October 6, 2009

Data binding for one-to-many association - part 2 (improved)

Improving on the solution...
In a previous posting, I wrote a small sample of how to handle the data binding for a one-to-many association.  While my approach worked, I had a couple nagging issues that I had to resolve.  One, based on the code,  the form field names and the way Groovy builds the map of request parameters,  I had some questions about combining the bindData() method along with also doing objectInstance.properties = properties.  Two, I had this nagging feeling that this could be handle better.

Issue 1
Based on the naming of the form fields that represent objects in the collection, these fields actually appear twice in the params map.  Field addresses[0].street appears in the params map with a key=addresses[0].street and it also appears in a map within the params map with a key=addresses[0].  This 'sub-map' contains all of the fields of the Address object.  My example used this 'sub-map' when calling the bindData() method.  This all works fine.  After completing the iteration across the addresses[x] sub-map entries within the params map, the last statement in the controller was personInstance.properties = params  which then handles the binding for ALL the form fields, including the addresses[0].state field.  This means that I was actually binding the association data twice!  Using the debugger in Eclispe, I was easily able to verify this.

Below is the output of printing the contents of the params object after posting to the PersonController.  The fifth line shows an example of the 'sub-maps' that I mention.
[
addresses[2].city:Arlington, 
lastName:Miller, 
addresses[0].city:Flower Mound, 
addresses[1].zip:66666, 
addresses[0]:[zip:75022, street:1 Main Street, city:Flower Mound], 
addresses[1]:[zip:66666, street:99 Palm Beach Circle, city:Palm Beach], 
addresses[0].zip:75022, 
addresses[2]:[zip:75066,street:1234 Cowboy Way, city:Arlington ], 
addresses[2].zip:75066, 
addrCount:3, 
addresses[0].street:1 Main Street, 
action:save, 
addresses[2].street:1234 Cowboy Way, 
controller:person,
addresses[1].city:Palm Beach,
firstName:Mike, 
addresses[1].street:99 Palm Beach Circle
] 

Issue 2
That nagging feeling just wouldn't go away.  I believed that there had to be a better way and I could swear I actually read about another option.  This isn't really a Grails issue, but more a Spring data binding issue, so with the right set of search terms, my Google searches found what I was looking for (and had actually read before).  Matt Fleming has written an initial posting and then a followup on this subject so you can read more there.  Short answer: define the list as a LazyList which knows what data type to create when it actually does add elements to the list.

The Initial Problem
The initial problem encountered was a NullPointerException because the collection was not initialized with the correct number and type of entries.   My sample solution handled that by creating the Person object first, which contains an empty list.   Then it created each of the Address objects and added those to the list.  Lastly, we did the binding using the whole params map.

Using Matt's solution, you can see the updated Person object below using the LazyList and defining the collection type to Address.

import org.apache.commons.collections.FactoryUtils
import org.apache.commons.collections.list.LazyList

class Person {

    static hasMany = [addresses:Address]
    
    static constraints = {
    }
    
    Person() {
        //addresses = []
    }
    
    String firstName
    String lastName
    List
addresses = LazyList.decorate( new ArrayList(), FactoryUtils.instantiateFactory(Address.class)); String toString() { firstName + " " + lastName + " Addresses:"+addresses } }


The updated controller code is shown below.   No reason to call the bindPerson(params), we just create the Person object with the params map and Grails & Spring do the rest!

def save = {
        //def personInstance = bindPerson(params)
        def personInstance = new Person(params)
        if(!personInstance.hasErrors() && personInstance.save()) {
            flash.message = "Person ${personInstance.id} created"
            redirect(action:show,id:personInstance.id)
        }
        else {
            render(view:'create',model:[personInstance:personInstance])
        }
    } 

1 comment:

  1. I find this works in FF 6.0.2 but fails in IE8.
    In IE8 the address doubles up each field. For example if you have 2 addresses in IE8, the show view for a person has the address listed as, street,street city,city zip,zip

    ReplyDelete