GORM 的陷阱 (第一部分)

工程 | Peter Ledbrook | 2010年6月23日 | ...

你是 Grails 新手吗?或者你是否遇到了第一个 GORM 的“怪癖”?如果是这样,那么你一定要阅读这个关于 GORM 陷阱的系列文章。这些文章不仅会揭示那些常常让人们困惑的小特性,还会解释 GORM 为什么会表现出这些行为。

希望您已经知道 GORM 是 Grails 自带的数据库访问库。它基于可能是最流行的 Java ORM:Hibernate。可以想象,Hibernate 是一个强大且灵活的库,它为 GORM 带来了巨大的好处。但使用它是有代价的:GORM 用户遇到的许多问题都源于 Hibernate 的工作方式。GORM 尽量隐藏其实现细节,但偶尔还是会暴露出来。

在本文的其余部分,我将描述将对象持久化到数据库的基础知识。听起来很简单,但即使是这么基本的操作,GORM 的行为也可能不像您预期的那样。

当我调用 save() 时,我指的是保存!

保存领域实例时遇到的问题可能是开发人员首先会遇到的。有多少人经历过“我已经保存了,为什么它不在数据库里?”的阶段?如果这对您有任何安慰的话,我也经历过。为什么会发生这种情况?有几种可能性。

不要忘记验证!

每次保存领域实例时,Grails 都会使用您定义的约束来验证它。如果领域实例中的任何值违反了这些约束,保存就会失败,并且约束错误会附加到领域实例上。麻烦的是,这种保存领域实例的失败是默默发生的:除非您检查 return value ofsave()或调用hasErrors().

当您将用户数据绑定到领域实例时,这通常是您想要的行为。如果用户输入碰巧不符合约束,这 hardly 是一个惊喜。在这种情况下抛出异常是不合适的,特别是当您的 Web 应用程序有相当多的并发用户时。在这种情况下,最好检查 return value ofsave()并做出相应的反应(如果保存失败,它返回null,否则返回领域实例)

def book = new Book(params)
if (!book.save()) {
    // Save failed! Present the errors to the user.
    ...
}

另一方面,当您在 BootStrap 或 Grails 控制台中设置测试数据时,您通常期望保存能够成功。如果存在任何验证错误,则意味着您自己犯了错误。在这种情况下,您不想费心检查每个save()的 return value,如果您能让 Grails 为验证失败抛出异常,您会非常乐意。这不是默认行为,但您可以通过failOnError参数轻松开启。

book.save(failOnError: true)

如果您坚持,您甚至可以将其设为默认值:只需在 grails-app/conf/Config.groovy 中将grails.gorm.failOnError转换为设置为true即可。别忘了,所有领域属性都隐式带有nullable: false

约束!

这可能是领域实例在您期望时未能保存的最常见原因。那么另一个原因是?

Hibernate 的会话

偶尔,您可能会发现自己保存了一个领域实例,却发现随后的查询无法获取它,即使该实例通过了验证。这是使用 Hibernate 作为底层的原因所带来的更广泛问题的症状。

Hibernate 是一个基于会话的 ORM 框架。

hibernate-session-in-action

这一点非常重要,任何希望真正精通 GORM 的人都必须了解什么是会话以及它对应用程序有什么影响。那么,什么是会话?它基本上是后端是数据库的对象的内存缓存。当您保存一个新的领域实例时,它会被隐式地附加到会话中,即添加到缓存中,并成为一个 Hibernate 管理的对象。但此时它可能尚未持久化到数据库! 下图说明了这种行为

当您保存实例时,它们将立即可从会话中获取。但是 Hibernate 会自行决定何时将新实例持久化到数据库,这意味着它可以优化 SQL 语句的顺序。通常您不会注意到这些,因为 Grails 和 Hibernate 会处理好一切,但偶尔您会被 caught out。

book.save(flush: true)

不过,正如您所期望的,Grails 允许您控制数据何时实际持久化到数据库。您是否曾在示例中看到过这样的代码?flush: true

强制 Hibernate 立即将所有更改持久化到数据库。这对应于所谓的刷新会话不过,正如您所期望的,Grails 允许您控制数据何时实际持久化到数据库。您是否曾在示例中看到过这样的代码?现在的危险是,您可能会到处添加

。不要这样做。让 Hibernate 做它的工作,只在您必须手动刷新会话时才这样做,或者至少只在一批更新的末尾这样做。只有当您在数据库中看不到本应存在的数据时,才应该真正使用它。我知道这有点含糊,但这种操作的必要情况取决于数据库实现和其他因素。手动刷新在与另一个应用程序或正在访问您的数据库但不在您当前会话中的内部服务交互时非常有用。

如果您对会话仍然有点模糊,请不用担心——我们将反复讨论 Hibernate 会话,因为它对许多与 GORM 相关的陷阱至关重要。事实上,这也是人们遇到下一个问题的原因。

现在我在您不希望我保存的时候保存了?!save()未能持久化领域实例(当您调用save()时)相当普遍。现在考虑相反的情况:在没有相应调用

的情况下持久化对象。如果您还没有遇到过这种行为,我几乎可以保证您会遇到的。那么为什么可能会发生这种情况?BookHibernate 支持脏检查的概念。这意味着 Hibernate 会检查领域实例的(持久化)属性的任何值是否在从数据库中检索出该实例后发生了变化,并会将这些更改持久化到数据库。这有点拗口,希望一个例子能帮助您理解这个解释。假设我们有一个domain class,其中有还是作者title

def b = Book.findByAuthor(params.author)
b.title = b.title.reverse()

属性,并且控制器操作中有以下代码:save()请注意,这里没有调用

  1. 。当请求完成后,您会发现 book 的 title 在数据库中已被反转——更改已被持久化,而无需显式保存。这是因为
  2. book 已附加到会话(通过查询检索);domain class,其中有title 属性是持久化的(所有属性都是持久化的,除非配置为 transient);以及
  3. 到会话关闭时,该属性的值已发生更改。

让我们更详细地看一下这些。首先,我之前提到过对象可以“附加”到会话,即由 Hibernate 管理并由数据库支持,但对象是如何附加的呢?如果您以任何方式使用 GORM 检索领域实例,例如通过get()方法或任何类型的查询,那么该对象会自动与会话关联。如果您只是通过new关键字创建一个新实例,那么该对象在调用save()方法之前不会附加到会话。

其次,领域类属性默认是持久化的,即它们在数据库中有匹配的列来存储它们的值。您可以通过将它们的名称添加到静态transients列表属性来将属性设为 transient,这意味着它们的值不会存储在数据库中。

最后,我提到当会话关闭时发生的更改会被持久化。我的意思是什么?为了通过 Hibernate 对数据库进行任何操作,您必须有一个打开的会话。一旦会话关闭,您就不能再使用它进行数据库访问了。此外,会话在关闭时会被刷新,这就是为什么上面的控制器操作结束时会持久化更改(Grails 会在请求开始时自动打开会话,并在结束时关闭它)。

您能否阻止此类更改被持久化?当然。一种选择是在领域实例上调用save():如果任何属性的值验证失败,更改将不会被持久化。当然,如果值是有效的,但您仍然不想持久化它们,可以调用discard()在您的实例上。这不会重置实例属性的值,但可以确保它们不会保存到数据库中。

这些只是几个陷阱,信息量相当大。关键在于理解 Hibernate 会话如何影响领域实例的持久化。即使您还没有完全理解它,未来的 GORM 陷阱文章也会提供更多信息和示例。

总的来说,我建议您始终使用save()来持久化对象,而不是依赖于脏检查。它在代码中清楚地表明您希望持久化更改,并且这些更改会同时得到验证。我也建议您始终检查save()的 return value,尽管在设置引导程序或测试数据时,您最好使用failOnError: true选项。

如果您此时对 GORM 感到害怕或犹豫,请不要这样。它确实让与数据库交互变得轻松有趣,并且通过这一系列文章获得的知识,您将有信心处理可能出现的任何问题。

下次见!

获取 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一份简单的订阅。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看所有