ไฮเบอร์เนตไม่สามารถเริ่มต้นพร็อกซี - ไม่มีเซสชัน

1. ภาพรวม

การทำงานกับ Hibernate เราอาจจะได้พบข้อผิดพลาดว่า: org.hibernate.LazyInitializationException: ไม่สามารถเริ่มต้นพร็อกซี่ - ไม่เซสชัน

ในบทช่วยสอนฉบับย่อนี้เราจะดูสาเหตุที่แท้จริงของข้อผิดพลาดอย่างละเอียดและเรียนรู้วิธีหลีกเลี่ยง

2 การทำความเข้าใจข้อผิดพลาด

การเข้าถึงอ็อบเจ็กต์ที่โหลดแบบขี้เกียจนอกบริบทของเซสชันไฮเบอร์เนตที่เปิดอยู่จะส่งผลให้เกิดข้อยกเว้นนี้

สิ่งสำคัญคือต้องทำความเข้าใจว่า Session , Lazy Initialisation และProxy Object คืออะไรและมารวมกันได้อย่างไรในกรอบHibernate

  • เซสชันเป็นบริบทการคงอยู่ที่แสดงถึงการสนทนาระหว่างแอปพลิเคชันและฐานข้อมูล
  • Lazy Loadingหมายความว่าวัตถุจะไม่ถูกโหลดไปยังบริบทเซสชันจนกว่าจะเข้าถึงด้วยรหัส
  • Hibernate สร้างคลาสย่อยProxy Objectแบบไดนามิกที่จะเข้าสู่ฐานข้อมูลเมื่อเราใช้ออบเจ็กต์ครั้งแรกเท่านั้น

ข้อผิดพลาดนี้หมายความว่าเราพยายามดึงวัตถุที่โหลดแบบขี้เกียจจากฐานข้อมูลโดยใช้วัตถุพร็อกซี แต่เซสชันไฮเบอร์เนตปิดไปแล้ว

3. ตัวอย่างสำหรับLazyInitializationException

มาดูข้อยกเว้นในสถานการณ์ที่เป็นรูปธรรม

เราต้องการสร้างออบเจ็กต์Userแบบธรรมดาที่มีบทบาทที่เกี่ยวข้อง มาใช้ JUnit เพื่อแสดงข้อผิดพลาดLazyInitializationException

3.1. ไฮเบอร์เนตยูทิลิตี้คลาส

ขั้นแรกให้กำหนดคลาสHibernateUtilเพื่อสร้างSessionFactoryด้วยการกำหนดค่า

เราจะใช้ฐานข้อมูลHSQLDBในหน่วยความจำ

3.2. เอนทิตี

นี่คือเอนทิตีผู้ใช้ของเรา:

@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 roles; } 

และเอนทิตีบทบาทที่เกี่ยวข้อง:

@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; }

ในฐานะที่เราสามารถดูมีความสัมพันธ์แบบหนึ่งต่อหลายระหว่างผู้ใช้และบทบาท

3.3. การสร้างผู้ใช้ที่มีบทบาท

ต่อไปมาสร้างสองออบเจ็กต์บทบาท :

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();

3.4. กำลังเรียกบทบาท

ในสถานการณ์แรกเราจะดูวิธีดึงบทบาทของผู้ใช้ด้วยวิธีที่เหมาะสม:

@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(); }

ที่นี่เราเข้าถึงวัตถุภายในเซสชันดังนั้นจึงไม่มีข้อผิดพลาด

3.5. การเรียกบทบาทล้มเหลว

ในสถานการณ์ที่สองเราจะเรียกเมธอดgetRolesนอกเซสชัน:

@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()); }

ในกรณีที่เราพยายามที่จะเข้าถึงบทบาทหลังจากช่วงที่ถูกปิดและเป็นผลให้รหัสพ่นLazyInitializationException

4. วิธีหลีกเลี่ยงข้อผิดพลาด

ลองมาดูวิธีแก้ปัญหาที่แตกต่างกันสี่วิธีเพื่อเอาชนะข้อผิดพลาด

4.1. เปิดเซสชันในชั้นบน

แนวทางปฏิบัติที่ดีที่สุดคือการเปิดเซสชันในเลเยอร์การคงอยู่ตัวอย่างเช่นการใช้รูปแบบ DAO

เราสามารถเปิดเซสชันในชั้นบนเพื่อเข้าถึงวัตถุที่เกี่ยวข้องได้อย่างปลอดภัย ตัวอย่างเช่นเราสามารถเปิดเซสชันในเลเยอร์View

ด้วยเหตุนี้เราจะเห็นเวลาตอบสนองที่เพิ่มขึ้นซึ่งจะส่งผลต่อประสิทธิภาพของแอปพลิเคชัน

การแก้ปัญหานี้เป็นการต่อต้านรูปแบบในแง่ของหลักการ Separation of Concerns นอกจากนี้ยังอาจทำให้เกิดการละเมิดความสมบูรณ์ของข้อมูลและการทำธุรกรรมที่ใช้เวลานาน

4.2. กำลังเปิดคุณสมบัติenable_lazy_load_no_trans

คุณสมบัติไฮเบอร์เนตนี้ใช้เพื่อประกาศนโยบายส่วนกลางสำหรับการดึงอ็อบเจ็กต์ที่โหลดแบบขี้เกียจ

โดยค่าเริ่มต้นแห่งนี้เป็นเท็จ การเปิดใช้งานหมายความว่าแต่ละการเข้าถึงเอนทิตีที่โหลดแบบเกียจคร้านจะถูกรวมไว้ในเซสชันใหม่ที่ทำงานในธุรกรรมใหม่:

ไม่แนะนำให้ใช้คุณสมบัตินี้เพื่อหลีกเลี่ยงข้อผิดพลาดLazyInitializationExceptionเนื่องจากจะทำให้แอปพลิเคชันของเราทำงานช้าลง นี่เป็นเพราะเราจะจบลงด้วยปัญหา n + 1 พูดง่ายๆก็คือนั่นหมายถึงหนึ่ง SELECT สำหรับผู้ใช้และ N SELECT เพิ่มเติมเพื่อดึงบทบาทของผู้ใช้แต่ละคน

แนวทางนี้ไม่มีประสิทธิภาพและยังถือเป็นการต่อต้านรูปแบบ

4.3. ใช้กลยุทธ์FetchType.EAGER

เราสามารถใช้กลยุทธ์นี้ร่วมกับคำอธิบายประกอบ@OneToMany ได้เช่น:

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

นี่เป็นโซลูชันที่ถูกบุกรุกสำหรับการใช้งานเฉพาะเมื่อเราต้องการดึงคอลเล็กชันที่เกี่ยวข้องสำหรับกรณีการใช้งานส่วนใหญ่ของเรา

ดังนั้นจึงง่ายกว่ามากในการประกาศประเภทการดึงข้อมูลEAGERแทนที่จะดึงคอลเล็กชันอย่างชัดเจนสำหรับกระแสธุรกิจส่วนใหญ่ที่แตกต่างกัน

4.4. ใช้การดึงเข้าร่วม

เราสามารถใช้คำสั่งJOIN FETCHในJPQLเพื่อดึงคอลเลกชันที่เกี่ยวข้องตามต้องการตัวอย่างเช่น:

SELECT u FROM User u JOIN FETCH u.roles

หรือเราสามารถใช้ Hibernate Criteria API:

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

ที่นี่เราระบุคอลเล็กชันที่เกี่ยวข้องซึ่งควรดึงมาจากฐานข้อมูลพร้อมกับวัตถุผู้ใช้ในการเดินทางไปกลับเดียวกัน การใช้แบบสอบถามนี้ช่วยเพิ่มประสิทธิภาพในการทำซ้ำเนื่องจากไม่จำเป็นต้องดึงข้อมูลวัตถุที่เกี่ยวข้องแยกกัน

นี่เป็นโซลูชันที่มีประสิทธิภาพและละเอียดที่สุดเพื่อหลีกเลี่ยงข้อผิดพลาดLazyInitializationException

5. สรุป

ในบทความนี้เราได้เห็นวิธีการที่จะจัดการกับorg.hibernate.LazyInitializationException: พร็อกซี่ไม่สามารถเริ่มต้น - ไม่มีเซสชันข้อผิดพลาด

เราได้สำรวจแนวทางต่างๆพร้อมกับปัญหาด้านประสิทธิภาพ สิ่งสำคัญคือต้องใช้โซลูชันที่เรียบง่ายและมีประสิทธิภาพเพื่อหลีกเลี่ยงผลกระทบต่อประสิทธิภาพ

ในที่สุดเราก็ได้เห็นว่าวิธีการดึงข้อมูลเข้าร่วมเป็นวิธีที่ดีในการหลีกเลี่ยงข้อผิดพลาดอย่างไร

เช่นเคยรหัสสามารถใช้ได้บน GitHub