【IT168技术文档】Grails 对象关系映射(Grails Object Relational Mapping,GORM)API 是 Grails Web 框架的核心部分之一。“精通 Grails:GORM - 有趣的名称,严肃的技术” 向您介绍了 GORM 的基础知识,包括简单的一对多关系。之后的 “使用 Ajax 实现多对多关系” 教您使用 GORM 建模越来越复杂的类关系。现在您将看到 GORM 的 “ORM” 如何能够灵活处理遗留数据库中不符合 Grails 标准命名约定的表名与列名。
备份并恢复数据
无论什么时候处理数据库中的现有数据,都要有一份最新的备份。著名的墨菲法则(Murphy's Law )的墨菲(Murphy)就像是我的守护神。什么样的错误都有可能发生,所以还是未雨绸缪的好。
备份
除了用常规备份软件备份目标数据库外,我还建议再保存一份数据的纯文本副本。这样就能够用相同的数据集轻松地创建测试和开发数据库了,还可以轻松地跨数据库服务器移动数据(例如,在 MySQL 和 DB2 之间来回移动数据)。
您将再一次使用本系列一直开发的 Trip Planner 应用程序。清单 1 是一个名为 backupAirports.groovy 的 Groovy 脚本,它备份了 airport 表的记录。它用了三条语句、不足 20 行的代码连接到了数据库,从表中选定了每一行,并将数据作为 XML 导出。
清单 1. backupAirports.groovy
"jdbc:mysql://localhost/trip?autoReconnect=true",
"grails",
"server",
"com.mysql.jdbc.Driver")
x = new groovy.xml.MarkupBuilder()
x.airports{
sql.eachRow("select * from airport order by id"){ row ->
airport(id:row.id){
version(row.version)
name(row.name)
city(row.city)
state(row.state)
country(row.country)
iata(row.iata)
lat(row.lat)
lng(row.lng)
}
}
}
清单 1 中的第一条语句创建了一个新的 groovy.sql.Sql 对象。这是一个标准 JDBC 类集的瘦 Groovy facade,包括 Connection、Statement 和 ResultSet。您可能已经认出了 newInstance 工厂方法的四个参数了:JDBC 连接字符串、用户名、密码以及 JDBC 驱动程序(在 grails-app/conf/DataSource.groovy 中也可以找到相同值)。
下一条语句创建了 groovy.xml.MarkupBuilder。该类允许您动态创建 XML 文档。
最后一条语句(以 x.airports 开头)创建了 XML 树。XML 文档的根元素为 airports。它还为数据库的每一行创建了一个 airport 元素,该元素带有 id 属性。嵌套于 airport 元素的元素有 version、name 和 city 元素(想了解更多关于 Groovy Sql 和 MarkupBuilder 用途的信息,参见 参考资料)。
清单 2 展示了由此得到的 XML:
清单 2. 来自备份脚本的 XML 输出
<airport id='1'>
<version>2</version>
<name>Denver International Airport</name>
<city>Denver</city>
<state>CO</state>
<country>US</country>
<iata>den</iata>
<lat>39.8583188</lat>
<lng>-104.6674674</lng>
</airport>
<airport id='2'>...</airport>
<airport id='3'>...</airport>
</airports>
在备份脚本中,一定要按照主键顺序拖出记录。当恢复这个数据时,一定要按相同的顺序插入值,以确保外键值同样匹配(关于这一点我将在下一小节进一步详述)。
注意,该脚本是完全独立于 Grails 框架的。要使用它,就一定要在您的系统上安装 Groovy(参见 参考资料,查找下载与安装说明)。另外,类路径中一定要有 JDBC 驱动程序 JAR。可以在运行脚本时进行指定。在 UNIX® 中,要输入:
当然了,在 Windows® 上,相应的文件路径和 JAR 分隔符是不同的。在 Windows 中,则需要输入:
由于我经常使用 MySQL,所以我将一份该 JAR 的副本保存在了我的主目录(在 UNIX 上为 /Users/sdavis,在 Windows 上为 c:\Documents and Settings\sdavis)中的 .groovy/lib 目录中。当从命令行运行 Groovy 脚本时,该目录中的 JAR 会自动包含在类路径中。
清单 1 中的脚本将输出写到了屏幕。要将数据保存在一个文件中,可以在运行脚本时重定向输出:
恢复数据
从数据库中获取出数据仅仅是成功了一半。还要再将数据恢复到数据库中。清单 3 中展示的 restoreAirports.groovy 脚本用 Groovy XmlParser 读入了 XML,构造了一个 SQL insert 语句,并用了一个 Groovy SQL 对象来执行该语句(要了解更多关于 XmlParser的信息,参见 参考资料)。
清单 3. 从 XML 中恢复数据库记录的 Groovy 脚本
f = new File(args[0])
println f
sql = groovy.sql.Sql.newInstance(
"jdbc:mysql://localhost/aboutgroovy?autoReconnect=true",
"grails",
"server",
"com.mysql.jdbc.Driver")
items = new groovy.util.XmlParser().parse(f)
items.item.each{item ->
println "${item.@id} -- ${item.title.text()}"
sql.execute(
"insert into item (version, title, short_description, description,
url, type, date_posted, posted_by) values(?,?,?,?,?,?,?,?)",
[0, item.title.text(), item.shortDescription.text(), item.description.text(),
item.url.text(), item.type.text(), item.datePosted.text(),
item.postedBy.text()]
)
}
}
else{
println "USAGE: itemsRestore [filename]"
}
要运行该脚本,需要输入:
切记,对于要工作的表之间的关系而言,关系的一 的方面的主键字段一定要与关系的多 的方面的外键字段相匹配。例如,储存于 airport 表的 id 列中的值一定要与 flight 表的 arrival_airline_id 列中的值相同。
为了确保自动编号的 id 字段被恢复为相同的值,一定要在恢复表前将它们全部删除。这样就可以在下次启动 Grails 重新创建表时将自动编号重置为 0。
将机场数据安全地备份之后(大概其他表中的数据也已经安全备份了),那么现在您就可以开始试验一些新的 “遗留” 数据了。不懂么?看完下一小节您就会明白了。
导入新的机场数据
美国地质勘探局(United States Geological Survey,USGS)发表了一个全面的美国机场的列表,包括 IATA 代码和纬度/经度(参见 参考资料)。果然,USGS 字段与现行使用的 Airport 类不相匹配。虽然可以改变 Grails 类,使它与 USGS 表中的名称相匹配,但是这要大量改写应用程序。相反,本文不需要这样做,而是探讨几种不同的技术,在后台将现有的 Airport 类无缝映射到新的、不同的表模式中。
首先,需要将 USGS “遗留” 数据导入到数据库。然后运行清单 4 中的 createUsgsAirports.groovy 脚本,创建新表(该脚本假设您正在使用 MySQL。由于每个数据库创建新表的语法有所不同,所以使用其他数据库时,需要对该脚本做出适当修改)。
清单 4. 创建 USGS 机场表
"jdbc:mysql://localhost/trip?autoReconnect=true",
"grails",
"server",
"com.mysql.jdbc.Driver")
ddl = """
CREATE TABLE usgs_airports (
airport_id bigint(20) not null,
locid varchar(4),
feature varchar(80),
airport_name varchar(80),
state varchar(2),
county varchar(50),
latitude varchar(30),
longitude varchar(30),
primary key(airport_id)
);
"""
sql.execute(ddl)
看一看清单 5 中的 usgs-airports.xml。它是 GML 格式的一个例子。该 XML 要比清单 2 中由备份脚本创建的简单的 XML 复杂一些。这其中,每一个元素都处在一个名称空间中,而且元素嵌套得更深。
清单 5. GML 格式的 USGS 机场表
<ogr:FeatureCollection
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ogr.maptools.org/airports.xsd"
xmlns:ogr="http://ogr.maptools.org/"
xmlns:gml="http://www.opengis.net/gml">
<gml:featureMember>
<ogr:airprtx020 fid="F0">
<ogr:geometryProperty>
<gml:Point>
<gml:coordinates>-156.042831420898438,19.73573112487793</gml:coordinates>
</gml:Point>
</ogr:geometryProperty>
<ogr:AREA>0.000</ogr:AREA>
<ogr:PERIMETER>0.000</ogr:PERIMETER>
<ogr:AIRPRTX020>1</ogr:AIRPRTX020>
<ogr:LOCID>KOA</ogr:LOCID>
<ogr:FEATURE>Airport</ogr:FEATURE>
<ogr:NAME>Kona International At Keahole</ogr:NAME>
<ogr:TOT_ENP>1271744</ogr:TOT_ENP>
<ogr:STATE>HI</ogr:STATE>
<ogr:COUNTY>Hawaii County</ogr:COUNTY>
<ogr:FIPS>15001</ogr:FIPS>
<ogr:STATE_FIPS>15</ogr:STATE_FIPS>
</ogr:airprtx020>
</gml:featureMember>
<gml:featureMember>...</gml:featureMember>
<gml:featureMember>...</gml:featureMember>
</ogr:FeatureCollection>
现在,创建如清单 6 所示的 restoreUsgsAirports.groovy 脚本。要获取具有名称空间的元素,需要声明几个 groovy.xml.Namespace 变量。与前面的 restoreAirport.groovy 脚本(清单 3)中使用的简单的点表示法不同,这里的具有名称空间的元素要用方括号括上。
清单 6. 将 USGS 机场数据恢复到数据库
f = new File(args[0])
println f
sql = groovy.sql.Sql.newInstance(
"jdbc:mysql://localhost/trip?autoReconnect=true",
"grails",
"server",
"com.mysql.jdbc.Driver")
FeatureCollection = new groovy.util.XmlParser().parse(f)
ogr = new groovy.xml.Namespace("http://ogr.maptools.org/")
gml = new groovy.xml.Namespace("http://www.opengis.net/gml")
FeatureCollection[gml.featureMember][ogr.airprtx020].each{airprtx020 ->
println "${airprtx020[ogr.LOCID].text()} -- ${airprtx020[ogr.NAME].text()}"
points = airprtx020[ogr.geometryProperty][gml.Point][gml.coordinates].text().split(",")
sql.execute(
"insert into usgs_airports (airport_id, locid, feature, airport_name, state,
county, latitude, longitude) values(?,?,?,?,?,?,?,?)",
[airprtx020[ogr.AIRPRTX020].text(),
airprtx020[ogr.LOCID].text(),
airprtx020[ogr.FEATURE].text(),
airprtx020[ogr.NAME].text(),
airprtx020[ogr.STATE].text(),
airprtx020[ogr.COUNTY].text(),
points[1],
points[0]]
)
}
}
else{
println "USAGE: restoreAirports [filename]"
}
在命令指示符处输入如下信息,将 usgs_airports.xml 文件中的数据插入到新创建的表中:
usgs-airports.xml
验证数据插入成功:从命令行登入 MySQL,确保数据已经就位,如清单 7 所示:
清单 7. 验证数据库中的 USGS 机场数据
mysql> desc usgs_airports;
+--------------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+-------+
| airport_id | bigint(20) | NO | PRI | | |
| locid | varchar(4) | YES | | NULL | |
| feature | varchar(80) | YES | | NULL | |
| airport_name | varchar(80) | YES | | NULL | |
| state | varchar(2) | YES | | NULL | |
| county | varchar(50) | YES | | NULL | |
| latitude | varchar(30) | YES | | NULL | |
| longitude | varchar(30) | YES | | NULL | |
+--------------+-------------+------+-----+---------+-------+
8 rows in set (0.01 sec)
mysql> select count(*) from usgs_airports;
+----------+
| count(*) |
+----------+
| 901 |
+----------+
1 row in set (0.44 sec)
mysql> select * from usgs_airports limit 1\G
*************************** 1. row ***************************
airport_id: 1
locid: KOA
feature: Airport
airport_name: Kona International At Keahole
state: HI
county: Hawaii County
latitude: 19.73573112487793
longitude: -156.042831420898438
禁用 dbCreate
遗留表就位之后,您需要做最后一件事:禁用 grails-app/conf/DataSource.groovy 中的 dbCreate 变量。回想一下 “GORM:有趣的名称,严肃的技术” 就会知道,如果相应的表不存在的话,该变量会指示 GORM 在后台创建它,并且会改变任何现有表,从而匹配 Grails 域类。因此,如果要处理遗留表的话,就一定要禁用该特性,这样 GORM 才不会破坏其他应用程序可能会用到的模式。
如果能够有选择地为特定的表启用或禁用 dbCreate 就好了。不幸的是,它是一个全局的 “全有或全无” 的设置。我遇到既有新表又有遗留表的情况时,会先允许 GORM 创建新表,然后禁用 dbCreate,导入现有的遗留表。在这样的情况下,您就会了解到有一个好的备份与恢复策略是多么重要了。
静态映射块
我将示范的第一个将域类映射到遗留表的策略是使用静态 mapping 块。大多数情况下我都会使用这个块,因为它感觉最像 Grails。我习惯将静态 constraints 块添加到域类,这样添加静态 mapping 块感觉起来和添加框架的其余部分是一致的。
将 grails-app/domain/Airport.groovy 文件复制到 grails-app/domain/AirportMapping.groovy。这个名称只是为了示范用的。因为将会有三个类全部映射回相同的表中,因此需要有一种方式来将每一个类单独命名(这在真实的应用程序中不大可能会发生)。
注释掉城市与国家字段,因为在新的表中没有这些字段。然后从 constraints 块中移除这些字段。现在添加 mapping 块,将 Grails 的名称链接到数据库名,如清单 8 所示:
清单 8. AirportMapping.groovy
static constraints = {
name()
iata(maxSize:3)
state(maxSize:2)
lat()
lng()
}
static mapping = {
table "usgs_airports"
version false
columns {
id column: "airport_id"
name column: "airport_name"
iata column: "locid"
state column: "state"
lat column: "latitude"
lng column: "longitude"
}
}
String name
String iata
//String city
String state
//String country = "US"
String lat
String lng
String toString(){
"${iata} - ${name}"
}
}
mapping 块的第一条语句将 AirportMapping 类链接到 usgs_airports 表。下一条语句通知 Grails 表没有 version 列(GORM 通常会创建一个 version 列来优化乐观锁定)。最后,columns 块将 Grails 名称映射到数据库名称。
注意,使用了这个映射技术,表中的某些特定的字段是可以忽略的。在这种情况下,feature 与 county 列未表示在域类中。要想让未储存于表中的字段存在于域类中,可以添加静态 transients 行。该行看起来与一对多关系中使用的 belongsTo 变量类似。例如,如果 Airport 类中有两个字段不需要储存到表中,代码会是这样的:
此处示范的 mapping 块仅仅涉及到此技术可以实现的皮毛而已。想了解更多的信息,参见 参考资料。
设置遗留表为只读
输入 grails generate-all AirportMapping,创建 控制器和 GSP 视图。由于此表实质上是一个查找表,所以请进入 grails-app/controllers/AirportMappingController.groovy,只留下 list 和 show 闭包。移除 delete、edit、update、create 以及 save(不要忘记从 allowedMethods 变量中移除 delete、edit 和 save。可以完全移除整个行,或者只留下方括号空集)。
要使该视图为只读,还需要做几个快捷的更改。首先,从 grails-app/views/airportMapping/list.gsp 顶端移除 New AirportMapping 链接。然后对 grails-app/views/airportMapping/show.gsp 做相同操作。最后,从 show.gsp 底部移除 edit 和 delete 按钮。
输入 grails run-app,验证 mapping 块可以运行。请看一下图 1 中展示的页面:
图 1. 验证 mapping 块可以运行

结合使用遗留 Java 类与 Hibernate 映射文件
了解了 mapping 块后,让我们再深入一步。不难想象如果拥有了遗留表,就有可能也拥有了遗留 Java 类。如果您想将现有 Java 代码与现有表中的数据融合,可以使用接下来的两个映射技术。
在 Java 1.5 引入注释之前,Hibernate 用户需要创建名为 HBM 文件的 XML 映射文件。回忆一下,GORM 是一个优于 Hibernate 的瘦 Groovy facade,因此,那些古老的 Hibernate 技巧仍然奏效也不足为奇。
首先,将遗留 Java 源文件复制到 src/java。如果使用包的话,要为每一个包名创建一个目录。例如,清单 9 中所示的 AirportHbm.java 文件位于 org.davisworld.trip 包中。这意味着该文件的完整路径应该是 src/java/org/davisworld/trip/AirportHbm.java。
清单 9. AirportHbm.java
public class AirportHbm {
private long id;
private String name;
private String iata;
private String state;
private String lat;
private String lng;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
// all of the other getters/setters go here
}
Java 文件一旦就位,就可以挨着它创建一个清单 10 中所示的名为 AirportHbmConstraints.groovy 的 “影子” 文件了。该文件中可以放置本应该位于域类中的静态 constraints 块。切记该文件一定要与 Java 类位于相同的包中。
清单 10. AirportHbmConstraints.groovy
static constraints = {
name()
iata(maxSize:3)
state(maxSize:2)
lat()
lng()
}
src 目录下的文件会在运行应用程序或者创建要部署的 WAR 文件时编译。如果已经编译了 Java 代码的话,也可以仅将它压缩为 JAR 文件并将其置于 lib 目录中。
接下来,让我们来建立控制器。按照约定优于配置的规定,控制器应该命名为 AirportHbmController.groovy。由于 Java 类位于一个包中,因此可以将控制器置于同一包中,或是在文件顶部导入 Java 类。我更偏爱导入的方法,如清单 11 所示:
清单 11. AirportHbmController.groovy
class AirportHbmController {
def scaffold = AirportHbm
}
接下来,将现有的 HBM 文件复制到 grails-app/conf/hibernate。应该会有一个如清单 12 所示的单一的 hibernate.cfg.xml 文件,您要在这里指定每一个类用的映射文件。在本例中,应该会有一个 AirportHbm.hbm.xml 文件的条目。
清单 12. hibernate.cfg.xml
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping resource="AirportHbm.hbm.xml"/>
</session-factory>
</hibernate-configuration>
每一个类都必须有它自己的 HBM 文件。该文件为先前使用的静态 mapping 块的 XML 等价体。清单 13 展示了 AirportHbm.hbm.xml:
清单 13. AirportHbm.hbm.xml
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
<class name="org.davisworld.trip.AirportHbm" table="usgs_airports">
<id name="id" column="airport_id">
<generator class="native"/>
</id>
<property name="name" type="java.lang.String">
<column name="airport_name" not-null="true" />
</property>
<property name="iata" type="java.lang.String">
<column name="locid" not-null="true" />
</property>
<property name="state" />
<property name="lat" column="latitude" />
<property name="lng" column="longitude" />
</class>
</hibernate-mapping>
包的完整名是参考 Java 类而指定的。剩余的条目将 Java 名映射到表名。name 和 iata 字段条目演示了长表单。由于 state 字段在 Java 代码中和表中是一样的,因此可以将其条目缩短。最后两个字段 — lat 与 lng — 演示了缩短了的语法(想了解更多关于 Hibernate 映射文件的信息,参见 参考资料)。
如果 Grails 仍在运行的话,重新启动它。现在应该能够在 http://localhost:8080/trip/airportHbm 看到 Hibernate 映射数据。
对 Java 类使用 Enterprise JavaBeans(EJB)3 注释
正如我在上面所提到的,Java 1.5 引入了注释。注释允许您通过添加 @ 前缀的方式直接向 Java 类添加元数据。Groovy 1.0 在其发行初期(2006 年 12 月)并不支持 Java 1.5 的诸如注释这样的语言特性。一年以后发行的 Groovy 1.5 则发生了翻天覆地的变化。这就意味着您也可以将 EJB3 注释的 Java 文件引入到一个现有的 Grails 应用程序中了。
再次启动 EJB3 注释的 Java 文件。将清单 14 展示的 AirportAnnotation.java 置于 src/java/org.davisworld.trip 中,紧挨着 AirportHbm.java 文件:
清单 14. AirportAnnotation.java
import javax.persistence.*;
@Entity
@Table(name="usgs_airports")
public class AirportAnnotation {
private long id;
private String name;
private String iata;
private String state;
private String lat;
private String lng;
@Id
@Column(name="airport_id", nullable=false)
public long getId() {
return id;
}
@Column(name="airport_name", nullable=false)
public String getName() {
return name;
}
@Column(name="locid", nullable=false)
public String getIata() {
return iata;
}
@Column(name="state", nullable=false)
public String getState() {
return state;
}
@Column(name="latitude", nullable=false)
public String getLat() {
return lat;
}
@Column(name="longitude", nullable=false)
public String getLng() {
return lng;
}
// The setter methods don't have an annotation on them.
// They are not shown here, but they should be in the file
// if you want to be able to change the values.
}
注意,一定要导入文件顶部的 javax.persistence 包。@Entity 与 @Table 注释了类声明,将它映射到了适当的数据库表中。其他的注释位于每一个字段的 getter 方法之上。所有的字段都应该有 @Column 注释,它将字段名映射到列名。主键也应该有一个 @ID 注释。
清单 15 中的 AirportAnnotationConstraints.groovy 文件与前面清单 10 中的例子没什么不同:
清单 15. AirportAnnotationConstraints.groovy
static constraints = {
name()
iata(maxSize:3)
state(maxSize:2)
lat()
lng()
}
AirportAnnotationController.groovy(清单 16 中所展示的)是按照通常的方式搭建的:
清单 16. AirportAnnotationController.groovy
class AirportAnnotationController {
def scaffold = AirportAnnotation
}
hibernate.cfg.xml 文件再次开始其作用。这回,语法有点不同。您直接将它指向类,而不是指向一个 HBM 文件,如清单 17 所示:
清单 17. hibernate.cfg.xml
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<mapping resource="AirportHbm.hbm.xml"/>
<mapping class="org.davisworld.trip.AirportAnnotation"/>
</session-factory>
</hibernate-configuration>
要让注释开始生效,还需要做最后一件事。Grails 并不是本来就被设置成可以查找 EJB3 注释的。而是需要导入 grails-app/conf/DataSource.groovy 中的一个特定的类,如清单 18 所示:
清单 18. DataSource.groovy
dataSource {
configClass = GrailsAnnotationConfiguration.class
pooled = false
driverClassName = "com.mysql.jdbc.Driver"
username = "grails"
password = "server"
}
一旦导入了 org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration 并允许 Spring 将其作为 configClass 而注入 dataSource 块之后,Grails 就会支持 EJB3 注释了,同时它还可以支持 HBM 文件和本地映射块。
如果忘了这最后一步的话(我几乎每一次在 Grails 中使用 EJB3 注释时都会忘记这一步 ),会得到如下的错误信息:
清单 19. 未注入 DataSource.groovy 中的 configClass 时抛出的异常
An AnnotationConfiguration instance is required to use
<mapping class="org.davisworld.trip.AirportAnnotation"/>
结束语
这样看来,将对象 映射 到 Grails 的关系 数据库中应该易如反掌(毕竟,这正是它被命名为 GORM 的原因)。一旦您有信心能够轻松备份和恢复数据,您就会有很多种方式使 Grails 符合遗留数据库中的非标准的命名约定。静态的 mapping 块是完成这个任务的最简单的方式,因为它与 Grails 最相似。但如果您的遗留 Java 类已经映射到了遗留数据库中的话,那就不用多此一举了。无论您使用 HBM 文件还是较新的 EJB3 注释,Grails 都可以直接利用您已经完成的成果,这样您就可以投身其他的任务了。
在下一篇文章中,您将有机会了解 Grails 事件模型。从构建脚本到单个的 Grails 工件(域类、控制器等),所有这些都会在应用程序的生命周期的关键点抛出事件。因此在下一篇文章中,您将学习如何设置侦听器来捕获这些事件,并使用定制操作回应。到那时,请尽情享受精通 Grails 带来的乐趣吧。