通常情况下,Web 开发可以概括为以下五方面内容:
- 表单提交(验证)与接收(接收用户数据的主要方式);
- 表单输出(允许用户修改数据的主要方式);
- 数据列表(显示用户查询结果的主要方式);
- 超级链接的构造与响应(使用户透明的提交简单数据或者跳转到其它页面);
- 使用 Session 维持用户会话;
下面,我们将通过阅读 Grails 生成的代码,来理解它是如何分别实现这几方面内容的。
再创建一个名为 Member 的 Domain Class:
grails create-domain-class Member |
编辑 Team 和 Member 这两个类,代码如下:
//Team.groovy |
上述代码描述了 Member 类的属性,并使用 belongsTo
和 hasmany
描述了 Team 类与 Member 类之间的一对多关系和多对一关系。Grails 会自动在数据库中为这两张表创建主外键。
接下来为 Member 类创建实现 CRUD 的 Controller 和 View。在控制台运行命令行 grails generate-all Member
,同时也输入 grails generate-all Team
更新 Team 类的 Controller 和 View。
下面按照前面定义的五个 Web 开发的基本任务对 Member 类自动生成的代码进行分析:
表单提交与接收
打开 grails-app/views/member/creat.gsp,关注表单提交相关的代码(仅摘录表单相关的代码):
<g:form action="save" method="post"> |
g:form 会生成 <from> 标签,同时指定的接收表单数据的 action 为 save。gsp 的表单提交,大量使用标准的 Html tag,如 <input>,这样可以降低学习成本,无需学习过多特定的 tag。Grails 在表单接收端的代码同样也十分简单。Grails 不再使用 Servlet 中冗长的 request.getParameter(XXX)
,它有类似 Struts 却比 Struts 更简单的方式。如上述代码,接收用户表单提交的数据并把它构造为 Domain Class,只需要一行代码:member.properties = params
。完整的 Controller Action 代码摘录如下:
class MemberController { |
在上述代码中,params 是一个 Map,包含全部表单提交的数据。把 Map 的值赋给 Domain Class 的 properties 属性后,Domain Class 就会把 Map 类的同名键的值赋给自身相应的属性,自动完成类型转换。这一行代码实现的功能通常需要十几行 Java 代码才能实现,由此也体现了 Grails 开发的“快”。
再补充一点,使用构造函数也可以实现用 params 给 Domain Class 赋值,如:
def member = new Member(params) |
由于 Member 是 Domain 类,所以它会被自动添加用于实现数据库增删查改的方法,执行 save 方法进行保存。如果保存成功,if 语句会执行“真”,否则会执行假。虽然数据库读写的任务并不是 Web 开发的基本任务,但如果能够简化这一操作,无疑会极大的提高开发效率,Grails 提供了一个良好的范例。
使 用 Domain 类的 properties 属性去获取表单的值是 Grails 下最常见的表单接收方式,但不是唯一的方式。如果要获取某个单一的属性值,还有更简单的方法:可以使用 Map 的 Key 取值。如只获取表单项 <input name="email"/> 的值,可使用:params.email 或者 params['email']。
更进一步,表单提交的内容还可以自动全部写入 Java 类的实例中。还是以上述表单为例把表单内容写入 Java 实例中,定义 Java 类如下:
JMember.java |
在接收的表单中编写如下代码:
class MemberController { |
也很神奇,不是 吗?JMember 是个 JavaBean,Controller 类特有的“bindData”方法可以为 JavaBean 填充数据。(提示:在 Groovy 语言中,JavaBean 的使用也被简化了,如 JMember 的实例 member 可以通过 member.name 去访问或修改 name 属性,而无需显示的调用 member.getName()
和 member.setName(xxx)
)
除 了给 Domain 类的 properties 属性赋值和使用 Controller 类的 bindData 方法外,Grais 还提供了一种表单提交的方法,叫做 Command Object。Domain Class 可以很好的支持表单验证,但它必须与数据库的一张表关联,如果某一表单提交不是为了保存数据到数据库中,则不能使用 Domain Class。那么如何让非 Domain Class 支持表单的验证?Command Object 正好可以解决这个问题。
在 文件夹 src/groovy 中创建一个名为 XXXCommand 的 Groovy 类,比如 SearchCommand.groovy,然后在 Command 类中加入约束条件(加入方法与 Domain Class 相同)。然后在 Controller 类里引用 Command Object,引入方法如下:
class MemberController { |
SearchCommand cmd-> 表示 Grails 在调用 search Action 的时候,会传递给 Action 一个参数,这个参数就是 SearchCommand 的实例,名叫 cmd。SearchCommand 和表单对应的代码如下:
class SearchCommand { |
<g:form controller="member" action=”search” method="post" > |
从上面的代码可以看出,Command 对象在表单的构造并无太大区别,只是在接收时存在一点区别,使用起来并不复杂。
![]() ![]() |
![]()
|
表单输出
表单的提交在 Grails 中十分简单,同样,表单输出也不难。正如上文,对于常见的 HTML 表单控件,只需要修改 value 属性,就可以显示控件中的内容:
<input type="text" value="${member?.phone?.encodeAsHTML()}" name='phone'/> |
上述代码重点在两个地方,一是问号“?”的作用,二是 encodeAsHTML
方法。member?.phone
表达的意思是:如果 member 不为空,返回 member.phone,否则返回空字符串,同样道理,如果 member.phone 不为空,返回 member.phone.encodeAsHTML()
。 从字面的意思可以看出,这个方法可以把字符串中的“<”、“>”等转换为 HTML 对应的 < > 等。这个方法可以根据具体业务,决定是否调用(其实对于 phone 这个属性而言,是没有必要调用它的,但 Grails 自动生成的代码调用了此方法,用户可以自己手动改为 member?.phone)。对于相对复杂一点的表单输出,如下拉框(select box),可以使用 grails 自带的标签库 <g:select>
:
<g:select optionKey="id" from="${Team.list()}" name='team.id' value="${member?.team?.id}"> |
此时就可以同样使用 value 属性来指定表单项的内容了。但是 ${}
中的变量又是从哪来的呢?与 Struts 类似,它来自 Controller。观察 edit Action:
def edit = { |
从 return 行对应的代码可以看到,Controller 向 Veiw 传递了 Model,这是典型的 MVC 模式。
![]() ![]() |
![]()
|
表单的验证
Grails 对提供 Domain 类和 Command Object 的表单提供了“一栈式”的验证支持。首先在 Domain 类或 Command 类中添加约束信息,比如 Member 类的代码:
class Member { |
在上述代码中加入对 Member 类的约束,调用 Domain 类对象的 validate
方法或 save
方法检验当前数据是否符合约束条件(Command 类可使用 hasError()
方法)。如果不符合,则返回 false
。
在相应的 gsp 页面中也需要有相应代码用于输出验证结果:
<g:hasErrors bean="${member}"> |
上述代码实现以列表的形式输出 member 对象包含的全部错误信息。相应的,如果想更有针对性的指明具体是哪个表单项提交的数据有问题,可以使用如下的代码:
<td valign='top' |
.error 的 CSS 项:
.errors { border: 1px solid red } |
Grails 对错误信息提供了 i18n(国际化)支持。可以通过修改 Contact/grails-app/i18n 不同语言的资源文件来定制输出错误信息。这里修改的是 messages_zh_CN.properties:
member.name.blank=用户名不能为空 |
这里要强调一点,一定要使用 UTF-8 格式来保存上述文件(不能使用 GB2312),保存完成后,无需手工调用 JDK 的 native2ascii.exe 去转换,因为在 Grails 启动的时候,会自动完成该转换。关于不同约束项对应的名称,可以通过 官方文档 获取。
![]() ![]() |
![]()
|
数据列表
数据列表是 Web 应用最重要的一种数据输出方式,使用 Grails 实现数据列表十分容易。用一句话概括就是:由 Controller 提供数据,使用 <g:each>
遍历数据,再用 ${}
输出数据。
<g:each in="${memberList}"> |
<g:each>
可以遍历数据,memberList 是 Controller 类中对应 list
Action 的返回值。遍历过程中每次取出一个元素,默认名称为“it
”,如果想改用其它名称,可以用 var
属性声明:<g:each in=”${memberList}” var=”member”>
。
![]() ![]() |
![]()
|
超级链接的构造与响应
超级链接通常在 Web 应用中可以实现类似按扭的功能。一方面指明要跳转到的页面,另一方面还要提交少量的数据。比如,在显示 Member 列表的页面里,加入一个用于显示 Member 的 Link,只需要加入如下代码:
<g:each in="${memberList}"> |
通过指定 controller
和 action
两个属性可以确定当前 Link 会跳转到哪个页面(如果不指定 controller,则应用当前的 controller),id
属性可以传递一个用于标记唯一性的属性值。而 <g:link>XXX</g:link>
中间的内容则为该链接显示的文本。
对应的 Action 接收 Link 数据的代码很简单:
def show = { |
通过 params.id
可以得到 Link 提交的数据。
对于传统的 ?ParameterName=value
的参数组织方式的链接,Grails 也提供了支持:可使用 g:link
标签的 params
属性,用一个 Map 给它赋值,则 Map 的 Key 被输出为参数名,而 Key 对应的 value 被输出为参数的值:
<g:link controller="member" |
上面的 Tag 最终会输出为 URL: /Contact/member/create?team.id=2
(假定 team.id 的值是 2)。
![]() ![]() |
![]()
|
Session 的使用
在 Grails 中使用 Session 十分简单,Controller/GSP 中可以访问一个名叫 session
的对象,它与 Map 的用法十分相似。比如说,在 session
中保存一个名叫 age 的对象,在 Controller/GSP 中,可以写成 session.age=XX
或 session['age']=XX
。调用 session 的 invalidate
方法,则可以清空 Session 的内容。
![]() ![]() |
![]()
|
DB 相关操作
上 面所讲的内容涵盖了 Web 开发的几个基本方面,理论上讲,掌握了这几个基本内容就可以进行 Web 开发了,但是 Grails 还提供了一些可以极大提高开发效率的内容,比如 DB 读写。DB 的读写虽然不属于 Web 范畴,但无可争议的是,几乎没有 Web 应用是不使用 DB 的。下面对 Grails 简化的 Hibernate 操作进行一下简单介绍:
表 2.DB 相关操作
save |
保存 Domain 对象的数据到对应的库表中(可能是 Insert 也可能是 Update) |
findBy |
动态方法,查找并返回第一条记录,方法名可以变化 如:findByName("Tom") 会返回所有 name 属性为 Tom 的对象(只返回第一条记录)findByNameAndPassword("Tom","Mot")会返回所有 name 属性为”Tom”并且 password 属性为”Mot”的对象 |
findAllBy |
与 findBy 类似,返回全部记录 |
executeQuery |
执行一个 HQL 的查询(HQL 的使用,请参考 Hibernate 官方文档) |
createCriteria |
执行一个 Hibernate 的 Criteria 查询(下文会有一个 Criteria 的试例) |
get |
返回指定 id 的对象 |
count |
执行”select count(*) from XX”的操作 |
delete |
执行删除操作。 |
更多 DB 相关的操作,请参考 相关文档。