Search code examples
springspring-boot

Issues with Lazy Loading and Transactions in JPA


I'm working on a Spring Boot application using JPA and Hibernate for ORM. I have a problem with lazy loading and transactions that I can't seem to resolve.

Here is my setup:

Spring Boot 2.5.2 Hibernate 5.4.32 MySQL 8.0.25

@Entity
public class Author {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;

    @OneToMany(mappedBy = "author", fetch = FetchType.LAZY)
    private List<Book> books;

    // Getters and setters
}

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String title;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "author_id")
    private Author author;

    // Getters and setters
}
@Service
public class AuthorService {
    
    @Autowired
    private AuthorRepository authorRepository;

    @Transactional
    public Author getAuthorWithBooks(Long authorId) {
        Author author = authorRepository.findById(authorId).orElseThrow(() -> new EntityNotFoundException("Author not found"));
        // Trying to access lazy-loaded books
        author.getBooks().size();
        return author;
    }
}
public interface AuthorRepository extends JpaRepository<Author, Long> {
}

When I call the getAuthorWithBooks method, it throws a LazyInitializationException if the author.getBooks().size() line is executed outside of the transactional method. However, I was under the impression that the @Transactional annotation would handle this.

How can I ensure that the books collection is properly loaded within the transactional context? Is there a better way to handle this kind of lazy loading in JPA?


Solution

  • If you want books to be loaded EAGER loading does that for you. But LAZY + JOIN FETCH does a quite similar thing.

    Have you considered using JOIN FETCH? Something like this in case of entityManager:

    public Author findAuthorByIdJoinFetch(Long authorId) {
    
        // create query
        TypedQuery<Author> query = entityManager.createQuery(
                                                "select a from Author a "
                                                    + "LEFT JOIN FETCH a.books "
                                                    + "where a.id = :data", Author.class);
        query.setParameter("data", authorId);
    
        // execute query
        Author author = query.getSingleResult();
    
        return author;
    }
    

    Or Custom Query

    public interface AuthorRepository extends JpaRepository<Author, Long> {
    
        @Query("select a from Author a " +
               "LEFT JOIN FETCH a.books " +
               "where a.id = :authorId")
        Author findAuthorByIdJoinFetch(@Param("authorId") Long authorId);
    }
    

    JOIN FETCH or LEFT JOIN FETCH in the query above.

    JOIN FETCH (inner join): This will only return the Author if they have at least one associated Book. If the Author has no books, they will not be included in the result.

    LEFT JOIN FETCH (left outer join): This will return the Author regardless of whether they have any associated Books. If the Author has no books, the books collection will be empty.

    And then you can call it wherever you want

    @Service
    public class AuthorService {
    
        private AuthorRepository authorRepository;
    
        @Autowired
        public AuthorService(AuthorRepository authorRepository) {
            this.authorRepository = authorRepository;
        }
    
        public Author getAuthorByIdWithBooks(Long authorId) {
            return authorRepository.findAuthorByIdJoinFetch(authorId);
        }
    }