GORM 常见问题 (第一部分)

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

您是 Grails 新手吗?或者您是否遇到过第一个 GORM 的“奇特之处”?如果是这样,那么您需要阅读这篇关于 GORM 常见问题的系列文章。文章不仅会重点介绍那些经常让人感到困惑的小问题,还会解释为什么 GORM 会以这种方式运行。

希望您已经知道 GORM 是 Grails 附带的数据库访问库。它基于可能是目前最流行的 Java ORM:Hibernate。正如您所料,Hibernate 是一个强大而灵活的库,它为 GORM 带来了巨大的好处。但使用它也需要付出代价:GORM 用户遇到的许多问题都源于 Hibernate 的工作方式。GORM 尽最大努力隐藏实现细节,但它们偶尔也会泄露出来。

在本文的其余部分,我将描述将对象持久化到数据库的基本知识。听起来很简单,但即使在如此基本的事情上,GORM 的工作方式也并非如您所料。

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

保存域实例的问题可能是开发人员首先遇到的问题。有多少人经历过“我保存了它,但为什么它不在数据库中?”阶段?如果这能让你感觉好一点,我也经历过。为什么会这样?有几种可能性。

不要忘记验证!

每次保存域实例时,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.failOnErrortrue在 grails-app/conf/Config.groovy 中。并且不要忘记,所有域属性都具有隐式nullable: false约束!

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

Hibernate 的会话

在极少数情况下,您可能会发现自己保存了一个域实例,但随后发现后续查询没有获取它,即使该实例通过了验证。这是使用 Hibernate 作为底层框架导致的更广泛问题的症状。

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

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

hibernate-session-in-action

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

如您所料,Grails 确实允许您控制何时实际将数据持久化到数据库中。您是否曾在示例中看到过这样的代码?

book.save(flush: true)

flush: true强制 Hibernate 立即将所有更改持久化到数据库中。它对应于所谓的*刷新会话*。

现在危险在于您将离开并放置flush: true到处都是。不要。让 Hibernate 完成其工作,并且仅在必须时或至少在更新批处理结束时手动刷新会话。您只应在未在数据库中看到应有的数据时使用它。我知道这有点模棱两可,但此类操作必要的环境取决于数据库实现和其他因素。手动刷新非常有用的一个领域是在您与另一个应用程序或内部服务交互时,这些应用程序或内部服务正在访问与您相同的数据库,但在您当前的会话之外。

如果您对会话仍然不太清楚,请不要担心——我们将反复回到 Hibernate 会话,因为它与 GORM 相关的许多常见问题都至关重要。事实上,这就是人们遇到下一个问题的原因。

现在您在我不希望您保存时保存了?!

当您调用时未能持久化域实例save()非常常见。现在考虑相反的情况:在没有相应调用save()的情况下持久化对象。如果您还没有遇到过这种行为,我几乎可以保证您会遇到的。那么它为什么会发生呢?

Hibernate 支持脏检查的概念。这意味着 Hibernate 会检查域实例的任何(持久)属性的值是否在*该实例从数据库中提取后*发生了更改,并将这些更改持久化到数据库中。这有点拗口,所以希望举个例子可以帮助澄清这个解释。假设我们有一个Book域类与titleauthor属性,以及控制器操作中的以下代码

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

请注意,这里没有调用save()。当请求完成后,您会发现书籍的标题已在数据库中反转——更改已在没有显式保存的情况下持久化。这是因为

  1. 该书籍已附加到会话(通过查询检索);
  2. title属性是持久的(除非配置为瞬态,否则所有属性都是持久的);并且
  3. 属性值在会话关闭时已更改。

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

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

最后,我提到如果更改在会话关闭时存在,则会持久化它们。我的意思是?为了通过 Hibernate 对数据库执行任何操作,您必须有一个打开的会话。会话关闭后,您便无法再将其用于数据库访问。此外,会话在关闭时会刷新,这就是为什么更改在上述控制器操作结束时会持久化的原因(Grails 会在请求开始时自动打开一个会话,并在请求结束时关闭它)。

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

对于仅仅几个常见问题,这已经是相当多的信息了。关键是要了解 Hibernate 会话如何影响域实例的持久性。即使您还没有完全理解,未来的 GORM 常见问题文章也将提供更多信息和示例。

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

如果您此时对 GORM 感到一丝畏惧或谨慎,请不要。它确实使处理数据库变得轻松有趣,并且通过从本系列文章中获得的信息,您将有信心处理可能出现的任何问题。

下次再见!

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

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

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部