大多数企业应用程序的一个主要组成部分是在关系数据库中传输对象的代码。最简单的解决方案通常是使用 ORM(对象关系映射)框架,它允许开发人员声明式地定义对象模型和数据库模式之间的映射,并以对象的形式表达数据库访问操作。这种高层次的方法显著减少了需要编写的数据库访问代码量,并提高了开发人员的生产力。
如今,有几种 ORM 框架正在使用中。例如,Hibernate1、TopLink2 和 OpenJPA3 框架在 Java 开发人员中很受欢迎,而 NHibernate4 被许多 .NET 开发人员使用。最近受到企业开发人员广泛关注的两个较新的 ORM 框架是 Ruby6 的 Active Record5 和 Groovy8 的 GORM (Grails Object Relational Mapping)7。这些新框架与传统的 ORM 框架不同之处在于,它们是用动态语言编写的,允许在运行时创建新的程序元素。Active Record 和 GORM 以可以显著简化应用程序的方式使用这些动态功能。
本文着眼于 GORM 的工作原理。它将 GORM 与 Hibernate 进行了比较和对比,重点关注三个方面:定义对象关系映射;对持久对象执行基本的保存、加载和删除操作;以及执行查询。它描述了 GORM 如何利用 Groovy 的动态特性来提供一种不同风格的 ORM,这种 ORM 有一些局限性,但对于许多应用程序来说更容易使用。
GORM 是 Grails 的持久化组件,Grails 是一个旨在简化 Web 开发的开源框架。Grails 是用 Groovy 编写的,Groovy 是一种运行在 JVM(Java 虚拟机)上的动态的、面向对象的语言。由于 Groovy 可以与 Java 无缝互操作,Grails 可以利用几个成熟的 Java 框架。特别是,GORM 使用了 Hibernate,一个流行的、健壮的 ORM 框架。
然而,GORM 远不止是 Hibernate 框架的简单包装器。相反,它提供了一种非常不同的 API。GORM 在两个方面有所不同。首先,Groovy 语言的动态特性使 GORM 能够完成在静态语言中不可能完成的事情。其次,Grails 中 CoC(约定优于配置)的普遍使用减少了使用 GORM 所需的配置量。让我们更详细地了解一下这两个原因。
GORM 严重依赖 Groovy 语言的动态功能。特别是,它广泛使用 Groovy 在运行时定义方法和属性的能力。在 Java 等静态语言中,属性访问或方法调用在编译时解析。相比之下,Groovy 直到运行时才解析属性访问和方法调用。Groovy 应用程序可以动态地定义方法和属性。
Groovy 提供了几种不同的方法来在运行时向类添加方法和属性。最简单的方法是定义 propertyMissing() 或 methodMissing() 方法。当应用程序尝试访问未定义的属性时,Groovy 运行时会调用 propertyMissing() 方法。类似地,当应用程序调用未定义的方法时,会调用 methodMissing() 方法。这些方法使对象表现得好像属性或方法存在一样。
第二种也是更复杂的方法是使用名为 ExpandoMetaClass 的绝妙工具。每个 Groovy 类都有一个 metaclass 属性,该属性返回一个 ExpandoMetaClass。应用程序可以通过操作这个元类向类添加方法或属性。例如,图 1 是一个代码片段,它向 String 类添加了一个方法,该方法将字符串与自身连接起来。
此代码片段获取 String 元类,并将其 doubleString 属性分配给一个闭包(一种匿名方法),该闭包实现了新方法。
Groovy 应用程序经常一起使用 methodMissing() 和 ExpandoMetaClass。第一次调用未定义的方法时,missingMethod() 使用 ExpandoMetaClass 定义该方法。下一次,将直接调用新定义的方法,从而绕过相对昂贵的 missingMethod() 机制。
稍后您将看到 Grails 如何使用 methodMissing() 和 ExpandoMetaClass 在运行时将持久化相关的方法和属性注入到领域类中,从而简化应用程序代码。
GORM 中的第二个关键思想是 CoC。它的前提是框架应该具有合理的默认值,并且不应要求开发人员显式配置每个方面;相反,只有特殊情况才需要配置。CoC 最初由 Rails 和 Grails 框架推广,但包括 Spring9 在内的主流 Java EE 框架也已开始采用这个概念。如今,开发人员期望现代 Java EE 框架比旧框架需要更少的配置。
CoC 在 Grails 中被广泛使用。例如,内置的默认值决定了如何将 HTTP 请求映射到处理程序类。类似地,GORM 具有定义要持久化的类以及如何包含列和表名称默认值的规则。由于 CoC,典型的 Grails 应用程序包含的配置代码和元数据比使用传统框架的应用程序少得多。
现在我们已经了解了 GORM 的关键基础,让我们学习如何使用它。
使用 ORM 框架的一个关键部分是指定对象模型如何映射到数据库。开发人员必须指定类如何映射到表,属性如何映射到列,以及关系如何映射到外键或连接表。本节将介绍这在使用传统 ORM 框架时是如何工作的,以及在 Grails 中是如何实现的。
Java 类的持久状态是它的字段或属性。字段是 Java 中实例变量的等价物。属性由遵循 JavaBeans10 命名约定的 getter 和 setter 方法定义。例如,getFoo() 和 setFoo() 定义了名为 foo 的属性。getter 和 setter 方法通常提供对与属性同名的字段的访问,尽管它们不是必须这样做。
Hibernate 应用程序可以使用 XML 或注解将领域类的字段或属性映射到数据库模式。图 2 的左侧显示了一个注解示例,右侧显示了一个 XML 示例。这两个示例都持久化了 Customer 类的字段,但是应用程序可以通过注解 getter 方法或从 XML 文档中省略 default-access 属性来持久化属性。
XML 和注解产生等效的元数据。它们都指定 Customer 类是持久化的。它们还指定 Hibernate 应该使用适合底层数据库的任何机制生成对象的主键,并将其存储在 id 字段中。version 字段配置为存储 Hibernate 维护的版本号。它们都持久化了 name 字段,并指定 accounts 字段表示一对多关系。
XML 和注解都具有表名和列名的默认值。表名默认为类名,列名默认为属性名。您可以使用额外的注解或 XML 属性和元素覆盖这些默认值。例如,您可以使用 @Table 注解或 <class> 元素的 name 属性来指定表名。
每种方法都有优点和缺点。XML 比注解的一个优势是它将 O/R 映射与 Java 代码分离,从而将领域类与 Hibernate 解耦。这种分离的一个问题是,保持映射和代码同步可能更困难。XML 也往往比注解更冗长。此外,XML 映射必须显式列出类的所有持久属性,而当使用注解时,某些基本类型(如 Customer.name)的字段会自动持久化。
另一个问题是,无论您是使用 XML 还是注解,您通常都需要添加字段来存储主键和版本号。主键字段通常是 Hibernate 或领域对象的客户端所需要的。版本号用于乐观锁定。然而,这些字段的麻烦在于,通常应用程序的业务逻辑不需要它们。必须将它们添加到每个领域类中,仅仅是为了支持持久化。
Grails 在定义 ORM 时严重依赖约定优于配置。它自动将 grails-app/domain 目录中的类视为持久化的。GORM 自动持久化每个类的属性。它从类名和属性名默认表名和列名。GORM 还向每个类添加主键和版本号属性。
以下是一个领域类示例。Customer 类有一个名为 name 的字段。此外,由于此字段具有默认可见性,Groovy 通过定义 getName() 和 setName() 方法自动定义 name 属性。
class Customer {
String name
}
GORM 自动将 Customer 类映射到 customer 表,并将 name 属性映射到 name 列。GORM 向类添加一个 id 属性,并将其映射到名为 id 的主键列。它还添加了一个 version 属性,并将其映射到 version 列。与传统的 ORM 框架不同,GORM 只需要很少的配置,前提是数据库模式与默认值匹配。
GORM 的另一个优点是,它将维护领域模型类的创建时间和最后更新时间。您只需在类上定义 lastUpdated 和 dateCreated 属性,GORM 就会自动更新它们。相比之下,当使用原生 Hibernate 时,您必须编写代码来执行此操作。
GORM 还通过使用静态属性以类似于其他语言中的注解的方式提供元数据,从而轻松地映射关系。例如,静态属性 hasMany 定义了领域类的一对多关系。hasMany 属性的值是一个 map。每个 map 条目定义一个一对多关系:它的键是存储集合的属性的名称,它的值是集合元素的类。对于每个一对多关系,GORM 添加一个属性来存储对象集合,以及维护关系的方法。
以下是如何映射 Customer 类和 Account 类之间的一对多关系的示例。
class Customer {
static hasMany = [accounts : Account]
}
class Account {
static belongsTo = Customer
Customer customer
}
帐户集合存储在名为 accounts 的属性中,GORM 在运行时将其添加到 Customer 类中。该关系使用 account 表中名为 customer_id 的外键进行映射。belongsTo 属性指定 Customer 拥有帐户,如果删除客户,则应删除该帐户。
GORM 还动态定义了几个用于管理此关系的方法。addToAccounts() 方法向集合添加一个帐户,removeFromAccounts() 方法从集合中删除一个帐户。这些方法还维护从 Account 到 Customer 的反向关系。通过自动定义这些方法(否则必须手动编写),GORM 简化了代码,并使其不易出错。
CoC 减少了所需的配置量。但是,有时您需要指定 ORM 的某些方面。例如,表名或列名可能与默认值不匹配,或者某个类可能具有不应持久化的派生属性。为了支持这些需求,GORM 允许您指定 ORM 的各个方面。然而,GORM 没有使用不同的配置语言(如 XML 或注解),而是在领域类中使用 Groovy 代码片段。
以下是如何覆盖默认表名和列名以及指定不应持久化属性的示例。
class Customer {
static transients = ["networth"]
static mapping = {
id column: ‘customer_id'
table ‘crc_customer'
columns {
name column: ‘customer_name'
}
}
def getNetworth() {
def networth = 0
accounts.each {networth + it.balance}
networth
}
...
}
在此示例中,transients 属性(属性名称列表)指定不持久化 networth 属性,该属性计算客户帐户的总余额,并由 getNetworth() 方法定义。mapping 属性将 Customer 类映射到 crc_customer 表;id 属性映射到 customer_id 列;name 属性映射到 customer_name 属性。
mapping 属性的值是一个 Groovy 闭包对象,它是一种匿名方法。尽管可能不太明显,但 mapping 闭包的主体是一系列方法调用。例如,“id column: ‘customer_id'”是对 id 方法的调用,其 map 参数包含一个条目,该条目以 column: 作为键,以 ‘customer_id' 作为值。
mapping 闭包是 DSL(领域特定语言)11 的一个示例,它是一种用于表示有关领域信息的迷你语言。Grails 将 DSL 用于各种配置任务。Groovy 应用程序通常也定义一个或多个 DSL。Groovy 语言的几个特性使其易于编写 DSL,包括闭包、字面列表和 map,以及灵活的语法,例如,不需要方法参数周围的括号。它们使开发人员能够编写高度可读和简洁的 DSL,而无需跳出语言之外并使用 XML 等机制。
应用程序需要保存、加载和删除持久对象。传统的 ORM 框架提供了一个 API 对象,该对象具有用于操作持久数据的方法。然而,GORM 采用了一种非常不同且更简单的方法,该方法利用了 Groovy 在运行时定义新方法的能力。
当使用传统的 ORM 框架时,应用程序通过调用 API 对象上的方法来操作持久数据。例如,Hibernate 应用程序使用 Session 对象,该对象表示与数据库的连接,用于保存、加载和删除持久对象。请注意,通常应用程序只需要保存新创建的对象。包括 Hibernate 在内的大多数 ORM 框架都会跟踪对持久对象的更改,并自动更新数据库。
图 3A 显示了一个代码片段,该片段说明了应用程序如何加载具有指定主键的帐户。此代码片段获取当前的 Session 并调用 get() 以加载指定的帐户。
应用程序的业务逻辑可以直接使用 Session。但是,这样做会违反关注点分离原则12。应用程序代码将是业务逻辑和持久化逻辑的混合体,这使其更复杂且更难以测试。它还将业务逻辑与 ORM 框架紧密耦合,考虑到 Java EE 框架的快速发展速度,这是不可取的。
更好的方法是使用 DAO(数据访问对象)模式13,该模式将数据访问逻辑封装在 DAO 类中。DAO 定义了用于持久化、加载和删除对象的方法。它还定义了查找器方法,这些方法执行查询,稍后将更详细地讨论。DAO 方法由业务逻辑调用,并调用 ORM 框架来访问数据库。
图 3B 显示了 Account 领域类的 Hibernate DAO 示例。此 DAO 由 AccountDao 接口(定义公共方法)和 AccountDaoImpl 类(实现接口并调用 Hibernate 以访问数据库)组成。
DAO 模式简化了业务逻辑并使其与 ORM 框架解耦,但它也有一些缺点。第一个问题是,许多 DAO 由样板代码组成,这些代码开发和维护起来很乏味。这导致一些开发人员放弃 DAO 模式,并编写直接调用 ORM 框架的业务逻辑,尽管这样做有缺点。
减少样板代码量的一种方法是使用通用 DAO14。它由一个超接口(定义 CRUD(创建、读取、更新、删除)操作)和一个超类(实现它们)组成。超接口和超类由实体类参数化,这使它们成为强类型的。应用程序 DAO 扩展了通用 DAO 接口和实现类。使用通用 DAO 消除了部分但不是全部的样板代码,因此它只是一个部分解决方案。
使用 DAO 的另一个问题是,某些应用程序类可能无法引用它们。现代 Java EE 应用程序使用称为依赖注入的机制来解析组件间引用15。当应用程序启动时,汇编器实例化每个应用程序组件,并向其注入对所需组件的引用。以这种方式解析组件间引用简化了组件并促进了松耦合。
然而,依赖注入的一个限制是,它不容易允许非组件(如领域对象)获取对组件(如 DAO)的引用。领域对象由应用程序而不是组件汇编器实例化。组件汇编器拦截此类对象的实例化并注入依赖项是很棘手的,尽管并非不可能16。因此,驻留在领域对象中的业务逻辑并不总是可以引用 DAO 等组件。
有几种方法可以解决此限制。可以使用依赖注入的服务等组件将 DAO 作为方法参数传递给不能使用依赖注入的领域类。这在某些情况下效果很好,但在更复杂的情况下,代码会因额外的参数而变得混乱。另一种解决方法是将需要使用 DAO 的代码移动到可以使用依赖注入的组件中。将业务逻辑移出实体的麻烦在于,它会降低设计质量,并导致贫血领域模型。
GORM 提供了一种不同风格的持久化 API。它没有提供 API 对象,而是将用于保存、加载和删除持久对象的方法注入到领域类中。这种机制使业务逻辑与底层 ORM 框架解耦,而无需使用 DAO。它还消除了应用程序代码获取对 ORM 框架 API 对象或 DAO 的引用的需要。
GORM 将几种方法注入到领域类中,包括 save()(保存新创建的对象);get()(按主键加载对象);以及 delete()(删除对象)。以下是使用这些方法的示例
Customer c = new Customer("John Doe")
if (!c.save())
fail "save failed"
Customer c2 = Customer.get(c.id)
c2.delete()
assertNull Customer.get(c.id)
此示例创建一个 Customer 对象,并通过调用 save() 将其保存在数据库中。然后,它通过调用 Customer.get() 加载客户。最后,它通过调用 delete() 删除客户。请注意,这些方法都没有在 Customer 类的源代码中定义。GORM 使用前面描述的 missingMethod()/ExpandoMetaClass 机制来实现它们。
GORM 动态定义的持久化方法消除了大量 DAO 代码,同时使应用程序代码与 ORM 框架解耦。GORM 回避了非组件如何获取对 DAO 的引用的问题。GORM 应用程序中的任何位置的代码都可以执行数据访问操作。当然,这是否总是合适是另一个问题,因为正如我稍后讨论的那样,这可能会导致数据库访问代码分散在整个应用程序中。
GORM 的一个重大限制是它不支持多个数据库。Hibernate 应用程序显式使用特定的会话,因此可以选择要访问的数据库。GORM 应用程序使用注入到领域类中的持久化方法,并且无法选择要使用的数据库。此外,在撰写本文时,用于配置 GORM 的机制不支持多个数据库。此限制可能会阻止许多应用程序使用 GORM,包括那些通过使用多个数据库进行水平扩展的应用程序(请参阅本期 Queue 中 Dan Pritchett 的“BASE:ACID 的替代方案”)。
应用程序可能不知道它需要加载的对象的主键。相反,它必须执行查询,根据对象的属性值检索对象。当使用传统的 ORM 框架时,应用程序通过调用框架提供的 API 对象上的方法来执行查询。此代码通常由 DAO 封装,以使应用程序与 ORM 框架解耦。与持久化方法一样,GORM 采用了一种不同的方法,这种方法通常可以简化应用程序代码。
Hibernate 提供了几种执行查询的方法。例如,应用程序可以使用 Query 接口来执行用 HQL(Hibernate 查询语言)编写的查询,HQL 是一种强大的、面向对象的文本查询语言。图 4A 是一个 DAO 查找器,用于检索余额低于某个最小值的帐户。
此方法获取 Session 并创建一个 Query 对象。然后,它设置查询的参数并执行查询,该查询返回 Account 对象列表。
Hibernate 应用程序还可以使用 Criteria Query API 来执行查询。此 API 提供了以编程方式构建查询的方法。当应用程序需要动态构建查询时,它尤其有用,因为它消除了连接查询字符串片段的需要。图 4B 是一个条件查询示例,用于查找低余额帐户。此代码片段为 Account 类创建一个 Criteria 对象。然后,它添加一个限制并执行查询。
DAO 查找器的一个问题是,大多数查找器都具有与示例相同的结构:创建查询,设置参数,然后执行查询。唯一的变量是查询和参数。与持久化方法一样,这些样板方法和包含它们的 DAO 开发、测试和维护起来很乏味。
GORM 具有动态查找器机制,该机制消除了编写简单查询和 DAO 查找器方法的需要。它使用 Groovy 的动态功能向领域类添加查找器方法。例如,应用程序可以找到低余额帐户,如图 5A 所示。如果方法名称遵循某些命名约定,则 missingMethod()/ExpandoMetaClass 机制会拦截对方法的调用,并定义一个解析方法名称以构建查询并执行它的方法。
GORM 动态查找器支持丰富的查询语言。查找器方法名称可以使用比较运算符,如 equals、less than 和 greater than。它们还可以使用 and、or 和 not 逻辑运算符。即使查询语言仅限于单个类的属性(没有连接),许多查询也可以表示为动态查找器。GORM 应用程序包含的数据访问代码少得多,并且对 Hibernate 框架的显式依赖性也少得多。此外,由于查找器方法在领域类上随时可用,因此 GORM 避免了需要解析组件间引用的问题。
这些查找器方法的一个潜在缺点是,方法名称是查询的定义。并非总是可以为封装实际实现的查询定义有意义的、揭示性的名称。因此,不断发展的业务需求可能会导致查找器方法的名称发生变化,从而增加了维护应用程序的成本。
对于需要执行更复杂查询的应用程序,GORM 提供了几种不同的选项。应用程序可以直接执行 HQL 查询。例如,应用程序可以执行 HQL 查询来检索低余额帐户,如图 5B 所示。此代码片段调用 findAll() 方法,GORM 将该方法注入到每个领域类中。它将 HQL 查询和参数列表作为参数。
此 API 的一个优点是,它允许应用程序执行 HQL 查询,而无需显式调用 Hibernate API。应用程序不必解决获取对 DAO 或其他组件的引用的问题。然而,一个缺点是 HQL 知识被硬编码到应用程序中。
另一个选项,当动态构造查询时尤其有用,是使用 GORM 条件查询,它包装了前面描述的 Hibernate Criteria API。与其他 API 一样,GORM 动态地将 createCriteria() 方法注入到领域类中。此方法允许应用程序构建和执行查询,而无需显式依赖 Hibernate API。
图 5C 是检索低余额帐户的查询的 GORM 条件查询版本。createCriteria() 方法返回一个用于构建查询的对象。应用程序通过调用 list() 来执行查询,list() 将 Groovy 闭包作为参数并返回匹配对象的列表。闭包参数包含方法调用,如 lt(),这些方法将限制添加到查询中。
应用程序可以使用这些 API 来执行动态查找器不支持的查询。一个潜在的缺点,可以被认为是 GORM 的弱点,是模块化不足和违反关注点分离原则。存在将领域类的数据访问操作分散在整个应用程序中的风险。一些数据访问方法由领域类定义,但其余方法与应用程序的业务逻辑混合在一起,这可以被认为是缺乏模块化。理想情况下,此类数据访问逻辑应封装在 DAO 中,但不幸的是,GORM 没有显式支持它们。
GORM 提供了一种创新的 O/R 映射风格,可以简化应用程序代码。它实现这一目标的关键方法之一是利用 Groovy 语言的动态特性。GORM 在运行时将持久化相关的方法注入到领域类中。它消除了大量数据访问方法和类,同时仍然使业务逻辑与 ORM 框架解耦。
GORM 广泛使用 CoC 简化了应用程序代码。如果 GORM 的表名和列名默认值与模式匹配,则可以将类映射到数据库模式,而几乎不需要或根本不需要配置。GORM 还为每个领域类注入主键和版本号字段,这进一步减少了所需的编码量。
GORM 有一些局限性。它不容易支持多个数据库。动态查找器方法不能具有封装查询的有意义的、揭示性的名称。GORM 缺乏对 DAO 类的支持,即使复杂的应用程序可能会从它们提供的改进的模块化中受益。使用遗留模式的应用程序将无法利用 CoC,因为它们需要显式配置 ORM。
尽管存在这些局限性,但各种应用程序的开发人员都会发现 GORM 非常有用。开发人员可以独立于 Grails 使用 GORM,但它的目标是 Web 应用程序开发人员,他们可以从 Grails 框架的快速开发能力中受益。此外,GORM 最适合用于开发访问单个数据库的应用程序,或使用使多个数据库显示为单个数据库的数据库中间件的应用程序。当开发人员可以控制数据库模式并利用 GORM 的 CoC 特性时,他们将获得 GORM 的最大好处。
我要感谢以下审稿人对本文草稿提供的有益反馈:Ajay Govindarajan、Azad Bolour、Dmitriy Volk、Brad Neighbors 和 Scott Davis。我还要感谢 SF Bay Groovy 和 Grails 聚会的成员以及匿名 审稿人,他们为本文提供了反馈。
CHRIS RICHARDSON 是一位拥有超过 20 年经验的开发人员和架构师。他是 POJOs in Action(Manning Publications,2006 年)的作者,该书描述了如何使用 POJO 和轻量级框架构建企业 Java 应用程序。他经营一家咨询和培训公司,专门帮助公司更快地构建更好的软件。他曾在 Insignia、BEA 和其他公司担任技术主管。Richardson 拥有英国剑桥大学计算机科学学位,居住在加利福尼亚州奥克兰。网站和博客:www.chrisrichardson.net。
最初发表于 Queue vol. 6, no. 3—
在 数字图书馆 中评论本文
Qian Li, Peter Kraft - 事务和无服务器天生一对
数据库支持的应用程序是无服务器计算令人兴奋的新领域。通过紧密集成应用程序执行和数据管理,事务性无服务器平台实现了现有无服务器平台或基于服务器的部署中不可能实现的许多新功能。
Pat Helland - 任何其他的名称的身份
新兴的系统和协议既收紧又放松了我们对身份的概念,这很好!它们使完成工作变得更容易。REST、IoT、大数据和机器学习都围绕着有意保持灵活且有时含糊不清的身份概念。身份的概念是我们分布式系统的基本机制的基础,包括可互换性、幂等性和不变性。
Raymond Blum, Betsy Beyer - 实现数字永恒
当今的信息时代正在为世界所依赖的数据创造新的用途和新的管理方式。世界正在从熟悉的物理文物转向更接近本质信息的新的表示方式。我们需要流程来确保知识的完整性和可访问性,以保证历史将被知晓和真实。
Graham Cormode - 数据素描
您是否曾经感到被源源不断的信息淹没?看起来永无止境的新电子邮件和短信要求持续关注,还有电话要接听,文章要阅读,还有敲门声要回应。将这些碎片拼凑在一起以跟踪重要内容可能是一个真正的挑战。为了应对这一挑战,流数据处理模型越来越受欢迎。其目的不再是捕获、存储和索引每一分钟的事件,而是快速处理每个观察结果,以便创建当前状态的摘要。