該接口設計簡單且高性能,使用標記模板字面量進行查詢,並提供連接池、事務和預准備語句等功能。
import { sql, SQL } from "bun";
// PostgreSQL(默認)
const users = await sql`
SELECT * FROM users
WHERE active = ${true}
LIMIT ${10}
`;
// 使用 MySQL
const mysql = new SQL("mysql://user:pass@localhost:3306/mydb");
const mysqlResults = await mysql`
SELECT * FROM users
WHERE active = ${true}
`;
// 使用 SQLite
const sqlite = new SQL("sqlite://myapp.db");
const sqliteResults = await sqlite`
SELECT * FROM users
WHERE active = ${1}
`;特性
- 標記模板字面量防止 SQL 注入
- 事務
- 命名和位置參數
- 連接池
BigInt支持- SASL 認證支持(SCRAM-SHA-256)、MD5 和明文
- 連接超時
- 返回行作為數據對象、數組數組或 Buffer
- 二進制協議支持使其更快
- TLS 支持(和認證模式)
- 使用環境變量自動配置
數據庫支持
Bun.SQL 為多個數據庫系統提供統一的 API:
PostgreSQL
當出現以下情況時使用 PostgreSQL:
- 連接字符串不匹配 SQLite 或 MySQL 模式(它是回退適配器)
- 連接字符串顯式使用
postgres://或postgresql://協議 - 未提供連接字符串且環境變量指向 PostgreSQL
import { sql } from "bun";
// 如果 DATABASE_URL 未設置或是 PostgreSQL URL 則使用 PostgreSQL
await sql`SELECT ...`;
import { SQL } from "bun";
const pg = new SQL("postgres://user:pass@localhost:5432/mydb");
await pg`SELECT ...`;MySQL
MySQL 支持內置於 Bun.SQL,提供相同的標記模板字面量接口,與 MySQL 5.7+ 和 MySQL 8.0+ 完全兼容:
import { SQL } from "bun";
// MySQL 連接
const mysql = new SQL("mysql://user:password@localhost:3306/database");
const mysql2 = new SQL("mysql2://user:password@localhost:3306/database"); // mysql2 協議也適用
// 使用選項對象
const mysql3 = new SQL({
adapter: "mysql",
hostname: "localhost",
port: 3306,
database: "myapp",
username: "dbuser",
password: "secretpass",
});
// 使用參數 - 自動使用預准備語句
const users = await mysql`SELECT * FROM users WHERE id = ${userId}`;
// 事務與 PostgreSQL 工作方式相同
await mysql.begin(async tx => {
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = ${userId}`;
});
// 批量插入
const newUsers = [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
];
await mysql`INSERT INTO users ${mysql(newUsers)}`;MySQL 連接字符串格式
MySQL 接受各種 URL 格式作為連接字符串:
// 標准 mysql:// 協議
new SQL("mysql://user:pass@localhost:3306/database");
new SQL("mysql://user:pass@localhost/database"); // 默認端口 3306
// mysql2:// 協議(與 mysql2 npm 包兼容)
new SQL("mysql2://user:pass@localhost:3306/database");
// 帶查詢參數
new SQL("mysql://user:pass@localhost/db?ssl=true");
// Unix socket 連接
new SQL("mysql://user:pass@/database?socket=/var/run/mysqld/mysqld.sock");MySQL 特定功能
MySQL 數據庫支持:
- 預准備語句:自動為參數化查詢創建,帶語句緩存
- 二進制協議:為預准備語句和准確的類型處理提供更好的性能
- 多結果集:支持存儲過程返回多個結果集
- 認證插件:支持 mysql_native_password、caching_sha2_password(MySQL 8.0 默認)和 sha256_password
- SSL/TLS 連接:與 PostgreSQL 類似的可配置 SSL 模式
- 連接屬性:客戶端信息發送到服務器用於監控
- 查詢流水線:執行多個預准備語句而無需等待響應
SQLite
SQLite 支持內置於 Bun.SQL,提供相同的標記模板字面量接口:
import { SQL } from "bun";
// 內存數據庫
const memory = new SQL(":memory:");
const memory2 = new SQL("sqlite://:memory:");
// 基於文件的數據庫
const sql1 = new SQL("sqlite://myapp.db");
// 使用選項對象
const sql2 = new SQL({
adapter: "sqlite",
filename: "./data/app.db",
});
// 對於簡單的文件名,顯式指定適配器
const sql3 = new SQL("myapp.db", { adapter: "sqlite" });SQLite 連接字符串格式
SQLite 接受各種 URL 格式作為連接字符串:
// 標准 sqlite:// 協議
new SQL("sqlite://path/to/database.db");
new SQL("sqlite:path/to/database.db"); // 不帶斜槓
// file:// 協議(也被識別為 SQLite)
new SQL("file://path/to/database.db");
new SQL("file:path/to/database.db");
// 特殊 :memory: 數據庫
new SQL(":memory:");
new SQL("sqlite://:memory:");
new SQL("file://:memory:");
// 相對和絕對路徑
new SQL("sqlite://./local.db"); // 相對於當前目錄
new SQL("sqlite://../parent/db.db"); // 父目錄
new SQL("sqlite:///absolute/path.db"); // 絕對路徑
// 帶查詢參數
new SQL("sqlite://data.db?mode=ro"); // 只讀模式
new SQL("sqlite://data.db?mode=rw"); // 讀寫模式(不創建)
new SQL("sqlite://data.db?mode=rwc"); // 讀寫創建模式(默認)NOTE
不帶協議的簡單文件名(如 `"myapp.db"`)需要顯式指定 `{ adapter: "sqlite" }` 以避免與 PostgreSQL 混淆。SQLite 特定選項
SQLite 數據庫支持額外的配置選項:
const sql = new SQL({
adapter: "sqlite",
filename: "app.db",
// SQLite 特定選項
readonly: false, // 以只讀模式打開
create: true, // 如果數據庫不存在則創建
readwrite: true, // 以讀寫模式打開
// 額外的 Bun:sqlite 選項
strict: true, // 啟用嚴格模式
safeIntegers: false, // 對整數使用 JavaScript 數字
});URL 中的查詢參數被解析以設置這些選項:
?mode=ro→readonly: true?mode=rw→readonly: false, create: false?mode=rwc→readonly: false, create: true(默認)
插入數據
你可以直接將 JavaScript 值傳遞給 SQL 模板字面量,轉義將為你處理。
import { sql } from "bun";
// 使用直接值的基本插入
const [user] = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
// 使用對象輔助器獲得更簡潔的語法
const userData = {
name: "Alice",
email: "alice@example.com",
};
const [newUser] = await sql`
INSERT INTO users ${sql(userData)}
RETURNING *
`;
// 展開為:INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')批量插入
你也可以將對象數組傳遞給 SQL 模板字面量,它將被展開為 INSERT INTO ... VALUES ... 語句。
const users = [
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com" },
{ name: "Charlie", email: "charlie@example.com" },
];
await sql`INSERT INTO users ${sql(users)}`;選擇要插入的列
你可以使用 sql(object, ...string) 來選擇要插入的列。每個列必須在對象上定義。
const user = {
name: "Alice",
email: "alice@example.com",
age: 25,
};
await sql`INSERT INTO users ${sql(user, "name", "email")}`;
// 僅插入 name 和 email 列,忽略其他字段查詢結果
默認情況下,Bun 的 SQL 客戶端將查詢結果返回為對象數組,其中每個對象代表一行,列名作為鍵。但是,在某些情況下,你可能希望以不同格式獲取數據。客戶端為此提供了兩種額外方法。
sql``.values() 格式
sql``.values() 方法將行作為值數組而不是對象返回。每行變成一個數組,其中值的順序與查詢中的列順序相同。
const rows = await sql`SELECT * FROM users`.values();
console.log(rows);這返回類似以下內容:
[
["Alice", "alice@example.com"],
["Bob", "bob@example.com"],
];sql``.values() 特別有用,如果查詢結果中返回重復列名。當使用對象(默認)時,最後一個列名用作對象中的鍵,這意味著重復列名會相互覆蓋——但當使用 sql``.values() 時,每列都存在於數組中,因此你可以通過索引訪問重復列的值。
sql``.raw() 格式
.raw() 方法將行作為 Buffer 對象數組返回。這對於處理二進制數據或出於性能原因可能有用。
const rows = await sql`SELECT * FROM users`.raw();
console.log(rows); // [[Buffer, Buffer], [Buffer, Buffer], [Buffer, Buffer]]SQL 片段
數據庫應用程序中的一個常見需求是根據運行時條件動態構建查詢。Bun 提供安全的方法來實現這一點,而不會冒 SQL 注入風險。
動態表名
當你需要動態引用表或模式時,使用 sql() 輔助器確保正確轉義:
// 安全地動態引用表
await sql`SELECT * FROM ${sql("users")}`;
// 帶模式限定
await sql`SELECT * FROM ${sql("public.users")}`;條件查詢
你可以使用 sql() 輔助器構建帶有條件子句的查詢。這允許你創建適應應用程序需求的靈活查詢:
// 可選 WHERE 子句
const filterAge = true;
const minAge = 21;
const ageFilter = sql`AND age > ${minAge}`;
await sql`
SELECT * FROM users
WHERE active = ${true}
${filterAge ? ageFilter : sql``}
`;更新中的動態列
你可以使用 sql(object, ...string) 來選擇要更新的列。每個列必須在對象上定義。如果未通知列,將使用所有鍵來更新行。
await sql`UPDATE users SET ${sql(user, "name", "email")} WHERE id = ${user.id}`;
// 使用對象中的所有鍵來更新行
await sql`UPDATE users SET ${sql(user)} WHERE id = ${user.id}`;動態值和 where in
值列表也可以動態創建,使 where in 查詢也變得簡單。你可以選擇傳遞對象數組並指定使用哪個鍵來創建列表。
await sql`SELECT * FROM users WHERE id IN ${sql([1, 2, 3])}`;
const users = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" },
];
await sql`SELECT * FROM users WHERE id IN ${sql(users, "id")}`;sql.array 輔助器
sql.array 輔助器從 JavaScript 數組創建 PostgreSQL 數組字面量:
// 為 PostgreSQL 創建數組字面量
await sql`INSERT INTO tags (items) VALUES (${sql.array(["red", "blue", "green"])})`;
// 生成:INSERT INTO tags (items) VALUES (ARRAY['red', 'blue', 'green'])
// 也適用於數字數組
await sql`SELECT * FROM products WHERE ids = ANY(${sql.array([1, 2, 3])})`;
// 生成:SELECT * FROM products WHERE ids = ANY(ARRAY[1, 2, 3])NOTE
`sql.array` 僅適用於 PostgreSQL。多維數組和 NULL 元素可能尚不支持。sql``.simple()
PostgreSQL 線路協議支持兩種類型的查詢:"simple"和"extended"。簡單查詢可以包含多個語句但不支持參數,而擴展查詢(默認)支持參數但只允許一個語句。
要在單個查詢中運行多個語句,使用 sql``.simple():
// 一個查詢中的多個語句
await sql`
SELECT 1;
SELECT 2;
`.simple();簡單查詢通常對數據庫遷移和設置腳本有用。
注意,簡單查詢不能使用參數(${value})。如果你需要參數,你必須將查詢拆分為單獨的語句。
文件中的查詢
你可以使用 sql.file 方法從文件讀取查詢並執行它,如果文件包含 $1, $2 等,你可以傳遞參數給查詢。如果未使用參數,每個文件可以執行多個命令。
const result = await sql.file("query.sql", [1, 2, 3]);不安全查詢
你可以使用 sql.unsafe 函數執行原始 SQL 字符串。謹慎使用,因為它不會轉義用戶輸入。如果未使用參數,每個查詢允許執行多個命令。
// 不帶參數的多個命令
const result = await sql.unsafe(`
SELECT ${userColumns} FROM users;
SELECT ${accountColumns} FROM accounts;
`);
// 使用參數(只允許一個命令)
const result = await sql.unsafe("SELECT " + dangerous + " FROM users WHERE id = $1", [id]);執行和取消查詢
Bun 的 SQL 是惰性的,這意味著它只會在被 await 或使用 .execute() 執行時才開始執行。 你可以通過調用查詢對象上的 cancel() 方法來取消當前正在執行的查詢。
const query = sql`SELECT * FROM users`.execute();
setTimeout(() => query.cancel(), 100);
await query;數據庫環境變量
sql 連接參數可以使用環境變量配置。客戶端按特定優先級順序檢查這些變量,並基於連接字符串格式自動檢測數據庫類型。
自動數據庫檢測
當不帶參數使用 Bun.sql() 或帶連接字符串使用 new SQL() 時,適配器基於 URL 格式自動檢測:
MySQL 自動檢測
當連接字符串匹配以下模式時自動選擇 MySQL:
mysql://...- MySQL 協議 URLmysql2://...- MySQL2 協議 URL(兼容別名)
// 這些都自動使用 MySQL(不需要適配器)
const sql1 = new SQL("mysql://user:pass@localhost/mydb");
const sql2 = new SQL("mysql2://user:pass@localhost:3306/mydb");
// 適用於 DATABASE_URL 環境變量
DATABASE_URL="mysql://user:pass@localhost/mydb" bun run app.js
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb" bun run app.jsSQLite 自動檢測
當連接字符串匹配以下模式時自動選擇 SQLite:
:memory:- 內存數據庫sqlite://...- SQLite 協議 URLsqlite:...- 不帶斜槓的 SQLite 協議file://...- 文件協議 URLfile:...- 不帶斜槓的文件協議
// 這些都自動使用 SQLite(不需要適配器)
const sql1 = new SQL(":memory:");
const sql2 = new SQL("sqlite://app.db");
const sql3 = new SQL("file://./database.db");
// 適用於 DATABASE_URL 環境變量
DATABASE_URL=":memory:" bun run app.js
DATABASE_URL="sqlite://myapp.db" bun run app.js
DATABASE_URL="file://./data/app.db" bun run app.jsPostgreSQL 自動檢測
對於不匹配 MySQL 或 SQLite 模式的連接字符串,PostgreSQL 是默認:
# 為這些模式檢測 PostgreSQL
DATABASE_URL="postgres://user:pass@localhost:5432/mydb" bun run app.js
DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" bun run app.js
# 或不匹配 MySQL 或 SQLite 模式的任何 URL
DATABASE_URL="localhost:5432/mydb" bun run app.jsMySQL 環境變量
MySQL 連接可以通過環境變量配置:
# 主連接 URL(首先檢查)
MYSQL_URL="mysql://user:pass@localhost:3306/mydb"
# 備選:帶 MySQL 協議的 DATABASE_URL
DATABASE_URL="mysql://user:pass@localhost:3306/mydb"
DATABASE_URL="mysql2://user:pass@localhost:3306/mydb"如果未提供連接 URL,MySQL 檢查這些單獨參數:
| 環境變量 | 默認值 | 說明 |
|---|---|---|
MYSQL_HOST | localhost | 數據庫主機 |
MYSQL_PORT | 3306 | 數據庫端口 |
MYSQL_USER | root | 數據庫用戶 |
MYSQL_PASSWORD | (空) | 數據庫密碼 |
MYSQL_DATABASE | mysql | 數據庫名稱 |
MYSQL_URL | (空) | MySQL 主連接 URL |
TLS_MYSQL_DATABASE_URL | (空) | SSL/TLS 連接 URL |
PostgreSQL 環境變量
以下環境變量可用於定義 PostgreSQL 連接:
| 環境變量 | 說明 |
|---|---|
POSTGRES_URL | PostgreSQL 主連接 URL |
DATABASE_URL | 備選連接 URL(自動檢測) |
PGURL | 備選連接 URL |
PG_URL | 備選連接 URL |
TLS_POSTGRES_DATABASE_URL | SSL/TLS 連接 URL |
TLS_DATABASE_URL | 備選 SSL/TLS 連接 URL |
如果未提供連接 URL,系統檢查以下單獨參數:
| 環境變量 | 回退變量 | 默認值 | 說明 |
|---|---|---|---|
PGHOST | - | localhost | 數據庫主機 |
PGPORT | - | 5432 | 數據庫端口 |
PGUSERNAME | PGUSER, USER, USERNAME | postgres | 數據庫用戶 |
PGPASSWORD | - | (空) | 數據庫密碼 |
PGDATABASE | - | 用戶名 | 數據庫名稱 |
SQLite 環境變量
當 DATABASE_URL 包含 SQLite 兼容 URL 時,可以通過它配置 SQLite 連接:
# 這些都被識別為 SQLite
DATABASE_URL=":memory:"
DATABASE_URL="sqlite://./app.db"
DATABASE_URL="file:///absolute/path/to/db.sqlite"注意: 使用 SQLite 時,PostgreSQL 特定環境變量(POSTGRES_URL、PGHOST 等)被忽略。
運行時預連接
Bun 可以在啟動時預連接到 PostgreSQL,通過在應用程序代碼運行之前建立數據庫連接來提高性能。這對於減少第一個數據庫查詢的連接延遲很有用。
# 啟用 PostgreSQL 預連接
bun --sql-preconnect index.js
# 適用於 DATABASE_URL 環境變量
DATABASE_URL=postgres://user:pass@localhost:5432/db bun --sql-preconnect index.js
# 可以與其他運行時標志組合
bun --sql-preconnect --hot index.js--sql-preconnect 標志將在啟動時使用配置的環境變量自動建立 PostgreSQL 連接。如果連接失敗,它不會使應用程序崩潰——錯誤將被優雅地處理。
連接選項
你可以通過傳遞選項給 SQL 構造函數來手動配置數據庫連接。選項根據數據庫適配器而異:
MySQL 選項
import { SQL } from "bun";
const sql = new SQL({
// 使用選項對象時 MySQL 必需
adapter: "mysql",
// 連接詳情
hostname: "localhost",
port: 3306,
database: "myapp",
username: "dbuser",
password: "secretpass",
// Unix socket 連接(hostname/port 的替代)
// socket: "/var/run/mysqld/mysqld.sock",
// 連接池設置
max: 20, // 池中最大連接數(默認:10)
idleTimeout: 30, // 30 秒後關閉空閒連接
maxLifetime: 0, // 連接生命周期秒數(0 = 永久)
connectionTimeout: 30, // 建立新連接時的超時
// SSL/TLS 選項
ssl: "prefer", // 或 "disable", "require", "verify-ca", "verify-full"
// tls: {
// rejectUnauthorized: true,
// ca: "path/to/ca.pem",
// key: "path/to/key.pem",
// cert: "path/to/cert.pem",
// },
// 回調
onconnect: client => {
console.log("已連接到 MySQL");
},
onclose: (client, err) => {
if (err) {
console.error("MySQL 連接錯誤:", err);
} else {
console.log("MySQL 連接已關閉");
}
},
});PostgreSQL 選項
import { SQL } from "bun";
const sql = new SQL({
// 連接詳情(適配器自動檢測為 PostgreSQL)
url: "postgres://user:pass@localhost:5432/dbname",
// 備選連接參數
hostname: "localhost",
port: 5432,
database: "myapp",
username: "dbuser",
password: "secretpass",
// 連接池設置
max: 20, // 池中最大連接數
idleTimeout: 30, // 30 秒後關閉空閒連接
maxLifetime: 0, // 連接生命周期秒數(0 = 永久)
connectionTimeout: 30, // 建立新連接時的超時
// SSL/TLS 選項
tls: true,
// tls: {
// rejectUnauthorized: true,
// requestCert: true,
// ca: "path/to/ca.pem",
// key: "path/to/key.pem",
// cert: "path/to/cert.pem",
// checkServerIdentity(hostname, cert) {
// ...
// },
// },
// 回調
onconnect: client => {
console.log("已連接到 PostgreSQL");
},
onclose: client => {
console.log("PostgreSQL 連接已關閉");
},
});SQLite 選項
import { SQL } from "bun";
const sql = new SQL({
// SQLite 必需
adapter: "sqlite",
filename: "./data/app.db", // 或 ":memory:" 用於內存數據庫
// SQLite 特定訪問模式
readonly: false, // 以只讀模式打開
create: true, // 如果數據庫不存在則創建
readwrite: true, // 允許讀寫操作
// SQLite 數據處理
strict: true, // 啟用嚴格模式以獲得更好的類型安全
safeIntegers: false, // 對超出 JS 數字范圍的整數使用 BigInt
// 回調
onconnect: client => {
console.log("SQLite 數據庫已打開");
},
onclose: client => {
console.log("SQLite 數據庫已關閉");
},
});SQLite 連接說明
- 連接池:SQLite 不使用連接池,因為它是基於文件的數據庫。每個
SQL實例代表單個連接。 - 事務:SQLite 通過保存點支持嵌套事務,與 PostgreSQL 類似。
- 並發訪問:SQLite 通過文件鎖處理並發訪問。使用 WAL 模式獲得更好的並發性。
- 內存數據庫:使用
:memory:創建僅在連接生命周期內存在的臨時數據庫。
動態密碼
當客戶端需要使用替代認證方案(如訪問令牌或連接到密碼輪換的數據庫)時,提供同步或異步函數,在連接時解析動態密碼值。
import { SQL } from "bun";
const sql = new SQL(url, {
// 其他連接配置
...
// 數據庫用戶的密碼函數
password: async () => await signer.getAuthToken(),
});SQLite 特定功能
查詢執行
SQLite 同步執行查詢,與使用異步 I/O 的 PostgreSQL 不同。但是,API 保持一致使用 Promise:
const sqlite = new SQL("sqlite://app.db");
// 與 PostgreSQL 工作方式相同,但在底層同步執行
const users = await sqlite`SELECT * FROM users`;
// 參數工作方式相同
const user = await sqlite`SELECT * FROM users WHERE id = ${userId}`;SQLite Pragmas
你可以使用 PRAGMA 語句配置 SQLite 行為:
const sqlite = new SQL("sqlite://app.db");
// 啟用外鍵
await sqlite`PRAGMA foreign_keys = ON`;
// 將日志模式設置為 WAL 以獲得更好的並發性
await sqlite`PRAGMA journal_mode = WAL`;
// 檢查完整性
const integrity = await sqlite`PRAGMA integrity_check`;數據類型差異
SQLite 具有比 PostgreSQL 更靈活的類型系統:
// SQLite 將數據存儲在 5 個存儲類中:NULL、INTEGER、REAL、TEXT、BLOB
const sqlite = new SQL("sqlite://app.db");
// SQLite 對類型更寬松
await sqlite`
CREATE TABLE flexible (
id INTEGER PRIMARY KEY,
data TEXT, -- 可以將數字存儲為字符串
value NUMERIC, -- 可以存儲整數、實數或文本
blob BLOB -- 二進制數據
)
`;
// JavaScript 值自動轉換
await sqlite`INSERT INTO flexible VALUES (${1}, ${"text"}, ${123.45}, ${Buffer.from("binary")})`;事務
要開始新事務,使用 sql.begin。此方法適用於 PostgreSQL 和 SQLite。對於 PostgreSQL,它從池中保留專用連接。對於 SQLite,它在單個連接上開始事務。
BEGIN 命令自動發送,包括你指定的任何可選配置。如果事務期間發生錯誤,將觸發 ROLLBACK 以確保進程順利繼續。
基本事務
await sql.begin(async tx => {
// 此函數中的所有查詢都在事務中運行
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`;
// 如果沒有拋出錯誤,事務自動提交
// 如果發生任何錯誤則回滾
});如果需要,也可以通過從回調函數返回帶查詢的數組來流水線化事務中的請求:
await sql.begin(async tx => {
return [
tx`INSERT INTO users (name) VALUES (${"Alice"})`,
tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`,
];
});保存點
SQL 中的保存點在事務內創建中間檢查點,允許部分回滾而不影響整個操作。它們在復雜事務中很有用,允許錯誤恢復和保持一致的結果。
await sql.begin(async tx => {
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
await tx.savepoint(async sp => {
// 這部分可以單獨回滾
await sp`UPDATE users SET status = 'active'`;
if (someCondition) {
throw new Error("回滾到保存點");
}
});
// 即使保存點回滾也繼續事務
await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});分布式事務
兩階段提交(2PC)是一種分布式事務協議,其中第一階段協調者通過確保數據已寫入並准備提交來准備節點,而第二階段根據協調者的決定最終確定節點提交或回滾。此過程確保數據持久性和正確的鎖管理。
在 PostgreSQL 和 MySQL 中,分布式事務持久化超出其原始會話,允許特權用戶或協調者稍後提交或回滾它們。這支持健壯的分布式事務、恢復過程和管理操作。
每個數據庫系統以不同方式實現分布式事務:
PostgreSQL 通過預准備事務原生支持它們,而 MySQL 使用 XA 事務。
如果分布式事務期間發生任何異常且未被捕獲,系統將自動回滾所有更改。當一切正常進行時,你保留稍後提交或回滾事務的靈活性。
// 開始分布式事務
await sql.beginDistributed("tx1", async tx => {
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
});
// 稍後,提交或回滾
await sql.commitDistributed("tx1");
// 或
await sql.rollbackDistributed("tx1");認證
Bun 支持 SCRAM-SHA-256(SASL)、MD5 和明文認證。推薦 SASL 以獲得更好的安全性。查看 Postgres SASL 認證 獲取更多信息。
SSL 模式概述
PostgreSQL 支持不同的 SSL/TLS 模式來控制如何建立安全連接。這些模式決定連接時的行為和執行的證書驗證級別。
const sql = new SQL({
hostname: "localhost",
username: "user",
password: "password",
ssl: "disable", // | "prefer" | "require" | "verify-ca" | "verify-full"
});| SSL 模式 | 說明 |
|---|---|
disable | 不使用 SSL/TLS。如果服務器需要 SSL 則連接失敗。 |
prefer | 首先嘗試 SSL,如果 SSL 失敗則回退到非 SSL。如果未指定則為默認模式。 |
require | 需要 SSL 但不驗證證書。如果無法建立 SSL 則失敗。 |
verify-ca | 驗證服務器證書由受信任的 CA 簽名。如果驗證失敗則失敗。 |
verify-full | 最安全的模式。驗證證書和主機名匹配。防止不受信任的證書和 MITM 攻擊。 |
與連接字符串一起使用
SSL 模式也可以在連接字符串中指定:
// 使用 prefer 模式
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=prefer");
// 使用 verify-full 模式
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=verify-full");連接池
Bun 的 SQL 客戶端自動管理連接池,這是一個用於多個查詢重用的數據庫連接池。這有助於減少為每個查詢建立和關閉連接的開銷,還有助於管理到數據庫的並發連接數。
const sql = new SQL({
// 池配置
max: 20, // 最大 20 個並發連接
idleTimeout: 30, // 30 秒後關閉空閒連接
maxLifetime: 3600, // 最大連接生命周期 1 小時
connectionTimeout: 10, // 連接超時 10 秒
});在發出查詢之前不會建立連接。
const sql = Bun.SQL(); // 不創建連接
await sql`...`; // 池啟動直到達到最大值(如果可能),使用第一個可用連接
await sql`...`; // 重用之前的連接
// 現在同時使用兩個連接
await Promise.all([
sql`INSERT INTO users ${sql({ name: "Alice" })}`,
sql`UPDATE users SET name = ${user.name} WHERE id = ${user.id}`,
]);
await sql.close(); // 等待所有查詢完成並關閉池中所有連接
await sql.close({ timeout: 5 }); // 等待 5 秒並關閉池中所有連接
await sql.close({ timeout: 0 }); // 立即關閉池中所有連接保留連接
Bun 允許你從池中保留連接,並返回包裝單個連接的客戶端。這可用於在隔離連接上運行查詢。
// 從池獲取獨佔連接
const reserved = await sql.reserve();
try {
await reserved`INSERT INTO users (name) VALUES (${"Alice"})`;
} finally {
// 重要:將連接釋放回池
reserved.release();
}
// 或使用 Symbol.dispose
{
using reserved = await sql.reserve();
await reserved`SELECT 1`;
} // 自動釋放預准備語句
默認情況下,Bun 的 SQL 客戶端自動為可以推斷查詢是靜態的查詢創建命名預准備語句。這提供更好的性能。但是,你可以通過在連接選項中設置 prepare: false 來更改此行為:
const sql = new SQL({
// ... 其他選項 ...
prepare: false, // 禁用服務器上持久化的命名預准備語句
});當設置 prepare: false 時:
查詢仍然使用"extended"協議執行,但它們使用 未命名預准備語句 執行,未命名預准備語句僅持續到下一個指定未命名語句為目標的 Parse 語句發出。
- 參數綁定仍然安全防止 SQL 注入
- 每個查詢由服務器從頭開始解析和計劃
- 查詢不會 流水線化
你可能希望使用 prepare: false 當:
- 在事務模式下使用 PGBouncer(盡管自 PGBouncer 1.21.0 起,當正確配置時支持協議級命名預准備語句)
- 調試查詢執行計劃
- 使用需要頻繁重新生成查詢計劃的動態 SQL
- 每個查詢不支持多個命令(除非你使用
sql``.simple())
注意,禁用預准備語句可能會影響頻繁使用不同參數執行的查詢的性能,因為服務器需要從頭開始解析和計劃每個查詢。
錯誤處理
客戶端為不同的失敗場景提供類型化錯誤。錯誤是特定於數據庫的並擴展自基本錯誤類:
錯誤類
import { SQL } from "bun";
try {
await sql`SELECT * FROM users`;
} catch (error) {
if (error instanceof SQL.PostgresError) {
// PostgreSQL 特定錯誤
console.log(error.code); // PostgreSQL 錯誤代碼
console.log(error.detail); // 詳細錯誤消息
console.log(error.hint); // PostgreSQL 的有用提示
} else if (error instanceof SQL.SQLiteError) {
// SQLite 特定錯誤
console.log(error.code); // SQLite 錯誤代碼(例如 "SQLITE_CONSTRAINT")
console.log(error.errno); // SQLite 錯誤號
console.log(error.byteOffset); // SQL 語句中的字節偏移(如果可用)
} else if (error instanceof SQL.SQLError) {
// 通用 SQL 錯誤(基類)
console.log(error.message);
}
}PostgreSQL 特定錯誤代碼
PostgreSQL 連接錯誤
| 連接錯誤 | 說明 |
|---|---|
ERR_POSTGRES_CONNECTION_CLOSED | 連接被終止或從未建立 |
ERR_POSTGRES_CONNECTION_TIMEOUT | 在超時期限內無法建立連接 |
ERR_POSTGRES_IDLE_TIMEOUT | 連接因不活動而關閉 |
ERR_POSTGRES_LIFETIME_TIMEOUT | 連接超過最大生命周期 |
ERR_POSTGRES_TLS_NOT_AVAILABLE | SSL/TLS 連接不可用 |
ERR_POSTGRES_TLS_UPGRADE_FAILED | 無法將連接升級到 SSL/TLS |
認證錯誤
| 認證錯誤 | 說明 |
|---|---|
ERR_POSTGRES_AUTHENTICATION_FAILED_PBKDF2 | 密碼認證失敗 |
ERR_POSTGRES_UNKNOWN_AUTHENTICATION_METHOD | 服務器請求未知認證方法 |
ERR_POSTGRES_UNSUPPORTED_AUTHENTICATION_METHOD | 服務器請求不支持的認證方法 |
ERR_POSTGRES_INVALID_SERVER_KEY | 認證期間服務器密鑰無效 |
ERR_POSTGRES_INVALID_SERVER_SIGNATURE | 服務器簽名無效 |
ERR_POSTGRES_SASL_SIGNATURE_INVALID_BASE64 | 無效 SASL 簽名編碼 |
ERR_POSTGRES_SASL_SIGNATURE_MISMATCH | SASL 簽名驗證失敗 |
查詢錯誤
| 查詢錯誤 | 說明 |
|---|---|
ERR_POSTGRES_SYNTAX_ERROR | 無效 SQL 語法(擴展自 SyntaxError) |
ERR_POSTGRES_SERVER_ERROR | PostgreSQL 服務器的一般錯誤 |
ERR_POSTGRES_INVALID_QUERY_BINDING | 無效參數綁定 |
ERR_POSTGRES_QUERY_CANCELLED | 查詢被取消 |
ERR_POSTGRES_NOT_TAGGED_CALL | 查詢調用時不帶標記調用 |
數據類型錯誤
| 數據類型錯誤 | 說明 |
|---|---|
ERR_POSTGRES_INVALID_BINARY_DATA | 無效二進制數據格式 |
ERR_POSTGRES_INVALID_BYTE_SEQUENCE | 無效字節序列 |
ERR_POSTGRES_INVALID_BYTE_SEQUENCE_FOR_ENCODING | 編碼錯誤 |
ERR_POSTGRES_INVALID_CHARACTER | 數據中的無效字符 |
ERR_POSTGRES_OVERFLOW | 數字溢出 |
ERR_POSTGRES_UNSUPPORTED_BYTEA_FORMAT | 不支持的二進制格式 |
ERR_POSTGRES_UNSUPPORTED_INTEGER_SIZE | 不支持的整數大小 |
ERR_POSTGRES_MULTIDIMENSIONAL_ARRAY_NOT_SUPPORTED_YET | 尚不支持多維數組 |
ERR_POSTGRES_NULLS_IN_ARRAY_NOT_SUPPORTED_YET | 尚不支持數組中的 NULL 值 |
協議錯誤
| 協議錯誤 | 說明 |
|---|---|
ERR_POSTGRES_EXPECTED_REQUEST | 期望客戶端請求 |
ERR_POSTGRES_EXPECTED_STATEMENT | 期望預准備語句 |
ERR_POSTGRES_INVALID_BACKEND_KEY_DATA | 無效後端密鑰數據 |
ERR_POSTGRES_INVALID_MESSAGE | 無效協議消息 |
ERR_POSTGRES_INVALID_MESSAGE_LENGTH | 無效消息長度 |
ERR_POSTGRES_UNEXPECTED_MESSAGE | 意外消息類型 |
事務錯誤
| 事務錯誤 | 說明 |
|---|---|
ERR_POSTGRES_UNSAFE_TRANSACTION | 檢測到不安全的事務操作 |
ERR_POSTGRES_INVALID_TRANSACTION_STATE | 無效事務狀態 |
SQLite 特定錯誤
SQLite 錯誤提供與 SQLite 標准錯誤代碼對應的錯誤代碼和編號:
常見 SQLite 錯誤代碼
| 錯誤代碼 | errno | 說明 |
|---|---|---|
SQLITE_CONSTRAINT | 19 | 約束違反(UNIQUE、CHECK、NOT NULL 等) |
SQLITE_BUSY | 5 | 數據庫被鎖定 |
SQLITE_LOCKED | 6 | 數據庫中的表被鎖定 |
SQLITE_READONLY | 8 | 嘗試寫入只讀數據庫 |
SQLITE_IOERR | 10 | 磁盤 I/O 錯誤 |
SQLITE_CORRUPT | 11 | 數據庫磁盤映像損壞 |
SQLITE_FULL | 13 | 數據庫或磁盤已滿 |
SQLITE_CANTOPEN | 14 | 無法打開數據庫文件 |
SQLITE_PROTOCOL | 15 | 數據庫鎖協議錯誤 |
SQLITE_SCHEMA | 17 | 數據庫模式已更改 |
SQLITE_TOOBIG | 18 | 字符串或 BLOB 超出大小限制 |
SQLITE_MISMATCH | 20 | 數據類型不匹配 |
SQLITE_MISUSE | 21 | 庫使用不正確 |
SQLITE_AUTH | 23 | 授權被拒絕 |
錯誤處理示例:
const sqlite = new SQL("sqlite://app.db");
try {
await sqlite`INSERT INTO users (id, name) VALUES (1, 'Alice')`;
await sqlite`INSERT INTO users (id, name) VALUES (1, 'Bob')`; // 重復 ID
} catch (error) {
if (error instanceof SQL.SQLiteError) {
if (error.code === "SQLITE_CONSTRAINT") {
console.log("約束違反:", error.message);
// 處理唯一約束違反
}
}
}數字和 BigInt
Bun 的 SQL 客戶端包括對超出 53 位整數范圍的大數字的特殊處理。工作原理如下:
import { sql } from "bun";
const [{ x, y }] = await sql`SELECT 9223372036854777 as x, 12345 as y`;
console.log(typeof x, x); // "string" "9223372036854777"
console.log(typeof y, y); // "number" 12345使用 BigInt 而不是字符串
如果你需要大數字作為 BigInt 而不是字符串,你可以通過在初始化 SQL 客戶端時將 bigint 選項設置為 true 來啟用:
const sql = new SQL({
bigint: true,
});
const [{ x }] = await sql`SELECT 9223372036854777 as x`;
console.log(typeof x, x); // "bigint" 9223372036854777n路線圖
還有一些事情我們尚未完成。
- 通過
--db-preconnectBun CLI 標志進行連接預加載 - 列名轉換(例如
snake_case到camelCase)。這主要受限於使用 WebKit 的WTF::String在 C++ 中更改大小寫的 unicode 感知實現。 - 列類型轉換
數據庫特定功能
認證方法
MySQL 支持多種自動協商的認證插件:
mysql_native_password- 傳統 MySQL 認證,廣泛兼容caching_sha2_password- MySQL 8.0+ 默認,更安全帶 RSA 密鑰交換sha256_password- 基於 SHA-256 的認證
客戶端自動處理服務器請求時的認證插件切換,包括非 SSL 連接上的安全密碼交換。
預准備語句和性能
MySQL 對所有參數化查詢使用服務器端預准備語句:
// 這自動在服務器上創建預准備語句
const user = await mysql`SELECT * FROM users WHERE id = ${userId}`;
// 預准備語句被緩存並重復用於相同查詢
for (const id of userIds) {
// 相同的預准備語句被重用
await mysql`SELECT * FROM users WHERE id = ${id}`;
}
// 查詢流水線 - 多個語句發送而無需等待
const [users, orders, products] = await Promise.all([
mysql`SELECT * FROM users WHERE active = ${true}`,
mysql`SELECT * FROM orders WHERE status = ${"pending"}`,
mysql`SELECT * FROM products WHERE in_stock = ${true}`,
]);多結果集
MySQL 可以從多語句查詢返回多個結果集:
const mysql = new SQL("mysql://user:pass@localhost/mydb");
// 使用 simple() 方法的多語句查詢
const multiResults = await mysql`
SELECT * FROM users WHERE id = 1;
SELECT * FROM orders WHERE user_id = 1;
`.simple();字符集和排序規則
Bun.SQL 自動為 MySQL 連接使用 utf8mb4 字符集,確保完整的 Unicode 支持包括表情符號。這是現代 MySQL 應用程序推薦的字符集。
連接屬性
Bun 自動發送客戶端信息到 MySQL 以更好地監控:
// 這些屬性自動發送:
// _client_name: "Bun"
// _client_version: <bun 版本>
// 你可以在 MySQL 的 performance_schema.session_connect_attrs 中看到這些類型處理
MySQL 類型自動轉換為 JavaScript 類型:
| MySQL 類型 | JavaScript 類型 | 說明 |
|---|---|---|
| INT, TINYINT, MEDIUMINT | number | 在安全整數范圍內 |
| BIGINT | string, number 或 BigInt | 如果值適合 i32/u32 大小則為 number,否則為 string 或 BigInt,基於 bigint 選項 |
| DECIMAL, NUMERIC | string | 保留精度 |
| FLOAT, DOUBLE | number | |
| DATE | Date | JavaScript Date 對象 |
| DATETIME, TIMESTAMP | Date | 帶時區處理 |
| TIME | number | 微秒總數 |
| YEAR | number | |
| CHAR, VARCHAR, VARSTRING, STRING | string | |
| TINY TEXT, MEDIUM TEXT, TEXT, LONG TEXT | string | |
| TINY BLOB, MEDIUM BLOB, BLOG, LONG BLOB | string | BLOB 類型是 TEXT 類型的別名 |
| JSON | object/array | 自動解析 |
| BIT(1) | boolean | MySQL 中的 BIT(1) |
| GEOMETRY | string | 幾何數據 |
與 PostgreSQL 的差異
雖然 API 是統一的,但有一些行為差異:
- 參數佔位符:MySQL 內部使用
?但 Bun 自動轉換$1, $2樣式 - RETURNING 子句:MySQL 不支持 RETURNING;使用
result.lastInsertRowid或單獨的 SELECT - 數組類型:MySQL 沒有像 PostgreSQL 那樣的原生數組類型
MySQL 特定功能
我們尚未實現 LOAD DATA INFILE 支持
PostgreSQL 特定功能
我們尚未實現這些:
COPY支持LISTEN支持NOTIFY支持
我們也尚未實現一些更不常見的功能:
- GSSAPI 認證
SCRAM-SHA-256-PLUS支持- Point 和 PostGIS 類型
- 所有多維整數數組類型(僅支持少數類型)
常見模式和最佳實踐
使用 MySQL 結果集
// INSERT 後獲取插入 ID
const result = await mysql`INSERT INTO users (name) VALUES (${"Alice"})`;
console.log(result.lastInsertRowid); // MySQL 的 LAST_INSERT_ID()
// 處理受影響的行
const updated = await mysql`UPDATE users SET active = ${false} WHERE age < ${18}`;
console.log(updated.affectedRows); // 更新的行數
// 使用 MySQL 特定函數
const now = await mysql`SELECT NOW() as current_time`;
const uuid = await mysql`SELECT UUID() as id`;MySQL 錯誤處理
try {
await mysql`INSERT INTO users (email) VALUES (${"duplicate@email.com"})`;
} catch (error) {
if (error.code === "ER_DUP_ENTRY") {
console.log("檢測到重復條目");
} else if (error.code === "ER_ACCESS_DENIED_ERROR") {
console.log("訪問被拒絕");
} else if (error.code === "ER_BAD_DB_ERROR") {
console.log("數據庫不存在");
}
// MySQL 錯誤代碼與 mysql/mysql2 包兼容
}MySQL 性能提示
- 使用連接池:根據你的工作負載設置適當的
max池大小 - 啟用預准備語句:它們默認啟用並提高性能
- 對批量操作使用事務:在事務中分組相關查詢
- 正確索引:MySQL 嚴重依賴索引進行查詢性能
- 使用
utf8mb4字符集:它默認設置並處理所有 Unicode 字符
常見問題
為什麼這是 `Bun.sql` 而不是 `Bun.postgres`?
計劃是將來添加更多數據庫驅動。現在添加了 MySQL 支持,這個統一的 API 支持 PostgreSQL、MySQL 和 SQLite。
我如何知道使用哪個數據庫適配器?
適配器從連接字符串自動檢測:
- 以 `mysql://` 或 `mysql2://` 開頭的 URL 使用 MySQL
- 匹配 SQLite 模式(`:memory:`、`sqlite://`、`file://`)的 URL 使用 SQLite
- 其他所有內容默認為 PostgreSQL
支持 MySQL 存儲過程嗎?
是的,存儲過程完全支持包括 OUT 參數和多個結果集:
```ts
// 調用存儲過程
const results = await mysql`CALL GetUserStats(${userId}, @total_orders)`;
// 獲取 OUT 參數
const outParam = await mysql`SELECT @total_orders as total`;
```
我可以使用 MySQL 特定的 SQL 語法嗎?
是的,你可以使用任何 MySQL 特定語法:
```ts
// MySQL 特定語法工作正常
await mysql`SET @user_id = ${userId}`;
await mysql`SHOW TABLES`;
await mysql`DESCRIBE users`;
await mysql`EXPLAIN SELECT * FROM users WHERE id = ${id}`;
```
為什麼不直接使用現有庫?
像 postgres.js、pg 和 node-postgres 這樣的 npm 包也可以在 Bun 中使用。它們是很好的選擇。
有兩個原因:
- 我們認為開發者有一個內置於 Bun 的數據庫驅動更簡單。你花在庫購物上的時間本可以用來構建你的應用程序。
- 我們利用一些 JavaScriptCore 引擎內部來使其更快地創建對象,這在庫中很難實現
致謝
非常感謝 @porsager 的 postgres.js 為 API 接口提供的靈感。