รู้ทัน Lombok: ทำไมไม่ควรใช้ @Data กับ JPA Entity และวิธีเขียนที่ถูกต้อง
Lombok เป็น Library ที่ช่วยลด Boilerplate code ได้อย่างดีเยี่ยม โดยเฉพาะ Annotation @Data ที่รวมเอา @ToString, @EqualsAndHashCode, @Getter, @Setter และ @RequiredArgsConstructor ไว้ในคำสั่งเดียว
แต่สำหรับการเขียน JPA Entity การใช้ @Data โดยไม่เข้าใจกลไกภายใน อาจนำไปสู่ปัญหาร้ายแรง 2 ประการ คือ Circular Reference (StackOverflowError) และ Performance/Logic Issues
บทความนี้จะอธิบายสาเหตุทางเทคนิคและแนวทางแก้ไขครับ
ปัญหาที่ 1: Infinite Loop จาก toString()
สาเหตุ (Cause)
@Data จะสร้าง method toString() ที่นำค่าของทุก Field ใน Class มาต่อกันเป็น String
ใน JPA เรามักมีความสัมพันธ์แบบ Bidirectional (สองทาง) เช่น Order มี OrderItem และ OrderItem ก็ผูกกลับไปหา Order
@Entity
@Data // <--- จุดเกิดเหตุ
public class Order {
@OneToMany(mappedBy = "order")
private List<OrderItem> items;
}
@Entity
@Data // <--- จุดเกิดเหตุ
public class OrderItem {
@ManyToOne
private Order order;
}
ผลลัพธ์ (Effect)
เมื่อมีการเรียก order.toString():
- Order จะเรียก
items.toString() - OrderItem แต่ละตัวจะเรียก
order.toString() - เกิดการเรียกวนซ้ำไปเรื่อยๆ จน Memory เต็ม และเกิด
java.lang.StackOverflowErrorทำให้ Application พังทันที
ปัญหาที่ 2: Performance และ Logic พังจาก equalsAndHashCode()
สาเหตุ (Cause)
@Data จะสร้าง equals() และ hashCode() โดยนำ ทุก Field ใน Class มาคำนวณ
ผลลัพธ์ (Effect)
1. Performance Issue (N+1 Query โดยไม่รู้ตัว)
หาก Entity มีความสัมพันธ์แบบ FetchType.LAZY การที่ equals() พยายามเข้าถึง Field นั้น จะไป Trigger ให้ Hibernate ยิง Query ไปที่ Database เพื่อดึงข้อมูลจริงออกมาทันที ทั้งที่เราแค่ต้องการเช็คความเท่ากันของ Object
2. Logic Issue (Set/Map ทำงานผิดพลาด)
ตามหลักการของ JPA ข้อมูลใน Entity ถือเป็น Mutable (เปลี่ยนแปลงได้) แต่ hashCode ควรจะคงที่
- หากเรานำ Entity ไปใส่ใน
HashSetหรือHashMap - ต่อมามีการแก้ไขค่าบาง Field (เช่น update ชื่อ) -> ค่า HashCode เปลี่ยน
- Collection นั้นจะหา Object ตัวเดิมไม่เจออีกเลย ทำให้ Logic ของโปรแกรมผิดเพี้ยน
วิธีแก้ไข (The Solution)
วิธีที่ดีที่สุดคือ "เลิกใช้ @Data กับ JPA Entity" และเลือกใช้ Annotation แยกตามความจำเป็นแทน ดังนี้:
1. ใช้ @Getter และ @Setter แยกกัน
เพื่อหลีกเลี่ยงการสร้าง method ที่ไม่จำเป็น
2. Override toString() และ Exclude ความสัมพันธ์
หากต้องการใช้ toString() จริงๆ ให้ใช้ @ToString.Exclude กับ Field ที่เป็นความสัมพันธ์ (Relationship) หรือเขียน toString เองโดยเลือกเฉพาะ Field ทั่วไป
3. จัดการ equals() และ hashCode() ให้ถูกหลัก JPA
สำหรับ JPA Entity การเช็คความเท่ากันควรดูที่ Primary Key (ID) เท่านั้น ไม่ใช่ดูที่ Data ทุก Field
ตัวอย่าง Code ที่ถูกต้อง (Best Practice)
@Entity
@Getter
@Setter
@RequiredArgsConstructor // หรือ @NoArgsConstructor ตามการใช้งาน
public class Order {
@Id
@GeneratedValue
private Long id;
private String orderNumber;
@OneToMany(mappedBy = "order")
@ToString.Exclude // แก้ปัญหาที่ 1: ตัดวงจร Circular Reference
private List<OrderItem> items;
// แก้ปัญหาที่ 2: เช็คความเท่ากันที่ ID เท่านั้น
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
Order order = (Order) o;
return id != null && Objects.equals(id, order.id);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
สรุป
การใช้ Lombok ไม่ใช่เรื่องผิด แต่ต้องใช้ให้ถูกที่
- DTO / POJO: ใช้
@Dataได้เต็มที่ สะดวกและรวดเร็ว - JPA Entity: ห้ามใช้
@Dataให้ใช้@Getter,@Setterและจัดการequals/hashCodeด้วย ID เท่านั้น เพื่อป้องกันปัญหา Memory Leak และ Performance ของ Database ครับ
No responses yet. Be the first to respond.