Kodokon kodokon.com

用 SQLite 做持久化:数据访问层

借助 better-sqlite3 的预编译语句、事务和一个专用仓储,实现快速而安全的持久化。

9 分钟 · 3 题

在 Kodokon 中打开本课

SQLite 运行在你的进程内部:没有服务器、没有网络,单次查询的延迟只在微秒量级。正是这一点,让 better-sqlite3同步API 不仅可以接受,而且往往比一个异步驱动更快:对于一次比一个事件循环 tick 还短的读取,为一个 promise 和一轮事件循环埋单毫无意义。代价确实存在于写入上:一个慢查询会阻塞事件循环。让你的查询保持有索引且短小,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 带来了哪双重好处?
    • 数据压缩和自动去重
    • 原子性(全有或全无)以及只做一次磁盘同步,而不是一千次
    • 表结构校验和创建缺失的索引