休眠 "failed to lazily initialize a collection" 运行时错误

Hibernate "failed to lazily initialize a collection" runtime error

这个问题已经被问过很多次了,但我仍然找不到适合我的用例的解决方案:

  1. 我有一个使用 Hibernate Struts2 的应用程序 5.x。

  2. 应用程序是 "Contacts app"。它有两个实体:一个 "Contact",它可以有零个或多个 "Notes".

  3. 这是我获取联系人的方式:

    @Override
    public List<Contact> getContacts() {
    //Note: Hibernate 5++ supports Java try-with-resource blocks
    try (Session session = HibernateUtil.openSession()) {
        List<Contact> contacts = session.createQuery("FROM Contact").list();
        return contacts;
        ...
    
  4. 效果很好。直到我尝试这样的事情:

    ObjectMapper mapper = new ObjectMapper();
    jsonString = mapper.writeValueAsString(contacts);
    

错误:

16:05:16.174 [http-nio-8080-exec-2] DEBUG org.apache.struts2.dispatcher.Dispatcher - Dispatcher serviceAction failed
com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize a collection of role: com.example.contactsapp.models.Contact.notes, could not initialize proxy - no Session (through reference chain: java.util.ArrayList[0]->com.example.contactsapp.models.Contact["notes"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:394) ~[jackson-databind-2.10.0.jar:2.10.0]
    ...
    at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:4094) ~[jackson-databind-2.10.0.jar:2.10.0]
    at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3404) ~[jackson-databind-2.10.0.jar:2.10.0]
    at com.example.contactsapp.actions.ContactsAction.getContacts(ContactsAction.java:36) ~[classes/:?]
    ...

可能的解决方案

  1. 不想 想要更改为 FetchType.Eager

  2. 因为我没有使用 Spring,所以我不能使用 @TransactionalOpenSessionInView。但如果有 Hibernate-only.

  3. 的等价物,我会很高兴
  4. 这些是我尝试过的东西(主要基于How to solve the “failed to lazily initialize a collection of role” Hibernate exception):

    @Override
    public List<Contact> getContactsFetchAll() {
      //Note: Hibernate 5++ supports Java try-with-resource blocks
      try (Session session = HibernateUtil.openSession()) {
        // Jackson mapper.writeValueAsString() => "failed to lazily initialize a collection" 
        // List<Contact> contacts = session.createQuery("FROM Contact").list();
    
        // Plan A: Causes same "failed to lazily initialize a collection" runtime error
        // List<Contact> contacts = session.createQuery("FROM Contact").list();
        // Hibernate.initialize(contacts);
    
        // Plan B: Still no-go: returns [] empty set
        // List<Contact> contacts = session.createQuery("SELECT c FROM Contact c JOIN FETCH c.notes n").list();
    
        // Plan C: Same: "failed to lazily initialize a collection of role..."
        // Query query = session.createQuery("FROM Contact");
        // Hibernate.initialize(query);
        // List<Contact> contacts = query.list(); 
    
        // Plan D: Same: "ERROR: failed to lazily initialize a collection of role"
        // List<Contact> contacts = session.createQuery("FROM Contact").list();
        // for (Contact c : contacts) {
        //    Set<Note> n = c.getNotes();
        // }
           return contacts;
        }
     }
    

问:有什么建议吗?

问:您需要任何其他信息吗?


我做了更多的研究。

Apparently,在这种特定情况下,Hibernate 根本不执行 "join"。

改为:

  1. 它执行 "select" 以获得 parent。
  2. 如果您尝试阅读任何 children 的内容,那么它会 另一个 "select",得到children。一"select"为parent,一秒为children。
  3. 如果实体是为 "eager fetch" 配置的,它总是 执行所有 selects up-front。
  4. 对于"lazy fetch",您必须在关闭会话之前尝试读取children(从而触发第二个select)。否则,如果您在会话关闭后尝试读取 child 数据,您将得到 "failed to lazily initialize a collection".
  5. Left Join 在这种情况下不起作用:数据库返回的结果集与实体不匹配。为了让它工作,我必须一次读取一行结果集,然后手动构建实体。
  6. 我仍在寻找一种方法让 "Join Fetch" 在我的场景中工作...

如果您在实体配置中使用延迟初始化进行 OneToMany 映射,并且在某些情况下您想要急切地获取集合。我认为您可以使用 @NamedQuery 在单个查询中获取列表(在您的情况下为 Left Join)。

我什至在发布问题之前就使用了这个 SO 线程:

How to solve the “failed to lazily initialize a collection of role” Hibernate exception

The problem is caused by accessing an attribute with the hibernate session closed. You have not a hibernate transaction in the controller.

Possible solutions:

  • Do all this logic, in the service layer, (with the @Transactional), not in the controller. There should be the right place to do this, it is part of the logic of the app, not in the controller (in this case, an interface to load the model). All the operations in the service layer should be transactional...

  • Use 'eager' instead of 'lazy'. Now you are not using 'lazy' .. it is not a real solution, if you want to use lazy, works like a temporary (very temporary) workaround.

  • use @Transactional in the Controller. It should not be used here, you are mixing service layer with presentation, it is not a good design.

  • use OpenSessionInViewFilter, many disadvantages reported, possible instability.

不幸的是,@TransactionalOpenSessionInView 在我的场景中不可用。 FetchType.EAGER 不是一个选项。 "doing everything inside the session" 正是我试图解决的问题。

另一个很好的回应,来自same thread

From my experience, I have the following methods to solved the famous LazyInitializationException:

  • Use Hibernate.initialize

    Hibernate.initialize(topics.getComments());

  • Use JOIN FETCH

You can use the JOIN FETCH syntax in your JPQL to explicitly fetch the child collection out. This is some how like EAGER fetching.

  • Use OpenSessionInViewFilter

我学到了什么:

  • List<Contact> contacts = session.createQuery("FROM Contact").list();:只有return条"parent"(联系人)记录,none子(备注)记录。

    解决方法:在会话仍处于活动状态时明确阅读每个子注释。这将触发第二个 "select",并获取子数据。

  • List<Contact> contacts = session.createQuery("SELECT c FROM Contact c INNER JOIN FETCH c.notes").list();: 没有 return 任何具有 0 个子注释的联系人。

    解决方法:更改代码以确保每个联系人至少有一个注释(通过在创建联系人时自动创建注释)。 "Join fetch" 只打了一个 SQL 电话。

无论如何 - 我让它工作了。