はじめに
この記事は自分が小規模アプリ開発でバックエンドを1から担当したときに、失敗を経て最終的に出来上がったNode-Postgres
のクラスを紹介するものです。一応Node-Postgres
は2019年8月の時のものを使用しています。改善点などありましたら是非是非教えてください!
Node-Postgres
のベストプラクティスが中々見つからない
どのサイトもチュートリアル的なものだったり、Pool
を使わずClient
をそのままインスタンス化して使用している古いものばかりで、ベストプラクティスを探すのに時間がかかりました。(最近はORMが主流だからなんですかねやっぱり)
結局見つからなかったのでドキュメント、GitHubであがっていた質問、テストで出てきたバグ等を参考にして更新していきました。
まずはコード
長々と説明するのもあれなのでまずはソースコード出します。こんな感じになりました。
const { Pool } = require('pg');
const connectionString = 'postgresql://postgres:postgres@localhost:5432/hogehoge_db';
const pool = new Pool({ connectionString });
/**
* Postgresクラス
*/
class Postgres {
/**
* Poolからclientを取得
* @return {Promise<void>}
*/
async init() {
this.client = await pool.connect();
}
/**
* SQLを実行
* @param query
* @param params
* @return {Promise<*>}
*/
async execute(query, params = []) {
return (await this.client.query(query, params)).rows;
}
/**
* 取得したクライアントを解放してPoolに戻す
* @return {Promise<void>}
*/
async release() {
await this.client.release(true);
}
/**
* Transaction Begin
* @return {Promise<void>}
*/
async begin() {
await this.client.query('BEGIN');
}
/**
* Transaction Commit
* @return {Promise<void>}
*/
async commit() {
await this.client.query('COMMIT');
}
/**
* Transaction Rollback
* @return {Promise<void>}
*/
async rollback() {
await this.client.query('ROLLBACK');
}
}
/**
* Postgresのインスタンスを返却
* @return {Promise<Postgres>}
*/
const getClient = async () => {
const postgres = new Postgres();
await postgres.init();
return postgres;
};
module.exports.getPostgresClient = getClient;
Pool
は一つ生成する
const pool = new Pool({ connectionString });
はクラスの外でインスタンス化しました。理由は再度使うClient
を保存する(?)コネクションプールなので、複数も必要ないからです。
Client
はPool
経由で生成する
async init() {
this.client = await pool.connect();
}
前のNode-Postgres
ではClient
を直接インスタンス化するようになっていましたが、今はPool.connect()
を使って生成することを公式でも推奨しています。
Node-Postgres Pooling
ちなみにconstructor
ではない理由は、async await
が使えなかったからです。これについては後述します。
使用するときは直接このクラスをnew
しない
/**
* Postgresのインスタンスを返却
* @return {Promise<Postgres>}
*/
const getClient = async () => {
const postgres = new Postgres();
await postgres.init();
return postgres;
};
module.exports.getPostgresClient = getClient;
ここでgetClient
メソッドを、(この名前が適切かは別として…)getPostgresClient
と言う名前でexports
しています。
constructor
ではasync await
が使えないため、メソッド内でnew
してinit()
メソッドを呼んでもらうことで確実にClient
インスタンスを生成するようにしています。
使う時
const { getPostgresClient } = require('postgres');
async function someFunc() {
const db = await getPostgresClient();
try {
const sql = `UPDATE ~`;
const params = ['1', 'name'];
await db.begin();
await db.execute(sql, params);
await db.commit();
} catch (e) {
await db.rollback();
throw e;
} finally {
await db.release();
}
}
失敗履歴
-
Postgres class
のインスタンス生成時の度に、new Pool()
を行っていた。- つまり
constructor
の中でnew Pool()
呼んでました。 - コネクションプールのメリットを完全に潰してました。当時の自分は何を考えていたのだろう…。
- つまり
- シングルトンパターンでPoolを管理していた。
- 短時間で10リクエスト送ると
client.release()
でエラーが多発し処理が止まりました。 - 表示されたエラーは
Release called on client which has already been released to the pool.
- 他で既に
release
したclientを再度リリースしていたらしいです。Client
オブジェクトまで全てのリクエストで共有されてたんですね。
2つめの失敗のコードの一部がこちら
- 他で既に
- 短時間で10リクエスト送ると
/**
* Poolから接続
* @return {Promise<void>}
*/
static async connect() {
try {
// poolが無いなら生成
if (typeof this.pool === 'undefined') {
this.pool = new Pool({
connectionString: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/hogehoge_db'
});
Log.info('Initial Postgres instantiation');
}
// poolから接続できるクライアントを取得
this.client = await this.pool.connect();
} catch (e) {
throw e;
}
}
ちなみにその他のメソッドは全てstatic
メソッドでした。
(ちなみにこの時はまだgetClient
メソッドはありませんでした。)
なぜあのエラーが出てしまうのかは、イベントループを勉強すればわかりそうな気がしているのでそのあたり探ってみます。
参考サイト
他にもあったんですが保存するのを忘れてました