领先一步
VMware 提供培训和认证,助您加速进步。
了解更多很高兴听到这些文章对人们有所帮助,因此我非常乐意为这个系列再添一篇。这次我将再次讨论关联,但重点放在它们何时被加载到内存中。
更新 2010年8月2日 我为了一对多关系增加了关于延迟加载的更多信息,因为有一些问题需要注意。
人们最先了解的 GORM 关系之一就是它们默认是惰性加载的。换句话说,当你从数据库获取一个领域实例时,它的任何关系都不会被加载。相反,GORM 只会在你实际使用某个关系时才加载它。
让我们通过上一篇文章中的例子来具体说明一下。
class Location {
String city
}
class Book {
String title
static constraints = {
title(blank: false)
}
}
class Author {
String name
Location location
static hasMany = [ books: Book ]
}
如果我们获取一个Author实例,在不执行额外查询的情况下,唯一可以使用的信息就是作者的名字。当我们尝试获取关联的地点或书籍时,会启动更多查询来获取我们需要的额外数据。
这确实是唯一合理的默认选项,特别是对于拥有长关联链的复杂模型。如果预取是默认选项,那么通过获取一个单独的实例,你可能会最终从数据库中拉取一半的数据。
尽管如此,这个选项并非没有代价。我将探讨惰性关联的三个副作用,以便你了解它们是什么,能够识别症状,并修复由此产生的任何问题。
关联的惰性加载涉及一些技巧。毕竟,你不希望上面的location属性返回null,对吧?所以 Hibernate 使用代理和自定义集合类来提供对惰性加载的集合和关联的透明访问——你无需担心它们尚未在内存中的事实。通常,这些代理在隐藏幕后工作方面做得很好,但偶尔实现会暴露出来。
例如,考虑这个领域模型
class Pet {
String name
}
class Dog extends Pet {
}
这是一个非常简单的继承层次结构,因此你不会期望有任何令人不快的事情。现在想象一下,我们在数据库中有一个 ID 为 1 的Dog实例。你认为以下代码会发生什么?
def pet = Pet.load(1)
assert pet instanceof Dog
直观地说,这应该是有效的。毕竟,ID 为 1 的宠物是Dog。那么为什么断言会失败呢?`load()` 方法并没有从数据库获取底层实例,而是返回一个代理,该代理会在需要时执行所需的查询,例如当你尝试访问非属性时。这个代理是id的一个动态子类,而不是的子类,所以instanceofDog检查会失败。即使在从数据库加载实例后,它仍然会失败!如图所示:改变Pet.load()
为Dog.load()转换为将解决问题,因为代理将是的动态子类。你也可以通过替换Dog为属性时。来使其工作,因为后者的实现会自动解开代理并返回底层的get()实例。事实上,Grails 在许多其他情况下都会努力执行这种自动解开,所以你不太可能遇到这个问题。这也是当你确实遇到问题时,它会如此令人惊讶的原因之一。Dog还有一种情况可能会引起一些麻烦,尽管这种情况应该相当罕见。想象一下你有一个类,
Person,它有一个到的关联的子类,所以目录下有一个 Java 类,如下所示:
class Person {
String name
Pet pet
}
的。这个pet,它有一个到关系是惰性的,所以当你获取。这个实例时,
def p = Person.get(1)
assert p.pet instanceof Dog
assert Pet.get(1) instanceof Dog
assert Pet.findById(1) instanceof Dog
assert Pet.list()[0] instanceof Dog
属性将是一个代理。通常情况下,GORM 会隐藏这一点,但请注意以下行为:,它有一个到假设我们有一个的子类,所以实例和一个 ID 为 1 的Dog实例,并且假设这两个通过。这个属性相关联,前三个断言将成功,但最后一个将不会。删除其他代码行,突然那个断言就会成功。嗯?
这种行为无疑令人困惑,但其根源在于 Hibernate 会话。当你从数据库检索,它有一个到时,它的。这个属性是一个代理。该代理存储在会话中,代表 ID 为 1 的的子类,所以实例。现在,Hibernate 会话保证,无论你在单个会话中从数据库中检索某个领域实例多少次,Hibernate 都会返回相同的对象。所以当我们调用Pet.get(1)时,*Hibernate* 会给我们代理。对应的断言之所以成功,是因为 GORM 会自动解开代理。对于findBy*()以及其他只能返回单个实例的查询,情况也是如此。
然而,GORM 不会为list(), findAllBy*()以及可以返回多个结果的其他查询解开代理。因此,Pet.list()[0]返回我们未解开代理的实例。如果,它有一个到尚未首先获取,Pet.list()将返回实际的实例:代理此时不在会话中,因此查询不必返回它。
你可以通过几种方式来防范这个问题。首先,你可以使用动态instanceOf()方法而不是改变运算符。它在所有 GORM 领域实例上都可用,并且是代理感知的。Pet.get(1).instanceOf(Dog)。其次,使用def声明变量,而不是静态领域类类型,否则你可能会遇到类转换异常。因此,与其使用
Person p = Person.get(1)
Dog dog = Pet.list()[0] // Throws ClassCastException!
,不如使用
def p = Person.get(1)
def dog = Pet.list()[0]
。使用这种方法,即使你正在处理代理,你仍然可以访问特定于Dog的任何属性或方法。
必须说,GORM 在保护开发人员免受代理影响方面做得非常出色。它们很少会泄露到你的应用程序代码中,尤其是在较新版本的 Grails 中。尽管如此,有些人仍然会遇到问题,因此了解症状和原因很有用。
我在最后一个示例中展示了会话的行为如何与惰性加载相结合可能产生一些有趣的结果。这种组合也是一个更常见的错误的原因:org.hibernate.LazyInitializationException.
正如我已经提到的,当你有一个惰性加载的关系时,如果你以后想导航该关系,Hibernate 就必须执行一个额外的查询。在正常情况下,这不成问题(除非你担心性能),因为 Hibernate 会透明地处理。但如果你尝试在不同的会话中访问该关系,会发生什么?
假设你在控制器操作中加载了 ID 为 1 的Author实例,并将其存储在 HTTP 会话中。此时,没有任何代码触及books集合。在下一个请求中,用户访问与此控制器操作对应的 URL:
class MyController {
def index = {
if (session.author) {
render "Author ${session.author.name} has written the books: ${session.author.books*.title}"
else {
render "No author in session"
}
}
...
}
这里的意图是,如果我们的 HTTP 会话包含一个作者变量,该操作将渲染该作者书籍的标题。除了在这种情况下它不会。它抛出了一个LazyInitializationException。
问题在于,我们称之为*分离对象*的Author实例。它是在一个 Hibernate 会话中加载的,然后该会话在请求结束时关闭。一旦对象的会话关闭,它就变成分离的,你无法访问它上的任何会导致查询的属性。
“但我的操作中有一个会话是打开的,为什么会出现这个问题?”我听到你喊。这是个好问题。不幸的是,这是一个新的 Hibernate 会话,它不知道我们的Author实例。只有当对象显式附加到新会话时,你才能访问其惰性关联。有几种技术可以做到这一点:
def author = session.author
// Re-attach object to session, but don't sync the data with the database.
author.attach()
// Re-attach object, but merge any changes with the data in the database.
// You *must* use the instance returned by the merge() method.
author = author.merge()
的attach()方法在领域实例在被分离对象检索后不太可能在数据库中发生更改的情况下很有用。如果数据可能已更改,则必须小心。请参阅Grails 参考指南,了解merge()还是refresh().
的行为。LazyInitializationException现在,如果你遇到
N+1 选择
Author.list().each { author ->
println author.location.city
}
让我们回到文章前面提到的作者/书籍/地点示例。想象一下,我们在数据库中有四个作者,并且我们运行以下代码:
将执行多少个查询?答案是五个:一个用于获取所有作者,然后每个作者一个用于检索相应的地点。这就是所谓的 N+1 选择问题,编写受其影响的代码非常容易。上面的例子乍一看似乎无害。在开发过程中,这实际上不是问题,但在部署到生产环境时,执行如此多的查询会损害应用程序的响应能力。因此,在应用程序向最终用户开放之前,分析应用程序的数据库使用情况是个好主意。最简单的方法是在grails-app/conf/DataSource.groovy
dataSource {
...
loggingSql = true
}
中启用 Hibernate 日志记录,以确保所有查询都记录到 stdout。当然,你可以在每个环境中启用它。另一种方法是使用像P6Spy这样的特殊数据库驱动程序,它可以拦截查询并记录它们。
那么如何避免这些额外的查询呢?通过预取关联而不是惰性加载。这种方法也解决了与惰性加载相关的其他问题。我提到的。
GORM 允许你覆盖每个关系默认的惰性加载行为。例如,我们可以配置 GORM 通过以下映射始终与作者一起加载作者的地点:
class Author {
String name
Location location
static hasMany = [ books: Book ]
static mapping = {
location fetch: 'join'
}
}
在这种情况下,地点不仅与作者一起加载,而且使用 SQL join 在同一个查询中检索。所以这段代码:
Author.list().each { a ->
println a.location.city
}
将只导致一个查询。你也可以使用lazy: false选项而不是fetch: 'join',但这将导致一个额外的查询来加载地点。换句话说,关联是预取的,但有一个单独的 SQL 选择。大多数时候你可能希望使用fetch: 'join'来最小化执行的查询数量,但有时它可能是更昂贵的方法。这真的取决于你的模型。
还有其他选项,但我在这里不详细介绍。它们在 Grails 用户指南的第 5.3.4 和 5.5.2.8 部分有完整文档记录,如果你想了解更多信息(尽管我会等待 Grails 1.3.4 版本,它将附带一些重要的文档更新)。
在领域类映射中配置预取的缺点是关联将*始终*预取。但是,如果你只偶尔需要这些信息呢?任何只想显示作者姓名的页面都会不必要地变慢,因为还必须加载地点。对于像这样的简单关联,成本可能很低,但对于集合来说,成本会更高。这就是为什么你还可以选择按查询预取关联。
查询是上下文敏感的,所以它们是指定是否应预取特定关联的理想位置。假设我们已经恢复到Author的默认行为,现在我们想获取所有作者并显示他们的城市。在此上下文中,当我们获取作者时,我们显然希望检索地点。方法如下:
Author.list(fetch: [location: 'join']).each { a ->
println a.location.city
}
我们所做的只是添加了一个fetch参数到查询中,并使用一个映射:关联名称 -> 获取模式。如果代码还显示了作者书籍的标题,我们也会将books关联添加到映射中。动态查找器支持完全相同的fetch选项。
Author.findAllByNameLike("John%", [ sort: 'name', order: 'asc', fetch: [location: 'join'] ]).each { a->
...
}
我们也可以用 Criteria 查询达到同样的目的:
def authors = Author.withCriteria {
like("name", "John%")
join "location"
}
以上所有内容也适用于一对多关系,但你需要考虑一些额外的因素。
我上面说过,你通常希望在预取关联时使用 join,但这套经验法则对于一对多关系并不奏效。为了理解为什么,请考虑这个查询:
Author.list(max: 2, fetch: [ books: 'join' ])
很可能,这只会返回一个Author实例。这可能不是你期望或想要的行为。那么发生了什么?
在底层,Hibernate 使用左外连接来获取每个作者的书籍。这意味着你会得到重复的Author实例:每个作者关联的书籍都有一个。如果你没有max选项,你不会看到这些重复项,因为 GORM 会删除它们。但问题在于max选项应用于*在*删除重复项*之前*的结果。所以在上面的例子中,Hibernate 只返回两个结果,这两个结果很可能都有相同的作者。然后 GORM 删除重复项,你最终得到一个Author实例。
这个问题在领域类映射配置和 Criteria 查询中都会发生。事实上,Criteria 查询默认不会从结果中删除重复项!对于这种混乱,只有一个合理的解决方案:对于一对多关系,始终使用“select”模式。例如,在领域映射中使用:lazy: false:
class Author {
...
static hasMany = [ books: Book ]
static mapping = {
location fetch: 'join'
books lazy: false
}
}
在查询中,根据你使用的是动态查找器还是 Criteria 查询,使用适当的设置:
import org.hibernate.FetchMode
Author.list(fetch: [ books: 'select' ])
Author.withCriteria {
fetchMode "books", FetchMode.SELECT
}
是的,你将得到一个额外的查询来获取集合,但它只有一个,并且你获得了一致性和简单性。如果你发现确实需要减少查询数量,那么你总是可以回退到 HQL。
除了与一对多的情况,GORM 中的预取很简单,如果你遵循一对多关系使用“select”获取模式的原则,这对它们也适用。主要工作是分析应用程序的数据库访问,以确定在哪里应该预取关联,或者专门使用 join。只需警惕过早优化!
正如你所见,关联的惰性加载会引发各种问题,特别是与 Hibernate 会话结合使用时。尽管存在这些问题,惰性加载仍然是对象图的合理默认选项,是一个重要功能。一旦你了解了它们,通常很容易识别并解决这些问题。如果什么都没有浮现在脑海中,你总是可以回退到明智地使用预取。
说了这么多,随着 Grails 版本号的提高,用户遇到这些问题的可能性越来越小。考虑到 Hibernate 在后台所做的工作,这真是一个令人印象深刻的技巧!