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