告别繁琐:用 better-sqlite3 玩转 Node.js SQLite 数据库
引言:SQLite 在 Node.js 世界的定位与挑战
在构建各种 Node.js 应用时,我们常常需要一个轻量级、无需独立服务器、易于部署和管理的数据库。SQLite 凭借其零配置、文件存储、ACID 事务支持等特性,成为了许多场景下的理想选择,例如:
- 桌面应用程序(Electron)
- 命令行工具
- 嵌入式系统
- 小型网站或后端服务(特别是读多写少的场景)
- 缓存层
- 开发阶段的原型或测试数据库
然而,传统的 Node.js 生态中,与 SQLite 交互的常用库 sqlite3
长期以来依赖于回调或异步操作(如 Promise),这在处理多个连续数据库操作时,常常会导致代码变得复杂、难以阅读和维护,形成所谓的“回调地狱”或 Promise 链的嵌套。尽管 Promise 改进了异步流程,但与同步风格的代码相比,仍然增加了心智负担,尤其是在简单的 CRUD 操作或事务处理中。
幸运的是,Node.js 社区中出现了一个强大的替代品——better-sqlite3
。正如其名字所示,它旨在提供一个“更好”的 SQLite 驱动。better-sqlite3
的核心优势在于它提供了一个同步(Synchronous)的 API。这意味着你可以像编写传统的同步代码一样与数据库交互,使得数据库操作的逻辑流程更加直观和清晰,极大地提高了开发效率,降低了理解成本。
本文将深入探讨 better-sqlite3
的各种特性、用法以及最佳实践,帮助你完全掌握这个 Node.js SQLite 领域的利器,彻底告别过去繁琐的异步处理,以更优雅、高效的方式玩转 SQLite 数据库。
第一章:为什么选择 better-sqlite3?告别繁琐的理由
在深入学习用法之前,我们先来详细了解一下 better-sqlite3
相较于传统方式的优势,这正是其能够帮助我们“告别繁琐”的核心所在。
1. 同步 API:代码流程一目了然
这是 better-sqlite3
最显著的特点。传统的 sqlite3
库是完全异步的,所有数据库操作都需要传入回调函数,或者使用 .get().then(...)
, .all().then(...)
这样的 Promise 链。
例如,使用 sqlite3
(Promise 版本):
“`javascript
const sqlite3 = require(‘sqlite3’).verbose();
const { open } = require(‘sqlite’);
async function getUser(userId) {
const db = await open({
filename: ‘./mydb.sqlite’,
driver: sqlite3.Database
});
try {
const user = await db.get(‘SELECT * FROM users WHERE id = ?’, userId);
if (user) {
console.log(‘Found user:’, user);
const orders = await db.all(‘SELECT * FROM orders WHERE user_id = ?’, user.id);
console.log(‘User orders:’, orders);
} else {
console.log(‘User not found.’);
}
} catch (err) {
console.error(err);
} finally {
await db.close();
}
}
getUser(1);
“`
这段代码使用了 async/await
已经比纯回调清晰许多,但仍然需要 await 关键字,并且错误处理需要依赖 try...catch
包裹异步操作。
对比使用 better-sqlite3
:
“`javascript
const Database = require(‘better-sqlite3’);
function getUser(userId) {
let db;
try {
db = new Database(‘./mydb.sqlite’); // 打开/创建数据库
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(userId); // 获取单个用户
if (user) {
console.log('Found user:', user);
const orders = db.prepare('SELECT * FROM orders WHERE user_id = ?').all(user.id); // 获取用户订单
console.log('User orders:', orders);
} else {
console.log('User not found.');
}
} catch (err) {
console.error(err.message); // 同步错误直接捕获
} finally {
if (db) {
db.close(); // 关闭数据库
}
}
}
getUser(1);
“`
可以看到,better-sqlite3
的代码流程与我们的大脑思维方式更加一致:执行一个操作,等待结果,然后基于结果执行下一个操作。没有 await
,没有 .then()
,代码结构扁平且直观,极大降低了编写和阅读的复杂度,尤其是在需要执行一系列依赖前一步结果的数据库操作时。这对于编写脚本、命令行工具或简单的服务路由处理函数来说,简直是福音。
2. 卓越的性能
better-sqlite3
底层使用了 C++ 实现,与 SQLite C API 直接交互,这使得它拥有非常高的性能。它避免了 Node.js 异步回调或 Promise 带来的额外开销。此外,它默认并鼓励使用预处理语句(Prepared Statements),这进一步提升了重复执行相同结构 SQL 语句时的效率,同时也增强了安全性。
在许多基准测试中,better-sqlite3
的性能都远超 sqlite3
,尤其是在大量插入或查询操作时。
3. 简洁直观的 API 设计
better-sqlite3
的 API 设计非常简洁。核心操作围绕 Database
对象和 Statement
对象展开。
new Database(...)
: 打开或创建一个数据库连接。db.prepare(sql)
: 编译一个 SQL 语句,返回一个Statement
对象。statement.run(...)
: 执行非 SELECT 语句(INSERT, UPDATE, DELETE, CREATE TABLE等),返回有关更改的信息。statement.get(...)
: 执行 SELECT 语句并返回一行结果。statement.all(...)
: 执行 SELECT 语句并返回所有行结果。statement.iterate(...)
: 执行 SELECT 语句并返回一个迭代器,适用于处理大量结果集而无需一次性加载到内存。db.transaction(...)
: 方便地执行事务。
这种基于“准备语句 -> 执行”的模式,既安全又高效,是推荐的数据库交互方式。
4. 强大的预处理语句支持(默认且推荐)
预处理语句是防止 SQL 注入的关键,也是提高性能的常用手段。better-sqlite3
将预处理语句作为核心用法。通过 db.prepare(sql)
获取 Statement
对象,然后使用该对象的 run
, get
, all
, iterate
方法传递参数,better-sqlite3
会自动为你处理参数绑定,确保安全和效率。
5. 事务支持的便利性
better-sqlite3
提供了非常便捷的方式来处理事务,特别是 db.transaction(() => { ... })()
语法,它能够自动处理事务的开始、提交和回滚(如果发生错误),让事务代码异常简洁和健壮。
6. 更好的错误处理
由于是同步 API,错误处理变得与常规的同步 JavaScript 代码一样:使用 try...catch
块就能捕获数据库操作抛出的异常。这比处理异步操作中的错误要直接得多。
7. TypeScript 支持
better-sqlite3
提供了良好的 TypeScript 类型定义,这对于使用 TypeScript 开发的开发者来说非常友好,可以在编译阶段捕获潜在的类型错误,提高代码质量。
总结:告别繁琐的底气
综上所述,better-sqlite3
通过提供同步 API、优异的性能、简洁直观的设计、强大的预处理语句和事务支持,显著降低了在 Node.js 中使用 SQLite 的复杂性,让开发者可以将更多精力放在业务逻辑而非繁琐的异步流程控制上。它确实是 Node.js SQLite 开发领域的一把“利器”,帮助我们“告别繁琐”。
第二章:Better-SQLite3 基础入门与核心用法
本章将带你一步步了解如何安装 better-sqlite3
,并掌握其最基本也是最常用的操作。
1. 安装
使用 npm 或 yarn 进行安装:
“`bash
npm install better-sqlite3
或
yarn add better-sqlite3
“`
安装过程可能会编译一些 C++ 代码,需要本地有 Python 和 C++ 编译环境(如 Node.js 的 node-gyp 依赖),但通常情况下,Node.js 安装时已经包含了必要的工具链,或者 better-sqlite3
提供了预编译的二进制包,大部分用户可以直接安装成功。
2. 连接/创建数据库
使用 new Database(filename, [options])
构造函数来连接或创建一个 SQLite 数据库文件。如果指定的文件不存在,它会被创建。
“`javascript
const Database = require(‘better-sqlite3’);
// 连接到或创建一个数据库文件
let db;
try {
db = new Database(‘my_app.db’); // 数据库文件名为 my_app.db
console.log(‘成功连接到数据库’);
// 你可以在 options 中指定一些参数
// 例如:readonly: true 打开一个只读连接
// verbose: console.log 打印所有执行的 SQL 语句 (调试时有用)
// db = new Database(‘my_app.db’, { verbose: console.log });
} catch (err) {
console.error(‘连接数据库时出错:’, err.message);
}
// … 执行数据库操作 …
// 操作完成后,务必关闭数据库连接
if (db) {
db.close();
console.log(‘数据库连接已关闭’);
}
“`
重要提示: 务必在应用退出或不再需要数据库连接时调用 db.close()
来关闭连接。虽然 Node.js 进程退出时会自动清理,但在长时间运行的服务中,显式关闭是良好的实践,可以确保所有写操作都已同步到磁盘。
3. 执行非查询语句 (INSERT, UPDATE, DELETE, CREATE TABLE 等) – 使用 run()
对于不返回结果集的 SQL 语句(如数据插入、更新、删除、表创建等),使用 db.prepare(sql).run(...)
方法。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
// 创建一个 users 表
db.prepare(CREATE TABLE IF NOT EXISTS users (
).run();
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER,
email TEXT UNIQUE
)
console.log(‘users 表已创建或已存在’);
// 插入一条数据
const insertStmt = db.prepare(‘INSERT INTO users (name, age, email) VALUES (?, ?, ?)’);
const result1 = insertStmt.run(‘张三’, 30, ‘[email protected]’);
console.log(‘插入用户 张三:’, result1.changes, ‘行受影响, 最后插入ID:’, result1.lastInsertRowid);
// 插入另一条数据
const result2 = insertStmt.run(‘李四’, 25, ‘[email protected]’);
console.log(‘插入用户 李四:’, result2.changes, ‘行受影响, 最后插入ID:’, result2.lastInsertRowid);
// 更新数据
const updateStmt = db.prepare(‘UPDATE users SET age = ? WHERE name = ?’);
const updateResult = updateStmt.run(31, ‘张三’);
console.log(‘更新用户 张三:’, updateResult.changes, ‘行受影响’);
// 删除数据
const deleteStmt = db.prepare(‘DELETE FROM users WHERE name = ?’);
const deleteResult = deleteStmt.run(‘李四’);
console.log(‘删除用户 李四:’, deleteResult.changes, ‘行受影响’);
} catch (err) {
console.error(‘执行SQL时出错:’, err.message);
} finally {
db.close();
}
“`
run()
方法返回一个对象,包含两个有用的属性:
* changes
: 表示受当前 SQL 语句影响的行数(对于 INSERT, UPDATE, DELETE)。
* lastInsertRowid
: 对于 INSERT 语句,表示新插入行的 ID。
重要: 注意我们使用了 db.prepare()
。虽然对于单次执行的简单语句,可以直接用 db.exec(sql)
,但 prepare
是更安全、更高效的默认选择,特别是当语句包含参数时。db.exec()
适用于执行不带参数的 DDL (Data Definition Language) 语句或多个由分号分隔的语句。
4. 执行查询语句并获取单行结果 – 使用 get()
当你的查询预期只返回一行结果时(例如通过主键查询),使用 db.prepare(sql).get(...)
。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
// 假设 users 表已存在并包含数据
const userStmt = db.prepare(‘SELECT id, name, age, email FROM users WHERE id = ?’);
const user1 = userStmt.get(1);
if (user1) {
console.log(‘获取用户 (ID=1):’, user1); // 结果是一个对象 { id: 1, name: ‘张三’, age: 31, email: ‘[email protected]’ }
} else {
console.log(‘未找到用户 (ID=1)’); // 如果未找到,get() 返回 undefined
}
const user100 = userStmt.get(100);
if (user100) {
console.log(‘获取用户 (ID=100):’, user100);
} else {
console.log(‘未找到用户 (ID=100)’); // 未找到,get() 返回 undefined
}
} catch (err) {
console.error(‘执行查询时出错:’, err.message);
} finally {
db.close();
}
“`
get()
方法返回一个对象代表查询结果的第一行。如果没有匹配的行,则返回 undefined
。
5. 执行查询语句并获取所有行结果 – 使用 all()
当你的查询预期返回多行结果时,使用 db.prepare(sql).all(...)
。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
// 插入一些示例数据
db.prepare(‘INSERT OR IGNORE INTO users (name, age, email) VALUES (?, ?, ?)’).run(‘王五’, 28, ‘[email protected]’);
db.prepare(‘INSERT OR IGNORE INTO users (name, age, email) VALUES (?, ?, ?)’).run(‘赵六’, 35, ‘[email protected]’);
// 获取所有用户
const allUsersStmt = db.prepare(‘SELECT id, name, age FROM users’);
const allUsers = allUsersStmt.all(); // all() 不带参数时获取所有行
console.log(‘所有用户:’);
console.log(allUsers); // 结果是一个对象数组 [{ id: 1, name: ‘张三’, age: 31 }, { id: 3, name: ‘王五’, age: 28 }, …]
// 获取年龄大于等于 30 岁的用户
const adultUsersStmt = db.prepare(‘SELECT name, age FROM users WHERE age >= ?’);
const adultUsers = adultUsersStmt.all(30); // all() 也可以带参数
console.log(‘年龄 >= 30 的用户:’);
console.log(adultUsers); // 结果是符合条件的用户数组
} catch (err) {
console.error(‘执行查询时出错:’, err.message);
} finally {
db.close();
}
“`
all()
方法返回一个数组,其中每个元素是一个对象,代表查询结果的一行。如果没有匹配的行,则返回一个空数组 []
。
6. 使用参数绑定
在上面的例子中,我们已经看到了使用 ?
作为占位符,并在 run
, get
, all
方法中传入参数的方式。better-sqlite3
支持以下几种参数绑定方式:
- 问号占位符 (?): 参数按顺序绑定。
javascript
stmt.run(param1, param2, ...);
stmt.get(param1, param2, ...);
stmt.all(param1, param2, ...); - 位置占位符 ($N):
$1
,$2
,$3
等,参数按顺序绑定。
javascript
const stmt = db.prepare('SELECT * FROM users WHERE id = $1 AND age = $2');
const user = stmt.get(userId, userAge); // $1 对应 userId, $2 对应 userAge - 命名占位符 (:name, @name, $name): 参数通过对象属性名绑定。
javascript
const stmt = db.prepare('SELECT * FROM users WHERE id = :userId AND age = @userAge');
const user = stmt.get({ userId: 1, userAge: 30 }); // :userId 对应 obj.userId, @userAge 对应 obj.userAge
推荐使用问号占位符或命名占位符,它们可以有效地防止 SQL 注入攻击,并且在重复执行时 SQLite 可以重用查询计划,提高性能。永远不要直接将变量拼接到 SQL 字符串中。
7. 错误处理
由于 better-sqlite3
是同步的,所有的数据库操作(除了数据库打开时的同步部分)如果失败,会直接抛出一个 Error。因此,你可以使用标准的 try...catch
块来捕获并处理这些错误。
“`javascript
const Database = require(‘better-sqlite3’);
let db;
try {
db = new Database(‘my_app.db’);
// 尝试插入一个 email 已存在的用户,会违反 UNIQUE 约束
db.prepare(‘INSERT INTO users (name, age, email) VALUES (?, ?, ?)’).run(‘测试用户’, 40, ‘[email protected]’);
} catch (err) {
console.error(‘发生数据库错误:’, err.message); // 捕获到 UNIQUE 约束错误
// err 对象通常包含更详细的 SQLite 错误信息
} finally {
if (db) {
db.close();
}
}
“`
确保在 try...catch...finally
结构中使用 finally
来保证数据库连接在任何情况下都能被尝试关闭。
第三章:深入 Better-SQLite3:高级特性与最佳实践
掌握了基础用法后,我们可以进一步探索 better-sqlite3
的更高级特性,并了解一些使用时的最佳实践,以充分发挥其优势。
1. 事务处理:确保操作的原子性
事务是数据库中非常重要的概念,它保证一组数据库操作要么全部成功,要么全部失败(回滚到初始状态),从而维护数据的完整性。better-sqlite3
提供了非常优雅的事务处理方式。
使用 db.transaction(func).deferred()
, .immediate()
, 或 .exclusive()
方法,其中 func
是一个包含事务逻辑的函数。调用返回的函数即可执行事务。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
// 假设我们有两个操作:给用户 A 减去积分,给用户 B 增加积分
// 这两个操作应该在一个事务里,确保要么都成功,要么都失败
const transferPoints = db.transaction((fromUserId, toUserId, points) => {
// 1. 给用户 A 减去积分
const deductStmt = db.prepare(‘UPDATE users SET age = age – ? WHERE id = ?’); // 用 age 模拟积分字段
const deductResult = deductStmt.run(points, fromUserId);
if (deductResult.changes === 0) {
// 如果用户 A 不存在或积分不足(此处简化为用户不存在),则抛出错误,事务将回滚
// 实际应用中需要更复杂的逻辑检查积分是否足够
throw new Error(`User with ID ${fromUserId} not found or insufficient points`);
}
// 模拟一个可能失败的操作,比如数据库写入错误或其他业务逻辑错误
// if (Math.random() < 0.2) { // 20% 的概率模拟失败
// throw new Error("Simulated network error during transfer");
// }
// 2. 给用户 B 增加积分
const addStmt = db.prepare('UPDATE users SET age = age + ? WHERE id = ?');
const addResult = addStmt.run(points, toUserId);
if (addResult.changes === 0) {
// 如果用户 B 不存在,则抛出错误,事务将回滚
throw new Error(`User with ID ${toUserId} not found`);
}
// 如果函数没有抛出错误,事务将自动提交
console.log(`成功从用户 ${fromUserId} 转移 ${points} 积分到用户 ${toUserId}`);
});
// 执行事务
// transferPoints(1, 3, 5); // 调用事务函数,从用户1转移5积分给用户3
// console.log(‘事务执行完毕 (成功)’);
// 尝试执行一个会失败的事务 (例如,用户ID不存在)
console.log(‘\n尝试执行一个会失败的事务…’);
try {
transferPoints(1, 999, 10); // 用户 999 不存在
} catch (txErr) {
console.error(‘事务执行失败:’, txErr.message); // 捕获到用户不存在的错误,事务会自动回滚
}
} catch (err) {
console.error(‘发生其他错误:’, err.message);
} finally {
db.close();
}
“`
db.transaction(() => { ... })()
模式是 better-sqlite3
推荐的事务用法。它创建并返回一个函数。当你调用这个函数时,它会在一个事务中执行你提供的回调函数 (() => { ... }
)。
* 如果在回调函数内部没有抛出错误,事务会自动提交(COMMIT
)。
* 如果在回调函数内部抛出了任何错误,事务会自动回滚(ROLLBACK
)。
这种模式极大地简化了事务的管理,无需手动写 BEGIN TRANSACTION
, COMMIT
, ROLLBACK
以及复杂的 try...catch
逻辑来处理回滚。
transaction()
方法有几个变种 (.deferred()
, .immediate()
, .exclusive()
),它们对应 SQLite 的不同锁定模式,影响并发写入时的行为。deferred()
是默认值,它会尽可能延迟获取写锁;immediate()
会在事务开始时尝试获取写锁;exclusive()
会获取排他锁,确保没有其他连接可以读写数据库。对于大多数简单的 Node.js 应用,使用默认的 deferred()
或明确使用 db.transaction(() => { ... })()
即可。
2. 批量插入:提升写入效率
向数据库插入大量数据时,逐条执行 INSERT
语句效率低下。将多次插入操作放在一个事务中执行可以显著提升性能,因为事务减少了磁盘 I/O 操作的次数。better-sqlite3
结合事务和预处理语句,使得批量插入非常方便和高效。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
const insertStmt = db.prepare(‘INSERT INTO users (name, age, email) VALUES (?, ?, ?)’);
const insertMany = db.transaction((users) => {
for (const user of users) {
insertStmt.run(user.name, user.age, user.email);
}
});
const newUsers = [
{ name: ‘钱七’, age: 22, email: ‘[email protected]’ },
{ name: ‘孙八’, age: 38, email: ‘[email protected]’ },
{ name: ‘周九’, age: 29, email: ‘[email protected]’ },
// … 更多用户数据
];
console.time(‘批量插入’);
insertMany(newUsers); // 调用批量插入事务函数
console.timeEnd(‘批量插入’);
console.log(成功插入 ${newUsers.length} 条用户数据
);
} catch (err) {
console.error(‘批量插入失败:’, err.message);
} finally {
db.close();
}
“`
通过将 insertStmt.run()
调用放在 db.transaction
的回调函数中,所有插入操作都在一个事务中完成,即使是插入成千上万条数据也能保持较高的效率。
3. 迭代器模式:处理海量查询结果
对于可能返回巨量数据的查询,一次性使用 all()
将所有结果加载到内存可能会导致内存溢出。better-sqlite3
提供了 iterate()
方法,它返回一个迭代器,你可以逐行处理结果,而无需将整个结果集存储在内存中。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
// 假设 users 表中有大量数据
const selectStmt = db.prepare(‘SELECT id, name, email FROM users’);
console.log(‘开始迭代处理用户数据…’);
let count = 0;
// 使用 for…of 循环迭代结果
for (const user of selectStmt.iterate()) {
// 在这里处理每一行数据
// console.log(处理用户: ID=${user.id}, Name=${user.name}
);
count++;
if (count % 1000 === 0) { // 每处理1000行打印一次进度
console.log(已处理 ${count} 行...
);
}
// 模拟一些处理时间
// await new Promise(resolve => setTimeout(resolve, 1)); // 如果在异步环境中,迭代器不可用
}
console.log(迭代处理完毕,共处理 ${count} 行数据。
);
} catch (err) {
console.error(‘迭代查询失败:’, err.message);
} finally {
db.close();
}
“`
iterate()
返回的迭代器是同步的,非常适合在 Node.js 脚本或那些不需要阻塞主事件循环(比如在 Express 路由处理中通常需要异步)但需要处理大结果集的场景下使用。注意,在使用迭代器期间,数据库连接是忙碌的,不能同时执行其他操作。
4. 数据库配置与 PRAGMA
better-sqlite3
允许你在打开数据库时通过 options 配置,也允许你执行 SQLite 的 PRAGMA
语句来调整数据库设置。
“`javascript
const Database = require(‘better-sqlite3’);
// 启用 WAL 模式 (Write-Ahead Logging),可以改善并发读写性能
// 在多进程/线程同时读写同一个数据库文件时特别有用
let db = new Database(‘my_app.db’);
try {
db.pragma(‘journal_mode = WAL’);
console.log(‘数据库日志模式设置为 WAL’);
// 获取当前 journal 模式
const mode = db.pragma(‘journal_mode’, { simple: true }); // simple: true 返回单个值
console.log(‘当前 journal 模式:’, mode); // 输出如: WAL
// 设置 busy timeout (当数据库文件被锁定时等待的时间,单位毫秒)
// 对于可能会有多个进程/线程访问的场景,设置一个 busy timeout 可以避免程序立即因为锁而崩溃
db.pragma(‘busy_timeout = 5000’); // 等待 5 秒
console.log(‘数据库忙碌超时设置为 5000ms’);
// 获取 busy timeout
const timeout = db.pragma(‘busy_timeout’, { simple: true });
console.log(‘当前忙碌超时设置:’, timeout); // 输出: 5000
} catch (err) {
console.error(‘PRAGMA 操作失败:’, err.message);
} finally {
if (db) {
db.close();
}
}
“`
db.pragma(sql, [options])
方法用于执行 PRAGMA 语句。
* sql
: PRAGMA 语句字符串。
* options.simple
: 如果设置为 true
且 PRAGMA 语句返回一个值,则直接返回该值而不是结果行数组。
配置 journal_mode
和 busy_timeout
是优化 SQLite 在 Node.js 应用中表现的常见手段,特别是当你遇到数据库锁定相关的错误时。
5. 类型处理
SQLite 本身是动态类型的,一个列可以存储任何类型的数据,尽管通常会指定一个亲和类型 (affinity type)。better-sqlite3
会根据查询结果的 SQLite 类型尝试将其映射到合适的 JavaScript 类型:
- SQLite INTEGER 映射到 JavaScript number。
- SQLite REAL 映射到 JavaScript number。
- SQLite TEXT 映射到 JavaScript string。
- SQLite BLOB 映射到 Node.js Buffer。
- SQLite NULL 映射到 JavaScript null。
布尔值通常存储为 0 或 1 的 INTEGER,better-sqlite3
默认不会自动转换,你需要根据需要手动处理。
对于日期和时间,SQLite 通常存储为 TEXT, REAL (Julian day number) 或 INTEGER (Unix epoch time)。better-sqlite3
也不会自动将其转换为 JavaScript Date 对象,你需要在查询后或插入前手动转换。
“`javascript
const Database = require(‘better-sqlite3’);
const db = new Database(‘my_app.db’);
try {
db.prepare(CREATE TABLE IF NOT EXISTS data_types (
).run();
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
count INTEGER,
price REAL,
is_active INTEGER, -- 存储 0 或 1
binary_data BLOB,
created_at INTEGER -- 存储 Unix 时间戳
)
const bufferData = Buffer.from(‘这是一个 BLOB 测试数据’);
const now = Math.floor(Date.now() / 1000); // 当前 Unix 时间戳
db.prepare(‘INSERT INTO data_types (name, count, price, is_active, binary_data, created_at) VALUES (?, ?, ?, ?, ?, ?)’).run(
‘示例项目’, 100, 19.99, 1, bufferData, now
);
const item = db.prepare(‘SELECT * FROM data_types WHERE id = ?’).get(1);
console.log(‘获取到的数据类型示例:’);
console.log(‘name (TEXT):’, typeof item.name, item.name); // string
console.log(‘count (INTEGER):’, typeof item.count, item.count); // number
console.log(‘price (REAL):’, typeof item.price, item.price); // number
console.log(‘is_active (INTEGER):’, typeof item.is_active, item.is_active); // number (0 or 1)
console.log(‘binary_data (BLOB):’, typeof item.binary_data, item.binary_data instanceof Buffer, item.binary_data.toString()); // object, true, “这是一个 BLOB 测试数据”
console.log(‘created_at (INTEGER):’, typeof item.created_at, item.created_at, new Date(item.created_at * 1000)); // number, unix timestamp, Date object
} catch (err) {
console.error(‘数据类型示例失败:’, err.message);
} finally {
db.close();
}
“`
理解 better-sqlite3
和 SQLite 的类型映射对于正确地存取和处理数据至关重要。
6. 运行时扩展 (Runtime Extensions)
SQLite 允许加载运行时扩展来增加新的功能,比如空间数据支持 (SpatiaLite)、全文搜索增强等。better-sqlite3
提供了 db.loadExtension(path)
方法来加载这些扩展(需要扩展是预编译好的 .so
, .dylib
, .dll
文件)。
javascript
// 假设你有一个 SQLite 扩展库文件,例如 libspatialite.so
// const Database = require('better-sqlite3');
// const db = new Database('my_app.db');
// try {
// db.loadExtension('/path/to/libspatialite.so');
// console.log('SpatiaLite 扩展加载成功');
// // 现在你可以使用 SpatiaLite 提供的 SQL 函数了
// // const result = db.prepare('SELECT ST_MakePoint(?, ?)').get(10, 20);
// // console.log(result);
// } catch (err) {
// console.error('加载扩展失败:', err.message);
// } finally {
// db.close();
// }
这个功能对于需要利用 SQLite 高级特性的应用非常有用。
第四章:实际案例:构建一个简单的命令行 Todo 应用
为了更好地理解 better-sqlite3
的实际应用,我们构建一个简单的命令行 Todo 应用,它可以添加、列出和完成 Todo 项。
“`javascript
!/usr/bin/env node
const Database = require(‘better-sqlite3’);
const process = require(‘process’);
const path = require(‘path’);
// 数据库文件放在用户主目录下的一个隐藏文件夹里
const dbPath = path.join(process.env.HOME || process.env.USERPROFILE, ‘.my_todo_app’, ‘todos.db’);
const fs = require(‘fs’);
const dbDir = path.dirname(dbPath);
// 确保数据库目录存在
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
let db;
try {
db = new Database(dbPath);
console.log(使用数据库文件: ${dbPath}
);
// 创建 todos 表
db.prepare(`
CREATE TABLE IF NOT EXISTS todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0, -- 0 未完成, 1 已完成
created_at INTEGER NOT NULL
)
`).run();
console.log('Todos 表已就绪.');
const args = process.argv.slice(2); // 获取命令行参数 (跳过 node 和 脚本路径)
const command = args[0];
switch (command) {
case 'add':
if (args.length < 2) {
console.error('用法: todo add <任务描述>');
process.exit(1);
}
const task = args.slice(1).join(' ');
const insertStmt = db.prepare('INSERT INTO todos (task, created_at) VALUES (?, ?)');
const result = insertStmt.run(task, Math.floor(Date.now() / 1000));
console.log(`添加任务成功 (ID: ${result.lastInsertRowid})`);
break;
case 'list':
const listStmt = db.prepare('SELECT id, task, completed FROM todos ORDER BY created_at DESC');
const todos = listStmt.all();
if (todos.length === 0) {
console.log('当前没有待办任务。');
} else {
console.log('待办任务列表:');
todos.forEach(todo => {
const status = todo.completed ? '[完成]' : '[待办]';
console.log(`- [${todo.id}] ${status} ${todo.task}`);
});
}
break;
case 'done':
if (args.length < 2) {
console.error('用法: todo done <任务ID>');
process.exit(1);
}
const taskId = parseInt(args[1], 10);
if (isNaN(taskId)) {
console.error('错误: 任务ID必须是数字。');
process.exit(1);
}
const doneStmt = db.prepare('UPDATE todos SET completed = 1 WHERE id = ?');
const doneResult = doneStmt.run(taskId);
if (doneResult.changes > 0) {
console.log(`任务 ${taskId} 已标记为完成。`);
} else {
console.log(`未找到任务 ID ${taskId} 或已完成。`);
}
break;
case 'remove': // 添加一个删除功能
if (args.length < 2) {
console.error('用法: todo remove <任务ID>');
process.exit(1);
}
const removeTaskId = parseInt(args[1], 10);
if (isNaN(removeTaskId)) {
console.error('错误: 任务ID必须是数字。');
process.exit(1);
}
const removeStmt = db.prepare('DELETE FROM todos WHERE id = ?');
const removeResult = removeStmt.run(removeTaskId);
if (removeResult.changes > 0) {
console.log(`任务 ${removeTaskId} 已删除。`);
} else {
console.log(`未找到任务 ID ${removeTaskId}。`);
}
break;
default:
console.log('未知命令。可用命令: add, list, done, remove');
console.log('用法:');
console.log(' todo add <任务描述>');
console.log(' todo list');
console.log(' todo done <任务ID>');
console.log(' todo remove <任务ID>');
process.exit(1);
}
} catch (err) {
console.error(‘发生错误:’, err.message);
process.exit(1); // 错误时退出并返回非零状态码
} finally {
// 确保数据库连接在程序退出前关闭
if (db) {
db.close();
}
}
// 为了作为命令行工具执行,你可能需要在文件顶部添加 #!/usr/bin/env node
// 并通过 chmod +x <文件名>
使其可执行
// 然后将其放到系统的 PATH 中,或直接使用 ./<文件名> <命令>
执行
“`
如何运行这个例子:
- 将上面的代码保存为
todo.js
文件。 - 在终端中,进入文件所在的目录。
- 运行
npm install better-sqlite3
安装依赖。 - 使脚本可执行:
chmod +x todo.js
- 运行命令:
./todo.js add 买牛奶
./todo.js add 写代码
./todo.js list
./todo.js done 1
(假设买牛奶的任务ID是1)./todo.js list
./todo.js remove 2
(假设写代码的任务ID是2)./todo.js list
这个例子清晰地展示了 better-sqlite3
在命令行工具中的优势:同步代码流程与脚本执行顺序完美契合,使得代码非常直观易懂。每个命令的处理都是一个简单的同步函数调用和数据库操作。
第五章:性能优化与注意事项
虽然 better-sqlite3
性能已经很出色,但在处理大量数据或并发访问时,仍然需要注意一些优化和限制。
1. 批量操作务必使用事务
再次强调,对于 INSERT, UPDATE, DELETE 的批量操作,务必将它们包裹在一个事务中。这是提升写入性能最有效的手段。
2. 重用预处理语句 (Statements)
如果你在循环或其他地方重复执行相同的 SQL 语句(只是参数不同),应该将 db.prepare()
调用放在循环外部,并在循环内部多次调用 statement.run()
或 statement.get()/all()
。预处理语句的编译和优化只需要执行一次。
3. 了解 SQLite 的并发限制
SQLite 是一个文件数据库,它的并发控制是通过文件锁实现的。在默认的 DELETE
日志模式下,写操作会锁定整个数据库文件,期间其他读写操作都会被阻塞。虽然 WAL
(Write-Ahead Logging) 模式可以显著改善读写并发(允许多个读操作与一个写操作并行),但同一时间仍然只允许一个写操作。
这意味着 better-sqlite3
(或任何 Node.js SQLite 库)不适合高并发、多进程写入的场景。如果你的应用需要处理大量并发写请求,考虑使用客户端/服务器架构的数据库(如 PostgreSQL, MySQL, MongoDB)。
如果在 Node.js 应用中可能出现多个请求同时访问数据库(例如在 Web 服务器中),并且可能会触发写操作,可以考虑:
* 使用 WAL 模式 (PRAGMA journal_mode = WAL
)。
* 设置 busy_timeout
允许等待一段时间获取锁。
* 在更高层面上管理并发,例如使用队列来顺序化写入操作。
* 如果并发需求很高,认真考虑是否需要切换到其他数据库。
better-sqlite3
在单线程 Node.js 进程内部执行同步操作时性能极佳,但如果多个 Node.js 进程或线程(通过 worker_threads)同时访问 同一个 数据库文件并尝试写入,就会遇到 SQLite 的并发写入瓶颈。
4. 索引的重要性
对于频繁用于查询条件的列,创建索引可以极大地提高 SELECT
语句的性能。
sql
CREATE INDEX index_name ON table_name (column_name);
CREATE INDEX index_name ON table_name (column1, column2); -- 复合索引
在应用启动时或数据库初始化时,确保你的表有合适的索引。
5. 避免在长时间运行的同步操作中持有数据库锁
在使用 iterate()
处理大量数据时,或者在自定义的同步函数中执行长时间运行的数据库操作时,要注意整个过程会阻塞 Node.js 事件循环。如果这是一个 Web 服务器,这意味着在此期间它无法响应其他请求。尽量将耗时的数据库操作(如果可能且需要)放在 Worker Threads 中执行,或者重新评估是否真的需要同步操作。然而,better-sqlite3
的核心优势就在于同步,所以在许多脚本或简单服务场景下,这并不是问题。只是在构建高并发 Web 服务时需要特别权衡。
总结:告别繁琐,拥抱高效
通过本文的详细介绍,你应该已经对 better-sqlite3
有了全面的认识。它的同步 API 设计、出色的性能、简洁直观的接口以及对预处理语句和事务的良好支持,使其成为在 Node.js 中使用 SQLite 数据库时的首选库,特别是在需要快速开发、代码逻辑清晰且对并发写入要求不那么极致的场景下。
使用 better-sqlite3
,你可以:
- 告别 复杂的异步回调或 Promise 链,用同步思维编写数据库交互代码。
- 拥抱 简洁直观的 API,快速实现数据库操作。
- 利用 预处理语句提升性能和安全性。
- 便捷地 管理事务,确保数据完整性。
- 高效地 进行批量数据处理。
当然,任何工具都有其适用范围。better-sqlite3
和 SQLite 的组合并非万能药。对于需要高并发写入、复杂分布式特性或超大规模数据集的应用,你仍然需要考虑更强大的数据库系统。但对于大多数轻量级 Node.js 应用、工具或开发原型,better-sqlite3
提供了一个无比强大且易用的解决方案,让你能够真正“告别繁琐”,更专注于构建你的核心功能。
现在,是时候在你的 Node.js 项目中引入 better-sqlite3
,亲身体验它带来的开发效率提升和代码简洁性了!