领先一步
VMware 提供培训和认证,助力您的进步。
了解更多您是 Grails 新手吗?或者您是否遇到了您的第一个 GORM "怪异"?如果是这样,那么您需要阅读本系列关于 GORM 陷阱的文章。这些文章不仅会强调那些经常让人意外的小特性,还会解释 GORM 为什么会这样表现。
希望您已经知道 GORM 是 Grails 附带的数据库访问库。它基于可能是最流行的 Java ORM:Hibernate。正如您可以想象的,Hibernate 是一个强大而灵活的库,它为 GORM 带来了巨大的好处。但使用它也有代价:GORM 用户遇到的许多问题都源于 Hibernate 的工作方式。GORM 尽最大努力隐藏实现细节,但有时它们确实会泄露出来。
在本文的其余部分,我将介绍对象持久化到数据库的基础知识。听起来很简单,但即使在如此基础的操作上,GORM 的工作方式可能也与您预期的大不相同。
保存领域实例时遇到的问题可能是开发者首先遇到的问题。有多少人经历过“我保存了,为什么它不在数据库里?”的阶段?如果这能让您感觉好些,我也经历过。为什么会发生这种情况?有几种可能性。
每次保存领域实例时,Grails 都会使用您定义的约束对其进行验证。如果领域实例中的任何值违反了这些约束,保存将失败,并且约束错误将附加到该领域实例。问题在于,这种保存失败是静默发生的:除非您检查save() 的返回值或调用hasErrors().
当您将用户数据绑定到领域实例时,这通常是您想要的行为。如果用户输入不符合约束,这一点也不奇怪。在这种情况下抛出异常并不合适,特别是当您的 Web 应用有相当多的并发用户时。在这种情况下,最好始终检查save() 的返回值并做出相应反应(它返回null如果保存失败,否则返回领域实例)
def book = new Book(params)
if (!book.save()) {
// Save failed! Present the errors to the user.
...
}
另一方面,当您在 BootStrap 或 Grails 控制台中设置测试数据时,您通常期望保存能成功。如果存在任何验证错误,那意味着是您的错误。在这种情况下,您不想费事检查每一个save() 的返回值的返回值,并且如果 Grails 在验证失败时抛出异常,您会感到非常高兴。这 *不是* 默认行为,但您可以通过failOnError参数轻松开启。
book.save(failOnError: true)
如果您坚持,您甚至可以将其设置为默认值:只需将grails.gorm.failOnError设置为true在 grails-app/conf/Config.groovy 中。并且别忘了,所有领域属性都有一个隐式的nullable: false约束!
这可能是当您期望领域实例被保存时,它们却未被保存的最常见原因。那么另一个原因是什么?
在极少数情况下,您可能会保存一个领域实例,却发现后续查询未能查到它,即使该实例通过了验证。这是使用底层 Hibernate 相关的一个更广泛问题的症状。
Hibernate 是一个基于会话的 ORM 框架。
这一点非常重要,任何希望真正熟练使用 GORM 的人都必须理解会话是什么以及它对应用程序有什么影响。那么会话是什么?它本质上是一个由数据库支持的、对象的内存缓存。当您保存一个新的领域实例时,它会隐式附加到会话,也就是说,它被添加到缓存中,并成为一个由 Hibernate 管理的对象。但此时它可能还未持久化到数据库! 以下图表说明了这种行为。
当您保存实例时,它们会立即从会话中可用。但 Hibernate 会自行决定何时将新实例持久化到数据库,这意味着它可以优化 SQL 语句的顺序。通常您不会注意到这一切,因为 Grails 和 Hibernate 会处理好这些事情,但偶尔您会被意料之外。
不过,正如您所料,Grails 确实允许您控制数据何时实际持久化到数据库。您是否在示例中见过这样的代码?
book.save(flush: true)
该flush: true参数强制 Hibernate 立即将所有更改持久化到数据库。这对应于所谓的 刷新会话 (flushing the session)。
现在的危险是您会到处都放flush: true参数。不要这样做。让 Hibernate 完成它的工作,只在必须时手动刷新会话,或者至少只在一批更新结束时刷新。您只应该在数据应该出现在数据库中却未出现时才真正使用它。我知道这有点含糊不清,但这种操作何时有必要取决于数据库实现和其他因素。手动刷新非常有用的一个领域是当您与另一个应用程序或内部服务交互时,它们访问的是与您相同的数据库,但不在您的当前会话之内。
如果您对会话仍然有点模糊,别担心——我们会一次又一次地回到 Hibernate 会话,因为它对于许多 GORM 相关陷阱至关重要。事实上,这就是人们遇到下一个问题的原因。
当您调用save() 的返回值时未能持久化领域实例是很常见的。现在考虑相反的情况:在没有相应的调用save() 的返回值的情况下,对象却被持久化了。如果您还没有遇到过这种行为,我几乎可以保证您会遇到。那么为什么会发生这种情况呢?
Hibernate 支持脏检查 (dirty-checking) 的概念。这意味着 Hibernate 会检查领域实例的(持久)属性值是否在*从数据库中取出该实例之后*发生了变化,并将这些更改持久化到数据库。这解释起来有点拗口,希望一个例子能帮助澄清。假设我们有一个Book领域类,包含title和author属性,并且以下代码在一个控制器 action 中:
def b = Book.findByAuthor(params.author)
b.title = b.title.reverse()
注意这里没有调用save() 的返回值。当请求完成后,您会发现该书的标题在数据库中被反转了——更改在没有显式保存的情况下被持久化了。这是因为:
让我们更详细地看一下这些。首先,我已经提到对象可以“附加”到会话,即由 Hibernate 管理并由数据库支持,但是对象是如何附加的呢?如果您以任何方式使用 GORM 检索领域实例,例如通过get()方法或任何类型的查询,那么该对象会自动与会话关联。如果您只是通过new关键字创建一个新实例,那么该对象在调用save() 的返回值方法之前是不会附加到会话的。
其次,领域类属性默认是持久化的,也就是说,它们在数据库中有匹配的列来存储其值。您可以通过将属性名称添加到静态transients列表属性来使属性成为瞬态的,这意味着它们的值不会存储在数据库中。
最后,我提到如果更改在会话关闭时存在,它们就会被持久化。这是什么意思呢?为了通过 Hibernate 对数据库进行任何操作,您必须有一个打开的会话。一旦会话关闭,您就不能再使用它进行数据库访问了。此外,会话在关闭时会被刷新,这就是为什么在我们上面控制器 action 结束时更改会被持久化的原因(Grails 在请求开始时自动打开一个会话,并在请求结束时关闭它)。
您能阻止这样的更改被持久化吗?当然。一个选项是调用save() 的返回值在您的领域实例上:如果任何属性值未能通过验证,更改将不会被持久化。当然,如果值是有效的但您仍然不想持久化它们,您可以调用discard()在您的实例上。这不会重置实例属性的值,但会确保它们不会保存到数据库中。
对于仅仅几个陷阱来说,这是相当多的信息需要消化。关键在于理解 Hibernate 会话如何影响领域实例的持久化。即使您还没有完全掌握,未来的 GORM 陷阱系列文章也会提供更多信息和示例。
通常来说,我建议您始终使用save() 的返回值来持久化对象,而不是依赖脏检查。这使得代码清晰地表明您想要持久化更改,并且这些更改会同时被验证。我还建议您始终检查save() 的返回值在应用程序代码中的返回值,尽管在设置 bootstrap 或测试数据时最好使用failOnError: true选项。
如果您此时对 GORM 感到一丝畏惧或担忧,请不要。它确实让使用数据库变得轻松有趣,并且通过本系列文章获得的信息,您将有信心处理可能出现的任何问题。
下次见!