6.1.6 数据绑定 - Reference Documentation
Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith
Version: null
6.1.6 数据绑定
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 by 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.
数据绑定是"绑定"输入的请求参数到一个对象的属性或者整个对象的行为。数据绑定将自动转换请求参数的类型,这些参数通常是通过表单提交来的String类型的值,而Groovy或者Java对象的属性很可能不是。Grails是使用Spring的基本数据绑定能力来完成数据绑定。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' Map constructor:def save() {
def b = new Book(params)
b.save()
}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
title and author request parameters would automatically be set on the domain class. You can use the properties property to perform data binding onto an existing instance:def save() {
def b = Book.get(params.id)
b.properties = params
b.save()
}绑定请求数据到领域模型
有两种方法将请求参数绑定到一个领域类的属性上。第一个是使用领域类的构造函数,只要参数类型是Map类型即可:def save() {
def b = new Book(params)
b.save()
}new Book(params)代码中发生的。将参数params对象传递给到领域类的构造器时,Grails就可以自动识别你正在试图绑定来自请求中的参数。因此,假设我们有一个如下面所示的输入请求:/book/save?title=The%20Stand&author=Stephen%20King
title和author请求参数将自动被设置到领域类上。你也可以使用已经存在实例的properties属性来执行数据绑定:def save() {
def b = Book.get(params.id)
b.properties = params
b.save()
}Data binding and Single-ended Associations
If you have aone-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:/book/save?author.id=20
.id suffix on the request parameter and look up the Author instance for the given id when doing data binding such as:def b = new Book(params)null by passing the literal String "null". For example:/book/save?author.id=null单关联的数据绑定
如果你有一个one-to-one或者many-to-one关联,你也可以利用Grails的数据绑定能力来更新它们,比如:/book/save?author.id=20
.id请求参数,并且在数据绑定的时候,会查找指定id的Author实例,比如:def b = new Book(params)null,只要传给的String类型的"null"即可。比如:/book/save?author.id=nullData 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 aSet based association (the default for a hasMany) then the simplest way to populate an association is to 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}" />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 use the subscript operator:<g:textField name="books[0].title" value="the Stand" /> <g:textField name="books[1].title" value="the Shining" />
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 two 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" />
<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" />
List using the same .id syntax as you would use with a single-ended association. For example:<g:select name="books[0].id" from="${bookList}" value="${author?.books[0]?.id}" /><g:select name="books[1].id" from="${bookList}" value="${author?.books[1]?.id}" /><g:select name="books[2].id" from="${bookList}" value="${author?.books[2]?.id}" />
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': '']"/>books0 if the empty option is chosen.Binding to a Map property works 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': '']"/>Map property images under a key of "cover".
多关联的数据绑定
如果你有一个one-to-many或者many-to-many的关联,那么根据关联类型的不同,将对应不同的数据绑定技术。如果你使用基于Set的关联(hasMany缺省就是此种关联),那么最简单的方法就是传递一个标识符列表。比如下边<g:select>的用法:<g:select name="books"
from="${Book.list()}"
size="5" multiple="yes" optionKey="id"
value="${author?.books}" />books。即便如此,类似的情况下,你想使用此技术来更新关联对象饿属性,将行不通。不过你可以通过下标(subscript)操作符的方式来实现,比如:<g:textField name="books[0].title" value="the Stand" /> <g:textField name="books[1].title" value="the Shining" />
Set的关联还是有一个严重问题,那就是你要更新的内容总是以同样的顺序渲染的,这是因为Set本来就没有顺序的概念,虽然你可以通过books0和books1来索引,但这并不能保证其关联顺序在服务器端也一致,当然你可以通过明确指定排序来比避免。如果你使用基于List的关联的话,这并不是问题,因为List已经是有序的,并且可以通过索引进行引用。基于Map的关联也是。还要注意的是,如果你正在绑定的关联是有大小的,最外侧的那个元素所在位置就是关联的大小:<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" />
<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" />
.id语法来绑定类型是List且已存在的关联,比如:<g:select name="books[0].id" from="${bookList}" value="${author?.books[0]?.id}" /><g:select name="books[1].id" from="${bookList}" value="${author?.books[1]?.id}" /><g:select name="books[2].id" from="${bookList}" value="${author?.books[2]?.id}" />
books List中各自独立的实体分别进行选择。同样的方法也可以用来删除特定的索引元素,比如:<g:select name="books[0].id"
from="${Book.list()}"
value="${author?.books[0]?.id}"
noSelection="['null': '']"/>books0的下拉框,当然只有选择项是空的时候才行。绑定到Map属性的工作方式也是如此,不过要将参数名称中的列表索引替换为映射(map)的键(key):<g:select name="images[cover].id"
from="${Image.list()}"
value="${book?.images[cover]?.id}"
noSelection="['null': '']"/>"cover"绑定到Map类型的images中。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
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 it to isolate only a subset of the parameters to bind.def b = new Book(params.book)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)多领域类的数据绑定
通过params对象,是有可能将将数据绑定到多个领域对象的。比如下面的输入请求:/book/save?book.title=The%20Stand&author.name=Stephen%20King
author.或者book. ,它们通常用来隔离参数所属的类型。Grails的params对象更象是一个多维的哈希表(hash),你可以单独地只绑定参数的一个子集。def b = new Book(params.book)book.title参数的前缀(第一个点以前部分)来隔离此领域的子参数的。同样我们也可以用于领域类Author:def a = new Author(params.author)Data Binding and Action Arguments
Controller action arguments are subject to request parameter data binding. There are 2 categories of controller action arguments. The first category is command objects. Complex types are treated as command objects. See the Command Objects section of the user guide for details. The other category is basic object types. Supported types are the 8 primitives, their corresponding type wrappers and java.lang.String. The default behavior is to map request parameters to action arguments by name:class AccountingController { // accountNumber will be initialized with the value of params.accountNumber
// accountType will be initialized with params.accountType
def displayInvoice(String accountNumber, int accountType) {
// …
}
}params.accountType request parameter has to be converted to an int. If type conversion fails for any reason, the argument will have its default value per normal Java behavior (null for type wrapper references, false for booleans and zero for numbers) and a corresponding error will be added to the errors property of the defining controller./accounting/displayInvoice?accountNumber=B59786&accountType=bogusValue
controller.errors.hasErrors() will be true, controller.errors.errorCount will be equal to 1 and controller.errors.getFieldError('accountType') will contain the corresponding error.If the argument name does not match the name of the request parameter then the @grails.web.RequestParameter annotation may be applied to an argument to express the name of the request parameter which should be bound to that argument:import grails.web.RequestParameterclass AccountingController { // mainAccountNumber will be initialized with the value of params.accountNumber // accountType will be initialized with params.accountType def displayInvoice(@RequestParameter('accountNumber') String mainAccountNumber, int accountType) { // … } }
数据绑定和操作参数
控制器的操作参数是绑定请求参数的主题,目前主要有2大类的操作参数。第一大类是命令对象,复杂类型的都被看作命令对象,更多详细请看本手册的命令对象章节。另外一大类是基本的对象类型,所支持的类型包括8个原生类型及其对应的包装类和java.lang.String。缺省情况下,从请求参数到操作参数的映射是通过名称来完成的,比如:class AccountingController { // accountNumber will be initialized with the value of params.accountNumber
// accountType will be initialized with params.accountType
def displayInvoice(String accountNumber, int accountType) {
// …
}
}params.accountType必须要转换成int才行。如果此转换失败了,不管什么原因导致,此参数将根据普通的Java行为进行设置(包装类是null,布尔类型是false,数字类型是0),并且产生一个相应的错误信息到控制器的errors属性中。/accounting/displayInvoice?accountNumber=B59786&accountType=bogusValue
controller.errors.hasErrors()将返回true,controller.errors.errorCount的数值是1,并且controller.errors.getFieldError('accountType')将包含其对应的出错信息。如果参数名称跟请求参数的名称并不匹配,那么可以使用@grails.web.RequestParameter注解解决,只需要将要转换的请求参数名传递给注解即可:import grails.web.RequestParameterclass AccountingController { // mainAccountNumber will be initialized with the value of params.accountNumber // accountType will be initialized with params.accountType def displayInvoice(@RequestParameter('accountNumber') String mainAccountNumber, int accountType) { // … } }
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. This results in a type conversion error. Grails will retain type conversion errors inside the errors property of a Grails domain class. For example:class Book {
…
URL publisherURL
}Book that uses the java.net.URL class to represent URLs. Given an incoming request such as:/book/save?publisherURL=a-bad-url
a-bad-url to the publisherURL property as 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!" }
grails-app/i18n/messages.properties file to use for the error. You can use a generic error message handler such as:typeMismatch.java.net.URL=The field {0} is not a valid URLtypeMismatch.Book.publisherURL=The publisher URL you specified is not a valid URL
数据绑定和类型转换错误
有时,在执行数据绑定时,可能不会把一个特殊的String转换到特殊的目标类型。这样,你就得到一个类型转换错误。Grails将类型转换错误保存到领域类的errors属性中。比如::class Book {
…
URL publisherURL
}Book,它使用了类java.net.URL来存储URLs。现在假设请求如下:/book/save?publisherURL=a-bad-url
a-bad-url绑定到publisherURL属性是不可能的,因为发生了类型不匹配的错误。你可以象下面那样进行检查:def b = new Book(params)if (b.hasErrors()) { println "The value ${b.errors.getFieldError('publisherURL').rejectedValue}" + " is not a valid URL!" }
grails-app/i18n/messages.properties文件来定义错误信息。你可以使用通用的错误消息处理,比如:typeMismatch.java.net.URL=The field {0} is not a valid URLtypeMismatch.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 and be persisted in 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
firstName and lastName properties will be bound.Another way to do this is is to use Command Objects as the target of data binding instead of domain classes. 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)bindData method also lets you exclude certain parameters that you don't want updated:def p = new Person()
bindData(p, params, [exclude: 'dateOfBirth'])def p = new Person()
bindData(p, params, [include: ['firstName', 'lastName]])
Note that if an empty List is provided as a value for the include parameter then all fields will be subject to binding if they are not explicitly excluded.
数据绑定和安全
当从请求参数进行批量地更新领域类地属性时,你要当心,绝不允许让用户的恶意数据绑定到领域类,并且持久化到数据库中。你可以使用下标操作符来限制那些属性可以被绑定到给定的领域类:def p = Person.get(1)p.properties['firstName','lastName'] = params
firstName和lastName属性将被绑定。另外一种方法是使用命令对象来替代领域类进行数据绑定。或者使用更灵活的bindData方法来绑定。bindData方法具有同样的数据绑定能力,不过可以是任意对象:def p = new Person()
bindData(p, params)bindData方法还可以排除那些你不想更新的某些参数/属性:def p = new Person()
bindData(p, params, [exclude: 'dateOfBirth'])def p = new Person()
bindData(p, params, [include: ['firstName', 'lastName]])
注意!如果include参数值是一个空的列表,并且没有指定排除的话,那么所有的字段属性将被绑定。

