はじめに
Node.jsからPostgreSQLに接続するとき、「接続はいつ張って、いつ閉じるべきか」「同時にたくさんのリクエストが来たらどうするか」といったコネクション管理の悩みにぶつかったことはないでしょうか。生のTCP接続を自前で使い回そうとすると、接続の枯渇やリークが起きやすく、思わぬ障害につながります。
node-postgres(npmパッケージ名はpg)は、この悩みをコネクションプールという仕組みでシンプルに解決してくれるPostgreSQLクライアントです。コールバック、Promise、async/awaitのいずれのスタイルにも対応し、Node.jsの標準的なLTS環境はもちろん、Bun・Deno・Cloudflare Workersといった実行環境でも使えます。
この記事では、node-postgresの特徴からインストール、基本的なクエリ実行、トランザクションやLISTEN/NOTIFYといった実践的な使い方までを順に解説していきます。
node-postgresとは
node-postgresは、Brian Carlson氏を中心に開発されているNode.js向けの非ブロッキングPostgreSQLクライアントです。純粋なJavaScript実装に加えて、ネイティブのlibpqバインディングを使うpg-nativeも選択でき、どちらも同じAPIで利用できます。GitHub上で13,000以上のスターを獲得しており、Node.jsエコシステムにおけるPostgreSQL接続の事実上の標準的な選択肢となっています。
pg、pg-pool、pg-cursorなどがモノレポで管理されており、コネクションプーリングやカーソルによるストリーミング取得など、必要に応じて機能を組み合わせられる構成になっています。
主な特徴
- コネクションプーリング -
Poolクラスが接続の再利用と上限管理を自動で行い、接続の枯渇を防ぐ - パラメータ化クエリ - プレースホルダとパラメータ配列を分けて渡すことで、SQLインジェクションを防ぎつつ安全にクエリを実行できる
- 豊富な型解析 - PostgreSQLの各データ型をJavaScriptの型に変換するルールをカスタマイズできる
- LISTEN/NOTIFY対応 - PostgreSQLのpub/sub機能を使ったリアルタイム通知をそのまま扱える
- マルチランタイム対応 - Node.jsの全LTSに加え、Bun・Deno・Cloudflare Workersでも動作する
インストール
npmを使う場合は次のコマンドでインストールします。
npm install pg
yarnやpnpmを使っている場合は以下のとおりです。
yarn add pg
# または
pnpm add pg
TypeScriptで型定義を使いたい場合は、@types/pgも合わせてインストールしておくと安心です。
npm install --save-dev @types/pg
基本的な使い方
まずは最もシンプルな例として、Clientを使って1回だけクエリを実行するコードを見てみましょう。node-postgresのquery()メソッドは、SQL文とパラメータ配列を渡すだけでPromiseを返してくれます。
const { Client } = require('pg')
const client = new Client({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'password',
database: 'mydb',
})
async function main() {
await client.connect()
const res = await client.query(
'SELECT id, name FROM users WHERE id = $1',
[1]
)
console.log(res.rows[0])
await client.end()
}
main()
$1のようなプレースホルダに配列の値を割り当てる形式なので、ユーザー入力をそのまま文字列連結する必要がなく、SQLインジェクションのリスクを避けられます。ただしClientは1接続を使い切りにする用途向きで、Webアプリケーションのように多数のリクエストを捌く場面では、次に紹介するPoolを使うのが基本になります。
実践的なユースケース
node-postgresには単発のクエリ以外にも、実務でよく使われる使い方のパターンがいくつかあります。ここでは代表的な4つを紹介します。
Poolによる接続の使い回し
Webアプリケーションのように、リクエストごとにデータベースへアクセスする場合、毎回Clientを作って接続・切断を繰り返すのは非効率です。node-postgresのPoolは、接続をあらかじめ複数張っておき、必要なときに貸し出して使い終わったら返却する仕組みを提供します。
const { Pool } = require('pg')
const pool = new Pool({
host: 'localhost',
database: 'mydb',
max: 20, // 最大接続数
idleTimeoutMillis: 30000,
})
app.get('/users/:id', async (req, res) => {
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[req.params.id]
)
res.json(result.rows[0])
})
pool.query()は内部で「接続の取得 → クエリ実行 → 接続の返却」を自動で行ってくれるため、明示的な接続管理を意識せずに使えます。maxオプションで同時接続数の上限を制御できるので、データベース側のコネクション上限を超えないように調整しておくと安心です。
トランザクションで複数クエリをまとめる
送金処理や在庫の引き当てのように、複数のクエリを「すべて成功するか、すべて失敗するか」でまとめたい場合はトランザクションを使います。node-postgresではBEGIN・COMMIT・ROLLBACKを明示的に発行しつつ、同一のクライアントで一連のクエリを実行します。
async function transferPoints(pool, fromId, toId, amount) {
const client = await pool.connect()
try {
await client.query('BEGIN')
await client.query(
'UPDATE accounts SET points = points - $1 WHERE id = $2',
[amount, fromId]
)
await client.query(
'UPDATE accounts SET points = points + $1 WHERE id = $2',
[amount, toId]
)
await client.query('COMMIT')
} catch (e) {
await client.query('ROLLBACK')
throw e
} finally {
client.release()
}
}
ポイントはpool.query()ではなく、pool.connect()で1本の接続を取り出して使い回すことです。トランザクション中に別の接続へクエリが分散してしまうと、BEGINとCOMMITが同一セッション内で完結しなくなってしまうため、必ず同じclientインスタンスでクエリを発行します。
LISTEN/NOTIFYでリアルタイム通知を受け取る
PostgreSQLにはLISTENとNOTIFYというpub/sub機能があり、node-postgresはこれをそのまま扱えます。データベース側で何らかの変更が起きたときに、アプリケーション側へリアルタイムで通知を届けたい場合に便利です。
const { Client } = require('pg')
const client = new Client()
await client.connect()
client.on('notification', (msg) => {
console.log('通知を受信:', msg.channel, msg.payload)
})
await client.query('LISTEN order_created')
// 別のセッションやトリガーから次のようなSQLを実行すると通知が届く
// NOTIFY order_created, '{"orderId": 123}'
notificationイベントのリスナーを登録したあとにLISTEN <チャンネル名>を発行しておくと、そのチャンネルに対してNOTIFYが発行されるたびにコールバックが呼ばれます。ジョブキューの進捗通知や、キャッシュの無効化トリガーなど、ポーリングを避けたい場面で活躍します。
カーソルで大量データをストリーミング処理する
数百万行に及ぶような大きな結果セットを一度にrowsとして受け取るとメモリを圧迫してしまいます。pg-cursorを組み合わせると、結果を少しずつ取り出しながら処理できます。
const { Pool } = require('pg')
const Cursor = require('pg-cursor')
const pool = new Pool()
const client = await pool.connect()
const cursor = client.query(
new Cursor('SELECT * FROM large_table')
)
async function processInBatches() {
const rows = await cursor.read(100)
if (rows.length === 0) {
await cursor.close()
client.release()
return
}
// 100件ずつバッチ処理
console.log(`${rows.length}件を処理`)
await processInBatches()
}
processInBatches()
cursor.read(100)のように件数を指定して呼び出すたびに、その分だけ結果が返ってきます。バッチ処理やCSVエクスポートのように、全件をメモリに載せずに順次処理したいケースで有効です。
まとめ
node-postgresは、Poolによるコネクション管理、パラメータ化クエリによる安全性、トランザクションやLISTEN/NOTIFY、カーソルによるストリーミングまでを一貫したAPIでカバーする、Node.js向けPostgreSQLクライアントの定番です。単発のクエリを投げるだけの用途から、Webアプリケーションの本格的なデータ層まで、規模に応じて無理なく使い方を広げていけます。
まずはPoolを使った基本的なクエリ実行から始めて、必要に応じてトランザクションやLISTEN/NOTIFYを取り入れていくと、無理なくステップアップできるはずです。