LoginSignup
41
33

More than 3 years have passed since last update.

`Node-Postgres` を使ったときに、試行錯誤してできたPostgresクラス

Last updated at Posted at 2019-10-12

はじめに

 この記事は自分が小規模アプリ開発でバックエンドを1から担当したときに、失敗を経て最終的に出来上がったNode-Postgresのクラスを紹介するものです。一応Node-Postgresは2019年8月の時のものを使用しています。改善点などありましたら是非是非教えてください!

Node-Postgresのベストプラクティスが中々見つからない

 どのサイトもチュートリアル的なものだったり、Poolを使わずClientをそのままインスタンス化して使用している古いものばかりで、ベストプラクティスを探すのに時間がかかりました。(最近はORMが主流だからなんですかねやっぱり)
 結局見つからなかったのでドキュメント、GitHubであがっていた質問、テストで出てきたバグ等を参考にして更新していきました。

まずはコード

長々と説明するのもあれなのでまずはソースコード出します。こんな感じになりました。

postgres.js
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は一つ生成する

postgres.js
const pool = new Pool({ connectionString });

はクラスの外でインスタンス化しました。理由は再度使うClientを保存する(?)コネクションプールなので、複数も必要ないからです。

ClientPool経由で生成する

postgres.js
async init() {
    this.client = await pool.connect();
}

前のNode-PostgresではClientを直接インスタンス化するようになっていましたが、今はPool.connect()を使って生成することを公式でも推奨しています。
Node-Postgres Pooling
ちなみにconstructorではない理由は、async awaitが使えなかったからです。これについては後述します。

使用するときは直接このクラスをnewしない

postgres.js
/**
 * 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インスタンスを生成するようにしています。

使う時

SomeFunc.js
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つめの失敗のコードの一部がこちら
postgres.js
/**
 * 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メソッドはありませんでした。)
なぜあのエラーが出てしまうのかは、イベントループを勉強すればわかりそうな気がしているのでそのあたり探ってみます。

参考サイト

他にもあったんですが保存するのを忘れてました

41
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
41
33