现在的位置: 首页 > 综合 > 正文

GORM – 有趣的名称,严肃的技术

2018年08月08日 ⁄ 综合 ⁄ 共 9404字 ⁄ 字号 评论关闭
 

在上个月开篇的 精通 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
方言的数据库都受支持。

关于本系列

Grails
是一种新型 Web 开发框架,它将常见的 Spring 和 Hibernate 等 Java
技术与当前流行的约定优于配置等实践相结合。Grails 是用 Groovy 编写的,它可以提供与遗留 Java
代码的无缝集成,同时还可以加入脚本编制语言的灵活性和动态性。学习完 Grails 之后,您将彻底改变看待 Web 开发的方式。

ORM 定义

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


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

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

面向对象数据库和 Grails

有些开发人员想通过直接支持对象的数据库来消除对象关系阻抗失谐。Ted Neward 在 developerWorks 上发表的连载
面向 Java 开发人员的 db4o 指南

就这一主题做了很好的阐述,展示了一个现代的面向对象数据库的应用。可喜的是,有许多企业级开发人员为 Grails 编写了 db4o
插件,表明面向对象的数据库同关系数据库一样处在黄金时代。与此同时,使用 GORM 和传统的关系数据库,是使用 Grails 时最好的持久性策略。

创建一对多关系

对于将 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 类中设置额外的一对多关系,那么可以将逗号分隔的键/值对放在方括号内。

连串删除

正如目前的模型所表示的,在数据库中可能会形成孤立的内容:
删除一个 Airline 会造成
Trip 记录指向一个不存在的航线。为了避免出现这种情况,可以在含多个方面的类中添加一个对应的 static belongsTo hashmap。

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

清单 5. AirlineController class

                
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

避免端口冲突错误

如果另一个应用程序已经在 8080 端口运行,那么请参阅上月的
文章 获取修改端口的操作说明。我在 9090 端口上运行我的 Grails 实例,完全避免了这个问题。

在浏览器中,应该看到
AirlineController
TripController 链接。单击

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 列转为
TEXTCLOB
BLOB 列。这些转变取决于在后台使用的数据库类型和它的 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 使用基于文件的数据存储。在 development
test 模式下,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 之外的数据库。



结束语

分享这篇文章……


digg
提交到 Digg

del.icio.us
发布到 del.icio.us

Slashdot
Slashdot 一下!

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

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

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

抱歉!评论已关闭.