借助 better-sqlite3 的预编译语句、事务和一个专用仓储,实现快速而安全的持久化。
在 Kodokon 中打开本课SQLite 运行在你的进程内部:没有服务器、没有网络,单次查询的延迟只在微秒量级。正是这一点,让 better-sqlite3 的同步API 不仅可以接受,而且往往比一个异步驱动更快:对于一次比一个事件循环 tick 还短的读取,为一个 promise 和一轮事件循环埋单毫无意义。代价确实存在于写入上:一个慢查询会阻塞事件循环。让你的查询保持有索引且短小,SQLite 每秒就能处理数万次读取。
npm install better-sqlite3打开数据库时,有两项设置能带来差别。WAL 模式(预写日志)允许在写入过程中进行读取,而不是锁住整个文件。而表结构则通过 CREATE TABLE IF NOT EXISTS 幂等地应用,在你引入真正的迁移工具之前,这已经足够好了。
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;
}数据访问层,也就是仓储,是唯一说 SQL 的模块。查询在构造时被预编译一次,随后在每次调用时复用:引擎不必重新解析 SQL 文本,而各个值通过占位符 ? 传入,绝不通过字符串拼接。这正是上一课的服务所期待的 findAll、findByEmail、insert 接口。
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();
},
};
}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 带来了哪双重好处?