2

I'm writing a simple Spring Data JPA application. I use MySQL database. There are two simple tables:

  1. Department
  2. Employee

Each employee works in some department (Employee.department_id).

I have two entity classes:

@Entity
public class Department {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @Basic(fetch = FetchType.LAZY)
    @OneToMany(mappedBy = "department")
    List<Employee> employees;
}

@Entity
public class Person {
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Id
    private Long id;

    @ManyToOne
    @JoinColumn
    private Department department;
}

The repositories:

@Repository
public interface DepartmentRepository extends JpaRepository<Department, Long> {}

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {}

There is a DepartmentService class:

@Service
@Transactional
public class DepartmentService {
    @Autowired
    DepartmentRepository departmentRepository;

    @Transactional
    List<Department> getAll() {
        departmentRepository.findAll();
    }

    @Transactional
    void getEmployees() {
        for(Department department: getAll()) {
            List<Employee> employees = department.getEmployees();
            for(Employee employee: employees)
                System.out.println(employee);
        }
    }
}

And the main class (I don't need web, so I use CommandLineRunner to run the application as a console application):

@SpringBootApplication
public class EmployeeDB implements CommandLineRunner {
    @Autowired
    DepartmentService departmentService;

    public static void main(String[] args) {
        SpringApplication.run(EmployeeDB.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        departmentService.getEmployees();
    }
}

I added @Transactional before the class DepartmentService and also before all its methods, but I still keep getting the following error:

 org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: employeeDB.Department.employees, could not initialize proxy - no Session

Stacktrace:

Caused by: org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: employeeDB.Department.employees, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:602) ~[hibernate-core-5.4.2.Final.jar:5.4.2.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:217) ~[hibernate-core-5.4.2.Final.jar:5.4.2.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:581) ~[hibernate-core-5.4.2.Final.jar:5.4.2.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:148) ~[hibernate-core-5.4.2.Final.jar:5.4.2.Final]
    at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:303) ~[hibernate-core-5.4.2.Final.jar:5.4.2.Final]
    at employeeDB.DepartmentService.getEmployees(DepartmentService.java:25) ~[main/:na]
    at employeeDB.EmployeeDB.run(EmployeeDB.java:41) [main/:na]
    at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:795) [spring-boot-2.3.3.RELEASE.jar:2.3.3.RELEASE]
    ... 5 common frames omitted

Why do I get this error? How can I solve it?

Andronicus
  • 25,419
  • 17
  • 47
  • 88
Daniel
  • 295
  • 2
  • 9
  • in your example there is no attempts to access contents of `department.getEmployees()` collection. could you add full stacktrace? – Vladimir Shefer Aug 31 '20 at 17:11
  • @ Vladimir Shefer I just print the list of employees to console. I've added more code and the stacktrace. – Daniel Aug 31 '20 at 17:22
  • Given the "default" visibility of the `getEmployees()` method I guess the `DepartmentService` lives in the same package as the `@SpringBootApplication` `EmployeeDB`. Could you please try to move the `DepartmentService` into a `service` package positioned directly under the package of the `@SpringBootApplication` `EmployeeDB`? – Michal Aug 31 '20 at 18:25
  • @ Michal Thank you - I added `public` before `getEmployees()` method and this solved the problem. But why? (If I move `DepartmentService` to `service` then the application doesn't compile because the method `getEmployee()` becomes not visible.) – Daniel Aug 31 '20 at 18:36

2 Answers2

3

Summarizing all the answers and comments above:

This happens because method DepartmentService::getEmployees is not public.

I added public before getEmployees() and this sloved the problem.

@Transactional annotation is powered by AOP, then only public methods could be handled by proxy.

This means that @Transactional is not actually applied to this method and the session is already closed an the moment of reading lazy collection.

Vladimir Shefer
  • 661
  • 3
  • 19
  • There's an issue filed for that to be fixed [here](https://github.com/spring-projects/spring-framework/issues/25582). I.e. it's not that "only public methods" can be intercepted, Spring currently actively ignores non-public ones, likely stemming from the times when JDK proxies where the primary proxying mechanism an with those indeed only public methods can be intercepted. – Oliver Drotbohm Sep 01 '20 at 13:30
2

@Transactional is not supposed to be used to fetch lazily loaded entities, this will lead to n+1 select problem.

You need to use proper mapping or query instead, for example in jpql:

select d
from Department d
left join fetch d.employees
Andronicus
  • 25,419
  • 17
  • 47
  • 88