0

This question has been asked many times, but I still can't find a solution for my use case:

  1. I have a Struts2 application using Hibernate 5.x.

  2. The application is a "Contacts app". It has two entities: a "Contact", which can have zero or more "Notes".

  3. Here's how I get the contacts:

    @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. It works great. Until I try something like this:

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

ERROR:

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/:?]
    ...

POSSIBLE SOLUTIONS

  1. I do NOT want to change to FetchType.Eager.

  2. Since I'm not using Spring, I can't use @Transactional or OpenSessionInView. But I'd love if there were equivalents for Hibernate-only.

  3. These are the things I've tried (based largely on 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;
        }
     }
    

Q: Any suggestions?

Q: Do you need any additional information?


I've done a bit more research.

Apparently, in this particular scenario, Hibernate doesn't do a "join" at all.

Instead:

  1. It does a "select" to get the parent.
  2. If you try to read something from any of the children, then it does ANOTHER "select", to get the children. One "select" for the parent, a second for the children.
  3. If the entity is configured for "eager fetch", it always does all selects up-front.
  4. For "lazy fetch", you MUST try to read the children (thus triggering the second select) before closing the session. Otherwise, if you try to read the child data after the session closes, you get "failed to lazily initialize a collection".
  5. A Left Join doesn't work in this scenario: the result set returned by the database simply doesn't match the entity. To make it work, I'd have to read the result set a row at a time, and build the entity manually.
  6. I'm still looking for a way to get "Join Fetch" to work in my scenario...
FoggyDay
  • 11,962
  • 4
  • 34
  • 48
  • 1
    As you have 0 or more roles, List contacts = em.createQuery("SELECT c FROM Contact c LEFT JOIN c.notes").list(); – Vivek Jan 15 '20 at 02:50
  • @Vivek: Correct. I guess that the JOIN FETCH query in "Plan B" only retrieved notes, instead of "Contacts + Notes". Q: So what do I need to do in order to get *ALL* the contacts, with *ALL* the notes, so I can return a complete list to the caller? And so the caller won't get "failed to lazily initialize" when he uses the list? Even though the session was closed before returning the list to the caller? – FoggyDay Jan 15 '20 at 03:12

2 Answers2

1

In case you are having OneToMany mapping with Lazy Initialization in entity configuration and in some scenario you want to fetch collection eagerly. I think you can use @NamedQuery for fetching List in single query (in your case Left Join).

Vivek
  • 376
  • 1
  • 14
0

I worked with this SO thread even before I posted my question:

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.

Unfortunately, @Transactional and and OpenSessionInView weren't available in my scenario. FetchType.EAGER wasn't an option. And "doing everything inside the session" was exactly the problem I was trying to resolve.

Another excellent response, from the 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

What I learned:

  • List<Contact> contacts = session.createQuery("FROM Contact").list();: only returns the "parent" (Contact) record, none of the child (Notes) records.

    WORKAROUND: Explicitly read each of the child notes while the session is still active. This triggers a second "select", and fetches the child data.

  • List<Contact> contacts = session.createQuery("SELECT c FROM Contact c INNER JOIN FETCH c.notes").list();: doesn't return any Contacts with 0 child Notes.

    WORKAROUND: Change the code to ensure that every contact has at least one note (by automatically creating a note when a Contact is created). "Join fetch" only makes one SQL call.

Anyway - I got it working.

FoggyDay
  • 11,962
  • 4
  • 34
  • 48