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

Transaction

transaction คือกลุ่มของ operation database ที่ทำงานเป็น atomic unit เดียว ที่เป็นไปตามหลักการ ACID: operation ทั้งหมด ต้องสำเร็จ หรือไม่มี operation ใดเลยที่จะถูกนำไปใช้ หาก operation ใดล้มเหลว transaction ทั้งหมดจะถูก rollback กลับสู่สถานะก่อนที่จะเริ่มต้น

ใน IndexedDB การอ่านและการเขียนทุกครั้งต้องเกิดขึ้นภายใน transaction ไม่มีวิธีเข้าถึง object store โดยตรง — คุณต้องผ่าน 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);

IndexedDB รองรับสองโหมด:

  • readonly — transaction readonly หลายตัวบน store เดียวกันสามารถทำงานพร้อมกันได้ ใช้โหมดนี้เมื่อต้องการอ่านข้อมูลเท่านั้น
  • readwrite — ล็อกแบบ exclusive บน store แต่ละตัวที่ระบุไว้ transaction readwrite ได้เพียงตัวเดียวเท่านั้นที่สามารถเข้าถึง store ที่กำหนดได้ในเวลาหนึ่ง transaction อื่นต้องรอจนกว่าจะเสร็จสิ้น

ควรเลือก readonly เมื่อไม่จำเป็นต้องเขียนข้อมูลเสมอ — ช่วยให้เกิด parallelism ได้ดีขึ้นและหลีกเลี่ยงการล็อกที่ไม่จำเป็น

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 เสร็จสมบูรณ์และการเปลี่ยนแปลงถูกบันทึกลงดิสก์แล้วเท่านั้น นี่คือจุดที่เหมาะสมในการยืนยันกับผู้ใช้ว่าข้อมูลถูกบันทึกแล้ว

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 ก่อนปิด

Browser Storage
ตัวเลือกBenefitCost
ACID guarantee ของ transactionoperation ทั้งหมดสำเร็จหรือ 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 ที่ตามมาจะ throw TransactionInactiveError แบบไม่ทันตั้งตัว
  • ลืมระบุ object store บางตัวตอนเปิด db.transaction() — เรียก tx.objectStore('name') กับ store ที่ไม่ได้อยู่ใน array แรกจะ throw NotFoundError แม้ store นั้นจะมีอยู่จริงใน database ก็ตาม
  • ผูก UI update ไว้กับ onsuccess ของ request แทน oncomplete ของ transactiononsuccess แจ้งแค่ request เดียวสำเร็จ แต่ transaction อาจยังไม่ commit จริง หากมี request อื่นล้มเหลวภายหลัง ข้อมูลที่ UI แสดงจะไม่ตรงกับที่ถูก rollback ไป

💡 ตัวอย่างจากของจริง

Notion — เปิด readwrite transaction ครอบคลุมหลาย object store พร้อมกัน (เช่น blocks กับ page metadata) เพื่อให้การอัปเดต block กับการอัปเดต timestamp ของหน้าเกิดขึ้นเป็น atomic unit เดียว

WhatsApp Web — ใช้ readonly transaction แยกจาก readwrite transaction เพื่อให้การโหลดประวัติแชท (อ่านอย่างเดียว) ทำงานพร้อมกันได้หลาย request โดยไม่ไปบล็อกการบันทึกข้อความใหม่ที่เข้ามา

การที่ transaction เป็น 'atomic' หมายความว่าอะไร?
โหมด transaction ใดที่อนุญาตให้หลาย transaction ทำงานพร้อมกันบน store เดียวกัน?
จะเกิดอะไรขึ้นถ้าคุณ await fetch() ภายใน readwrite transaction ก่อนออก store request ตัวที่สอง?