はじめに
この記事は自分が小規模アプリ開発でバックエンドを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メソッドはありませんでした。)
なぜあのエラーが出てしまうのかは、イベントループを勉強すればわかりそうな気がしているのでそのあたり探ってみます。
参考サイト
他にもあったんですが保存するのを忘れてました