Skip to content

Bun 原生實現了高性能的 SQLite3 驅動。要使用它,從內置的 bun:sqlite 模塊導入。

ts
import { Database } from "bun:sqlite";

const db = new Database(":memory:");
const query = db.query("select 'Hello world' as message;");
query.get();
txt
{ 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 數據庫:

ts
import { Database } from "bun:sqlite";

const db = new Database("mydb.sqlite");

要打開內存數據庫:

ts
import { Database } from "bun:sqlite";

// 所有這些都做同樣的事情
const db = new Database(":memory:");
const db = new Database();
const db = new Database("");

要以 readonly 模式打開:

ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { readonly: true });

如果文件不存在則創建數據庫:

ts
import { Database } from "bun:sqlite";
const db = new Database("mydb.sqlite", { create: true });

嚴格模式

默認情況下,bun:sqlite 要求綁定參數包含 $:@ 前綴,如果參數缺失則不會拋出錯誤。

要在參數缺失時拋出錯誤並允許不帶前綴綁定,在 Database 構造函數上設置 strict: true

ts
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 模塊導入加載

你也可以使用導入屬性加載數據庫。

ts
import db from "./mydb.sqlite" with { type: "sqlite" };

console.log(db.query("select * from users LIMIT 1").get());

這等同於:

ts
import { Database } from "bun:sqlite";
const db = new Database("./mydb.sqlite");

.close(throwOnError: boolean = false)

要關閉數據庫連接但允許現有查詢完成,調用 .close(false)

ts
const db = new Database();
// ... 做一些事情
db.close(false);

要關閉數據庫並在有任何待處理查詢時拋出錯誤,調用 .close(true)

ts
const db = new Database();
// ... 做一些事情
db.close(true);

NOTE

`close(false)` 在數據庫被垃圾回收時自動調用。多次調用是安全的,但第一次之後沒有效果。

using 語句

你可以使用 using 語句確保在 using 塊退出時關閉數據庫連接。

ts
import { Database } from "bun:sqlite";

{
  using db = new Database("mydb.sqlite");
  using query = db.query("select 'Hello world' as message;");
  console.log(query.get());
}
txt
{ message: "Hello world" }

.serialize()

bun:sqlite 支持 SQLite 的內置機制用於 序列化反序列化 數據庫到內存和從內存。

ts
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 實例上。查詢不會執行。

ts
const query = db.query(`select "Hello world" as message`);

NOTE

**"緩存"是什麼意思?**

緩存指的是編譯的預准備語句(SQL 字節碼),而不是查詢結果。當你多次使用相同的 SQL 字符串調用 db.query() 時,Bun 返回相同的緩存 Statement 對象而不是重新編譯 SQL。

使用不同參數值重用緩存語句是完全安全的:

ts
const query = db.query("SELECT * FROM users WHERE id = ?");
query.get(1); // ✓ 工作
query.get(2); // ✓ 也工作 - 參數每次都新鮮綁定
query.get(3); // ✓ 仍然工作

當你想要新鮮的 Statement 實例而不緩存時使用 .prepare() 而不是 .query(),例如如果你動態生成 SQL 且不想用一次性查詢填充緩存。

ts
// 編譯預准備語句而不緩存
const query = db.prepare("SELECT * FROM foo WHERE bar = ?");

WAL 模式

SQLite 支持 預寫日志模式(WAL),它顯著提高性能,特別是在有許多並發讀取器和單個寫入器的情況下。通常建議為大多數典型應用程序啟用 WAL 模式。

要啟用 WAL 模式,在應用程序開始時運行這個 pragma 查詢:

ts
db.run("PRAGMA journal_mode = WAL;");

什麼是 WAL 模式?

在 WAL 模式下,對數據庫的寫入直接寫入到單獨的文件稱為"WAL 文件"(預寫日志)。這個文件稍後將集成到主數據庫文件中。把它想象為待處理寫入的緩沖區。參考 SQLite 文檔 獲取更詳細的概述。

在 macOS 上,WAL 文件默認可能是持久的。這不是 bug,這是 macOS 配置系統版本 SQLite 的方式。


Statements

Statement 是_預准備查詢_,這意味著它已被解析並編譯成高效的二進制形式。它可以以高性能方式多次執行。

使用 Database 實例上的 .query 方法創建語句。

ts
const query = db.query(`select "Hello world" as message`);

查詢可以包含參數。這些可以是數字的(?1)或命名的($param:param@param)。

ts
const query = db.query(`SELECT ?1, ?2;`);
const query = db.query(`SELECT $param1, $param2;`);

值在執行查詢時綁定到這些參數。Statement 可以使用幾種不同方法執行,每種方法以不同形式返回結果。

綁定值

要將值綁定到語句,傳遞對象給 .all().get().run().values() 方法。

ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });

你也可以使用位置參數綁定:

ts
const query = db.query(`select ?1;`);
query.all("Hello world");

strict: true 讓你可以不帶前綴綁定值

默認情況下,$:@ 前綴在綁定值到命名參數時包含。要不帶這些前綴綁定,在 Database 構造函數中使用 strict 選項。

ts
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() 運行查詢並獲取結果作為對象數組。

ts
const query = db.query(`select $message;`);
query.all({ $message: "Hello world" });
txt
[{ message: "Hello world" }]

在內部,這調用 sqlite3_reset 並重復調用 sqlite3_step 直到返回 SQLITE_DONE

.get()

使用 .get() 運行查詢並獲取第一個結果作為對象。

ts
const query = db.query(`select $message;`);
query.get({ $message: "Hello world" });
txt
{ $message: "Hello world" }

在內部,這調用 sqlite3_reset 然後調用 sqlite3_step 直到不再返回 SQLITE_ROW。如果查詢不返回行,則返回 undefined

.run()

使用 .run() 運行查詢並獲取 undefined。這對修改模式的查詢(例如 CREATE TABLE)或批量寫入操作很有用。

ts
const query = db.query(`create table foo;`);
query.run();
txt
{
  lastInsertRowid: 0,
  changes: 0,
}

在內部,這調用 sqlite3_reset 並調用 sqlite3_step 一次。當你不關心結果時,不需要遍歷所有行。

lastInsertRowid 屬性返回最後插入數據庫的行的 ID。changes 屬性是查詢影響的行數。

.as(Class) - 將查詢結果映射到類

使用 .as(Class) 運行查詢並獲取結果作為類的實例。這讓你可以附加方法和 getter/setter 到結果。

ts
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);
txt
true
true

作為性能優化,類構造函數不被調用,默認初始化器不運行,私有字段不可訪問。這更像使用 Object.create 而不是 new。類的原型分配給對象,方法附加,getter/setter 設置,但構造函數不調用。

數據庫列作為屬性設置在類實例上。

.iterate() (@@iterator)

使用 .iterate() 運行查詢並增量返回結果。這對大型結果集很有用,你想一次處理一行而不將所有結果加載到內存中。

ts
const query = db.query("SELECT * FROM foo");
for (const row of query.iterate()) {
  console.log(row);
}

你也可以使用 @@iterator 協議:

ts
const query = db.query("SELECT * FROM foo");
for (const row of query) {
  console.log(row);
}

.values()

使用 values() 運行查詢並獲取所有結果作為數組數組。

ts
const query = db.query(`select $message;`);

query.values({ $message: "Hello world" });
query.values(2);
txt
[
  [ "Iron Man", 2008 ],
  [ "The Avengers", 2012 ],
  [ "Ant-Man: Quantumania", 2023 ],
]

在內部,這調用 sqlite3_reset 並重復調用 sqlite3_step 直到返回 SQLITE_DONE

.finalize()

使用 .finalize() 銷毀 Statement 並釋放與之關聯的任何資源。一旦最終化,Statement 不能再次執行。通常,垃圾回收器會為你做這個,但顯式最終化可能在性能敏感的應用中有用。

ts
const query = db.query("SELECT title, year FROM movies");
const movies = query.all();
query.finalize();

.toString()

Statement 實例上調用 toString() 打印擴展的 SQL 查詢。這對調試很有用。

ts
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)。執行查詢時將值綁定到這些參數:

ts
const query = db.query("SELECT * FROM foo WHERE bar = $bar");
const results = query.all({
  $bar: "bar",
});
txt
[{ "$bar": "bar" }]

編號(位置)參數也工作:

ts
const query = db.query("SELECT ?1, ?2");
const results = query.all("hello", "goodbye");
txt
[
	{
		"?1": "hello",
		"?2": "goodbye",
	},
];

整數

sqlite 支持有符號 64 位整數,但 JavaScript 只支持有符號 52 位整數或使用 bigint 的任意精度整數。

bigint 輸入在任何地方都支持,但默認情況下 bun:sqlite 返回整數為 number 類型。如果你需要處理大於 2^53 的整數,在創建 Database 實例時將 safeIntegers 選項設置為 true。這也驗證傳遞給 bun:sqlitebigint 不超過 64 位。

默認情況下,bun:sqlite 返回整數為 number 類型。如果你需要處理大於 2^53 的整數,你可以使用 bigint 類型。

safeIntegers: true

safeIntegerstrue 時,bun:sqlite 將返回整數為 bigint 類型:

ts
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);
txt
9007199254741093n

safeIntegerstrue 時,如果綁定參數中的 bigint 值超過 64 位,bun:sqlite 將拋出錯誤:

ts
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);
}
txt
BigInt value '81129638414606663681390495662081' is out of range

safeIntegers: false(默認)

safeIntegersfalse 時,bun:sqlite 將返回整數為 number 類型並截斷任何超過 53 位的位:

ts
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);
txt
9007199254741092

事務

事務是一種以_原子_方式執行多個查詢的機制;也就是說,要麼所有查詢都成功,要麼都不成功。使用 db.transaction() 方法創建事務:

ts
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 上下文。

ts
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)。

查看嵌套事務示例">

ts
// 設置
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 },
]);

事務還帶有 deferredimmediateexclusive 版本。

ts
insertCats(cats); // 使用 "BEGIN"
insertCats.deferred(cats); // 使用 "BEGIN DEFERRED"
insertCats.immediate(cats); // 使用 "BEGIN IMMEDIATE"
insertCats.exclusive(cats); // 使用 "BEGIN EXCLUSIVE"

.loadExtension()

要加載 SQLite 擴展,在你的 Database 實例上調用 .loadExtension(name)

ts
import { Database } from "bun:sqlite";

const db = new Database();
db.loadExtension("myext");

NOTE

**MacOS 用戶** 默認情況下,macOS 附帶 Apple 的專有 SQLite 構建,不支持擴展。要使用擴展,你需要安裝普通版本的 SQLite。
bash
brew install sqlite
which sqlite # 獲取二進制文件路徑

要指向 bun:sqlite 到新構建,在創建任何 Database 實例之前調用 Database.setCustomSQLite(path)。(在其他操作系統上,這是無操作。)傳遞路徑到 SQLite .dylib 文件,_不是_可執行文件。使用最近版本的 Homebrew 這類似於 /opt/homebrew/Cellar/sqlite/<version>/libsqlite3.dylib

ts
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)

ts
import { Database, constants } from "bun:sqlite";

const db = new Database();
// 確保 WAL 模式不是持久的
// 這防止 wal 文件在數據庫關閉後 lingering
db.fileControl(constants.SQLITE_FCNTL_PERSIST_WAL, 0);

value 可以是:

  • number
  • TypedArray
  • undefinednull

參考

ts
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 類型
stringTEXT
numberINTEGERDECIMAL
booleanINTEGER (1 或 0)
Uint8ArrayBLOB
BufferBLOB
bigintINTEGER
nullNULL

Bun學習網由www.bunjs.com.cn整理維護