Reputation: 27
I was trying to implement bi-directional relationships bettwen my entities.
Student
@Table(name = "students")
@Entity
public class Student {
@Id
// @GeneratedValue(strategy = GenerationType.AUTO)
private long album;
@NotNull
private String name;
@NotNull
private String surname;
@OneToMany(mappedBy = "student", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
private List<StudentSection> studentSections;
@Transactional(propagation=Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
public void addSection(Section section){
if(this.studentSections == null){
this.studentSections = new ArrayList<>();
}
StudentSection studentSectionToAdd = new StudentSection();
studentSectionToAdd.setStudent(this);
studentSectionToAdd.setSection(section);
this.studentSections.add(studentSectionToAdd); //here
section.addStudentSection(studentSectionToAdd);
}
}
the connecting entity in a ManyToMany relationship
@Table(name = "student_section")
@Entity
public class StudentSection {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private Integer grade;
private Date date;
@NotNull
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
@JoinColumn(name = "student_id")
private Student student;
@NotNull
@ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
@JoinColumn(name = "section_id")
private Section section;
}
and Section
@Table(name = "sections")
@Entity
public class Section {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
private String name;
@NotNull
private Integer sizeOfSection;
@NotNull
private Boolean isActive;
@OneToMany(mappedBy = "section", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH})
private List<StudentSection> studentSections;
void addStudentSection(StudentSection studentSection){
if(this.studentSections == null){
this.studentSections = new ArrayList<>();
}
this.studentSections.add(studentSection);
}
}
I ran into a problem with the Student.addSection() method. When trying to execute it I got an error on the this.studentSections.add(studentSectionToAdd);
line, saying
failed to lazily initialize a collection of role: Student.studentSections, could not initialize proxy - no Session
I read about it and found out that the best way to fix this is to add the @Transactional annotation to the method, however it didnt change anything and I cant get it to work.
I also tried moving the Student.addSection() method to
StudentServiceImpl
@Service
@Primary
public class StudentServiceImpl implements StudentService {
protected StudentRepository studentRepository;
@Autowired
public StudentServiceImpl(StudentRepository studentRepository) {
this.studentRepository = studentRepository;
}
@Override
@Transactional(propagation = Propagation.REQUIRED, readOnly = true, noRollbackFor = Exception.class)
public void addSection(Student student, Section section) {
if (student.getStudentSections() == null) {
student.setStudentSections(new ArrayList<>());
}
StudentSection studentSectionToAdd = new StudentSection();
studentSectionToAdd.setStudent(student);
studentSectionToAdd.setSection(section);
student.getStudentSections().add(studentSectionToAdd);
//section.addStudentSection(studentSectionToAdd);
}
}
but I still got the error.
I am also using CrudRepository to retrive entities from the database.
@Repository
public interface StudentRepository extends CrudRepository<Student, Long> {
Student findByName(String name);
}
This is where I call the method
@Component
public class DatabaseLoader implements CommandLineRunner {
private final StudentRepository studentRepository;
private final SectionRepository sectionRepository;
private final StudentSectionRepository studentSectionRepository;
private final StudentService studentService;
@Autowired
public DatabaseLoader(StudentRepository studentRepository, SectionRepository sectionRepository, StudentSectionRepository studentSectionRepository,
StudentService studentService) {
this.studentRepository = studentRepository;
this.sectionRepository = sectionRepository;
this.studentSectionRepository = studentSectionRepository;
this.studentService = studentService;
}
@Override
public void run(String... strings) throws Exception {
//Testing entities
Student student = new Student();
student.setAlbum(1L);
student.setName("student");
student.setSurname("test");
this.studentRepository.save(student);
Section section = new Section();
section.setName("section");
section.setSizeOfSection(10);
section.setIsActive(true);
this.sectionRepository.save(section);
//end
//Adding Student to a Section test
Student student1 = studentRepository.findByName("student");
//student1.setStudentSections(this.studentSectionRepository.findAllByStudent(student1));
Section section1 = sectionRepository.findByName("section");
//section1.setStudentSections(this.studentSectionRepository.findAllByStudent(student1));
studentService.addSection(student1, section1);
this.studentRepository.save(student1);
//end test
}
}
Also when I retrive StudentSection lists from the database here and set them im both objects before adding a new relationship it works fine, but this is not really the solution I am going for.
Upvotes: 0
Views: 1002
Reputation: 159086
The problem is that every call from the run()
method to studentRepository
and studentService
are separate sessions/transactions.
It's virtually as-if you did this:
...
beginTransaction();
this.studentRepository.save(student);
commit();
...
beginTransaction();
this.sectionRepository.save(section);
commit();
beginTransaction();
Student student1 = studentRepository.findByName("student");
commit();
beginTransaction();
Section section1 = sectionRepository.findByName("section");
commit();
// This does it's own transaction because of @Transactional
studentService.addSection(student1, section1);
beginTransaction();
this.studentRepository.save(student1);
commit();
Since transaction = session here, it means that student1
is detached, and that the lazy-loaded studentSections
collection cannot be loaded on-demand outside the session, and hence the code fails.
Inserting a new student and a new section and associating them should really be one transaction, so if a later step fails, it's all rolled back.
Which basically means that you want the entire run()
method to be one transaction, so in your case, it is the run()
method that should be @Transactional
, not the addSection()
method.
Generally, in a 3-tiered approach, you would put transaction boundaries on service layer:
Presentation tier. This is @Controller
classes, or the run()
method for a simple command-line program.
Logic tier. This is @Service
classes. This is where you put @Transactional
, so each service call is an atomic transaction, i.e. it either succeeds or it fails, as far as the database updates are concerned, no half updates.
Data tier. This is @Repository
and @Entity
classes.
As such, you should keep the instantiation and initialization of the Student
and Section
objects in the run()
method, but the rest of the code, incl. save()
, should be moved to a single method in a @Service
class.
Upvotes: 1
Reputation: 3304
About this
@Transactional(propagation=Propagation.REQUIRED, readOnly=true, noRollbackFor=Exception.class)
public void addSection(Section section){
@Transactional
works only for spring-managed beans and Entities are not managed by spring.
You get this exception because you try load a lazy relations outside a session (because your entity is actually in detached-state).
To re-attach --> entityManager.merge(student);
But the best thing to do is to load the relation at query-time. By using EntityGraph
for example -->
@EntityGraph(attributePaths="studentSections")
Student findByName(String name);
Upvotes: 0