Bun 提供原生綁定,透過現代、基於 Promise 的 API 來使用 PostgreSQL 資料庫。此介面設計簡潔且高效能,使用標籤樣板字串來進行查詢,並提供連線池、交易和預先編譯語句等功能。
import { sql } from "bun";
const users = await sql`
SELECT * FROM users
WHERE active = ${true}
LIMIT ${10}
`;
// Select with multiple conditions
const activeUsers = await sql`
SELECT *
FROM users
WHERE active = ${true}
AND age >= ${18}
`;
功能特色
標籤樣板字串,以防止 SQL 注入 | |
交易 | |
具名與位置參數 | |
連線池 | |
| |
SASL 驗證支援 (SCRAM-SHA-256)、MD5 和明文 | |
連線逾時 | |
以資料物件、陣列的陣列或 | |
二進制協定支援使其速度更快 | |
TLS 支援 (和驗證模式) | |
透過環境變數自動設定 |
插入資料
您可以將 JavaScript 值直接傳遞給 SQL 樣板字串,系統會為您處理跳脫字元。
import { sql } from "bun";
// Basic insert with direct values
const [user] = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
// Using object helper for cleaner syntax
const userData = {
name: "Alice",
email: "alice@example.com",
};
const [newUser] = await sql`
INSERT INTO users ${sql(userData)}
RETURNING *
`;
// Expands to: 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")}`;
// Only inserts name and email columns, ignoring other fields
查詢結果
預設情況下,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()
輔助程式來確保正確的跳脫字元
// Safely reference tables dynamically
await sql`SELECT * FROM ${sql("users")}`;
// With schema qualification
await sql`SELECT * FROM ${sql("public.users")}`;
條件式查詢
您可以使用 sql()
輔助程式來建置具有條件子句的查詢。這讓您可以建立彈性的查詢,以適應您的應用程式需求
// Optional WHERE clauses
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}`;
// uses all keys from the object to update the row
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``.simple()
PostgreSQL 線路協定支援兩種查詢類型:「簡單」和「擴充」。簡單查詢可以包含多個陳述式,但不支援參數,而擴充查詢 (預設值) 支援參數,但只允許一個陳述式。
若要在單一查詢中執行多個陳述式,請使用 sql``.simple()
// Multiple statements in one query
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 字串。請謹慎使用此功能,因為它不會跳脫使用者輸入。如果未使用參數,則允許每個查詢執行多個命令。
// Multiple commands without parameters
const result = await sql.unsafe(`
SELECT ${userColumns} FROM users;
SELECT ${accountColumns} FROM accounts;
`);
// Using parameters (only one command is allowed)
const result = await sql.unsafe(
"SELECT " + dangerous + " FROM users WHERE id = $1",
[id],
);
什麼是 SQL 注入?

執行與取消查詢
Bun 的 SQL 是延遲載入的,這表示只有在等待或使用 .execute()
執行時才會開始執行。 您可以呼叫查詢物件上的 cancel()
方法來取消目前正在執行的查詢。
const query = await sql`SELECT * FROM users`.execute();
setTimeout(() => query.cancel(), 100);
await query;
資料庫環境變數
sql
連線參數可以使用環境變數設定。用戶端會依特定的優先順序檢查這些變數。
以下環境變數可用於定義連線 URL
環境變數 | 描述 |
---|---|
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 | - | username | 資料庫名稱 |
連線選項
您可以透過將選項傳遞至 SQL 建構函式來手動設定資料庫連線
import { SQL } from "bun";
const db = new SQL({
// Required
url: "postgres://user:pass@localhost:5432/dbname",
// Optional configuration
hostname: "localhost",
port: 5432,
database: "myapp",
username: "dbuser",
password: "secretpass",
// Connection pool settings
max: 20, // Maximum connections in pool
idleTimeout: 30, // Close idle connections after 30s
maxLifetime: 0, // Connection lifetime in seconds (0 = forever)
connectionTimeout: 30, // Timeout when establishing new connections
// SSL/TLS options
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) {
// ...
// },
// },
// Callbacks
onconnect: client => {
console.log("Connected to database");
},
onclose: client => {
console.log("Connection closed");
},
});
動態密碼
當用戶端需要使用替代驗證方案 (例如存取權杖) 或連線至具有輪換密碼的資料庫時,請提供同步或非同步函式,該函式將在連線時解析動態密碼值。
import { SQL } from "bun";
const sql = new SQL(url, {
// Other connection config
...
// Password function for the database user
password: async () => await signer.getAuthToken(),
});
交易
若要開始新的交易,請使用 sql.begin
。此方法會為交易持續時間保留專用連線,並提供範圍限定的 sql
執行個體以在回呼函式中使用。回呼完成後,sql.begin
會解析為回呼的傳回值。
BEGIN
命令會自動傳送,包括您指定的任何選用設定。如果在交易期間發生錯誤,則會觸發 ROLLBACK
以釋放保留的連線並確保程序順利繼續。
基本交易
await sql.begin(async tx => {
// All queries in this function run in a transaction
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
await tx`UPDATE accounts SET balance = balance - 100 WHERE user_id = 1`;
// Transaction automatically commits if no errors are thrown
// Rolls back if any error occurs
});
如果需要,也可以透過從回呼函式傳回包含查詢的陣列來在交易中管線化請求,如下所示
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 => {
// This part can be rolled back separately
await sp`UPDATE users SET status = 'active'`;
if (someCondition) {
throw new Error("Rollback to savepoint");
}
});
// Continue with transaction even if savepoint rolled back
await tx`INSERT INTO audit_log (action) VALUES ('user_created')`;
});
分散式交易
兩階段提交 (2PC) 是一種分散式交易協定,其中階段 1 讓協調器準備節點,方法是確保資料已寫入且準備好提交,而階段 2 根據協調器的決策完成節點的提交或回滾。此程序可確保資料持久性和適當的鎖定管理。
在 PostgreSQL 和 MySQL 中,分散式交易會持續存在於其原始會話之外,允許具有權限的使用者或協調器稍後提交或回滾它們。這支援穩健的分散式交易、復原程序和管理作業。
每個資料庫系統以不同的方式實作分散式交易
PostgreSQL 透過預先編譯交易原生支援它們,而 MySQL 則使用 XA 交易。
如果在分散式交易期間發生任何未捕獲的例外狀況,系統將自動回滾所有變更。當一切正常進行時,您可以保持彈性,以便稍後提交或回滾交易。
// Begin a distributed transaction
await sql.beginDistributed("tx1", async tx => {
await tx`INSERT INTO users (name) VALUES (${"Alice"})`;
});
// Later, commit or rollback
await sql.commitDistributed("tx1");
// or
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 模式 | 描述 |
---|---|
停用 | 不使用 SSL/TLS。如果伺服器需要 SSL,則連線失敗。 |
偏好 | 先嘗試 SSL,如果 SSL 失敗,則回復為非 SSL。如果未指定模式,則為預設模式。 |
要求 | 需要 SSL,但不進行憑證驗證。如果無法建立 SSL,則失敗。 |
verify-ca | 驗證伺服器憑證是否由受信任的 CA 簽署。如果驗證失敗,則失敗。 |
verify-full | 最安全的模式。驗證憑證和主機名稱是否相符。防止不受信任的憑證和 MITM 攻擊。 |
搭配連線字串使用
SSL 模式也可以在連線字串中指定
// Using prefer mode
const sql = new SQL("postgres://user:password@localhost/mydb?sslmode=prefer");
// Using verify-full mode
const sql = new SQL(
"postgres://user:password@localhost/mydb?sslmode=verify-full",
);
連線池
Bun 的 SQL 用戶端會自動管理連線池,連線池是資料庫連線的集合,可重複用於多個查詢。這有助於減少為每個查詢建立和關閉連線的額外負荷,並且還有助於管理與資料庫的並行連線數。
const db = new SQL({
// Pool configuration
max: 20, // Maximum 20 concurrent connections
idleTimeout: 30, // Close idle connections after 30s
maxLifetime: 3600, // Max connection lifetime 1 hour
connectionTimeout: 10, // Connection timeout 10s
});
在建立查詢之前,不會建立連線。
const sql = Bun.sql(); // no connection are created
await sql`...`; // pool is started until max is reached (if possible), first available connection is used
await sql`...`; // previous connection is reused
// two connections are used now at the same time
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 all queries to finish and close all connections from the pool
await sql.close({ timeout: 5 }); // wait 5 seconds and close all connections from the pool
await sql.close({ timeout: 0 }); // close all connections from the pool immediately
保留連線
Bun 讓您可以從池中保留連線,並傳回包裝單一連線的用戶端。這可用於在隔離的連線上執行查詢。
// Get exclusive connection from pool
const reserved = await sql.reserve();
try {
await reserved`INSERT INTO users (name) VALUES (${"Alice"})`;
} finally {
// Important: Release connection back to pool
reserved.release();
}
// Or using Symbol.dispose
{
using reserved = await sql.reserve();
await reserved`SELECT 1`;
} // Automatically released
預先編譯語句
預設情況下,Bun 的 SQL 用戶端會為可以推斷查詢為靜態的查詢自動建立具名的預先編譯語句。這提供了更好的效能。但是,您可以透過在連線選項中設定 prepare: false
來變更此行為
const sql = new SQL({
// ... other options ...
prepare: false, // Disable persisting named prepared statements on the server
});
當設定 prepare: false
時
查詢仍然使用「擴充」協定執行,但它們是使用 未命名的預先編譯語句 執行的,未命名的預先編譯語句僅持續到下一個指定未命名語句作為目的地的 Parse 陳述式發出為止。
- 參數綁定仍然可以安全地防止 SQL 注入
- 伺服器會從頭開始剖析和規劃每個查詢
- 查詢將不會 管線化
在以下情況下,您可能想要使用 prepare: false
- 在交易模式中使用 PGBouncer (雖然自 PGBouncer 1.21.0 起,在正確設定時支援協定層級的具名預先編譯語句)
- 偵錯查詢執行計畫
- 使用需要頻繁重新產生查詢計畫的動態 SQL
- 每個查詢將不支援多個命令 (除非您使用
sql``.simple()
)
請注意,停用預先編譯語句可能會影響經常使用不同參數執行的查詢的效能,因為伺服器需要從頭開始剖析和規劃每個查詢。
錯誤處理
用戶端為不同的失敗情況提供類型化的錯誤
連線錯誤
連線錯誤 | 描述 |
---|---|
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 | 交易狀態無效 |
數字和 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
Roadmap
我們還有一些事情尚未完成。
- 透過
--db-preconnect
Bun CLI 旗標預先載入連線 - MySQL 支援:我們正在努力
- SQLite 支援:已規劃,但尚未開始。理想情況下,我們會以原生方式實作它,而不是包裝
bun:sqlite
。 - 欄位名稱轉換 (例如
snake_case
到camelCase
)。這主要受到 Unicode 感知的實作的阻礙,該實作使用 WebKit 的WTF::String
在 C++ 中變更大小寫。 - 欄位類型轉換
Postgres 專屬功能
我們尚未實作這些功能
COPY
支援LISTEN
支援NOTIFY
支援
我們也尚未實作一些較不常見的功能,例如
- GSSAPI 驗證
SCRAM-SHA-256-PLUS
支援- Point 和 PostGIS 類型
- 所有多維整數陣列類型 (僅支援幾種類型)
常見問題
為什麼這是 Bun.sql
而不是 Bun.postgres
?
計畫是在未來新增更多資料庫驅動程式。
為什麼不直接使用現有的程式庫?
npm 套件 (例如 postgres.js、pg 和 node-postgres) 也可以在 Bun 中使用。它們是很棒的選擇。
兩個原因如下
- 我們認為,對於開發人員來說,在 Bun 中內建資料庫驅動程式更簡單。您花在程式庫選購上的時間就是您可以建置應用程式的時間。
- 我們利用一些 JavaScriptCore 引擎內部元件來更快地建立物件,這些物件很難在程式庫中實作
致謝
非常感謝 @porsager 的 postgres.js,它為 API 介面提供了靈感。