網頁

2020/11/4

Spring Data JPA @OneToMany LazyInitializationException could not initialize proxy - no Session

Spring Data JPA撈取一對多(One To Many)物件時,發生錯誤org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.abc.demo.entity.Department.employeeList, could not initialize proxy - no Session

entity關係為一個Department對多個Employee設定如下。

Department

package com.abc.demo.entity;

import lombok.*;

import javax.persistence.*;
import java.io.Serializable;
import java.util.List;
import java.util.Objects;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Department implements Serializable {
    private static final Long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy="department")
    private List<Employee> employeeList;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Department that = (Department) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "Department{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }

}

Employee

package com.abc.demo.entity;

import lombok.*;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Objects;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
public class Employee implements Serializable {
    private static final Long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name="DEPARTMENT_ID") // 外鍵欄位名稱
    private Department department;

    private String name;

    private Integer age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Employee employee = (Employee) o;
        return Objects.equals(id, employee.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }

    @Override
    public String toString() {
        return "Employee{" +
                "id=" + id +
                ", department=" + department +
                ", name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

}

使用下面findById_employeeListHasValue()測試時撈取DepartmentemployeeList並調用其方法時出現錯誤。

DepartmentRepositoryTests

package com.abc.demo.repository;

import com.abc.demo.entity.Department;
import com.abc.demo.entity.Employee;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.Collections;
import java.util.List;

@SpringBootTest
class DepartmentRepositoryTests {

    @Autowired
    private DepartmentRepository departmentRepository;

    @Test
    void findById_hasValue() {
        Department department = departmentRepository.findById(1L)
                .orElse(null);

        Assertions.assertNotNull(department);
    }

    @Test
    void findById_employeeListHasValue() {
        List<Employee> employeeList = departmentRepository.findById(1L)
                .map(Department::getEmployeeList)
                .orElse(Collections.emptyList());

        Assertions.assertEquals(2, employeeList.size()); /* throw LazyInitializationException: 
                                                                     failed to lazily initialize a collection of role: com.abc.demo.entity.Department.employeeList,
                                                                     could not initialize proxy - no Session */
    }
}

出現LazyInitializationException是因為在Hibernate的persistence context的session外試圖取得懶加載(lazy-load)的物件employeeList

findById_employeeListHasValue()中取得employeeList@OneToManyfetch屬性預設為javax.persistence.FetchType.LAZY,所以到此Hibernate都尚未去資料庫取得資料,直到調用了employeeList的方法才去資料庫取資料。而findById_employeeListHasValue()方法無交易管理,所以取得employeeList後即離開了persistence context,導致在呼叫employeeList.size()發生此錯誤。

解決方法一為在findById_employeeListHasValue()前掛上@Transactional,將整個方法設為交易,在方法結束前不會離開persistence context的session。

@Transactional // <--
@Test
void findById_employeeListHasValue() {
    List<Employee> employeeList = departmentRepository.findById(1L)
            .map(Department::getEmployeeList)
            .orElse(Collections.emptyList());

    Assertions.assertEquals(2, employeeList.size());
}

解決方法二為把Department.employeeList前的@OneToManyfetch設為FetchType.EAGER,也就是立即載入關聯物件。但這樣就會沒有懶加載的效果並導致效能低落,由其是多邊物件數量很多的時候。

Department

public class Department implements Serializable {
    ...
    
    @OneToMany(mappedBy="department", fetch = FetchType.EAGER)
    private List<Employee> employeeList;
    
    ...
}

解決方法三為在配置檔設定spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true。作用為每次在取得lazy-load物件時,會自動開啟一個新的session來取得。

application.properites

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

解決方法四為在Repository使用JPQLJOIN FETCH查詢。

DepartmentRepository

package com.abc.demo.repository;

import com.abc.demo.entity.Department;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;

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

    @Query("SELECT d FROM Department d " +
            " JOIN FETCH d.employeeList " +
            " WHERE d.id = :id")
    @Override
    Optional<Department> findById(@Param("id") Long id);
}


沒有留言:

張貼留言