Kodokon kodokon.com

SQLiteによる永続化:データアクセス層

better-sqlite3をプリペアドステートメント、トランザクション、専用のリポジトリとともに使い、速く安全な永続化を実現しましょう。

9 分 · 3 問

このレッスンを Kodokon で開く

SQLiteはあなたのプロセスの中で動きます。サーバーもネットワークもなく、クエリごとのレイテンシはマイクロ秒のオーダーです。それこそがbetter-sqlite3同期的なAPIを、許容できるどころか、しばしば非同期ドライバーより速くしている理由です。ティックより短い読み取りのために、プロミスとイベントループの一巡の代償を払う意味はありません。トレードオフは書き込みで現実になります。遅いクエリはループをブロックするのです。クエリにインデックスを張って短く保てば、SQLiteは毎秒数万回の読み取りをこなします。

BASH
npm install better-sqlite3
ネイティブモジュール。必要なら、インストール時にコンパイルされます。

データベースを開くとき、二つの設定が違いを生みます。WALモード(ライトアヘッドロギング)は、ファイル全体をロックする代わりに、書き込み中の読み取りを許します。そしてスキーマはCREATE TABLE IF NOT EXISTSで冪等に適用されます。本物のマイグレーションツールを導入する前なら、これで十分すぎるほどです。

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:接続、設定、冪等なスキーマ。

データアクセス層、すなわちリポジトリは、SQLを話す唯一のモジュールです。クエリは構築時に一度だけ準備され、その後は呼び出しごとに再利用されます。エンジンはSQLテキストを再解析せず、値はプレースホルダー?を通って渡され、決して連結を通りません。これはまさに、前のレッスンのサービスが期待するfindAllfindByEmailinsertというインターフェースです。

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:すべてがコミットされるか、すべてがロールバックされるか。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. なぜbetter-sqlite3の同期的なAPIはNodeにおいて矛盾ではないのですか?
    • Nodeがネイティブモジュールを別のスレッドで実行するから
    • SQLiteはプロセス内にあるから。クエリは、そうでなければ避けられる非同期のオーケストレーションよりも安く済む
    • 書き込みが自動的にキューに入れられるから
    • WALモードがすべてのクエリを非ブロッキングにするから
  2. プリペアドステートメントの?プレースホルダーは何を保証しますか?
    • 値がSQLとは別に渡され、決してコードとして実行されない
    • 値が保存される前に暗号化される
    • クエリが自動的にディスクにキャッシュされる
  3. db.transactionは千回のインサートに対して、どんな二重の利点をもたらしますか?
    • データの圧縮と自動的な重複排除
    • 原子性(全か無か)と、千回ではなく一度きりのディスク同期
    • スキーマの検証と、欠けているインデックスの作成