Grails. Hibernate懒加载多个对象

5

我在Grails中使用代理对象时遇到了困难。 假设我有以下内容:

class Order {
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(name="xxx", joinColumns = {@JoinColumn(name = "xxx")}, inverseJoinColumns = {@JoinColumn(name = "yyy")})
    @OrderBy("id")
      @Fetch(FetchMode.SUBSELECT)
      private List<OrderItem> items;
    } 

class Customer {
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
    @JoinColumn(name = "xxx",insertable = false, nullable = false)
    private OrderItem lastItem;

    private Long lastOrderId;
}

在某个控制器类中

//this all happens during one hibernate session.
    def currentCustomer = Customer.findById(id)
//at this point currentCustomer.lastItem is a javassist proxy

def lastOrder = Order.findById(current.lastOrderId)
//lastOrder.items is a proxy
//Some sample actions to initialise collections 
lastOrder.items.each { println "${it.id}"}

在迭代后,lastOrder.items 仍包含 currentCustomer.lastItem 的代理。例如,如果在 lastOrder.items 集合中有 4 个项目,它看起来像这样:
  • 对象
  • 对象
  • javassist 代理(所有字段都为 null,包括 id 字段)。这是与 currentCustomer.lastItem 中相同的对象。
  • 对象
此外,该代理对象的所有属性都设置为 null,并且在调用 getters 方法时不会初始化。我必须在每个 lastOrder.items 元素上手动调用 GrailsHibernateUtils.unwrapIdProxy(),以确保其中没有代理(这基本上会导致 EAGER 获取)。
这一个代理对象会导致一些非常奇怪的异常,在测试阶段很难跟踪。
有趣的事实是:如果我改变操作的顺序(先加载订单,然后再加载客户),则 lastOrder.items 中的每个元素都会被初始化。
问题是:是否有一种方法告诉 Hibernate 在访问集合时应初始化它们,而不管集合中是否已存在任何元素的代理?

如果it是代理,则println "${it.id}"不会触发初始化,您必须获取一个非ID属性。 - Ian Roberts
@IanRoberts 有官方文档的参考吗?因为从我的角度来看,对于代理对象,初始化Id字段确实是有意义的。特别是在使用ManyToOne(外键存储在父对象中)的情况下。 - WeMakeSoftware
是的,你说得对。我误解了你的评论。问题在于代理的id字段为空(在调试视图和控制台日志中显示)。 - WeMakeSoftware
lastOrder.items是一个OrderItem的集合,而currentCustomer.lastItem是一个Order。但你说"lastOrder.items还包含了currentCustomer.lastItem的代理"。你能澄清一下你的意思吗? - sharakan
OrderItem是否有任何字段本身是对其他实体的引用,无论是直接还是通过@Embeddables间接引用的? - sharakan
显示剩余2条评论
2个回答

5
我认为这里发生的是一种有趣的交互作用,它涉及到第一级缓存(存储在Hibernate的Session实例中)和相关对象上不同的FetchType。
当您加载Customer时,它会与任何与其一起加载的对象一起放入Session缓存中。这包括OrderItem对象的代理对象,因为您使用了FetchType.LAZY。Hibernate只允许一个实例与任何特定ID相关联,因此任何进一步操作将始终使用该代理来处理具有该ID的OrderItem。如果您要求同一个Session以另一种方式获取特定的OrderItem,就像通过加载包含它的Order那样,那么该Order将具有代理,因为有Session级别的身份规则。
这就是为什么反转顺序会“起作用”的原因。首先加载Order,它的集合是FetchType.EAGER,因此它(以及一级缓存)已经完全实现了OrderItem的实例。现在加载一个Customer,它的lastItem设置为已经加载的OrderItem实例之一,然后你就有了一个真正的OrderItem,而不是代理对象。
你可以在Hibernate手册中看到身份规则的说明:

对于附加到特定会话的对象……Hibernate保证JVM标识与数据库标识相匹配。

所有这些说法,即使你获得了一个OrderItem代理,只要相关的Session仍然活动,它也应该能够正常工作。我不会期望代理ID字段在调试器或类似工具中显示为已填充,因为代理以“特殊”的方式处理事情(即,它不是POJO)。但是,它应该像其基类一样响应方法调用。因此,如果您有一个OrderItem.getId()方法,在调用时肯定会返回ID,并且在任何其他方法上也是如此。但由于它是惰性初始化的,因此其中一些调用可能需要数据库查询。
可能唯一的真正问题是,让任何特定的OrderItem都可以是代理或非代理很令人困惑。也许您想简单地改变关系,使它们都是惰性的或都是急切的?

就我所知,将多对多关系设置为EAGER,而将多对一关系设置为LAZY有点奇怪。这正好与通常的设置相反,因此我建议考虑更改它(尽管我显然不知道您的整个用例)。一种思考方式是:如果完全获取一个OrderItem是如此昂贵以至于在查询Customer时成为问题,那么一次性加载所有OrderItem肯定也太昂贵了吧?反之,如果加载所有OrderItem足够便宜,那么当获取Customer时抓取它肯定也很便宜?


这可能解释了这种情况。奇怪的异常 - 代理对象上的id属性为空,并且当通过元素集合访问时,代理本身未初始化。 - WeMakeSoftware
@Funtik 好的,我更新了我的答案,包括我对此的看法以及你应该如何前进。祝你好运。 - sharakan
起初看起来有些奇怪,但是在95%的情况下,当我们加载订单对象时,我们需要订单项。另一方面,客户在几乎所有情况下都会被使用(因此我们尝试优化数据库访问)。 - WeMakeSoftware
@Funtik 根据OrderItem本身是否有集合,您可能需要查看FetchModes。FetchMode.JOIN可以解决您的DB hits问题,同时还允许您使用Customer进行EAGER获取。 - sharakan

0

我认为你可以通过这种方式或使用强制进行急切加载

def lastOrder = Order.withCriteria(uniqueResult: true) {
       eq('id', current.lastOrderId)
       items{}
   }

或者使用带有“fetch all”的HQL查询


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接