【问题标题】:Grails. Hibernate lazy loading multiple objects圣杯。 Hibernate 延迟加载多个对象
【发布时间】:2013-02-10 00:35:15
【问题描述】:

我在使用 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 代理(所有字段为空,包括 id 字段)。这与 currentCustomer.lastItem 中的对象相同。
  • 对象

此外,这个代理对象的所有属性都设置为 null,并且在调用 getter 时它没有被初始化。我必须在 lastOrder.items 内的每个元素上手动调用 GrailsHibernateUtils.unwrapIdProxy() 以确保里面没有代理(这基本上会导致 EAGER 获取)。

这个代理对象会导致一些非常奇怪的异常,在测试阶段很难跟踪。

有趣的事实:如果我更改操作的顺序(首先加载订单,然后加载客户)lastOrder.items 中的每个元素都会被初始化。

问题是:有没有办法告诉 Hibernate 当它们被触摸时它应该初始化集合,不管集合中的任何元素是否已经在会话中被代理?

【问题讨论】:

  • 如果it 是代理,那么println "${it.id}" 不会触发初始化,您必须为此获取一个非ID 属性。
  • @IanRoberts 有官方文档参考吗?因为从我的角度来看,对于代理对象,初始化 Id 字段确实很有意义。特别是在使用ManyToOne的情况下(外键存储在父对象中)
  • Hibernate 中的代理是一个只知道 ID 的持有者。它可以响应获取 ID 的请求,而无需访问数据库,但是当您向它询问其他任何内容时,它会使用 ID(它已经知道)来查询数据库并加载填充了所有属性的真实对象。代理委托上的后续方法直接调用底层对象。
  • 是的,你是对的。我误解了你的评论。问题是代理的id字段为空(在调试视图和控制台日志中显示)
  • lastOrder.items 是一个集合,而 currentCustomer.lastItem 是一个订单。然而你说“lastOrder.items 仍然包含 currentCustomer.lastItem 的代理”。你能澄清一下你的意思吗?

标签: java hibernate grails


【解决方案1】:

我认为这里发生的是一级缓存(存储在 Hibernate 的 Session 实例中)和在相关对象上具有不同的 FetchType 之间的有趣交互。

当您加载 Customer 时,它会连同加载的所有对象一起被放入 Session 缓存中。这包括OrderItem 对象的代理对象,因为您有FetchType.LAZY。 Hibernate 只允许一个实例与任何特定 ID 相关联,因此任何进一步的操作将在具有该 ID 的 OrderItem 上进行操作将始终使用该代理。如果您要求相同的Session 以另一种方式获取特定的OrderItem,就像您通过加载包含它的Order 一样,那么Order 将拥有代理,因为会话级身份规则。

这就是为什么当你颠倒顺序时它会“起作用”。首先加载Order,它的集合是FetchType.EAGER,所以它(和一级缓存)已经完全实现了OrderItem的实例。现在加载一个Customer,它的lastItem 设置为已加载的OrderItem 实例之一,然后你有一个真正的OrderItem,而不是代理。

您可以看到Hibernate manual中记录的身份规则:

对于附加到特定 Session 的对象... 用于数据库身份的 JVM 身份由 Hibernate 保证。

话虽如此,即使您获得了OrderItem 代理,只要关联的Session 仍然处于活动状态,它就应该可以正常工作。我不一定希望代理 ID 字段显示为在调试器中填充或类似内容,这仅仅是因为代理以“特殊”方式处理事物(即,它不是 POJO)。但它应该以与基类相同的方式响应方法调用。因此,如果您有一个OrderItem.getId() 方法,它当然应该在调用时返回 ID,并且在任何其他方法上也是如此。因为它是延迟初始化的,所以其中一些调用可能需要数据库查询。

这里唯一真正的问题可能只是拥有它会让人感到困惑,因此任何特定的OrderItem 都可能是代理或不是代理。也许您想简单地改变关系,让它们要么都懒惰,要么都渴望?

对于它的价值,您将 ManyToMany 关系设置为 EAGER 并将 ManyToOne 关系设置为 LAZY 有点奇怪。这与通常的设置完全相反,所以我至少会考虑改变它(尽管我显然不知道你的整个用例)。一种思考方式:如果完全获取 OrderItem 的成本太高以至于在查询 Customer 时出现问题,那么一次加载所有它们肯定也太昂贵了?或者相反,如果它足够便宜来加载所有这些,那么当你得到Customer 时,它肯定足够便宜来抓住它吗?

【讨论】:

  • 这可能解释了这种情况。奇怪的异常 - 代理对象的 id 属性为 null 并且代理本身在通过元素集合访问时未初始化
  • @Funtik 好的,我用我的想法更新了我的答案,以及你应该如何前进。祝你好运。
  • 一开始看起来很奇怪,但是当我们在 95% 的情况下加载 Order 对象时,我们需要 OrderItems。另一方面,客户几乎用于所有情况(因此我们尝试优化数据库命中)。
  • @Funtik 取决于 OrderItem 本身是否有集合,您可能需要查看FetchModes。 FetchMode.JOIN 可以解决您的数据库命中问题,同时还允许您使用 Customer 进行 EAGER 提取。
【解决方案2】:

我认为您可以通过这种方式或使用强制预先加载

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

或将 HQL 查询与“获取所有”一起使用

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 2011-02-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多