สิ่งที่น่ารู้เกี่ยวกับ ID ใน table ที่ database สร้างให้ผ่าน JPA
ช่วงนี้เรากำลังทำ project ที่มีเนื้องานจะต้อง refresh ข้อมูลใหม่ทุกวันใส่ลงไปใน database ในรูปแบบของ data dump เนื่องจากต้องต่อกับระบบ legacy ที่ไม่สามารถดึงข้อมูลตามแต่ละวันบางส่วนออกมาได้ แน่นอนว่าความท้าทายคือต้องทำงานกับข้อมูลจำนวนเยอะมากเป็นล้าน ๆ record จึงต้องปรับปรุงระบบให้สามารถเพิ่ม record ลงไปได้เร็วที่สุด ระบบงานที่ใช้พัฒนาบน Spring Boot framework ที่ใช้ JPA ดังนั้นเรามาเข้าใจกันหน่อยว่า JPA จะจัดการ ID ใน table ที่ database สร้างให้ (auto-generated ID) ได้อย่างไร แล้วมันมีผลต่อ performance ใน application อย่างไร
รูปแบบที่ JPA จะสร้าง ID ให้
ใน JPA เราสามารถเลือกรูปแบบที่ JPA จะสร้าง ID ให้ผ่าน annotation @GeneratedValue
ได้ โดยในขณะที่เขียนบทความนี้ จะมี 4 รูปแบบด้วยกัน
@Id | |
@GeneratedValue(strategy = GenerationType.???) | |
@Column(nullable = false) | |
private Long id; |
- IDENTITY: สร้าง ID โดยอาศัยจาก column ที่เป็น primary key (auto-increment column เช่น serial จะได้ 1, 2, 3, …) ซึ่งก่อนเพิ่ม record ลงไปใน table ผ่าน INSERT ตัว JPA จะขอ ID ใหม่ จาก database แล้ว return กลับมาให้ไปแปะใน record ใหม่
-
SEQUENCE: สร้าง ID โดยอาศัยจากสิ่งที่เรียกว่า sequence แทน ซึ่งสิ่งที่พิเศษเหนือจาก auto-increment column คือ
- ถ้าเรามี transaction หลาย ๆ อันที่เพิ่ม record พร้อมกัน จะไม่ได้ ID เหมือนกันกลับออกไป
- เราสามารถใช้ sequence เดียวกันกับหลาย table ได้ ในกรณีไม่ต้องการให้ primary key ซ้ำกัน แต่ต้องระวังเรื่อง sequence หมดเร็วด้วยนะ (ถ้าเป็นตัวเลข ค่าสูงสุดคือ is
2^63-1
)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersCREATE SEQUENCE hibernate_sequence START 1 INCREMENT 1; -
TABLE: คล้าย ๆ กับ SEQUENCE แต่จะใช้ table เพื่อเก็บ ค่า auto-increment
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersCREATE TABLE hibernate_sequences ( sequence_name varchar(255) not null, next_val number(19,0), primary key (sequence_name) ) - AUTO: ให้ persistence provider อย่างเช่น Hibernate เลือก 1 ใน 3 ข้อที่กล่าวมา หลักการคร่าว ๆ คือ
- ถ้า column นั้นมี type เป็น UUID ก็จะใช้ UUID
- ไม่ใช่ UUID (เช่น ตัวเลข) ก็จะใช้แบบ SEQUENCE
- ในบาง database ที่ไม่รองรับ sequence (เช่น MySQL) ก็จะใช้ TABLE หรือ IDENTITY (ขึ้นอยู่กับ version ของ persistence provider)
จะสังเกตว่ารูปแบบ SEQUENCE เป็นตัวเลือกที่ดีในมุมมองของ performance ถ้า column เป็นในรูปแบบของตัวเลข เพราะ TABLE จะต้องดึงข้อมูลจากอีก table นึง ในขณะที่ IDENTITY จะต้องมี statement ในการส่ง ID ใหม่เพิ่มมา 1 อันต่อการเพิ่ม 1 record ลง table
พูดถึงการทำ batch insert
หากเราพูดถึงการทำ batch insert เพื่อลดจำนวนครั้งที่จะต้องสั่ง database นั้น IDENTITY จะไม่รองรับ เพราะ JPA ต้องไปขอ ID ใหม่ทุกครั้ง และ database จะไม่สามารถการันตีลำดับของ record ที่เพิ่มเข้ามากับ ID ที่ถูกสร้างขึ้นมาใหม่ได้ ตรงกันข้ามกับ SEQUENCE ที่แยกออกจาก table ไปโดยไม่สนใจแล้วว่า record จะถูกเพิ่มเข้าไปใน table ได้ไหม ทำให้ JPA สามารถใช้ persistence provider จอง ID ไว้ได้เลย
โดยเราจะต้อง configure sequence ในส่วนของ increment เพิ่ม ก็จะทำให้ JPA ไปดึง sequence มาแค่ครั้งเดียวต่อจำนวน increment ส่งผลให้ performance จะออกมาดีขึ้น ตัวอย่างเช่น
Id | |
@SequenceGenerator(name = "pet_seq", | |
sequenceName = "pet_sequence", | |
initialValue = 1, allocationSize = 20) | |
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "pet_seq") | |
@Column(name = "id", nullable = false) | |
private Long id; // allocationSize จะต้องเท่ากับ increment ใน sequence นะ ไม่งั้น application จะ crash |
CREATE sequence pet_seq START 1 INCREMENT 50; | |
SELECT nextval ('pet_seq'); -- จะได้ 1 ซึ่งเป็นค่าเริ่มต้น จึงต้องไปหาค่าถัดไปใน sequence | |
SELECT nextval ('pet_seq'); -- จะได้ 51 คำนวนแล้วจะสามารถเพิ่ม record ด้วย ID ตั้งแต่ 1-50 | |
INSERT INTO pet (name, id) VALUES (?, ?); -- id = 1 | |
INSERT INTO pet (name, id) VALUES (?, ?); -- id = 2 | |
-- หลังจากเพิ่มไป 50 อัน แล้วยังมี record เหลืออยู่ ต้องหาค่าถัดไปใน sequence | |
SELECT nextval ('pet_seq'); -- จะได้ 101 คำนวนแล้วจะสามารถเพิ่ม record ด้วย ID ตั้งแต่ 51-100 | |
INSERT INTO pet (name, id) VALUES (?, ?); -- id = 51 |
แต่ถ้าถ้าเราใช้ SEQUENCE ดั้งเดิมโดยไม่ configure อะไร (increment = 1) ผลที่ออกมาก็ไม่ได้ต่างจาก IDENTITY สักเท่าไร เพราะ JPA ก็ต้องไปดึง ID ใหม่จาก sequence ในทุก ๆ record ที่จะเพิ่มเข้าไปอยู่ดี
SELECT nextval ('hibernate_sequence') -- จะได้ 1 ซึ่งเป็นค่าเริ่มต้น จึงต้องไปหาค่าถัดไปใน sequence | |
SELECT nextval ('hibernate_sequence') -- จะได้ 2 คำนวนแล้วจะสามารถเพิ่ม record ด้วย ID ตั้งแต่ 1-1 | |
INSERT INTO pet (name, id) VALUES (?, ?) | |
-- หลังจากเพิ่มไป 1 อัน แล้วยังมี record เหลืออยู่ ต้องหาค่าถัดไปใน sequence | |
SELECT nextval ('hibernate_sequence') -- จะได้ 3 คำนวนแล้วจะสามารถเพิ่ม record ด้วย ID ตั้งแต่ 2-2 | |
INSERT INTO pet (name, id) VALUES (?, ?) |
รูปแบบ SEQUENCE จะมีข้อเสียคือถ้า application ถูก restart ระหว่าง batch insert อยู่ ก็จะทำให้ ID บางส่วนที่ยังเพิ่มไปไม่ถึงหายไปถาวร เพราะ sequence ได้เพิ่มไปแล้ว
อีกข้อเสียนึงคือถ้ามีหลาย application ต่อ database เดียวกัน ทุก application ควรจะดึง ID จาก sequence เดียวกัน ไม่งั้นอาจจะเกิด conflict กันเพราะพยายามจะเพิ่ม record ที่มี primary key เดียวกันอยู่แล้วนั่นเอง