概要とアーキテクチャの特徴
PGliteは、WebAssembly (WASM) にコンパイルされ、クライアントサイドJavaScriptライブラリとしてパッケージ化された、完全に動作する軽量なPostgreSQLエンジンです。ElectricSQLによって開発されたPGliteを使用することで、開発者は個別のPostgreSQLサーバー、Dockerコンテナ、または外部デーモンを必要とせずに、Node.jsランタイム、Webブラウザ、またはモバイル環境(React Native/Capacitor経由)でPostgreSQLデータベースを直接実行できます。
本稿では、Node.js環境におけるPGliteの初期化、操作、ベンチマーク、およびハイブリッド同期ワークフローのシミュレーション手順について解説します。
主なアーキテクチャの特徴
💡 WASM駆動のPostgreSQLエンジン: WebAssemblyにコンパイルされた実際のPostgreSQLエンジンを実行するため、SQL構文の互換性が維持されます。
🛠️ 環境に応じたストレージ抽象化: Node.js環境ではローカルファイルシステムを自動的に利用してデータを永続化し、ブラウザ環境ではIndexedDBにフォールバックします。
⚡ ネットワークオーバーヘッドの排除: データベースクエリがインプロセスで実行されるため、従来のデータベース接続に伴うネットワーク遅延が発生しません。
プロジェクトのセットアップと環境設定
ES Modulesを使用するように構成されたNode.js環境を構築し、必要なPGlite依存関係をインストールします。
# 1. プロジェクトディレクトリの作成と移動
mkdir pglite-demo
cd pglite-demo
# 2. package.jsonの初期化とES Modulesの 有効化
npm init -y
npm pkg set type="module"
# 3. PGliteライブラリのインストール
npm install @electric-sql/pglite
実装コード (index.js)
プロジェクトのルートディレクトリに index.js ファイルを作成し、データベースの初期化、トランザクションを使用した一括挿入ベンチマーク、標準的なCRUD操作、およびオフラインファーストの同期サイクルのシミュレーションを実行するロジックを実装します。
import { PGlite } from "@electric-sql/pglite";
// PGliteインスタンスの初期化
// ローカルの "./pgdata" ディレクトリに永続的なPostgreSQLデータベースを作成またはロードします。
// Node.js環境ではローカルファイルシステムを対象とし、ブラウザ環境ではIndexedDBがデフォルトとなります。
const db = new PGlite("./pgdata");
async function main() {
console.log("🚀 PGlite (Postgres in WASM) 起動中...\n");
// 1. テーブルの初期化 (DDLの実行)
await db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT,
synced BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW()
);
`);
console.log("✅ 'notes' テーブル準備完了 (Postgres Engine 稼働)\n");
// ---------------------------------------------------------
// 2. パフォーマンスベンチマーク: バルクデータの挿入
// ---------------------------------------------------------
console.log("📊 [ベンチマーク] メモ100件の挿入テスト開始...");
const start = performance.now();
// トランザクションブロックを使用して操作をバッチ処理します。
// ネットワークオーバーヘッドがないため、メモリ内のWASM実行は高速です。
await db.transaction(async (tx) => {
for (let i = 0; i < 100; i++) {
await tx.query(
"INSERT INTO notes (title, content) VALUES ($1, $2)",
[`メモ #${i}`, `これは ${i} 回目のテストメモです。`]
);
}
});
const end = performance.now();
console.log(`⚡ 完了! 所要時間: ${(end - start).toFixed(2)}ms`);
console.log(` (1件あたりの平均処理速度: ${((end - start) / 100).toFixed(2)}ms)\n`);
// ---------------------------------------------------------
// 3. CRUDシナリオの実行
// ---------------------------------------------------------
console.log("📝 [CRUD シナリオ] 実行");
// [Create] - 単一レコードを挿入し、作成された行を返す
const newNote = await db.query(
"INSERT INTO notes (title, content) VALUES ($1, $2) RETURNING *",
["重要ミーティング", "午後2時: Q3ロードマップ議論"]
);
console.log("1. メモ作成:", newNote.rows[0]);
// [Update] - 作成されたレコードの内容を更新
const updatedNote = await db.query(
"UPDATE notes SET content = $1 WHERE id = $2 RETURNING *",
["午後3時に変更: Q3ロードマップ議論", newNote.rows[0].id]
);
console.log("2. メモ修正:", updatedNote.rows[0]);
// [Read] - 直近のレコードを3件取得
const list = await db.query("SELECT * FROM notes ORDER BY created_at DESC LIMIT 3");
console.log("3. 直近のメモ参照 (Top 3):");
console.table(list.rows.map(n => ({ id: n.id, title: n.title, content: n.content })));
// ---------------------------------------------------------
// 4. ハイブリッド同期のシミュレーション
// ---------------------------------------------------------
console.log("\n🔄 [同期] バックエンド同期シミュレーション...");
// 未同期のローカルレコード (synced = false) を取得
const unsyncedParams = await db.query("SELECT * FROM notes WHERE synced = false");
const unsyncedCount = unsyncedParams.rows.length;
if (unsyncedCount > 0) {
console.log(` -> 同期対象: ${unsyncedCount}件検出`);
// リモートサーバーへのデータ送信を模したネットワーク遅延 (500ms) のシミュレーション
await new Promise(r => setTimeout(r, 500));
console.log(" -> ☁️ バックエンド送信完了 (Mock Server)");
// ローカルデータベースの状態を更新し、同期済みとしてマーク
const ids = unsyncedParams.rows.map(n => n.id);
await db.query("UPDATE notes SET synced = true WHERE id = ANY($1)", [ids]);
console.log(" -> ✅ ローカルDBステータスを 'Synced' に更新完了");
} else {
console.log(" -> 同期するデータはありません。");
}
// ---------------------------------------------------------
// 実行終了
// ---------------------------------------------------------
console.log("\n🎉 すべてのデモが正常に完了しました。");
}
main().catch((err) => {
console.error("❌ エラー発生:", err);
});
実行手順と期待される出力
スクリプトを実行するには、ターミナルで以下のコマンドを実行します。
node index.js
期待されるコンソール出力例
🚀 PGlite (Postgres in WASM) 起動中...
✅ 'notes' テーブル準備完了 (Postgres Engine 稼働)
📊 [ベンチマーク] メモ100件の挿入テスト開始...
⚡ 完了! 所要時間: 42.50ms
(1件あたりの平均処理速度: 0.43ms)
📝 [CRUD シナリオ] 実行
1. メモ作成: { id: 101, title: '重要ミーティング', ... }
2. メモ修正: { id: 101, title: '重要ミーティング', content: '午後3時に変更...', ... }
3. 直近のメモ参照 (Top 3):
┌─────────┬──────────────┬────────────────────────────┐
│ (index) │ id │ title │
├─────────┼──────────────┼────────────────────────────┤
│ 0 │ 101 │ '重要ミーティング' │
│ 1 │ 100 │ 'メモ #99' │
│ 2 │ 99 │ 'メモ #98' │
└─────────┴──────────────┴────────────────────────────┘
🔄 [同期] バックエンド同期シミュレーション...
-> 同期対象: 101件検出
-> ☁️ バックエンド送信完了 (Mock Server)
-> ✅ ローカルDBステータスを 'Synced' に更新完了
🎉 すべてのデモが正常に完了しました。
アーキテクチャおよび性能の検証ポイント
この実装により、PGliteの3つの重要な機能特性が実証されます。
1. インプロセス実行による低遅延
トランザクションブロック内で100件のレコードを順次挿入する処理は、数十ミリ秒(例: 約42.50ms、1レコードあたり約0.43ms)で完了します。従来のクライアント・サーバー型データベース構成では、ネットワークの往復、TCPハンドシェイク、およびコネクションプーリングのオーバーヘッドにより、同様の処理に秒単位の時間を要することがあります。PGliteは、データベースエンジンをインプロセスで実行することで、極めて低いオーバーヘッドを実現します。
2. PostgreSQL構文との互換性
軽量なキーバリューストアや簡易的なSQL風エンジンとは異なり、PGliteは実際のPostgreSQLエンジンを動作させています。そのため、以下を含む標準的なPostgreSQLのSQL構文をそのままサポートします。
・ 複雑なDDL (CREATE TABLE IF NOT EXISTS)
・ トランザクション制御 (db.transaction)
・ RETURNING句を伴うデータ操作 (INSERT ... RETURNING *)
・ 配列パラメータを用いた高度なクエリ演算 (WHERE id = ANY($1))
3. データの永続性
このスクリプトを複数回実行すると、データが ./pgdata ディレクトリ内に永続化されていることが確認できます。自動インクリメントされる主キー(id)は1にリセットされず、実行をまたいで増加し続けます(例: 2回目以降の実行では101から開始)。これにより、PGliteが単なるメモリ内の一時的なモックではなく、永続的で信頼性の高いデータベースエンジンとして機能していることが証明されます。
Operational Notes
💡 ストレージの選択: Node.js環境ではファイルシステムが使用されますが、ブラウザ環境で動作させる場合は、自動的にIndexedDBが選択されます。環境ごとのストレージアダプタの挙動に留意してください。
⚠️ リソース消費: WASM上でフル機能のPostgreSQLを実行するため、メモリ消費量は一般的なキーバリューストアよりも大きくなります。リソース制限の厳しいエッジ環境や古いモバイル端末に展開する場合は、実機でのメモリプロファイリングを推奨します。
⚠️ 同時実行制御: PGliteはシングルプロセス指向の設計となっています。同一 of データディレクトリに対して複数のNode.jsプロセスから同時に書き込みアクセスを行うと、ロック競合やデータ破損の原因となるため、適切なアクセス制御の設計が必要です。