Transaction
transaction คืออะไร?
หัวข้อที่มีชื่อว่า “transaction คืออะไร?”transaction คือกลุ่มของ operation database ที่ทำงานเป็น atomic unit เดียว ที่เป็นไปตามหลักการ ACID: operation ทั้งหมด ต้องสำเร็จ หรือไม่มี operation ใดเลยที่จะถูกนำไปใช้ หาก operation ใดล้มเหลว transaction ทั้งหมดจะถูก rollback กลับสู่สถานะก่อนที่จะเริ่มต้น
ใน IndexedDB การอ่านและการเขียนทุกครั้งต้องเกิดขึ้นภายใน transaction ไม่มีวิธีเข้าถึง object store โดยตรง — คุณต้องผ่าน transaction ก่อนเสมอ
การเปิด transaction
หัวข้อที่มีชื่อว่า “การเปิด transaction”ใช้ db.transaction() เพื่อเปิด transaction ใหม่ โดยรับสองอาร์กิวเมนต์: อาร์เรย์ของชื่อ store ที่ต้องการเข้าถึง และ string mode
// ทรานแซกชันแบบอ่านอย่างเดียว — สำหรับการอ่านเท่านั้นconst tx = db.transaction(['storeName'], 'readonly');
// ทรานแซกชันแบบอ่าน-เขียน — สำหรับการอ่านและการเขียนconst tx = db.transaction(['storeName'], 'readwrite');เมื่อได้ transaction แล้ว ให้เรียก tx.objectStore('storeName') เพื่อรับ reference ของ IDBObjectStore ที่จำเป็นสำหรับการออก request:
const store = tx.objectStore('storeName');const request = store.get(42);โหมดของ transaction
หัวข้อที่มีชื่อว่า “โหมดของ transaction”IndexedDB รองรับสองโหมด:
readonly— transaction readonly หลายตัวบน store เดียวกันสามารถทำงานพร้อมกันได้ ใช้โหมดนี้เมื่อต้องการอ่านข้อมูลเท่านั้นreadwrite— ล็อกแบบ exclusive บน store แต่ละตัวที่ระบุไว้ transaction readwrite ได้เพียงตัวเดียวเท่านั้นที่สามารถเข้าถึง store ที่กำหนดได้ในเวลาหนึ่ง transaction อื่นต้องรอจนกว่าจะเสร็จสิ้น
ควรเลือก readonly เมื่อไม่จำเป็นต้องเขียนข้อมูลเสมอ — ช่วยให้เกิด parallelism ได้ดีขึ้นและหลีกเลี่ยงการล็อกที่ไม่จำเป็น
การรับฟัง Event ของ transaction
หัวข้อที่มีชื่อว่า “การรับฟัง Event ของ transaction”transaction จะ fire สาม event ที่ควรจัดการ:
tx.oncomplete = () => { // request ทั้งหมดในทรานแซกชันสำเร็จและถูก commit แล้ว console.log('Transaction committed successfully');};
tx.onerror = (event) => { // request อย่างน้อยหนึ่งตัวล้มเหลว — ทรานแซกชันถูก rollback console.error('Transaction error:', tx.error);};
tx.onabort = () => { // ทรานแซกชันถูก abort อย่างชัดเจนผ่าน tx.abort() console.warn('Transaction aborted');};oncomplete จะ fire หลังจาก request ทุกตัวภายใน transaction เสร็จสมบูรณ์และการเปลี่ยนแปลงถูกบันทึกลงดิสก์แล้วเท่านั้น นี่คือจุดที่เหมาะสมในการยืนยันกับผู้ใช้ว่าข้อมูลถูกบันทึกแล้ว
พฤติกรรม Auto-Commit
หัวข้อที่มีชื่อว่า “พฤติกรรม Auto-Commit”transaction ของ IndexedDB ใช้ auto-commit: เมื่อ pending request ตัวสุดท้ายใน transaction เสร็จสิ้นและ JavaScript call stack กลับสู่ event loop โดยไม่มี request ใหม่เข้าคิว transaction จะ commit โดยอัตโนมัติ
ซึ่งหมายความว่าคุณต้องออก request ทั้งหมดใน chain แบบ synchronous — success handler ของ request หนึ่งจะ trigger request ถัดไป หากคุณ yield ไปยัง event loop ระหว่าง request (เช่น การ await fetch() หรือห่อ logic ใน setTimeout) transaction จะ commit ไปแล้วเมื่อ code ของคุณ resume และ request ใหม่ที่ออกต่อ transaction นั้นจะเกิด error
// ผิด — ห้ามทำแบบนี้tx.oncomplete = async () => { /* ... */ }; // ไม่ใช่ pattern ที่ถูกต้อง
const store = tx.objectStore('items');store.get(1).onsuccess = async (e) => { const data = await fetch('/api/extra'); // yield ไปยัง event loop — ทรานแซกชันหายไปแล้ว! store.put({ ...e.target.result, extra: data }); // ERROR: ทรานแซกชัน commit ไปแล้ว};// ถูกต้อง — เก็บ request ทั้งหมดไว้ใน synchronous microtask chain เดียวกันconst store = tx.objectStore('items');const getReq = store.get(1);getReq.onsuccess = (e) => { store.put({ ...e.target.result, updated: true }); // ออกแบบ synchronous — ยังอยู่ใน tx};ตัวอย่างที่รันได้
หัวข้อที่มีชื่อว่า “ตัวอย่างที่รันได้”โค้ดด้านล่างเปิด database สร้าง object store items เปิด readwrite transaction เพิ่ม record และรอ oncomplete ก่อนปิด
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| ACID guarantee ของ transaction | operation ทั้งหมดสำเร็จหรือ rollback พร้อมกัน ข้อมูลไม่มีวันอยู่ในสถานะครึ่ง ๆ กลาง ๆ | ต้องเขียนทุก request แบบ synchronous chain เคร่งครัด ห้าม yield ไปยัง event loop กลางทาง |
readonly mode | รันพร้อมกันหลาย transaction บน store เดียวกันได้ เพิ่ม parallelism | ใช้เขียนข้อมูลไม่ได้เลย ต้องเปิด transaction ใหม่เป็น readwrite ถ้าต้องการเขียน |
readwrite mode | เขียนข้อมูลได้ พร้อม lock แบบ exclusive ป้องกัน race condition บน store เดียวกัน | transaction อื่นที่ต้องการ store เดียวกันต้องรอคิว ลด throughput ถ้าเปิดค้างไว้นาน |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”await fetch()หรือsetTimeoutแทรกกลาง transaction — เมื่อ call stack ว่างโดยไม่มี pending request transaction จะ auto-commit ทันที request ที่ตามมาจะ throwTransactionInactiveErrorแบบไม่ทันตั้งตัว- ลืมระบุ object store บางตัวตอนเปิด
db.transaction()— เรียกtx.objectStore('name')กับ store ที่ไม่ได้อยู่ใน array แรกจะ throwNotFoundErrorแม้ store นั้นจะมีอยู่จริงใน database ก็ตาม - ผูก UI update ไว้กับ
onsuccessของ request แทนoncompleteของ transaction —onsuccessแจ้งแค่ request เดียวสำเร็จ แต่ transaction อาจยังไม่ commit จริง หากมี request อื่นล้มเหลวภายหลัง ข้อมูลที่ UI แสดงจะไม่ตรงกับที่ถูก rollback ไป
💡 ตัวอย่างจากของจริง
Notion — เปิด
readwritetransaction ครอบคลุมหลาย object store พร้อมกัน (เช่น blocks กับ page metadata) เพื่อให้การอัปเดต block กับการอัปเดต timestamp ของหน้าเกิดขึ้นเป็น atomic unit เดียวWhatsApp Web — ใช้
readonlytransaction แยกจากreadwritetransaction เพื่อให้การโหลดประวัติแชท (อ่านอย่างเดียว) ทำงานพร้อมกันได้หลาย request โดยไม่ไปบล็อกการบันทึกข้อความใหม่ที่เข้ามา