[FIXED] How to avoid LazyInitializationException with nested collections in JPA-Hibernate?

Issue

Mandatory background info:

As part of my studies to learn Spring, I built my usual app – a little tool that saves questions and later creates randomized quizzes using them.

Each subject can have any number of topics, which in turn may have any number of questions, which once again in turn may have any number of answers.

Now, the problem proper:

I keep getting LazyInitializationExceptions.

What I tried last:

I changed almost each and every collection type used to Sets.
Also felt tempted to set the enable_lazy_load_no_trans property to true, but I’ve consistently read this is an antipattern to avoid.

The entities proper: (only fields shown to avoid wall of code-induced fatigue)

Subject:

@Entity
@Table(name = Resources.TABLE_SUBJECTS)
public class Subject implements DomainObject
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = Resources.ID_SUBJECT)
    private int subjectId;

    @Column(name="subject_name", nullable = false)
    private String name;

    @OneToMany(
            mappedBy = Resources.ENTITY_SUBJECT,
            fetch = FetchType.EAGER
    )
    private Set<Topic> topics;
}

Topic:

@Entity
@Table(name = Resources.TABLE_TOPICS)
public class Topic implements DomainObject
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "topic_id")
    private int topicId;

    @Column(name = "name")
    private String name;

    @OneToMany(
            mappedBy = Resources.ENTITY_TOPIC,
            orphanRemoval = true,
            cascade = CascadeType.MERGE,
            fetch = FetchType.EAGER
    )
    private Set<Question> questions;

    @ManyToOne(
            fetch = FetchType.LAZY
    )
    private Subject subject;
}

Question:

@Entity
@Table(name = Resources.TABLE_QUESTIONS)
public class Question implements DomainObject
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = Resources.ID_QUESTION)
    private int questionId;

    @Column(name = "statement")
    private String statement;

    @OneToMany(
            mappedBy = Resources.ENTITY_QUESTION,
            orphanRemoval = true,
            cascade = CascadeType.MERGE
    )
    private Set<Answer> answers;

    @ManyToOne(
            fetch = FetchType.LAZY
    )
    private Topic topic;
}

Answer:

@Entity
@Table(name = Resources.TABLE_ANSWERS)
public class Answer implements DomainObject
{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = Resources.ID_ANSWER)
    private int answerId;

    @Column(name = "answer_text", nullable = false)
    private String text;

    @Column(name = "is_correct", nullable = false)
    private Boolean isCorrect;

    @ManyToOne(
            fetch = FetchType.LAZY
    )
    private Question question;
}

I’m using interfaces extending JpaRepository to perform CRUD operations. I tried this to fetch stuff, without luck:

public interface SubjectRepository extends JpaRepository<Subject, Integer>
{
    @Query
    Optional<Subject> findByName(String name);

    @Query(value = "SELECT DISTINCT s FROM Subject s " +
            "LEFT JOIN FETCH s.topics AS t " +
            "JOIN FETCH t.questions AS q " +
            "JOIN FETCH q.answers as a")
    List<Subject> getSubjects();
}

Now, the big chunk of text Spring Boot deigns to throw at me – the stack trace:

Caused by: org.hibernate.LazyInitializationException: could not initialize proxy [org.callisto.quizmaker.domain.Subject#1] - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final]
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final]
    at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final]
    at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95) ~[hibernate-core-5.6.4.Final.jar:5.6.4.Final]
    at org.callisto.quizmaker.domain.Subject$HibernateProxy$B8rwBfBD.getTopics(Unknown Source) ~[main/:na]
    at org.callisto.quizmaker.service.QuizMakerService.activeSubjectHasTopics(QuizMakerService.java:122) ~[main/:na]
    at org.callisto.quizmaker.QuizMaker.checkIfActiveSubjectHasTopics(QuizMaker.java:307) ~[main/:na]
    at org.callisto.quizmaker.QuizMaker.createNewQuestion(QuizMaker.java:117) ~[main/:na]
    at org.callisto.quizmaker.QuizMaker.prepareMainMenu(QuizMaker.java:88) ~[main/:na]
    at org.callisto.quizmaker.QuizMaker.run(QuizMaker.java:65) ~[main/:na]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:769) ~[spring-boot-2.6.3.jar:2.6.3]

This exception happens when I call this line of code:

boolean output = service.activeSubjectHasTopics();

Which, in turn, calls this method on a service class:

public boolean activeSubjectHasTopics()
{
    if (activeSubject == null)
    {
        throw new NullPointerException(Resources.EXCEPTION_SUBJECT_NULL);
    }

    return !activeSubject.getTopics().isEmpty();
}

The activeSubjectHasTopics method gets called in this context:

private void createNewQuestion(View view, QuizMakerService service)
{
    int subjectId = chooseOrAddSubject(view, service);

    service.setActiveSubject(subjectId);

    if (checkIfActiveSubjectHasTopics(view, service))
    {
        chooseOrAddTopic(view, service, subjectId);
    }

    do
    {
        createQuestion(view, service);

        createAnswers(view, service);
    }
    while(view.askToCreateAnotherQuestion());

    service.saveDataToFile();

    prepareMainMenu(view, service);
}


private boolean checkIfActiveSubjectHasTopics(View view, QuizMakerService service)
{
    boolean output = service.activeSubjectHasTopics();

    if (!output)
    {
        view.printNoTopicsWarning(service.getActiveSubjectName());

        String topicName = readTopicName(view);

        createNewTopic(service, topicName);
    }

    return output;
}

Solution

I was able to track down the cause of the issue thanks to a comment from Christian Beikov – to quote:

Where do you get this activeSubject object from? If you don’t load it
as part of the transaction within activeSubjectHasTopics, then this
won’t work as the object is already detached at this point, since it
was loaded through a different transaction.

The activeSubject object was defined as part of the service class containing the activeSubjectHasTopics method, and was initialized by a different transaction as he pointed out.

I was able to fix the problem by annotating that service class as @Transactional and storing the IDs of the objects I need instead of the objects themselves.

Answered By – Emiliano De Santis

Answer Checked By – Katrina (Easybugfix Volunteer)

Leave a Reply

(*) Required, Your email will not be published