精通 Grails: GORM - 有趣的名称,严肃的技术,理解数据库和 Grails
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 方言的数据库都受支持。
关系数据库出现于 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 所示):
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 所示):
class Airline { String name String url String frequentFlyer String notes } |
现在要将这两个类链接起来。为了计划一个通过 Xyz 航线到芝加哥的旅行,在 Groovy 代码中的表示方法与在 Java 代码中的表示方法相同 — 要在 Trip
类中添加一个 Airline
属性(如清单 3 所示)。这个技术称为对象组合(object composition)(请参阅 参考资料)。
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 所示:
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 所示),这样就能看出新的一对多关系的效果:
class AirlineController { def scaffold = Airline } |
还记得在上一期的文章中说过 def scaffold
的功能是告诉 Grails 在运行的时候动态创建基本的 list()
、save()
和 edit()
方法。它还告诉 Grails 动态创建 GroovyServer Page(GSP)视图。请确保 TripController
和 AirlineController
都包含 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 |
|
在浏览器中,应该看到 AirlineController
和 TripController
链接。单击 AirlineController
链接,填写 Xyz 航线的详细信息,如图 1 所示:
如果不喜欢字段按照字母顺序排序,也不用担心。在下一节就能改变这种方式。
现在新建一个旅程,如图 2 所示。请注意 Airline
的组合框。添加到 Airline
表的每个记录都在这里显示。不用担心 “泄漏” 主键 — 在下一节将会看到如何添加更具描述性的标签。
前面刚刚了解了在 Airline POGO 上添加提示(静态的 hasMany
)如何影响表在幕后的创建方式以及前端生成的视图。这种使用裸对象 修饰域对象的模式(请参阅 参考资料)在 Grails 中应用得非常广泛。将这条信息直接添加到 POGO 内,就消除了对外部 XML 配置文件的需求。所有信息都在一个位置内,可以显著提高生产率。
例如,如果想消除显示在组合框中的主键的泄漏,只要在 Airline
类中添加 toString
方法就可以,如清单 6 所示:
清单 6. 在 Airline
中添加 toString
方法
- classAirline{
- statichasMany=[trip:Trip]
- Stringname
- Stringurl
- StringfrequentFlyer
- Stringnotes
- StringtoString(){
- returnname
- }
- }
从现在开始,在组合框中显示的值就是航线的名称。这里真正酷的地方在于:如果 Grail 依然在运行,那么只要保存 Airline.groovy,修改就会生效。请在浏览器中新建一个 Trip
,看看这样做的效果。因为视图是动态生成的,所以能够迅速地在文本编辑器和浏览器之间来回切换,直到看到合适的视图 — 不需要重新启动服务器。
现在我们来解决字段按字母顺序排序的问题。要解决这个问题,需要向 POGO 添加另一个配置:static constraints
块。请按清单 7 所示的顺序将字段添加到这个块(这些约束不影响列在表中的顺序 — 只影响在视图中的顺序)。
清单 7. 修改
Airline
中的字段顺序 - classAirline{
- staticconstraints={
- name()
- url()
- frequentFlyer()
- notes()
- }
- statichasMany=[trip:Trip]
- Stringname
- Stringurl
- StringfrequentFlyer
- Stringnotes
- StringtoString(){
- returnname
- }
- }
将修改保存到 Airline.groovy 文件,在浏览器中新建一个航线。现在里面的字段应该按照在清单 7 中指定的顺序出现,如图 3 所示:
图 3. 自定义的字段顺序
在您准备责备我没有必要在 POGO 中输入两次字段名称而违背 DRY 原则(不要重复你自己)时(请参阅 参考资料),请稍等一下,因为将它们放在独立的块内有很好的理由。清单 7 的 static constraints
块内的大括号不会总是空白。
除了指定字段顺序, static constraints
块还允许在里面放置一些验证规则。例如,可以在 String
字段上施加长度限制(默认是 255 个字符)。这样就能确保 String
值与指定的模式(例如电子邮件地址或 URL)匹配。甚至还能将字段设置为可选或必需的。关于可用的验证规则的完整列表,请参阅 Grails 的联机文档(请参阅 参考资料)。
清单 8 显示的 Airline
类中在约束块内添加了验证规则:
- classAirline{
- staticconstraints={
- name(blank:false,maxSize:100)
- url(url:true)
- frequentFlyer(blank:true)
- notes(maxSize:1500)
- }
- statichasMany=[trip:Trip]
- Stringname
- Stringurl
- StringfrequentFlyer
- Stringnotes
- StringtoString(){
- returnname
- }
- }
保存修改后的 Airline.groovy 文件,在浏览器中新建一条航线。如果违反了验证规则,会收到警告,如图 4 所示:
图 4. 验证警告
可以在 grails-app/i18n 目录的 messages.properties 文件中对警告消息进行自定义。请注意,默认的消息已经用多种语言进行了本地化(请参阅 Grail 联机文档中的验证一节,了解如何在每个类、每个字段的基础上创建自定义消息)。
清单 8 中的多数约束只影响视图层,但是有两个约束也会影响持久层。例如,数据库中的 name
列现在是 100 个字符长。notes
字段除了从输入字段转为视图的文本区域之外(对于大于 255 个字符的字段会进行这个转换),还从 VARCHAR
列转为 TEXT
、CLOB
或 BLOB
列。这些转变取决于在后台使用的数据库类型和它的 Hibernate 方言 — 当然,这些也是可以修改的。
可以使用任何常用的配置方法覆盖 Hibernate 的默认设置:HBM 映射文件或者标注。但是 Grails 提供了第三种方式,这种方式采用了裸对象的形式。只要向 POJO 添加一个 static mapping
块,就能覆盖默认的表和字段名称,如清单 9 所示:
- classAirline{
- staticmapping={
- table'some_other_table_name'
- columns{
- namecolumn:'airline_name'
- urlcolumn:'link'
- frequentFlyercolumn:'ff_id'
- }
- }
- staticconstraints={
- name(blank:false,maxSize:100)
- url(url:true)
- frequentFlyer(blank:true)
- notes(maxSize:1500)
- }
- statichasMany=[trip:Trip]
- Stringname
- Stringurl
- StringfrequentFlyer
- Stringnotes
- StringtoString(){
- returnname
- }
- }
如果要在新的 Grails 应用程序中使用现有的遗留表,那么这个映射块会特别有帮助。虽然这里只介绍了点皮毛,但 ORM DSL 提供的功能远不止是重新映射表和字段的名称。每个列的默认数据类型都可以覆盖。可以调整主键的生成策略,甚至指定复合主键。可以修改 Hibernate 的缓存设置,调整外键关联使用的字段,等等。
要记住的要点是所有这些设置都集中在一个地方:POGO 内。
目前所做的工作都集中在单个类的调整上。下面我们要回过头来做一些全局性的修改。所有域类共享的特定于数据库的配置保存在一个公共文件内:grails-app/conf/DataSource.groovy,如清单 10 所示。请将这个文件放在一个文本编辑器内仔细查看:
- 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'
- }
- //environmentspecificsettings
- environments{
- development{
- dataSource{
- dbCreate="create-drop"//oneof'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
块内能够修改用来连接数据库的 driverClassName
、username
和 password
。hibernate
块用来调整缓存设置(除非是 Hibernate 专家,否则不要在这里进行任何调整)。真正有意思的是 environments
块。
还记得在上一期的文章中介绍过 Grails 能够在三种模式下运行:开发模式、测试模式和生产模式。在输入 grails prod run-app
时,就是告诉 Grails 使用 production
块中的数据库设置。如果希望根据环境调整 username
和 password
的设置,只要将这些设置从 dataSource
块复制到每个 environment
块,并修改设置的值即可。 environment
块中的设置覆盖 dataSource
块中的设置。
url
设置是 JDBC 的连接字符串。请注意在 production
模式下,HSQLDB 使用基于文件的数据存储。在 development
和 test
模式下,HSQLDB 使用内存中的数据存储。上个月我介绍过如果想让 Trip
的记录在服务器重新启动之后保留,应该在 production
模式下运行。现在您应该知道如何在 development
和 test
模式下进行设置以实现这一功能 — 只要将 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
数据库:
$ mysql --user=root mysql> create database trip; mysql> use trip; mysql> grant all on trip.* to [email protected] 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 之外的数据库。