Question

(I know similar problems are all over SO but I cannot find a proper solution.)

I have a Spring scheduled task which reads and writes from the database via Spring Repositories/Hibernate, including a many-to-many relationship between two entities, requiring proper session management for the lazily initialized collection.

However, Spring appears not to properly manage the transaction despite the annotation.

What am I doing wrong?

(I should mention throwing a @Transactional on the same method that as a @Scheduled does work but causes the entire scheduled task to be a transaction, whereas I want persistBannerCourse to be transactional.)

Stack trace first, followed by relevant code:

org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: edu.ucdavis.dss.dw.entities.Instructor.courses, could not initialize proxy - no Session
at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:572) ~[AbstractPersistentCollection.class:4.3.1.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:212) ~[AbstractPersistentCollection.class:4.3.1.Final]
at org.hibernate.collection.internal.AbstractPersistentCollection.readElementExistence(AbstractPersistentCollection.java:319) ~[AbstractPersistentCollection.class:4.3.1.Final]
at org.hibernate.collection.internal.PersistentBag.contains(PersistentBag.java:288) ~[PersistentBag.class:4.3.1.Final]
at edu.ucdavis.dss.dw.entities.Instructor.addCourse(Instructor.java:130) ~[Instructor.class:?]
at edu.ucdavis.dss.dw.entities.Course.addInstructor(Course.java:111) ~[Course.class:?]
at edu.ucdavis.dss.dw.entities.Course.addInstructor(Course.java:100) ~[Course.class:?]
at edu.ucdavis.dss.dw.tasks.BannerTasks.persistBannerCourse(BannerTasks.java:184) ~[BannerTasks.class:?]
at edu.ucdavis.dss.dw.tasks.BannerTasks.bannerImport(BannerTasks.java:80) ~[BannerTasks.class:?]
at edu.ucdavis.dss.dw.tasks.BannerTasks$$FastClassBySpringCGLIB$$d1348e2.invoke(<generated>) ~[ReflectUtils.class:?]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[MethodProxy.class:4.0.4.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:640) ~[CglibAopProxy$DynamicAdvisedInterceptor.class:4.0.4.RELEASE]
at edu.ucdavis.dss.dw.tasks.BannerTasks$$EnhancerBySpringCGLIB$$46afeb46.bannerImport(<generated>) ~[ReflectUtils.class:?]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:1.8.0]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:1.8.0]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:1.8.0]
at java.lang.reflect.Method.invoke(Method.java:483) ~[?:1.8.0]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) ~[ScheduledMethodRunnable.class:4.0.4.RELEASE]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) [DelegatingErrorHandlingRunnable.class:4.0.4.RELEASE]
at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) [?:1.8.0]
at java.util.concurrent.FutureTask.runAndReset(FutureTask.java:308) [?:1.8.0]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$301(ScheduledThreadPoolExecutor.java:180) [?:1.8.0]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:294) [?:1.8.0]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [?:1.8.0]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [?:1.8.0]
at java.lang.Thread.run(Thread.java:744) [?:1.8.0]

Course.java: package edu.ucdavis.dss.dw.entities;

import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.JoinTable;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Table(name = "Courses", uniqueConstraints = {
        @UniqueConstraint(name = "Courses_CRNs", columnNames = { "Crn" })
},
indexes = {
        @Index(name = "Courses_Titles", columnList = "Title")
})
public class Course implements Serializable
{
private long id;
private String crn;
private String title;
private List<Instructor> instructors = new ArrayList<Instructor>(0);
private Term term;
private Department department; /* may be null in rare cases */

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "CourseId", unique = true, nullable = false)
public long getId()
{
    return this.id;
}

public void setId(long id)
{
    this.id = id;
}

@Basic(optional = false)
@Column(name = "Crn", nullable = false, length = 5)
public String getCrn()
{
    return this.crn;
}

public void setCrn(String crn)
{
    this.crn = crn;
}

@Basic(optional = false)
@Column(name = "Title", nullable = false, length = 30)
public String getTitle()
{
    return this.title;
}

public void setTitle(String title)
{
    this.title = title;
}

@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
//@LazyCollection(LazyCollectionOption.FALSE)
@JoinTable(name = "Courses_Instructors", joinColumns = { 
        @JoinColumn(name = "CourseId", nullable = false, updatable = false) }, 
        inverseJoinColumns = { @JoinColumn(name = "InstructorId", 
                nullable = false, updatable = false) })
public List<Instructor> getInstructors()
{
    return this.instructors;
}

public void setInstructors(List<Instructor> instructors)
{
    this.instructors = instructors;
}

public void addInstructor(@NotNull @Valid Instructor instructor) {
    addInstructor(instructor, true);
}

public void addInstructor(@NotNull @Valid Instructor instructor, boolean add) {
    if (instructor != null) {
        if(getInstructors().contains(instructor)) {
            getInstructors().set(getInstructors().indexOf(instructor), instructor);
        } else {
            getInstructors().add(instructor);
        }
        if(add) {
            instructor.addCourse(this, false);
        }
    }
}

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TermId", nullable = false)
@NotNull
public Term getTerm() {
    return this.term;
}

public void setTerm(Term term) {
    this.term = term;
}

@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "DepartmentId", nullable = true)
public Department getDepartment()
{
    return this.department;
}

public void setDepartment(Department department)
{
    this.department = department;
}

@Override
public String toString() {
    return String.format(
            "Course[id=%d, title='%s', crn='%s', term_code='%s']",
            id, title, crn, term.getCode());
}
}

Instructor.java:

package edu.ucdavis.dss.dw.entities;

import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.ManyToMany;
import javax.persistence.Table;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.hibernate.annotations.LazyCollection;
import org.hibernate.annotations.LazyCollectionOption;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity
@Table(name = "Instructors", indexes = {
        @Index(name = "Instructors_Names", columnList = "LastName, FirstName, MiddleInitial")
})
public class Instructor implements Serializable
{
private long id;
private String firstName, middleInitial, emailAddress;

@NotNull
private String lastName;

@NotNull
private String employeeId;

private List<Course> courses = new ArrayList<Course>(0);

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "InstructorId", unique = true, nullable = false)
public long getId()
{
    return this.id;
}

public void setId(long id)
{
    this.id = id;
}

@Basic
@Column(name = "employeeId")
public String getEmployeeId()
{
    return this.employeeId;
}

public void setEmployeeId(String employeeId)
{
    this.employeeId = employeeId;
}

@Basic
@Column(name = "FirstName")
public String getFirstName()
{
    return this.firstName;
}

public void setFirstName(String firstName)
{
    this.firstName = firstName;
}

@Basic
@Column(name = "LastName")
public String getLastName()
{
    return this.lastName;
}

public void setLastName(String lastName)
{
    this.lastName = lastName;
}

@Basic
@Column(name = "MiddleInitial")
public String getMiddleInitial()
{
    return this.middleInitial;
}

public void setMiddleInitial(String middleInitial)
{
    this.middleInitial = middleInitial;
}

@Basic
public String getEmailAddress()
{
    return this.emailAddress;
}

public void setEmailAddress(String emailAddress)
{
    this.emailAddress = emailAddress;
}

@ManyToMany(fetch = FetchType.LAZY, mappedBy = "instructors")
//@LazyCollection(LazyCollectionOption.FALSE)
public List<Course> getCourses() {
    return this.courses;
}

public void setCourses(List<Course> courses) {
    this.courses = courses;
}

public void addCourse(@NotNull @Valid Course course) {
    addCourse(course, true);
}

public void addCourse(@NotNull @Valid Course course, boolean add) {
    if (course != null) {
        if(getCourses().contains(course)) {
            getCourses().set(getCourses().indexOf(course), course);
        }
        else {
            getCourses().add(course);
        }
        if (add) {
            course.addInstructor(this, false);
        }
    }
}
}

BannerTasks.java:

package edu.ucdavis.dss.dw.tasks;

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.inject.Inject;
import javax.validation.ConstraintViolationException;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import edu.ucdavis.dss.dw.entities.BannerCourse;
import edu.ucdavis.dss.dw.entities.BannerInstructor;
import edu.ucdavis.dss.dw.entities.Course;
import edu.ucdavis.dss.dw.entities.Department;
import edu.ucdavis.dss.dw.entities.Instructor;
import edu.ucdavis.dss.dw.entities.Term;
import edu.ucdavis.dss.dw.site.CourseManager;

@Service
public class BannerTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
private static final Logger log = LogManager.getLogger();
private static int runCount = 0;

//@Inject BannerRepository bannerRepository;
@Inject CourseManager courseManager;

@Scheduled(fixedRate = 10000)
public void bannerImport() {
    runCount++;
    if(runCount == 1) {
        long startTime = new Date().getTime();
        long finishTime;

        log.info("Running Banner import at " + dateFormat.format(new Date()));


            log.info("Beginning Banner course pull ...");
            log.info("Done querying Banner, parsing rows ...");

            BannerCourse course = new BannerCourse();
            course.setTitle("Group Study");
            course.setCrn("57958");
            course.setTermCode("201301");
            course.setTermDescription("201301");
            course.setDepartmentCode("HPH");
            course.setDepartmentDescription("HPHDESC");

            BannerInstructor instructor = new BannerInstructor();
            instructor.setEmployeeId("989999999");
            instructor.setFirstName(".");
            instructor.setMiddleInitial(null);
            instructor.setLastName("The Staff");
            instructor.setEmailAddress(null);

            course.addInstructor(instructor);

            persistBannerCourse(course);

        finishTime = new Date().getTime();

        log.info("Banner import finished at " + dateFormat.format(new Date()) + ". Took " + (finishTime - startTime) / 1000 + " seconds.");
        //long finishCourses = this.courseManager.countCourses();
        //log.info("There are now " + finishCourses + " courses stored locally, difference: " + (finishCourses - startCourses) + ".");
    }
}

/* Ensures the passed in BannerCourse is persisted in the local schema */
//@Transactional(noRollbackFor=ConstraintViolationException.class)
@Transactional(propagation=Propagation.MANDATORY)
public void persistBannerCourse(BannerCourse bannerCourse) {
    // Handle the term
    Term term = this.courseManager.getTermByCode(bannerCourse.getTermCode());
    if(term == null) {
        log.info("Term is null, creating ...");
        term = new Term();
        term.setCode(bannerCourse.getTermCode());
        term.setName(bannerCourse.getTermDescription());
        this.courseManager.saveTerm(term);
    } else {
        log.info("Term is not null.");
    }

    // Handle course basics
    Course course = this.courseManager.getCourseByCrnAndTerm(bannerCourse.getCrn(), term.getId());
    if(course == null) {
        course = new Course();

        course.setCrn(bannerCourse.getCrn());
        course.setTitle(bannerCourse.getTitle());
        course.setTerm(term);

        // Handle the department
        Department department = this.courseManager.getDepartmentByCode(bannerCourse.getDepartmentCode());
        if(department == null) {
            try {
                log.info("Department is null, creating ...");
                department = new Department();
                department.setCode(bannerCourse.getDepartmentCode());
                department.setName(bannerCourse.getDepartmentDescription());
                this.courseManager.saveDepartment(department);
            } catch(ConstraintViolationException e) {
                log.info("Unable to save department locally due to validation errors:" + e.getConstraintViolations());
                department = null;
            }
        } else {
            log.info("Department is not null.");
        }

        if(department != null) {
            course.setDepartment(department);
        }

        // Handle the instructors
        for(BannerInstructor bannerInstructor : bannerCourse.getInstructors()) {
            Instructor instructor = this.courseManager.getInstructorByEmployeeId(bannerInstructor.getEmployeeId());
            if(instructor == null) {
                log.info("Instructor is null, creating ...");
                instructor = new Instructor();
                instructor.setFirstName(bannerInstructor.getFirstName());
                instructor.setLastName(bannerInstructor.getLastName());
                instructor.setMiddleInitial(bannerInstructor.getMiddleInitial());
                instructor.setEmployeeId(bannerInstructor.getEmployeeId());

                try {
                    this.courseManager.saveInstructor(instructor);
                } catch(ConstraintViolationException e) {
                    log.info("Unable to save instructor locally due to validation errors:" + e.getConstraintViolations());
                    instructor = null;
                }
            } else {
                log.info("Instructor is not null.");
            }

            if(instructor != null) {
                course.addInstructor(instructor);
            }
        }

        this.courseManager.saveCourse(course);
    } else {
        log.info(String.format("Course already exists: '%s'", course.getTitle()));
    }
}
}

CourseManager.java:

package edu.ucdavis.dss.dw.site;

import edu.ucdavis.dss.dw.entities.Course;
import edu.ucdavis.dss.dw.entities.Department;
import edu.ucdavis.dss.dw.entities.Instructor;
import edu.ucdavis.dss.dw.entities.Term;

import java.util.List;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;

import org.springframework.validation.annotation.Validated;

@Validated
public interface CourseManager
{
List<Instructor> getInstructors();

List<Course> getCourses();

List<Department> getDepartments();

void saveInstructor(@NotNull @Valid Instructor instructor);

void saveCourse(Course course);

void saveDepartment(@NotNull @Valid Department department);

void saveTerm(Term term);

Term getTermById(Long id);

Department getDepartmentByCode(String departmentCode);

Term getTermByCode(String termCode);

Instructor getInstructorByEmployeeId(String employeeId);

Course getCourseByCrnAndTerm(String crn, long termId);

long countCourses();
}

DefaultCourseManager.java:

package edu.ucdavis.dss.dw.site;

import edu.ucdavis.dss.dw.entities.Course;
import edu.ucdavis.dss.dw.entities.Department;
import edu.ucdavis.dss.dw.entities.Instructor;
import edu.ucdavis.dss.dw.entities.Term;
import edu.ucdavis.dss.dw.repositories.CourseRepository;
import edu.ucdavis.dss.dw.repositories.DepartmentRepository;
import edu.ucdavis.dss.dw.repositories.InstructorRepository;
import edu.ucdavis.dss.dw.repositories.TermRepository;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.inject.Inject;

import java.util.ArrayList;
import java.util.List;

@Service
public class DefaultCourseManager implements CourseManager
{
@Inject InstructorRepository instructorRepository;
@Inject CourseRepository courseRepository;
@Inject DepartmentRepository departmentRepository;
@Inject TermRepository termRepository;

@Override
@Transactional
public List<Instructor> getInstructors()
{
    return this.toList(this.instructorRepository.findAll());
}

@Override
@Transactional
public List<Course> getCourses()
{
    return this.toList(this.courseRepository.findAll());
}

@Override
@Transactional
public List<Department> getDepartments()
{
    return this.toList(this.departmentRepository.findAll());
}

private <E> List<E> toList(Iterable<E> i)
{
    List<E> list = new ArrayList<>();
    i.forEach(list::add);
    return list;
}

@Override
@Transactional
public void saveInstructor(Instructor instructor)
{
    this.instructorRepository.save(instructor);
}

@Override
@Transactional
public void saveCourse(Course course)
{
    this.courseRepository.save(course);
}

@Override
@Transactional
public void saveDepartment(Department department)
{
    this.departmentRepository.save(department);
}

@Override
@Transactional
public void saveTerm(Term term)
{
    this.termRepository.save(term);
}

@Override
@Transactional
public Term getTermById(Long id)
{
    return this.termRepository.findOne(id);
}

@Override
@Transactional
public Department getDepartmentByCode(String departmentCode)
{
    return this.departmentRepository.getOneByCode(departmentCode);
}

@Override
@Transactional
public Term getTermByCode(String termCode)
{
    return this.termRepository.getOneByCode(termCode);
}

@Override
@Transactional
public Instructor getInstructorByEmployeeId(String employeeId)
{
    return this.instructorRepository.getOneByEmployeeId(employeeId);
}

@Override
@Transactional
public Course getCourseByCrnAndTerm(String crn, long termId)
{
    return this.courseRepository.getOneByCrnAndTermId(crn, termId);
}

@Override
@Transactional
public long countCourses()
{
    return this.courseRepository.count();
}
}
Was it helpful?

Solution

You seem to have stumbled upon a classic problem with the Spring AOP abstraction. While you have annotated your persistBannerCourse method with @Transactional, however you are calling it from within the same class. That means that the proxy that handles the transaction code, is never invoked.

Check out this relevant SO question.

The best solution would be to refactor your code and move persistBannerCourse to another class. Another solution is described here

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top