Mateusz
Mateusz

Reputation: 27

@Transactional not working in Spring Boot with CrudRepository

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

Answers (2)

Andreas
Andreas

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

CodeScale
CodeScale

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

Related Questions