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