Home > Net >  How to avoid LazyInitializationException with nested collections in JPA-Hibernate?
How to avoid LazyInitializationException with nested collections in JPA-Hibernate?

Time:09-09

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;
}

CodePudding user response:

Not helpful if you change your structure to set. If you need to get the entities you need to explicitly include FETCH clause in your hql queries. You'll work your way by checking out the Hibernate documentation: https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/performance.html#performance-fetching

CodePudding user response:

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.

  • Related