精通Grails:Grails与遗留数据库

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

sql = groovy.sql.Sql.newInstance(   "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 元素。

清单 2 展示了由此得到的 XML:

清单 2. 来自备份脚本的 XML 输出

     2   Denver International Airport   Denver   CO   US   den   39.8583188   -104.6674674    ...  ...

在备份脚本中,一定要按照主键顺序拖出记录。当恢复这个数据时,一定要按相同的顺序插入值,以 确保外键值同样匹配(关于这一点我将在下一小节进一步详述)。

注意,该脚本是完全独立于 Grails 框架的。要使用它,就一定要在您的系统上安装 Groovy。另外, 类路径中一定要有 JDBC 驱动程序 JAR。可以在运行脚本时进行指定。在 UNIX® 中,要输入:

groovy -classpath /path/to/mysql.jar:. backupAirports.groovy

当然了 ,在 Windows® 上,相应的文件路径和 JAR 分隔符是不同的。在 Windows 中,则需要输入:

groovy -classpath c:/path/to/mysql.jar;. backupAirports.groovy

由于我经常使用 MySQL,所以我将一份该 JAR 的副本保存在了我的主目录(在 UNIX 上为 /Users/sdavis,在 Windows 上为 c:/Documents and Settings/sdavis)中的 .groovy/lib 目录中。当 从命令行运行 Groovy 脚本时,该目录中的 JAR 会自动包含在类路径中。

清单 1 中的脚本将输 出写到了屏幕。要将数据保存在一个文件中,可以在运行脚本时重定向输出:

groovy  backupAirports.groovy > airports.xml

恢复数据

从数据库中获取出数据仅 仅是成功了一半。还要再将数据恢复到数据库中。清单 3 中展示的 resToreAirports.groovy 脚本用 Groovy XmlParser 读入了 XML,构造了一个 SQL insert 语句,并用了一个 Groovy SQL 对象来执行该 语句。

清单 3. 从 XML 中恢复数据库记录的 Groovy 脚本if(args.size()) {  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]"}

要运行该脚本,需要输入:

groovy resToreAirports.groovy airports.xml

切记,对于要工作的表之间的关系而言,关系的一 的方面的主键字段一定要与关系的多 的方面的外 键字段相匹配。例如,储存于 airport 表的 id 列中的值一定要与 flight 表的 arrival_airline_id 列中的值相同。

转换 USGS 数据

USGS 将机场的数据提供为一个 shapefile。这是一个交换地理数据的固定文件格式。一个 shapefile 至少由三个文件组成。.shp 文件包含地理数据 — 此处指每个机场的纬度/经度。.shx 文件是一个空间 索引。.dbf 文件是 — 您猜对了 — 一个很好的古老的 dBase 文件,它包含了所有的非空间数据(此处 指机场名、IATA 代码等)。

我使用了 Geospatial Data Abstraction Library(GDAL)— 一套处理地理数据的开源的命令行工具 集 — 来将 shapefile 转换为文中使用的地理标记语言(Geography Markup Language,GML)文件。我 使用的具体的命令为:

ogr2ogr -f ”GML” airports.xml airprtx020.shp

您还可以使用 GDAL 将数据转换为逗号分割值(comma-separated value,CSV)、GeoJSON、Keyhole 标记语言(Keyhole Markup Language,KML)以及其他很多种格式。

为了确保自动编号的 id 字段被恢复为相同的值,一定要在恢复表前将它们全部删除。这样就可以在 下次启动 Grails 重新创建表时将自动编号重置为 0。

将机场数据安全地备份之后(大概其他表中的数据也已经安全备份了),那么现在您就可以开始试验 一些新的 “遗留” 数据了。不懂么?看完下一小节您就会明白了。

导入新的机场数据

美国地质勘探局(United States Geological Survey,USGS)发表了一个全面的美国机场的列表,包 括 IATA 代码和纬度/经度。果然,USGS 字段与现行使用的 Airport 类不相匹配。虽然可以改变 Grails 类,使它与 USGS 表中的名称相匹配,但是这要大量改写应用程序。相反,本文不需要这样做,而是探讨 几种不同的技术,在后台将现有的 Airport 类无缝映射到新的、不同的表模式中。

首先,需要将 USGS “遗留” 数据导入到数据库。然后运行清单 4 中的 createUsgsAirports.groovy 脚本,创建新表(该脚本假设您正在使用 MySQL。由于每个数据库创建新表 的语法有所不同,所以使用其他数据库时,需要对该脚本做出适当修改)。

清单 4. 创建 USGS 机场表

sql = groovy.sql.Sql.newInstance(   "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 机场表

                    -156.042831420898438,19.73573112487793             0.000    0.000    1    KOA    Airport    Kona International At Keahole    1271744    HI    Hawaii County    15001    15       ...  ...

现在,创建如清单 6 所示的 resToreUsgsAirports.groovy 脚本。要获取具有名称空间的元素,需要 声明几个 groovy.xml.Namespace 变量。与前面的 resToreAirport.groovy 脚本(清单 3)中使用的简 单的点表示法不同,这里的具有名称空间的元素要用方括号括上。

清单 6. 将 USGS 机场数据恢复到数据库

if(args.size()){  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)  gr = 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 文件中的数据插入到新创建的表中:

groovy resToreUsgsAirports.groovyusgs-airports.xml

验证数据插入成功:从命令行登入 MySQL,确保数据已经就位,如清单 7 所示:

清单 7. 验证数据库中的 USGS 机场数据

$ mysql --user=grails -p -- database=tripmysql> 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: Airportairport_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

class AirportMapping{  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 类中有两个字段不需要储 存到表中,代码会是这样的:

static transients = ["tempField1", "tempField2"]

此处示范的 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

package org.davisworld.trip;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

package org.davisworld.tripstatic constraints = {  name()  iata(maxSize:3)  state(maxSize:2)  lat()  lng()}

src 目录下的文件会在运行应用程序或者创建要部署的 WAR 文件时编译。如果已经编译了 Java 代码 的话,也可以仅将它压缩为 JAR 文件并将其置于 lib 目录中。

接下来,让我们来建立控制器。按照约定优于配置的规定,控制器应该命名为 AirportHbmController.groovy。由于 Java 类位于一个包中,因此可以将控制器置于同一包中,或是在 文件顶部导入 Java 类。我更偏爱导入的方法,如清单 11 所示:

清单 11. AirportHbmController.groovy

import  org.davisworld.trip.AirportHbmclass AirportHbmController {  def scaffold = AirportHbm}

接下来,将现有的 HBM 文件复制到 grails-app/conf/hibernate。应该会有一个如清单 12 所示的单 一的 hibernate.cfg.xml 文件,您要在这里指定每一个类用的映射文件。在本例中,应该会有一个 AirportHbm.hbm.xml 文件的条目。

清单 12. hibernate.cfg.xml

          

每一个类都必须有它自己的 HBM 文件。该文件为先前使用的静态 mapping 块的 XML 等价体。清单 13 展示了 AirportHbm.hbm.xml:

清单 13. AirportHbm.hbm.xml

                                                                        

包的完整名是参考 Java 类而指定的。剩余的条目将 Java 名映射到表名。name 和 iata 字段条目演 示了长表单。由于 state 字段在 Java 代码中和表中是一样的,因此可以将其条目缩短。最后两个字段 — lat 与 lng — 演示了缩短了的语法。

如果 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

package org.davisworld.trip;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

package org.davisworld.tripstatic constraints = {  name()  iata(maxSize:3)  state(maxSize:2)  lat()  lng()}

AirportAnnotationController.groovy(清单 16 中所展示的)是按照通常的方式搭建的:

清单 16. AirportAnnotationController.groovy

import  org.davisworld.trip.AirportAnnotationclass AirportAnnotationController {  def scaffold = AirportAnnotation}

hibernate.cfg.xml 文件再次开始其作用。这回,语法有点不同。您直接将它指向类,而不是指向一 个 HBM 文件,如清单 17 所示:

清单 17. hibernate.cfg.xml

               

要让注释开始生效,还需要做最后一件事。Grails 并不是本来就被设置成可以查找 EJB3 注释的。而 是需要导入 grails-app/conf/DataSource.groovy 中的一个特定的类,如清单 18 所示:

清单 18. DataSource.groovy

import  org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfigurationdataSource {  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 时抛出的异常

org.hibernate.MappingException:An AnnotationConfiguration instance is required to use

结束语

这样看来,将对象 映射 到 Grails 的关系 数据库中应该易如反掌(毕竟,这正是它被命名为 GORM 的原因)。一旦您有信心能够轻松备份和恢复数据,您就会有很多种方式使 Grails 符合遗留数据库中的 非标准的命名约定。静态的 mapping 块是完成这个任务的最简单的方式,因为它与 Grails 最相似。但 如果您的遗留 Java 类已经映射到了遗留数据库中的话,那就不用多此一举了。无论您使用 HBM 文件还 是较新的 EJB3 注释,Grails 都可以直接利用您已经完成的成果,这样您就可以投身其他的任务了。

在下一篇文章中,您将有机会了解 Grails 事件模型。从构建脚本到单个的 Grails 工件(域类、控 制器等),所有这些都会在应用程序的生命周期的关键点抛出事件。因此在下一篇文章中,您将学习如何 设置侦听器来捕获这些事件,并使用定制操作回应。到那时,请尽情享受精通 Grails 带来的乐趣吧。

本文配套源码

所有的胜利,与征服自己的胜利比起来,都是微不足道

精通Grails:Grails与遗留数据库

相关文章:

你感兴趣的文章:

标签云: