精通 Grails: GORM - 有趣的名称,严肃的技术

理解数据库和 Grails

任何好的 Web 框架都需要一个可靠的持久性策略。在 精通 Grails 的第二期文章中,Scott Davis 介绍了 Grails 的对象关系映射(Grails Object Relational Mapping,GORM)API。在本文中将看到用 GORM 能够轻松地在 Grails 应用程序中创建表关系、实施数据验证规则、修改关系数据库。

Scott Davis, 主编, AboutGroovy.com

Scott DavisScott Davis 是国际知名作家、演讲家、软件开发人员。他出版的书籍有 Groovy Recipes: Greasing the Wheels of JavaGIS for Web Developers: Adding Where to Your ApplicationThe Google Maps APIJBoss At Work



2008 年 3 月 07 日

在上个月开篇的 精通 Grails 文章中,介绍了名为 Grails 的新 Web 框架。Grails 结合了最新的实践,例如模型-视图-控制器的关注点隔离和约定优于配置。通过将这些实践与其中内置的 scaffolding 功能结合,使用 Grails 只需花几分钟就能建立并运行起一个 Web 站点。

这篇文章的重点是使用 Grails 可以实现简化的另一领域:使用 Grail 对象关系映射(Grails Object Relational Mapping,GORM)API 进行持久化。我将首先介绍什么是对象关系映射器(object-relational mapper,ORM),以及如何创建一对多关系。然后将学习数据验证(确保应用程序不会出现无用信息输入/无用信息输出(garbage in/garbage out)问题)。然后将看到如何使用 Grails ORM 的领域特定语言(domain-specific language,DSL),使用 DSL 语句能够在幕后对普通的旧 Groovy 对象(plain old Groovy objects,POGO)的持久化方式进行微调。最后,将看到能够轻松地切换到另一个关系数据库。任何有 JDBC 驱动程序和 Hibernae 方言的数据库都受支持。

ORM 定义

关系数据库出现于 20 世纪 70 年代末,但是软件开发人员至今依然在寻求有效的方法来存入和取出数据。当今软件的基础并不是多数流行数据库所使用的关系理论,而是基于面向对象的原则。

为此产生了一整套称为 ORM 的程序,用来缓解在数据库和面向对象的代码之间来回转移数据的痛苦。Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA)是处理这一问题的三个流行的 Java API(请参阅 参考资料),不过它们都并不完美。这个问题如此持久(不是故意一语双关,而是实情),以至于有了自己专用的术语对象关系阻抗失谐(请参阅 参考资料)。

GORM 是在 Hibernate 上的一层薄薄的 Groovy 层。(我猜 “Gibernate” 不像 “GORM” 那样容易上口)。这意味着现有的所有 Hibernate 技巧仍然有用 — 例如,HBM 映射文件和标注得到全面支持 — 但这篇文章的重点是 GORM 带来的有趣功能。

创建一对多关系

对于将 POGO 保存到数据库表所面临的挑战,很容易被低估。实际上,如果只是将一个 POGO 映射到一个表,那么工作相当简单 —POGO 的属性恰好映射到表列。但是当对象模型稍稍变复杂一点,例如有两个彼此相关的 POGO,那么事情将很快变得困难起来。

例如,请看上个月 文章 中开始的旅行规划网站。显然,Trip POGO 在应用程序中有重要的作用。请在文本编辑器中打开 grails-app/domain/Trip.groovy(如清单 1 所示):

清单 1. Trip
class Trip { 
  String name
  String city
  Date startDate
  Date endDate
  String purpose
  String notes
}

清单 1 中的每个属性都轻松漂亮地映射到 Trip 表中的对应字段。还记得在上一期的文章中说过,在 Grail 启动时,所有存储在 grails-app/domain 目录下的 POGO 都会自动创建对应的表。默认情况下,Grails 使用内嵌的 HSQLDB 数据库,但是到本文结束时,就能够使用自己喜欢的其他任意关系数据库。

旅程中经常要包含飞行,所以还应该创建一个 Airline 类(如清单 2 所示):

清单 2. Airline
class Airline { 
  String name
  String url
  String frequentFlyer
  String notes
}

现在要将这两个类链接起来。为了计划一个通过 Xyz 航线到芝加哥的旅行,在 Groovy 代码中的表示方法与在 Java 代码中的表示方法相同 — 要在 Trip 类中添加一个 Airline 属性(如清单 3 所示)。这个技术称为对象组合(object composition)(请参阅 参考资料)。

清单 3.在 Trip 类中添加 Airline 属性
class Trip { 
  String name
  String city
  ...
  Airline airline
}

对于软件模型来说,这种表示方法非常合适,但是关系数据库采取的表示方法略有不同。表中的每个记录都有一个惟一的 ID,称为主键。向 Trip 表添加一个 airline_id 字段,就能将一个记录与另一个记录链接在一起(在这个示例中,“Xyz航线” 记录与 “芝加哥旅行” 记录链接)。这称为一对多 关系:一个航线能够与多个旅行关联。(在 Grails 的联机文档中,可以找到一对一和多对多关系的示例,请参阅 参考资料。)

这样形成的数据库模式只有一个问题。您可能对数据库成功地进行了规范化(请参阅 参考资料),但是现在表中的列与软件模型就失去了同步。如果将 Airline 字段替换成 AirlineId 字段,那么实现的细节(在数据库中持久化 POGO)就泄漏 到了对象模型。Joel Spolsky 将这种情况称为 抽象泄漏法则(Law of Leaky Abstractions)(请参阅 参考资料)。

GORM 有助于缓解抽象泄漏问题,它支持使用对 Groovy 有意义的方式表示对象模型,由 GORM 在幕后处理关系数据库的问题。但是正如即将看到的,如果需要,覆盖默认设置也很容易。GORM 并不是隐藏数据库细节的不透明的 抽象层,而是一个半透明的 层 — 它尝试在不进行用户干预的情况下执行正确的工作,但是如果用户需要对它的行为进行自定义,它也可以提供支持。这样它就提供了两方面的好处。

现在已经在 POGO 类 Trip 中添加了 Airline 属性。要完成一对多关系,还要在 Trip 这个 POGO 中添加一个 hasMany 设置,如清单 4 所示:

清单 4. 在 Airline 中建立一对多关系
class Airline { 
  static hasMany = [trip:Trip]

  String name
  String url
  String frequentFlyer
  String notes
}

静态的 hasMany 设置是个 Groovy 的 hashmap:键是 trip;值是 Trip 类。如果要在 Airline 类中设置额外的一对多关系,那么可以将逗号分隔的键/值对放在方括号内。

现在在 grails-app/controllers 中迅速创建一个 AirlineController 类(如清单 5 所示),这样就能看出新的一对多关系的效果:

清单 5. AirlineController class
class AirlineController { 
  def scaffold = Airline
}

还记得在上一期的文章中说过 def scaffold 的功能是告诉 Grails 在运行的时候动态创建基本的 list()save()edit() 方法。它还告诉 Grails 动态创建 GroovyServer Page(GSP)视图。请确保 TripControllerAirlineController 都包含 def scaffold。如果曾经因为输入 grails generate-all 在 grails-app/views 中生成过任何 GSP 工件,例如 trip 目录或者是 airline 目录,都应该删除它们。对于这个示例,需要确保既允许 Grails 动态搭建控制器,又允许它动态搭建视图。

现在域类和控制器类都已经就位,请启动 Grails。请输入 grails prod run-app 在生产模式下运行应用程序。如果一切正常,应该看到欢迎消息:

Server running. Browse to http://localhost:8080/trip-planner

在浏览器中,应该看到 AirlineControllerTripController 链接。单击 AirlineController 链接,填写 Xyz 航线的详细信息,如图 1 所示:

图 1. 一对多关系:一方
一对多关系:一方

如果不喜欢字段按照字母顺序排序,也不用担心。在下一节就能改变这种方式。

现在新建一个旅程,如图 2 所示。请注意 Airline 的组合框。添加到 Airline 表的每个记录都在这里显示。不用担心 “泄漏” 主键 — 在下一节将会看到如何添加更具描述性的标签。

图 2. 一对多关系:多方
一对多关系:多方

裸对象

前面刚刚了解了在 Airline POGO 上添加提示(静态的 hasMany)如何影响表在幕后的创建方式以及前端生成的视图。这种使用裸对象 修饰域对象的模式(请参阅 参考资料)在 Grails 中应用得非常广泛。将这条信息直接添加到 POGO 内,就消除了对外部 XML 配置文件的需求。所有信息都在一个位置内,可以显著提高生产率。

例如,如果想消除显示在组合框中的主键的泄漏,只要在 Airline 类中添加 toString 方法就可以,如清单 6 所示:

清单 6. 在 Airline 中添加 toString 方法
class Airline { 
  static hasMany = [trip:Trip]

  String name
  String url
  String frequentFlyer
  String notes
    
  String toString(){
    return name
  }
}

从现在开始,在组合框中显示的值就是航线的名称。这里真正酷的地方在于:如果 Grail 依然在运行,那么只要保存 Airline.groovy,修改就会生效。请在浏览器中新建一个 Trip,看看这样做的效果。因为视图是动态生成的,所以能够迅速地在文本编辑器和浏览器之间来回切换,直到看到合适的视图 — 不需要重新启动服务器。

现在我们来解决字段按字母顺序排序的问题。要解决这个问题,需要向 POGO 添加另一个配置:static constraints 块。请按清单 7 所示的顺序将字段添加到这个块(这些约束不影响列在表中的顺序 — 只影响在视图中的顺序)。

清单 7. 修改 Airline 中的字段顺序
class Airline { 
  static constraints = {
    name()
    url()
    frequentFlyer()
    notes()  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes
  
  String toString(){
    return name
  }
}

将修改保存到 Airline.groovy 文件,在浏览器中新建一个航线。现在里面的字段应该按照在清单 7 中指定的顺序出现,如图 3 所示:

图 3. 自定义的字段顺序
自定义的字段顺序

在您准备责备我没有必要在 POGO 中输入两次字段名称而违背 DRY 原则(不要重复你自己)时(请参阅 参考资料),请稍等一下,因为将它们放在独立的块内有很好的理由。清单 7 的 static constraints 块内的大括号不会总是空白。


数据验证

除了指定字段顺序, static constraints 块还允许在里面放置一些验证规则。例如,可以在 String 字段上施加长度限制(默认是 255 个字符)。这样就能确保 String 值与指定的模式(例如电子邮件地址或 URL)匹配。甚至还能将字段设置为可选或必需的。关于可用的验证规则的完整列表,请参阅 Grails 的联机文档(请参阅 参考资料)。

清单 8 显示的 Airline 类中在约束块内添加了验证规则:

清单 8. 将数据验证添加到 Airline
class Airline { 
  static constraints = {
    name(blank:false, maxSize:100)
    url(url:true)
    frequentFlyer(blank:true)
    notes(maxSize:1500)  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes

  String toString(){
    return name
  }
}

保存修改后的 Airline.groovy 文件,在浏览器中新建一条航线。如果违反了验证规则,会收到警告,如图 4 所示:

图 4. 验证警告
验证警告

可以在 grails-app/i18n 目录的 messages.properties 文件中对警告消息进行自定义。请注意,默认的消息已经用多种语言进行了本地化(请参阅 Grail 联机文档中的验证一节,了解如何在每个类、每个字段的基础上创建自定义消息)。

清单 8 中的多数约束只影响视图层,但是有两个约束也会影响持久层。例如,数据库中的 name 列现在是 100 个字符长。notes 字段除了从输入字段转为视图的文本区域之外(对于大于 255 个字符的字段会进行这个转换),还从 VARCHAR 列转为 TEXTCLOBBLOB 列。这些转变取决于在后台使用的数据库类型和它的 Hibernate 方言 — 当然,这些也是可以修改的。


Grails ORM 的 DSL

可以使用任何常用的配置方法覆盖 Hibernate 的默认设置:HBM 映射文件或者标注。但是 Grails 提供了第三种方式,这种方式采用了裸对象的形式。只要向 POJO 添加一个 static mapping 块,就能覆盖默认的表和字段名称,如清单 9 所示:

清单 9. 使用 GORM DSL
class Airline { 
  static mapping = {
    table 'some_other_table_name'
    columns {
      name column:'airline_name'
      url column:'link'
      frequentFlyer column:'ff_id'
    }
  }

  static constraints = {
    name(blank:false, maxSize:100)
    url(url:true)
    frequentFlyer(blank:true)
    notes(maxSize:1500)  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes

  String toString(){
    return name
  }
}

如果要在新的 Grails 应用程序中使用现有的遗留表,那么这个映射块会特别有帮助。虽然这里只介绍了点皮毛,但 ORM DSL 提供的功能远不止是重新映射表和字段的名称。每个列的默认数据类型都可以覆盖。可以调整主键的生成策略,甚至指定复合主键。可以修改 Hibernate 的缓存设置,调整外键关联使用的字段,等等。

要记住的要点是所有这些设置都集中在一个地方:POGO 内。


理解 DataSource.groovy

目前所做的工作都集中在单个类的调整上。下面我们要回过头来做一些全局性的修改。所有域类共享的特定于数据库的配置保存在一个公共文件内:grails-app/conf/DataSource.groovy,如清单 10 所示。请将这个文件放在一个文本编辑器内仔细查看:

清单 10. DataSource.groovy
dataSource {
  pooled = false
  driverClassName = "org.hsqldb.jdbcDriver"
  username = "sa"
  password = ""
}
hibernate {
  cache.use_second_level_cache=true
  cache.use_query_cache=true
  cache.provider_class='org.hibernate.cache.EhCacheProvider'
}
// environment specific settings
environments {
  development {
    dataSource {
      dbCreate = "create-drop" // one of 'create', 'create-drop','update'
      url = "jdbc:hsqldb:mem:devDB"
    }
  }
  test {
    dataSource {
      dbCreate = "update"
      url = "jdbc:hsqldb:mem:testDb"
    }
  }
  production {
    dataSource {
      dbCreate = "update"
      url = "jdbc:hsqldb:file:prodDb;shutdown=true"
    }
  }
}

dataSource 块内能够修改用来连接数据库的 driverClassNameusernamepasswordhibernate 块用来调整缓存设置(除非是 Hibernate 专家,否则不要在这里进行任何调整)。真正有意思的是 environments 块。

还记得在上一期的文章中介绍过 Grails 能够在三种模式下运行:开发模式、测试模式和生产模式。在输入 grails prod run-app 时,就是告诉 Grails 使用 production 块中的数据库设置。如果希望根据环境调整 usernamepassword 的设置,只要将这些设置从 dataSource 块复制到每个 environment 块,并修改设置的值即可。 environment 块中的设置覆盖 dataSource 块中的设置。

url 设置是 JDBC 的连接字符串。请注意在 production 模式下,HSQLDB 使用基于文件的数据存储。在 developmenttest 模式下,HSQLDB 使用内存中的数据存储。上个月我介绍过如果想让 Trip 的记录在服务器重新启动之后保留,应该在 production 模式下运行。现在您应该知道如何在 developmenttest 模式下进行设置以实现这一功能 — 只要将 url 设置从 production 复制过来即可。当然,将 Grails 指向 DB2、MySQL 或者其他传统的基于文件的数据库也可以解决记录消失的问题(立刻就会介绍 DB2 和 MySQL 的设置)。

dbCreate 的值在不同的环境下会产生不同的行为。它是底层的 hibernate.hbm2ddl.auto 设置的别名,负责指定 Hibernate 在幕后如何管理表。将 dbCreate 设为 create-drop,就是告诉在启动的时候创建 表,在关闭的时候删除 表。如果将值改为 create,那么 Hibernate 会在需要的时候创建新表和修改现有表,但是重新启动之间的所有记录都会被删除。production 模式的默认值 —update— 会在重新启动之间保持所有数据,也会在需要的时候创建或修改表。

如果对传统的数据库使用 Grails,那么我强烈推荐注释掉 dbCreate 的值。这样就告诉 Hibernate 不要触及数据库的模式。虽然这意味着必须自行保持数据模型与底层数据库同步,但这可以大大减少愤怒的 DBA 为了弄清楚谁在未经允许的情况下不断修改数据库表而发来的质问邮件。

添加自定义环境也很容易。例如,公司中可能有一个 beta 程序。只要在 DataSource.groovy 中其他块之后创建一个 beta 块即可(也可以针对与数据库无关的设置在 grails-app/conf/Config.groovy 中添加一个 environments 块)。要在 beta 模式下启动 Grails,请输入 grails -Dgrails.env=beta run-app


修改数据库

如果通过 dbCreate 设置允许 Hibernate 管理表,那么只需三步就能迅速地将 Grails 指向新表:创建数据库并登录,将 JDBC 驱动程序复制到 lib 目录,调整 DataSource.groovy 中的设置。

对于不同的产品,创建数据库和用户的操作过程有很大差异。对于 DB2 来说,可以按照一份联机的详细教程逐步进行(请参阅 参考资料)。创建了数据库和用户之后,请调整 DataSource.groovy,让它使用清单 11 中的值(这里显示的值假设使用的数据库名为 trip)。

清单 11. DataSource.groovy 的 DB2 设置
  driverClassName = "com.ibm.db2.jcc.DB2Driver"
  username = "db2admin"
  password = "db2admin"
  url = "jdbc:db2://localhost:50000/trip"

如果安装了 MySQL,那么请使用清单 12 所示的步骤登录为 root 用户,并创建 trip 数据库:

清单 12. 创建 MySQL 数据库
$ mysql --user=root
mysql> create database trip;
mysql> use trip;
mysql> grant all on trip.* to grails@localhost identified by 'server';
mysql> flush privileges;
mysql> exit
$ mysql --user=grails -p --database=trip

创建了数据库和用户之后,请调整 DataSource.groovy,让它使用清单 13 所示的值:

清单 13. DataSource.groovy 的 MySQL 设置
  driverClassName = "com.mysql.jdbc.Driver"
  username = "grails"
  password = "server"
  url = "jdbc:mysql://localhost:3306/trip?autoreconnect=true"

创建了数据库,将驱动程序 JAR 复制到 lib 目录,而且调整了 DataSource.groovy 中的值之后,多次输入 grails run-app。现在的 Grails 使用的就是 HSQLDB 之外的数据库。


结束语

现在对本期的 GORM 介绍做一小结。通过本文,您应该很好地理解了什么是 ORM、如何管理验证和表关系以及如何用自己选择的数据库替换 HSQLDB。

这个系列的下一篇文章将重点放在 Web 层上。在下篇文章中将学习 GSP 的更多内容以及各种 Groovy TagLib。还将看到如何将 GSP 拆分成多个部分— 即能够在多个页面上重用的标记片段。最后,还将学会如何自定义在搭建的视图中使用的默认模板。

最后,希望您喜欢精通 Grails 系列文章。

参考资料

学习

获得产品和技术

  • DB2:下载 DB2 9.5 数据服务器的测试驱动程序。
  • Grails:下载 Grails 的最新发行版。

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology, Web development, Open source
ArticleID=293693
ArticleTitle=精通 Grails: GORM - 有趣的名称,严肃的技术
publish-date=03072008