การ iterate ด้วย Cursors และ Ranges
Cursor คืออะไร?
หัวข้อที่มีชื่อว่า “Cursor คืออะไร?”การอ่าน record เดียวด้วยคีย์ผ่าน objectStore.get(key) นั้นรวดเร็ว แต่บางครั้งคุณจำเป็นต้อง เดินผ่าน record จำนวนมาก — เพื่อกรอง รวบรวม หรือแปลงข้อมูลระหว่างทาง นั่นคือสิ่งที่ cursor มีไว้เพื่อ
Cursor ชี้ไปที่ record หนึ่งในแต่ละครั้ง คุณเปิด cursor บน object store (หรือ index) ประมวลผล record ปัจจุบัน จากนั้นเรียก cursor.continue() เพื่อเลื่อนไปยัง record ถัดไป เมื่อไม่มี record เหลืออีก cursor จะ resolve เป็น null ที่เป็นสัญญาณว่าการ iterate เสร็จสิ้นแล้ว
const request = objectStore.openCursor();
request.onsuccess = (event) => { const cursor = event.target.result; if (cursor) { console.log(cursor.key, cursor.value); cursor.continue(); // advance to next record } else { console.log('Done — no more records'); }};การเปิด Cursor
หัวข้อที่มีชื่อว่า “การเปิด Cursor”objectStore.openCursor(query, direction) — ทั้งสองอาร์กิวเมนต์เป็นตัวเลือก
query— เป็นIDBKeyRangeหรือคีย์เฉพาะ ละเว้น (หรือส่งnull) เพื่อ iterate record ทั้งหมดdirection—"next"(ค่าเริ่มต้น เรียงจากน้อยไปมาก),"prev"(เรียงจากมากไปน้อย),"nextunique"หรือ"prevunique"
// All records, ascending order (default)objectStore.openCursor();
// All records, descending orderobjectStore.openCursor(null, 'prev');
// Only records with keys 2–3 (inclusive on both ends)objectStore.openCursor(IDBKeyRange.bound(2, 3));IDBKeyRange — การกรองด้วยคีย์
หัวข้อที่มีชื่อว่า “IDBKeyRange — การกรองด้วยคีย์”IDBKeyRange ช่วยให้คุณระบุชุดย่อยของคีย์โดยไม่ต้องสแกนทุก record method factory ทั้งสี่ได้แก่:
| method | สิ่งที่จับคู่ |
|---|---|
IDBKeyRange.only(value) | คีย์เพียงหนึ่งเดียวที่ตรงกันพอดี |
IDBKeyRange.lowerBound(lower, open?) | คีย์ที่ ≥ lower (ส่ง true เพื่อไม่รวม lower เอง) |
IDBKeyRange.upperBound(upper, open?) | คีย์ที่ ≤ upper (ส่ง true เพื่อไม่รวม upper เอง) |
IDBKeyRange.bound(lower, upper, lowerOpen?, upperOpen?) | คีย์ระหว่าง lower และ upper รวมขอบทั้งสองโดยค่าเริ่มต้น |
IDBKeyRange.only(42) // key === 42IDBKeyRange.lowerBound(10) // key >= 10IDBKeyRange.lowerBound(10, true) // key > 10IDBKeyRange.upperBound(50) // key <= 50IDBKeyRange.bound(10, 50) // 10 <= key <= 50IDBKeyRange.bound(10, 50, true, true) // 10 < key < 50การห่อการ iterate Cursor ด้วย Promise
หัวข้อที่มีชื่อว่า “การห่อการ iterate Cursor ด้วย Promise”callback แบบ onsuccess ดิบๆ นั้นยุ่งยาก การห่อรูปแบบนี้ไว้ใน helper จะทำให้ใช้ร่วมกับ async/await ได้สะดวกขึ้น:
function iterateCursor(store, range = null) { return new Promise((resolve, reject) => { const results = []; const req = store.openCursor(range); req.onsuccess = (e) => { const cursor = e.target.result; if (cursor) { results.push(cursor.value); cursor.continue(); } else { resolve(results); } }; req.onerror = () => reject(req.error); });}ตัวอย่างรัน: iterate record ทั้งหมด แล้ว iterate ตาม Range
หัวข้อที่มีชื่อว่า “ตัวอย่างรัน: iterate record ทั้งหมด แล้ว iterate ตาม Range”การสาธิตด้านล่างนี้:
- เปิด database และเติมข้อมูลคะแนนผู้เล่นสี่ record
- ใช้ cursor เพื่อ iterate record ทั้งหมด และแสดงผลแต่ละรายการ
- ใช้
IDBKeyRange.bound(2, 3)เพื่อ iterate เฉพาะ record ที่มีid2 และ 3 - ลบ database หลังจากนั้นเพื่อไม่ให้เหลือร่องรอย
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
ใช้ cursor + IDBKeyRange แทน getAll() | ประมวลผลทีละ record โดยไม่ต้องโหลดข้อมูลทั้งหมดเข้าหน่วยความจำพร้อมกัน เหมาะกับ dataset ขนาดใหญ่ | โค้ดซับซ้อนกว่า ต้องจัดการ callback และเรียก continue() เองทุกครั้ง |
ใช้ getAll() ตรงๆ | เขียนโค้ดสั้น อ่านง่าย ได้ผลลัพธ์เป็น array ทันที | โหลด record ทั้งหมดเข้าหน่วยความจำในครั้งเดียว ไม่เหมาะกับ store ขนาดใหญ่ |
กรองด้วย IDBKeyRange ก่อน iterate | ลดจำนวน record ที่ต้องสแกน ประหยัดเวลา | ต้องรู้โครงสร้างของคีย์ล่วงหน้าเพื่อสร้าง range ที่ถูกต้อง |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ลืมเรียก
cursor.continue()ภายใน handler ของonsuccess— การ iterate จะหยุดนิ่งเงียบๆ โดยไม่มี error ใดๆ ให้เห็น ต้องเรียกทุกครั้งที่ยังมี record เหลือ - คิดว่า
IDBKeyRange.bound()ไม่รวมขอบทั้งสองด้านโดยค่าเริ่มต้น — ค่าเริ่มต้นคือรวมทั้ง lower และ upper ต้องส่งtrueในอาร์กิวเมนต์ที่สามหรือสี่อย่างชัดเจนถ้าต้องการ exclusive - เปิด cursor ใหม่ทุกครั้งที่ต้องการ record ถัดไป — cursor ตัวเดียวสามารถเลื่อนไปเรื่อยๆ ด้วย
continue()ไม่จำเป็นต้องเปิดใหม่ ทำแบบนั้นจะเสีย performance และทำให้โค้ดสับสน
💡 ตัวอย่างจากของจริง
Figma — ใช้ cursor เพื่อ iterate ผ่าน object จำนวนมากใน local cache ของไฟล์ design โดยไม่ต้องโหลดทั้งหมดเข้าหน่วยความจำในคราวเดียว ช่วยให้ canvas render ได้ลื่นแม้ไฟล์มีขนาดใหญ่
Excalidraw — เมื่อ persist ข้อมูลภาพวาดไว้ฝั่ง client จะใช้
IDBKeyRangeกรอง element เฉพาะช่วงที่ต้องการแสดงผล แทนที่จะดึงทุก record ออกมาทั้งหมด