Bun 原生实现了高性能的 SQLite3 驱动。要使用它,从内置的 bun:sqlite 模块导入。
import { Database } from "bun:sqlite";
const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get();{ message: "Hello world" }API 简单、同步且快速。感谢 better-sqlite3 及其贡献者启发了 bun:sqlite 的 API。
特性包括:
- 事务
- 参数(命名和位置参数)
- 预准备语句
- 数据类型转换(
BLOB变为Uint8Array) - 无需 ORM 即可将查询结果映射到类 -
query.as(MyClass) - 任何 JavaScript SQLite 驱动中最快的性能
bigint支持- 多查询语句(例如
SELECT 1; SELECT 2;)在单次调用 database.run(query) 中
对于读查询,bun:sqlite 模块比 better-sqlite3 快约 3-6 倍,比 deno.land/x/sqlite 快 8-9 倍。每个驱动都针对 Northwind Traders 数据集进行了基准测试。查看并运行 基准测试源码。
Database
要打开或创建 SQLite3 数据库:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite");要打开内存数据库:
import { Database } from "bun:sqlite";
// 所有这些都做同样的事情
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");要以 readonly 模式打开:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });如果文件不存在则创建数据库:
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });严格模式
默认情况下,bun:sqlite 要求绑定参数包含 $、: 或 @ 前缀,如果参数缺失则不会抛出错误。
要在参数缺失时抛出错误并允许不带前缀绑定,在 Database 构造函数上设置 strict: true:
import { Database } from "bun:sqlite";
const strict = new Database(":memory:", { strict: true });
// 因为拼写错误抛出错误:
const query = strict.query("SELECT $message;").all({ messag: "Hello world" });
const notStrict = new Database(":memory:");
// 不抛出错误:
notStrict.query("SELECT $message;").all({ messag: "Hello world" });通过 ES 模块导入加载
你也可以使用导入属性加载数据库。
import db from "./mydb.sqlite" with { type: "sqlite" };
console.log(db.query("select * from users LIMIT 1").get());这等同于:
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");.close(throwOnError: boolean = false)
要关闭数据库连接但允许现有查询完成,调用 .close(false):
const db = new Database();
// ... 做一些事情
db.close(false);要关闭数据库并在有任何待处理查询时抛出错误,调用 .close(true):
const db = new Database();
// ... 做一些事情
db.close(true);NOTE
`close(false)` 在数据库被垃圾回收时自动调用。多次调用是安全的,但第一次之后没有效果。using 语句
你可以使用 using 语句确保在 using 块退出时关闭数据库连接。
import { Database } from "bun:sqlite";
{
using db = new Database("mydb.sqlite");
using query = db.query("select 'Hello world' as message;");
console.log(query.get());
}{ message: "Hello world" }.serialize()
bun:sqlite 支持 SQLite 的内置机制用于 序列化 和 反序列化 数据库到内存和从内存。
const olddb = new Database("mydb.sqlite");
const contents = olddb.serialize(); // => Uint8Array
const newdb = Database.deserialize(contents);在内部,.serialize() 调用 sqlite3_serialize。
.query()
在你的 Database 实例上使用 db.query() 方法来 准备 SQL 查询。结果是 Statement 实例,将缓存在 Database 实例上。查询不会执行。
const query = db.query(`select "Hello world" as message`);NOTE
**"缓存"是什么意思?**缓存指的是编译的预准备语句(SQL 字节码),而不是查询结果。当你多次使用相同的 SQL 字符串调用 db.query() 时,Bun 返回相同的缓存 Statement 对象而不是重新编译 SQL。
使用不同参数值重用缓存语句是完全安全的:
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ 工作
query.get(2); // ✓ 也工作 - 参数每次都新鲜绑定
query.get(3); // ✓ 仍然工作当你想要新鲜的 Statement 实例而不缓存时使用 .prepare() 而不是 .query(),例如如果你动态生成 SQL 且不想用一次性查询填充缓存。
// 编译预准备语句而不缓存
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");WAL 模式
SQLite 支持 预写日志模式(WAL),它显著提高性能,特别是在有许多并发读取器和单个写入器的情况下。通常建议为大多数典型应用程序启用 WAL 模式。
要启用 WAL 模式,在应用程序开始时运行这个 pragma 查询:
db.run("PRAGMA journal_mode = WAL;");什么是 WAL 模式?
在 WAL 模式下,对数据库的写入直接写入到单独的文件称为"WAL 文件"(预写日志)。这个文件稍后将集成到主数据库文件中。把它想象为待处理写入的缓冲区。参考 SQLite 文档 获取更详细的概述。
在 macOS 上,WAL 文件默认可能是持久的。这不是 bug,这是 macOS 配置系统版本 SQLite 的方式。
Statements
Statement 是_预准备查询_,这意味着它已被解析并编译成高效的二进制形式。它可以以高性能方式多次执行。
使用 Database 实例上的 .query 方法创建语句。
const query = db.query(`select "Hello world" as message`);查询可以包含参数。这些可以是数字的(?1)或命名的($param 或 :param 或 @param)。
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);值在执行查询时绑定到这些参数。Statement 可以使用几种不同方法执行,每种方法以不同形式返回结果。
绑定值
要将值绑定到语句,传递对象给 .all()、.get()、.run() 或 .values() 方法。
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });你也可以使用位置参数绑定:
const query = db.query(`select ?1;`);
query.all("Hello world");strict: true 让你可以不带前缀绑定值
默认情况下,$、: 和 @ 前缀在绑定值到命名参数时包含。要不带这些前缀绑定,在 Database 构造函数中使用 strict 选项。
import { Database } from "bun:sqlite";
const db = new Database(":memory:", {
// 不带前缀绑定值
strict: true,
});
const query = db.query(`select $message;`);
// strict: true
query.all({ message: "Hello world" });
// strict: false
// query.all({ $message: "Hello world" });.all()
使用 .all() 运行查询并获取结果作为对象数组。
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });[{ message: "Hello world" }]在内部,这调用 sqlite3_reset 并重复调用 sqlite3_step 直到返回 SQLITE_DONE。
.get()
使用 .get() 运行查询并获取第一个结果作为对象。
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });{ $message: "Hello world" }在内部,这调用 sqlite3_reset 然后调用 sqlite3_step 直到不再返回 SQLITE_ROW。如果查询不返回行,则返回 undefined。
.run()
使用 .run() 运行查询并获取 undefined。这对修改模式的查询(例如 CREATE TABLE)或批量写入操作很有用。
const query = db.query(`create table foo;`);
query.run();{
lastInsertRowid: 0,
changes: 0,
}在内部,这调用 sqlite3_reset 并调用 sqlite3_step 一次。当你不关心结果时,不需要遍历所有行。
lastInsertRowid 属性返回最后插入数据库的行的 ID。changes 属性是查询影响的行数。
.as(Class) - 将查询结果映射到类
使用 .as(Class) 运行查询并获取结果作为类的实例。这让你可以附加方法和 getter/setter 到结果。
class Movie {
title: string;
year: number;
get isMarvel() {
return this.title.includes("Marvel");
}
}
const query = db.query("SELECT title, year FROM movies").as(Movie);
const movies = query.all();
const first = query.get();
console.log(movies[0].isMarvel);
console.log(first.isMarvel);true
true作为性能优化,类构造函数不被调用,默认初始化器不运行,私有字段不可访问。这更像使用 Object.create 而不是 new。类的原型分配给对象,方法附加,getter/setter 设置,但构造函数不调用。
数据库列作为属性设置在类实例上。
.iterate() (@@iterator)
使用 .iterate() 运行查询并增量返回结果。这对大型结果集很有用,你想一次处理一行而不将所有结果加载到内存中。
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
console.log(row);
}你也可以使用 @@iterator 协议:
const query = db.query("SELECT * FROM foo");
for (const row of query) {
console.log(row);
}.values()
使用 values() 运行查询并获取所有结果作为数组数组。
const query = db.query(`select $message;`);
query.values({ $message: "Hello world" });
query.values(2);[
[ "Iron Man", 2008 ],
[ "The Avengers", 2012 ],
[ "Ant-Man: Quantumania", 2023 ],
]在内部,这调用 sqlite3_reset 并重复调用 sqlite3_step 直到返回 SQLITE_DONE。
.finalize()
使用 .finalize() 销毁 Statement 并释放与之关联的任何资源。一旦最终化,Statement 不能再次执行。通常,垃圾回收器会为你做这个,但显式最终化可能在性能敏感的应用中有用。
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();.toString()
在 Statement 实例上调用 toString() 打印扩展的 SQL 查询。这对调试很有用。
import { Database } from "bun:sqlite";
// 设置
const query = db.query("SELECT $param;");
console.log(query.toString()); // => "SELECT NULL"
query.run(42);
console.log(query.toString()); // => "SELECT 42"
query.run(365);
console.log(query.toString()); // => "SELECT 365"在内部,这调用 sqlite3_expanded_sql。参数使用最近绑定的值扩展。
参数
查询可以包含参数。这些可以是数字的(?1)或命名的($param 或 :param 或 @param)。执行查询时将值绑定到这些参数:
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
$bar: "bar",
});[{ "$bar": "bar" }]编号(位置)参数也工作:
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");[
{
"?1": "hello",
"?2": "goodbye",
},
];整数
sqlite 支持有符号 64 位整数,但 JavaScript 只支持有符号 52 位整数或使用 bigint 的任意精度整数。
bigint 输入在任何地方都支持,但默认情况下 bun:sqlite 返回整数为 number 类型。如果你需要处理大于 2^53 的整数,在创建 Database 实例时将 safeIntegers 选项设置为 true。这也验证传递给 bun:sqlite 的 bigint 不超过 64 位。
默认情况下,bun:sqlite 返回整数为 number 类型。如果你需要处理大于 2^53 的整数,你可以使用 bigint 类型。
safeIntegers: true
当 safeIntegers 为 true 时,bun:sqlite 将返回整数为 bigint 类型:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: true });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);9007199254741093n当 safeIntegers 为 true 时,如果绑定参数中的 bigint 值超过 64 位,bun:sqlite 将抛出错误:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: true });
db.run("CREATE TABLE test (id INTEGER PRIMARY KEY, value INTEGER)");
const query = db.query("INSERT INTO test (value) VALUES ($value)");
try {
query.run({ $value: BigInt(Number.MAX_SAFE_INTEGER) ** 2n });
} catch (e) {
console.log(e.message);
}BigInt value '81129638414606663681390495662081' is out of rangesafeIntegers: false(默认)
当 safeIntegers 为 false 时,bun:sqlite 将返回整数为 number 类型并截断任何超过 53 位的位:
import { Database } from "bun:sqlite";
const db = new Database(":memory:", { safeIntegers: false });
const query = db.query(`SELECT ${BigInt(Number.MAX_SAFE_INTEGER) + 102n} as max_int`);
const result = query.get();
console.log(result.max_int);9007199254741092事务
事务是一种以_原子_方式执行多个查询的机制;也就是说,要么所有查询都成功,要么都不成功。使用 db.transaction() 方法创建事务:
const insertCat = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insertCat.run(cat);
});在这个阶段,我们还没有插入任何猫!对 db.transaction() 的调用返回一个新函数(insertCats),它_包装_执行查询的函数。
要执行事务,调用这个函数。所有参数将传递给包装函数;包装函数的返回值将由事务函数返回。包装函数也可以访问在执行事务时定义的 this 上下文。
const insert = db.prepare("INSERT INTO cats (name) VALUES ($name)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insert.run(cat);
return cats.length;
});
const count = insertCats([{ $name: "Keanu" }, { $name: "Salem" }, { $name: "Crookshanks" }]);
console.log(`Inserted ${count} cats`);驱动程序将在调用 insertCats 时自动 begin 事务,并在包装函数返回时 commit。如果抛出异常,事务将回滚。异常将像往常一样传播;它不被捕获。
NOTE
**嵌套事务** — 事务函数可以从其他事务函数内部调用。这样做时,内部事务成为 [保存点](https://www.sqlite.org/lang_savepoint.html)。查看嵌套事务示例">
// 设置
import { Database } from "bun:sqlite";
const db = Database.open(":memory:");
db.run("CREATE TABLE expenses (id INTEGER PRIMARY KEY AUTOINCREMENT, note TEXT, dollars INTEGER);");
db.run("CREATE TABLE cats (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, age INTEGER)");
const insertExpense = db.prepare("INSERT INTO expenses (note, dollars) VALUES (?, ?)");
const insert = db.prepare("INSERT INTO cats (name, age) VALUES ($name, $age)");
const insertCats = db.transaction(cats => {
for (const cat of cats) insert.run(cat);
});
const adopt = db.transaction(cats => {
insertExpense.run("adoption fees", 20);
insertCats(cats); // 嵌套事务
});
adopt([
{ $name: "Joey", $age: 2 },
{ $name: "Sally", $age: 4 },
{ $name: "Junior", $age: 1 },
]);事务还带有 deferred、immediate 和 exclusive 版本。
insertCats(cats); // 使用 "BEGIN"
insertCats.deferred(cats); // 使用 "BEGIN DEFERRED"
insertCats.immediate(cats); // 使用 "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // 使用 "BEGIN EXCLUSIVE".loadExtension()
要加载 SQLite 扩展,在你的 Database 实例上调用 .loadExtension(name)
import { Database } from "bun:sqlite";
const db = new Database();
db.loadExtension("myext");NOTE
**MacOS 用户** 默认情况下,macOS 附带 Apple 的专有 SQLite 构建,不支持扩展。要使用扩展,你需要安装普通版本的 SQLite。brew install sqlite
which sqlite # 获取二进制文件路径要指向 bun:sqlite 到新构建,在创建任何 Database 实例之前调用 Database.setCustomSQLite(path)。(在其他操作系统上,这是无操作。)传递路径到 SQLite .dylib 文件,_不是_可执行文件。使用最近版本的 Homebrew 这类似于 /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib。
import { Database } from "bun:sqlite";
Database.setCustomSQLite("/path/to/libsqlite.dylib");
const db = new Database();
db.loadExtension("myext");.fileControl(cmd: number, value: any)
要使用高级 sqlite3_file_control API,在你的 Database 实例上调用 .fileControl(cmd, value)。
import { Database, constants } from "bun:sqlite";
const db = new Database();
// 确保 WAL 模式不是持久的
// 这防止 wal 文件在数据库关闭后 lingering
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);value 可以是:
numberTypedArrayundefined或null
参考
class Database {
constructor(
filename: string,
options?:
| number
| {
readonly?: boolean;
create?: boolean;
readwrite?: boolean;
safeIntegers?: boolean;
strict?: boolean;
},
);
query<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
prepare<ReturnType, ParamsType>(sql: string): Statement<ReturnType, ParamsType>;
run(sql: string, params?: SQLQueryBindings): { lastInsertRowid: number; changes: number };
exec = this.run;
transaction(insideTransaction: (...args: any) => void): CallableFunction & {
deferred: (...args: any) => void;
immediate: (...args: any) => void;
exclusive: (...args: any) => void;
};
close(throwOnError?: boolean): void;
}
class Statement<ReturnType, ParamsType> {
all(...params: ParamsType[]): ReturnType[];
get(...params: ParamsType[]): ReturnType | null;
run(...params: ParamsType[]): {
lastInsertRowid: number;
changes: number;
};
values(...params: ParamsType[]): unknown[][];
finalize(): void; // 销毁语句并清理资源
toString(): string; // 序列化为 SQL
columnNames: string[]; // 结果集的列名
columnTypes: string[]; // 基于第一行实际值的类型(先调用 .get()/.all())
declaredTypes: (string | null)[]; // 来自 CREATE TABLE 模式的类型(先调用 .get()/.all())
paramsCount: number; // 语句期望的参数数量
native: any; // 表示语句的本机对象
as<T>(Class: new (...args: any[]) => T): Statement<T, ParamsType>;
}
type SQLQueryBindings =
| string
| bigint
| TypedArray
| number
| boolean
| null
| Record<string, string | bigint | TypedArray | number | boolean | null>;数据类型
| JavaScript 类型 | SQLite 类型 |
|---|---|
string | TEXT |
number | INTEGER 或 DECIMAL |
boolean | INTEGER (1 或 0) |
Uint8Array | BLOB |
Buffer | BLOB |
bigint | INTEGER |
null | NULL |