Hibernate could not initialize proxy – no Session

Đây có lẽ là một lỗi kinh điển đối với những ai mới bắt đầu làm quen với JPA – Hibernate, kể cả các developer có thời gian làm việc nhiều với nó vẫn có thể mắc phải.

Mình còn nhớ dự án trước của mình cả đám dev 1, 2 năm kinh nghiệm đều bị mắc chỗ này do truy vấn khi transaction đã kết thúc. 

Nguyên nhân lỗi

Trong khi làm việv với JPA và Hibernate là một JPA provider đôi lúc chúng ta sẽ gặp lỗi thế này org.hibernate.LazyInitializationException : could not initialize proxy – no Session, đây là một lỗi khá phổ biến nhưng nếu các bạn lơ là thì có thể dẫn đến sửa được nhưng mà sai cách đấy nhé.

Trước khi tìm cách giải quyết chúng ta cần làm rõ một số khái niệm sau:

  • Session là một persistence context đại diện cho một mối liên lạc giữa ứng dụng và database.
  • Lazy Loading nghĩa là một object sẽ không được tải lên persistence context ngay lập tức mà nó chỉ được tải khi chúng ta cần sử dụng.
  • Hibernate tạo ra một Proxy Object subclass  chỉ lấy dữ liệu từ dataabse trong lần đầu tiên object được sử dụng.

Từ những khái niệm trên, thì lỗi này có thể hiểu rằng chúng ta đang cố gắng tải một object lazy-loading từ database trong khi Hibernate Session đã bị đóng.

Ví dụ LazyInitializationException

Các bạn có thể tạo ra Session Factory hay EntityManager Factory để dùng chúng tạo ra các session mỗi khi làm việc với database. Nếu chưa biết thì mình đã có hướng dẫn tạo EntityManager Factory trong bài viết này các bạn có thể tham khảo thêm.

Entities class

Để đi đến ví dụ, trước tiên chúng ta cần ánh xạ các entity bằng Java class.

@Entity
@Table(name = "user")
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
 
    @Column(name = "first_name")
    private String firstName;
    
    @Column(name = "last_name")
    private String lastName;
    
    @OneToMany
    private Set<Role> roles;
    
}

Role entity có quan hệ many-to-one với User.

@Entity
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
 
    @Column(name = "role_name")
    private String roleName;
}

Tạo dữ liệu đầu vào.

Tạo một User gồm 2 role Admin và DBA.

Role admin = new Role("Admin");
Role dba = new Role("DBA");

User user = new User("Bob", "Smith");
user.addRole(admin);
user.addRole(dba);
Session session = sessionFactory.openSession();
session.beginTransaction();
user.getRoles().forEach(role -> session.save(role));
session.save(user);
session.getTransaction().commit();
session.close();

Trường hợp đúng

@Test
public void whenAccessUserRolesInsideSession_thenSuccess() {
 
    User detachedUser = createUserWithRoles();
 
    Session session = sessionFactory.openSession();
    session.beginTransaction();
		
    User persistentUser = session.find(User.class, detachedUser.getId());
		
    Assert.assertEquals(2, persistentUser.getRoles().size());
		
    session.getTransaction().commit();
    session.close();
}

Ở đây, chúng ta truy cập các object bên trong Session vì thế sẽ không có lỗi xảy ra ở đây.

Trường hợp sai

Trong ví dụ này mình sẽ truy cập các Role của User khi Session đã bị đóng.

@Test
public void whenAccessUserRolesOutsideSession_thenThrownException() {
		
    User detachedUser = createUserWithRoles();
 
    Session session = sessionFactory.openSession();
    session.beginTransaction();
		
    User persistentUser = session.find(User.class, detachedUser.getId());
		
    session.getTransaction().commit();
    session.close();
 
    thrown.expect(LazyInitializationException.class);
    System.out.println(persistentUser.getRoles().size());
}

Ở đây, sau khi Session bị đóng, mình đã cố truy cập Role thông qua user.getRoles() nên kết quả chúng ta sẽ nhận được LazyInitializationException.

Cách tránh LazyInitializationException

Trong phần này chúng ta sẽ cùng điểm qua một số cách để tránh gặp LazyInitializationException.

enable_lazy_load_no_trans property

Thuộc tính này cho phép các lazy-loading object sẽ luôn được tải kể cả khi Session bị đóng lại thì sẽ có một Session khác mở ra để tải chúng. Kết quả là chúng ta sẽ say good bye với LazyInitializationException.

<property name="hibernate.enable_lazy_load_no_trans" value="true"/>

Nghe có vẽ hấp dẫn thế nhưng trong thực tế thì không sử dụng tính năng này vì chúng ta sẽ không thể nào quản lý được phạm vi của một Session. Dẫn đến việc ảnh hưởng đến hiệu năng của chương trình một cách nghiêm trọng, tệ hơn là chúng ta có thể gặp n+1 problem và không thể kiểm soát chúng.

Sử dụng FetchType.EAGER Strategy

Để sử dụng FetchType.Eager chúng ta cần ánh xạ User entity như sau:

@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "user_id")
private Set<Role> roles;

FetchType.Eager sẽ khiến các Role sẽ được tải lên cùng lúc với User khi truy vấn. Vì thế các danh sách Role sẽ luôn sẵn sàng để sử dụng kể cả khi Session kết thúc vì chúng đã được tải lên trước đó.

Một nhược điểm rất lớn của cách này đó là dữ liệu sẽ luôn được tải lên kể cả khi không cần sử dụng đến. Nó cũng sẽ gây ra hệ quả đối với hiệu năng của chương trình.

Sử dụng Join Fetching

Đây có lẽ là cách làm tốt nhất cho phép linh hoạt tải dữ liệu lazy hoặc eager tuỳ ý. 

SELECT u FROM User u JOIN FETCH u.roles

Đoạn code trên sẽ khiến các Role được tải cùng lúc với User. Nhưng hãy lưu ý rằng bạn có nhu cầu sử dụng các Role này nhé. 

Ngoài ra chúng ta cũng có thể thao tác tương tự thông qua Criteria.

Criteria criteria = session.createCriteria(User.class);
criteria.setFetchMode("roles", FetchMode.EAGER);

Tóm lược

Qua bài viết trên chúng ta đã có thể hiểu và giải quyết được LazyInitializationException khi làm việc với Hibernate, JPA. Ngoài ra hãy nhớ rằng Fetch Join là cách tối ưu nhất để giảm thiểu lỗi này. 

Nguồn tham khảo 

https://www.baeldung.com/hibernate-initialize-proxy-exception

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x