ข้ามไปยังเนื้อหา

การ iterate ด้วย Cursors และ Ranges

การอ่าน 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');
}
};

objectStore.openCursor(query, direction) — ทั้งสองอาร์กิวเมนต์เป็นตัวเลือก

  • query — เป็น IDBKeyRange หรือคีย์เฉพาะ ละเว้น (หรือส่ง null) เพื่อ iterate record ทั้งหมด
  • direction"next" (ค่าเริ่มต้น เรียงจากน้อยไปมาก), "prev" (เรียงจากมากไปน้อย), "nextunique" หรือ "prevunique"
// All records, ascending order (default)
objectStore.openCursor();
// All records, descending order
objectStore.openCursor(null, 'prev');
// Only records with keys 2–3 (inclusive on both ends)
objectStore.openCursor(IDBKeyRange.bound(2, 3));

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 === 42
IDBKeyRange.lowerBound(10) // key >= 10
IDBKeyRange.lowerBound(10, true) // key > 10
IDBKeyRange.upperBound(50) // key <= 50
IDBKeyRange.bound(10, 50) // 10 <= key <= 50
IDBKeyRange.bound(10, 50, true, true) // 10 < key < 50

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

การสาธิตด้านล่างนี้:

  1. เปิด database และเติมข้อมูลคะแนนผู้เล่นสี่ record
  2. ใช้ cursor เพื่อ iterate record ทั้งหมด และแสดงผลแต่ละรายการ
  3. ใช้ IDBKeyRange.bound(2, 3) เพื่อ iterate เฉพาะ record ที่มี id 2 และ 3
  4. ลบ database หลังจากนั้นเพื่อไม่ให้เหลือร่องรอย
Browser Storage
ตัวเลือกBenefitCost
ใช้ 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 ออกมาทั้งหมด

Cursor จะ resolve เป็นค่าใดเมื่อไม่มีระเบียนเหลืออีกให้วนซ้ำ?
factory ใดของ IDBKeyRange ที่สร้าง range ครอบคลุมคีย์ตั้งแต่ 5 ถึง 10 รวมขอบทั้งสองด้าน?
จะเกิดอะไรขึ้นหากลืมเรียก cursor.continue() ภายใน handler ของ onsuccess?
คุณต้องการวนซ้ำเฉพาะระเบียนที่มีคีย์มากกว่า 10 อย่างเคร่งครัด range ใดถูกต้อง?