Data binding is the act of "binding" incoming request parameters onto the properties of an object or an entire graph of objects. Data binding should deal with all necessary type conversion since request parameters, which are typically delivered via a form submission, are always strings whilst the properties of a Groovy or Java object may well not be.
Grails uses
Spring's underlying data binding capability to perform data binding.
Binding Request Data to the Model
There are two ways to bind request parameters onto the properties of a domain class. The first involves using a domain classes' implicit constructor:
def save = {
def b = new Book(params)
b.save()
}
The data binding happens within the code
new Book(params)
. By passing the
params object to the domain class constructor Grails automatically recognizes that you are trying to bind from request parameters. So if we had an incoming request like:
/book/save?title=The%20Stand&author=Stephen%20King
Then the
title
and
author
request parameters would automatically get set on the domain class. If you need to perform data binding onto an existing instance then you can use the
properties property:
def save = {
def b = Book.get(params.id)
b.properties = params
b.save()
}
This has exactly the same effect as using the implicit constructor.
Data binding and Single-ended Associations
If you have a
one-to-one
or
many-to-one
association you can use Grails' data binding capability to update these relationships too. For example if you have an incoming request such as:
Grails will automatically detect the
.id
suffix on the request parameter and look-up the
Author
instance for the given id when doing data binding such as:
An association property can be set to
null
by passing the literal
String
"null". For example:
/book/save?author.id=null
Data Binding and Many-ended Associations
If you have a one-to-many or many-to-many association there are different techniques for data binding depending of the association type.
If you have a
Set
based association (default for a
hasMany
) then the simplest way to populate an association is to simply send a list of identifiers. For example consider the usage of
<g:select>
below:
<g:select name="books"
from="${Book.list()}"
size="5" multiple="yes" optionKey="id"
value="${author?.books}" />
This produces a select box that allows you to select multiple values. In this case if you submit the form Grails will automatically use the identifiers from the select box to populate the
books
association.
However, if you have a scenario where you want to update the properties of the associated objects the this technique won't work. Instead you have to use the subscript operator:
<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
However, with
Set
based association it is critical that you render the mark-up in the same order that you plan to do the update in. This is because a
Set
has no concept of order, so although we're referring to
books0
and
books1
it is not guaranteed that the order of the association will be correct on the server side unless you apply some explicit sorting yourself.
This is not a problem if you use
List
based associations, since a
List
has a defined order and an index you can refer to. This is also true of
Map
based associations.
Note also that if the association you are binding to has a size of 2 and you refer to an element that is outside the size of association:
<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" />
Then Grails will automatically create a new instance for you at the defined position. If you "skipped" a few elements in the middle:
<g:textField name="books[0].title" value="the Stand" />
<g:textField name="books[1].title" value="the Shining" />
<g:textField name="books[5].title" value="Red Madder" />
Then Grails will automatically create instances in between. For example in the above case Grails will create 4 additional instances if the association being bound had a size of 2.
You can bind existing instances of the associated type to a
List
using the same
.id
syntax as you would use with a single-ended association. For example:
<g:select name="books[0].id" from="${Book.list()}" value="${author?.books[0]?.id}" />
<g:select name="books[1].id" from="${Book.list()}" value="${author?.books[1]?.id}" />
<g:select name="books[2].id" from="${Book.list()}" value="${author?.books[2]?.id}" />
Would allow individual entries in the
books List
to be selected separately.
Entries at particular indexes can be removed in the same way too. For example:
<g:select name="books[0].id" from="${Book.list()}" value="${author?.books[0]?.id}" noSelection="['null': '']"/>
Will render a select box that will remove the association at
books0
if the empty option is chosen.
Binding to a
Map
property works in exactly the same way except that the list index in the parameter name is replaced by the map key:
<g:select name="images[cover].id" from="${Image.list()}" value="${book?.images[cover]?.id}" noSelection="['null': '']"/>
This would bind the selected image into the
Map
property
images
under a key of
"cover"
.
Data binding with Multiple domain classes
It is possible to bind data to multiple domain objects from the
params object.
For example so you have an incoming request to:
/book/save?book.title=The%20Stand&author.name=Stephen%20King
You'll notice the difference with the above request is that each parameter has a prefix such as
author.
or
book.
which is used to isolate which parameters belong to which type. Grails'
params
object is like a multi-dimensional hash and you can index into to isolate only a subset of the parameters to bind.
def b = new Book(params['book'])
Notice how we use the prefix before the first dot of the
book.title
parameter to isolate only parameters below this level to bind. We could do the same with an
Author
domain class:
def a = new Author(params['author'])
Data binding and type conversion errors
Sometimes when performing data binding it is not possible to convert a particular String into a particular target type. What you get is a type conversion error. Grails will retain type conversion errors inside the
errors property of a Grails domain class. Take this example:
class Book {
…
URL publisherURL
}
Here we have a domain class
Book
that uses the Java concrete type
java.net.URL
to represent URLs. Now say we had an incoming request such as:
/book/save?publisherURL=a-bad-url
In this case it is not possible to bind the string
a-bad-url
to the
publisherURL
property os a type mismatch error occurs. You can check for these like this:
def b = new Book(params)if(b.hasErrors()) {
println "The value ${b.errors.getFieldError('publisherURL').rejectedValue} is not a valid URL!"
}
Although we have not yet covered error codes (for more information see the section on
Validation), for type conversion errors you would want a message to use for the error inside the grails-app/i18n/messages.properties file. You can use a generic error message handler such as:
typeMismatch.java.net.URL=The field {0} is not a valid URL
Or a more specific one:
typeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL
Data Binding and Security concerns
When batch updating properties from request parameters you need to be careful not to allow clients to bind malicious data to domain classes that end up being persisted to the database. You can limit what properties are bound to a given domain class using the subscript operator:
def p = Person.get(1)p.properties['firstName','lastName'] = params
In this case only the
firstName
and
lastName
properties will be bound.
Another way to do this is instead of using domain classes as the target of data binding you could use
Command Objects. Alternatively there is also the flexible
bindData method.
The
bindData
method allows the same data binding capability, but to arbitrary objects:
def p = new Person()
bindData(p, params)
However, the
bindData
method also allows you to exclude certain parameters that you don't want updated:
def p = new Person()
bindData(p, params, [exclude:'dateOfBirth'])
Or include only certain properties:
def p = new Person()
bindData(p, params, [include:['firstName','lastName]])