1

I am using Spring 4 and Hibernate 4 is a scenario where I have two classes, Person and Book, which map to two tables in the database. The relationship is 1:M from Person to Book, i.e. a Person can have many Books.

The relationship in the Person table is defined as:

@OneToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY)
@JoinColumn(name="PERSON_ID")
private List<Book> books;

And in Book:

@ManyToOne      
private Person person;

With appropriate getter and setter methods.

But in my method to display the Books for a Person:

// Calls books.jsp for a Person.
@RequestMapping(value = "/books", method = RequestMethod.GET)
public String listBooks(@RequestParam("id") String id, 
                        Model model) {    
    logger.info(PersonController.class.getName() + ".listBooks() method called.");                 

    Person person = personService.get(Integer.parseInt(id));        
    // List<Book> books = bookService.listBooksForPerson(person.getId());

    // Set view.
    model.addAttribute("person", person);
    // model.addAttribute("books", books);
    return "view/books";
}

I find that Hibernate returns:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: library.model.Person.books, could not initialize proxy - no Session

So I have two alternatives:

  • I could use EAGER fetching - this does work but ignores the whole LAZY fetching principle.
  • Resolve the issue. This is my preference given that this is an application for myself and others to learn from.

If the answer to this issue is to do with specifying / using transactions in the service or DAO code, typically my database updates are annotated @TRANSACTIONAL in the service classes. and implemented as:

 @Override
public void insert(Person person)  {
    logger.info(PersonDAOImpl.class.getName() + ".insert() method called.");

    Session session = sessionFactory.openSession();
    Transaction transaction = session.getTransaction();        
    try {
        transaction.begin();
        session.save(person);
        transaction.commit();            
    }
    catch(RuntimeException e) {
        transaction.rollback();
        throw e;
    }
    finally {
        session.close();
    }
}

My DAO get methods don't use JOINs in their SQL, e.g.:

@Override
public List<Book> listBooksForPerson(Integer id) {
    logger.info(BookDAOImpl.class.getName() + ".listBooksForPerson() method called.");

    Session session = sessionFactory.openSession();          
    try {    
        Query query = session.createQuery("FROM Book WHERE PERSON_ID = " + id);  
        return query.list();                                  
    }        
    finally {
        session.close();
    }         
}

I'm a bit of a Spring newbie and I've read a few questions about this problem on the site and understand the nature of the problem, re EAGER or LAZY fetching of dependent data when a Hibernate session has closed. But I do not know how best to solve it in the code given above.

Can anyone advise?

Well, a method like this fails:

@Override
public List<Book> listBooksForPerson(Integer id) {
    logger.info(PersonDAOImpl.class.getName() + ".listBooksForPerson() method called.");

    Session session = sessionFactory.openSession();          
    try {    
        Query query = session.createQuery("FROM Person, Book WHERE Person.ID = " + id + " AND Person.ID = Book.PERSON_ID");  
        return query.list();                                  
    }        
    finally {
        session.close();
    }         
}

Because of Your page request has caused a QuerySyntaxException: Invalid path: 'null.ID' [FROM library.model.Person, library.model.Book WHERE Person.ID = 1 AND Person.ID = Book.PERSON_ID] error:

I've also tried the following:

@Override
public Person getPersonAndBooks(Integer id) {
    logger.info(PersonDAOImpl.class.getName() + ".listBooksForPerson() method called.");

    Session session = sessionFactory.openSession();          
    try {    

        SQLQuery query = session.createSQLQuery("SELECT * FROM Person JOIN Book ON Person.ID = book.PERSON_ID AND Person.ID = " + id);  

        // HOW TO GET PERSON OBJECT HERE WITH BOOKS?

    }        
    finally {
        session.close();
    }         
}

But how do I map the output of the query to my Person object type where Person is:

// Attributes.    
@Id
@Column(name = "ID", unique = true, nullable = false)    
@GeneratedValue(strategy=GenerationType.AUTO)
private Integer id;

@Column(name = "NAME", nullable = false, length=50)      
private String name;

@Column(name = "ADDRESS", nullable = false, length=100)
private String address;

@Column(name = "TELEPHONE", nullable = false, length=10)
private String telephone;

@Column(name = "EMAIL", nullable = false, length=50)
private String email;

@OneToMany(cascade=CascadeType.ALL, fetch = FetchType.LAZY) 
@JoinColumn(name="PERSON_ID")
private List<Book> books;

Can anyone advise?

This query was resolved by adjusting the DAO method to get a Person to:

@Override
public Person get(Integer personId) {
    logger.info(PersonDAOImpl.class.getName() + ".get() method called.");

    Session session = sessionFactory.openSession();

    try {                         
        Query query = session.createQuery(" from Person as p left join fetch p.books where p.personId = :personId").setParameter("personId", personId);                      
        Person person = (Person) query.list().get(0); 
        Hibernate.initialize(person.getBooks());            
        return person;

        // return (Person) session.get(Person.class, personId);
    }        
    finally {
        session.close();            
    }         
}

Where method listBooksForPerson above has been deleted.

But I am still using the one end of the relationship between the tables as the owner, so to speak. The Book table now has no use of a @ManyToOne tag or instance of Person. This was the only way to get LAZY fetching (appearing) to work in the end.

Mr Morgan
  • 2,215
  • 15
  • 48
  • 78
  • Like this: http://stackoverflow.com/questions/1139985/in-spring-with-jpa-hibernate-how-do-i-keep-a-session-open-to-avoid-lazy-initial?rq=1 – Loc Phan Jun 21 '14 at 02:42

1 Answers1

1

As you may already be aware, the issue is that once an entity is sent to your view, it is detached from the EntityManager so lazy loading won't work at that point. If you try to use personService.get in listBooks, the Person retrieved is detached from the EntityManager before books is loaded. Therefore when you try to access books in your view you'll get that error.

The answer is to ensure the data you'll need for the view is loaded into the entity before the entity is detached. Don't use personService.get; instead use an HQL query and the JOIN FETCH syntax to load the lazy fields you need. Try something like:

SELECT p FROM Person p LEFT JOIN FETCH p.books WHERE p.id = ...

In this case, books will be loaded and you can use it in your view.

You can cascade join fetches as well. For example, if Book contained a lazy collection of Chapter which you needed to work with, you could use:

SELECT p FROM Person p LEFT JOIN FETCH p.books book LEFT JOIN FETCH book.chapters WHERE p.id = ...

In Person, all books--and all chapters within those books--would be available to the view.

Alvin Thompson
  • 5,388
  • 3
  • 26
  • 39
  • So let me see if I understand this correctly: I could have a PersonDAO method called from the controller through the service, wherein I execute `SELECT p FROM Person p LEFT JOIN FETCH p.books WHERE p.id = ...' And once this data is retrieved into the controller, it is simply written to the view by specifying the page name? – Mr Morgan Jun 21 '14 at 02:59
  • @MrMorgan: I'm not sure what you mean by "simply written to the view", but it's accessible to the view (specifically, `books.jsp`). I imagine you have code in there that iterates over `Person.books` to produce the listing. That code was the source of your error and it will now work. – Alvin Thompson Jun 21 '14 at 03:08
  • Have tried what I think you mean and have amended the question to show a method that causes an SQL error because of syntax, although I'm not sure why. But are you suggesting that my person.get method could use the SQL SELECT p FROM Person p LEFT JOIN FETCH p.books WHERE p.id = ...? – Mr Morgan Jun 21 '14 at 03:24
  • @MrMorgan: You're not even using `listBooksForPerson` in the code you posted (it's commented out), so I'm not sure why you'd try to modify that... – Alvin Thompson Jun 21 '14 at 03:35
  • I've tried calling a new version of listBooksForPerson from the Person controller but it doesn't work with lazy fetching. So I'm going to amend getPerson so that it uses an SQL JOIN with Book to get the books for the Person in one go. Person has an arraylist of Book. But this can wait until tomorrow, it's dawn here. Thanks for help and I will update the question tomorrow. – Mr Morgan Jun 21 '14 at 03:39
  • @MrMorgan: In `PersonService`, you can add a method `getWithBooks` if you want. It be similar to `get` except use the HQL syntax I specified. Then use `personService.getWithBooks` instead of `personService.get` inside of your `listBooks` method in the controller. – Alvin Thompson Jun 21 '14 at 03:41
  • That's what I will try tomorrow. Thanks. – Mr Morgan Jun 21 '14 at 03:44
  • @MrMorgan: don't change `get` but rather add a new method. For most cases where you need a Person I imagine you don't need `books`, so changing the normal method to retrieve a Person to include it would defeat the purpose of using lazy loading, just like using `EAGER` would. – Alvin Thompson Jun 21 '14 at 03:46
  • I've added some more text to the question regarding how I can get results from a cross table SQL query to make up a Person instance. – Mr Morgan Jun 21 '14 at 13:17
  • @MrMorgan: Don't use an SQL query. Since you're using Hibernate, create an HQL query like the one I showed you. Consult the Hibernate documentation for information on how to do this. – Alvin Thompson Jun 23 '14 at 14:01
  • FAO Alvin Thompson:I have added to the question details of the solution to this question. – Mr Morgan Jun 23 '14 at 14:53