Monday, February 15, 2010

Input for One-to-Many Relationships

One of the requirements we have on this project is to have a dynamic list of values that another domain object has a relationship with - the parent domain object would have a list of selected child values. While this could be accomplished relatively easily using multiple select boxes, things get a little more complicated when each selected value has a separate field associated with it.

Now, that wasn't exactly the most coherent explanation of the problem, so let's have an example. Suppose we have a 401k management system that has allows the user to choose among a list of available funds, and a percentage to allocate to each selected fund. All input should be present on a single page.

The list for available funds is dynamic, so it has its own domain class (and default generated grails pages). It also doesn't make sense to have a field for allocation % to this domain class, as every user would have a different percentage.
class Fund {
String name
BigDecimal netAssetValue
}


We solve the problem of where to put the allocation percentage by creating a second domain class which effectively maps a Fund with a percent value. We also establish the one-to-many relationship between this new class our main domain class.
class SelectedFund {
static belongsTo = [portfolio:Portfolio]

Fund fund
Double allocationPercentage

static constraints = {
fund(nullable:false)
allocationPercentage(nullable:false, min:0d)
}
}


We complete the one-to-many relationship in our main domain Portfolio class
class Portfolio {
static hasMany = [selectedFund:SelectedFund]

String name
String ssn
BigDecimal balance

static constraints = {
balance(nullable:false, min:0d)
}
}


Simple enough, but now comes a second problem - tying into the Grails validation and data binding framework. We want the validations to kick off for both the Portfolio class and SelectedFund classes. We also would like the nice highlighting of errored fields that Grails provides. Unfortunately, this is easier said than done, and manual work is required both on the controller and on the gsp side.

First, the controller. There is no automatic way to construct our SelectedFund objects, so this is our first task. We can imagine the UI as a checkbox for each Fund and an associated textfield for the allocation value.

The first step is to clear the portfolio object of any previously SelectedFunds. Note the double loop - we cannot simply iterate over the selectedFund and remove them, as that causes a ConcurrentModification exception. It appears the underlying collection grails uses to store associations is not synchronized.
if (portfolio.selectedFund) {
for (Object fund : Fund.findAll()) {
for (Object sf : portfolio.selectedFund) {
if (sf.fund.id == fund.id) {
portfolio.removeFromSelectedFund(sf)
}
}
}
}


The second step is to collect the list of Funds selected. Note the inline comments
// if only one cbox is selected, html returns a String
// if multiple cboxes are selected, a list is returned
def idList = params['fundIds']
if (idList instanceof String) {
idList = []
idList << params['fundIds']
}

def fundList = idList.collect{Fund.get(it)}


Finally we construct a SelectedFund object for each selected Fund. The second line of the closure is particularly interesting. The right of the = is some gui Grails magic - Grails will only bind to the object parameters prefixed with the passed string. Also, by assigning object field values using '.properties' we hook into the Grails validation lifecycle.
fundList.each {
def sf = new SelectedFund(fund:it)
sf.properties = params['fundIds'+it.id]
portfolio.addToSelectedFund(sf)
}


Separate to building objects from input fields but just as important, is the validation of the SelectedFund object. I was hoping that any errors encountered during object construction would propagate to the parent Portfolio object, but this was not the case. As a result, when we save we must check each SelectedFund for errors manually. Note - this is only handles errors encountered during data binding (i.e. - passing alpha characters to a numerical field), something we'll examine in further detail later when we implement the view.
def hasErrors = false
for (Object o: portfolio.selectedFund) {
if (o.hasErrors()) {
hasErrors = true
break
}
}
return hasErrors



There are also manual steps needed on gsp side - our solution is to use a custom tag. For the sake of brevity, we will skip the code for rendering the html checkbox and textfield elements, which is relatively straightforward, if not tedious. What is interesting is handling errors encountered during validation.

From the documentation, we learn that there are two phases of validation. The first is during data binding, the second during execution of declared constraints. Unfortunately for us, errors encountered during each phase are put onto the errorList of different objects - for data binding, it is on the SelectedFund object, for constraints, it is on the Portfolio object. The manual validation we provided in the controller will ensure that all validations are performed, but some steps are necessary on the view to display errors on the UI.

Finding a binding error is simple - the following code snippet assumes the custom tag has been passed the portfolio object, and is looping over each portfolio.selectedFund object.
def itemHasErrors = false

if (aSelectedFund?.hasErrors()) { itemHasErrors = true }

Finding errors on the Portfolio bean is not as simple. We must iterate over over every error and determine if the error is for a SelectedFund. We take advantage of portfolio.errors, which is an instance of the Spring Errors interface, where each error in the list is an instance of ObjectError.
else if (portfolio.errors?.allErrors) {
for (Object o: portfolio.errors.allErrors) {
if (o.getArgument[1] == SelectedFund.class) { itemHasErrors = true }
}
}


we can now use the itemHasErrors value to determine the stylesheet class to use for the html objects.

One drawback immediately evident, is the formatting of the allocation percentage. If we had created a property editor as described in an earlier post, there is no way of using it, as we are setting the value manually in the custom tag.

No comments:

Post a Comment