Kodokon kodokon.com

การเก็บข้อมูลถาวรด้วย SQLite: ชั้นการเข้าถึงข้อมูล

ใช้ better-sqlite3 พร้อม prepared statement, ทรานแซกชัน และ repository เฉพาะทาง เพื่อการเก็บข้อมูลถาวรที่รวดเร็วและปลอดภัย

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

SQLite ทำงาน ภายในโปรเซสของคุณ ไม่มีเซิร์ฟเวอร์ ไม่มีเครือข่าย ความหน่วงต่อคำสั่งอยู่ในระดับไมโครวินาที นั่นคือสิ่งที่ทำให้ API แบบ ซิงโครนัส ของ better-sqlite3 ไม่เพียงแต่ยอมรับได้ แต่มักเร็วกว่าไดรเวอร์แบบอะซิงโครนัสด้วยซ้ำ ไม่มีเหตุผลที่จะต้องจ่ายค่า promise และการวนรอบ event loop ให้กับการอ่านที่สั้นกว่าหนึ่ง tick การแลกเปลี่ยนนั้นมีจริงในฝั่งการเขียน คือ คำสั่งที่ช้าจะบล็อกลูป จงทำให้คำสั่งของคุณมีดัชนีและสั้น แล้ว SQLite จะรับมือการอ่านได้หลายหมื่นครั้งต่อวินาที

BASH
npm install better-sqlite3
มอดูลเนทีฟ มันจะคอมไพล์ตอนติดตั้งหากจำเป็น

เมื่อเปิดฐานข้อมูล มีการตั้งค่าสองอย่างที่สร้างความแตกต่าง โหมด WAL (write-ahead logging) อนุญาตให้อ่านได้ระหว่างที่กำลังเขียน แทนที่จะล็อกทั้งไฟล์ และสคีมาถูกนำมาใช้แบบ idempotent ด้วย CREATE TABLE IF NOT EXISTS ซึ่งดีเพียงพอแล้วก่อนที่คุณจะนำเครื่องมือ migration จริงจังมาใช้

JAVASCRIPT
import Database from 'better-sqlite3';

export function createDb(path = 'app.db') {
  const db = new Database(path);
  db.pragma('journal_mode = WAL');
  db.exec(`CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT NOT NULL UNIQUE,
    name TEXT NOT NULL
  )`);
  return db;
}
connection.js: การเปิด, การตั้งค่า, สคีมาแบบ idempotent

ชั้นการเข้าถึงข้อมูล คือ repository เป็นมอดูลเดียวที่พูดภาษา SQL คำสั่งถูกเตรียม (prepare) ไว้ ครั้งเดียว ตอนสร้าง แล้วนำกลับมาใช้ซ้ำในทุกการเรียก เอนจินไม่ต้องแจงข้อความ SQL ใหม่ และค่าต่าง ๆ ถูกส่งผ่าน placeholder ? ไม่ใช่ผ่านการต่อสตริง มันคืออินเทอร์เฟซ findAll, findByEmail, insert ที่ service จากบทที่แล้วคาดหวังไว้พอดี

JAVASCRIPT
export function createUserRepository(db) {
  const insertStmt = db.prepare(
    'INSERT INTO users (email, name) VALUES (?, ?)',
  );
  const byEmailStmt = db.prepare(
    'SELECT * FROM users WHERE email = ?',
  );
  const allStmt = db.prepare(
    'SELECT * FROM users ORDER BY id',
  );
  return {
    insert(user) {
      const info = insertStmt.run(user.email, user.name);
      return { id: info.lastInsertRowid, ...user };
    },
    findByEmail(email) {
      return byEmailStmt.get(email);
    },
    findAll() {
      return allStmt.all();
    },
  };
}
เตรียมครั้งเดียว เรียกใช้พันครั้ง คือ run, get, all
JAVASCRIPT
import { createDb } from './connection.js';

const db = createDb();
const insertStmt = db.prepare(
  'INSERT INTO users (email, name) VALUES (?, ?)',
);
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertStmt.run(user.email, user.name);
  }
});
insertMany([
  { email: 'ada@example.com', name: 'Ada' },
  { email: 'linus@example.com', name: 'Linus' },
]);
db.transaction: commit ทั้งหมด หรือ rollback ทั้งหมด

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ทำไม API แบบซิงโครนัสของ better-sqlite3 จึงไม่ขัดแย้งกันเองใน Node?
    • เพราะ Node รันมอดูลเนทีฟในเธรดแยกต่างหาก
    • เพราะ SQLite อยู่ในโปรเซส คำสั่งจึงมีต้นทุนน้อยกว่าการจัดการแบบอะซิงโครนัสที่มันเลี่ยงไปได้
    • เพราะการเขียนถูกจัดคิวโดยอัตโนมัติ
    • เพราะโหมด WAL ทำให้ทุกคำสั่งไม่บล็อก
  2. placeholder ? ใน prepared statement รับประกันอะไร?
    • ค่าถูกส่งแยกจาก SQL และไม่มีทางถูกรันเป็นโค้ดได้เลย
    • ค่าถูกเข้ารหัสก่อนถูกจัดเก็บ
    • คำสั่งถูกแคชลงดิสก์โดยอัตโนมัติ
  3. db.transaction ให้ประโยชน์สองต่ออะไรบ้างสำหรับการ insert พันครั้ง?
    • การบีบอัดข้อมูลและการกำจัดข้อมูลซ้ำโดยอัตโนมัติ
    • ความเป็นอะตอม (ทั้งหมดหรือไม่มีเลย) และการ sync ลงดิสก์เพียงครั้งเดียวแทนที่จะเป็นพันครั้ง
    • การตรวจสอบสคีมาและการสร้างดัชนีที่ขาดหายไป