見出し画像

Webアプリでのトランザクション処理の定石って? 〜try with resources, using〜

プログラム自学案内の36回目です。今回は、前々回の記事の課題を解くための3つめのポイント、トランザクションとコネクション管理について簡単に解説したあと、実装例を紹介したいと思います。これまでの記事はこちらから。


課題のおさらい:PostgreSQLへのデータ追加・削除

まずは課題のおさらいをします。3回前の記事で作った磯野家画面に、追加、削除ボタンを足します。

追加ボタン、削除ボタン

トランザクショナルなWebアプリのポイント

では、課題を解くための3つのポイントの最後のひとつ、トランザクションとコネクションについて説明します。

コネクションを取得する処理を追加

とりあえずこのコードをご覧ください。

db/index.js

const pg = require("pg")

const pool = new pg.Pool({
    database: 'mydb',
    user: 'watanabe',
    password: 'pass',
    max: 10
})

async function query(text, params) {
    return await pool.query(text, params)
}

// この関数を追加
async function getClient() {
    return await pool.connect();
}

module.exports = { query: query, getClient: getClient };

元々のコードに、getClient() という関数を追加しました。この関数の中にある pool.connect() は、コネクションが貯めこまれた コネクションプール から1つ、コネクションを取り出します。

では、なぜこんな処理を足す必要があるのでしょうか? INSERT文や
DELETE文の実行だけなら、コネクション取り出さずとも、すぐ上に書かれている関数、query ( pool.query() )で全て出来るのに、です。

思い出してください:トランザクションはコネクションごとに行われます

一般に、Web画面でボタンを押したときに行われる、登録・更新・削除操作は一まとめに成功するか失敗するかのどちらかであってほしいものです、すなわち、1つのトランザクションとして行われなければいけません。

しかし、pool.query() を使ってしまうと、リンク先の取説にも書いてありますが、DBに対する操作を、1つのトランザクションの中でまとめて行うことが出来ません。

pool.query() はコネクションプールの中で、使われていないコネクションを つどつど探しだし、そのコネクションでSQL文を実行します。立て続けにpool.query()で複数のSQL文が実行されると、それらは複数のコネクションをまたがる可能性があります。トランザクションについて、かつて説明した次の記事で思い出してみてください。トランザクションは、1つのコネクションの中で完結すべきものであって、コネクションをまたがったトランザクションなんて作れないのです。

なので、登録・更新・削除操作をするときには、あらかじめ「コネクションプール」から1つ、コネクションを取り出して、そのコネクションの中でSQL文を実行しなければならないというわけです。

取得したコネクションは、最後にかならずプールに戻す

pool.connect()で取得したコネクションは、プールからのいわば借り物です。さいごに client.release()で解放、すなわちコネクションプールに返さなければいけません。たとえプログラムを続行できないような例外が起きようとも、必ずです。ここで、例外処理の finally の出番となります。

前々回の記事 では、トランザクションの BEGIN, COMMIT, ROLLBACK を try, catchのどこに書くかは必然的に決まることを紹介しましたが、コネクションの取得、解放がどこに書かれるかも必然的に決まります。これらを合わせると、次に示す構造がトランザクション処理の定石になります。


    // pool.connect()でコネクションを取得
    const client = await db.getClient();

    try {
        // トランザクション開始
        await client.query('BEGIN');
        ...
        ...
        // いろいろSQL文を発行
        ...
        ...      
        ...
        // トランザクション完了
        await client.query('COMMIT');
    
    } catch (e) {
    
        // トランザクション中断
        await client.query('ROLLBACK');
        throw e;
    
    } finally {
        // 完了、中断どっちでも必ず最後にコネクションを解放する
        client.release();
    }

node-postgresの取説に書かれている、トランザクションのサンプル もまさにこの構造ですね。

try with resources と using

使った資源は、使い終わったら(例外で使うのを中断したときも)最後に必ず、手放したり、閉めたりする。 こういう後片付けの処理は、コネクション以外でも、プログラミングをしていて頻繫にでくわすパターンです。JavaScriptではこのパターンのとき、finallyのなかに「閉める」「手放す」「返す」という処理を書くことになります。しかしこの処理は、えてして書き忘れたり書き誤ったりするものです。

JavaScript以外の言語のうちいくつかには、「閉める」「手放す」「返す」処理を 書かずに確実に行わせる ための構文があります。たとえば Java言語ではこれを try with resources 構文と言います。2023年9月現在、JavaScriptはまだこの構文を持ちません(遅れてますね~)が、近い将来、「using」というキーワードをつかって同様の構文でプログラミングできるようになるかもしれません。Qiitaのこちらの記事に解説されています。

実装例を紹介

では、課題の解答例を紹介します。3回前の記事 では、磯野家テーブルをWeb画面で表示する実装を紹介しました。ここに追加・削除ボタンの機能をどう足すかを紹介します。

DBへの接続

再掲です。getClient()という関数を追加しています。

db/index.js

const pg = require("pg")

const pool = new pg.Pool({
    database: 'mydb',
    user: 'watanabe',
    password: 'pass',
    max: 10
})

async function query(text, params) {
    return await pool.query(text, params)
}

// この関数を追加
async function getClient() {
    return await pool.connect();
}

module.exports = { query: query, getClient: getClient };

Model

磯野家テーブルに追加、削除するコードをModelに書きます。また、磯野家テーブルを読むコードの関数名もfamilyからlistに変えました。最近3回の記事で解説した内容のほぼすべてがここに入っています。

models/family.js

const db = require('../db');

async function list() {
    result = await db.query('SELECT full_name, age FROM isono_family');
    return result.rows;
}


async function remove(name, age) {

    const client = await db.getClient();
    try {
        await client.query('BEGIN');
        const result = await client.query(
            'DELETE FROM isono_family WHERE full_name=$1 AND age=$2',
            [name, age]
        );

        if (result.rowCount != 1) {
            throw new Error(`エラー: 削除結果は1件であってほしいのに、${result.rowCount}件でした`)
        }

        await client.query('COMMIT');
        return `${name}さん ${age}才が去りました`;

    } catch (e) {
        await client.query('ROLLBACK');
        throw e;

    } finally {
        client.release();
    }
}



async function add(name, age) {

    const client = await db.getClient();
    try {
        await client.query('BEGIN');
        await client.query(
            'INSERT INTO isono_family VALUES ($1, $2)',
            [name, age]
        );

        const result = await client.query(
            'SELECT COUNT(*) FROM isono_family'
        );

        const numberOfPeaple = result.rows[0].count;
        if (numberOfPeaple >= 10) {
            throw new Error(`${numberOfPeaple}人も? 家族はそんなにたくさん増やせません`)
        }

        await client.query('COMMIT');
        return `${name}さん ${age}才が家族に加わりました`;

    } catch (e) {
        await client.query('ROLLBACK');
        throw e;

    } finally {
        client.release();
    }
}


module.exports = {
    list: list,
    add: add,
    remove: remove
};

Controller

HTTPリクエストの Method、Path、Body と Model の family.add(), family.remove()とを対応させます。

HTTPリクエストの Method、Path、Bodyって何? という方はこちらでおさらいしてください。

controllers/router.js


// (前略)

router.get('/family', async function (req, res, next) {
    const data = await family.list(); // family() を family.list()に変えた
    res.render('family.html', { "family": data });
});

router.post('/add-family', async function (req, res, next) {
    const data = await family.add(req.body.name, req.body.age);
    res.render('family_result.html', { "message": data });
});

router.post('/remove-family', async function (req, res, next) {
    const data = await family.remove(req.body.name, req.body.age);
    res.render('family_result.html', { "message": data });
});


// (以下略)

View

磯野家画面に削除、追加ボタンを足します。このやり方の基本も、過去の記事ですでに解説しています。それを応用するわけです。

views/family.html

(前略)

<body>
    <h1> Family </h1>
    <table>
        <tr>
            <th>なまえ</th>
            <th>年齢</th>
        </tr>
        {{#family}}
        <tr>
            <td>{{full_name}}</td>
            <td class="number">{{age}}</td>
            <td>
                <form method="POST" action="/remove-family">
                   <input type="hidden" name="name" value="{{full_name}}">
                   <input type="hidden" name="age" value="{{age}}">
                   <button type="submit">削除</button>
                </form>           
            </td>
        </tr>
        {{/family}}
    </table>

    <hr>
    <form method="POST" action="/add-family">
        なまえ: <input type="text" name="name" size="6"> さん
        <br>
        年齢: <input type="text" name="age" size="3"> 才
        <button type="submit">追加</button>
    </form>
    <hr>

    <a href="/">ホームへ</a>

</body>

</html>

結果画面も作ります。

views/family_result.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="/stylesheets/style.css">
    <title> Family </title>
</head>

<body>
    <h1> {{message}} </h1>
    <hr>
    <a href="/family">磯野家の人々</a>
    <a href="/">ホームへ</a>
    
</body>

</html>

まとめと次回予告

今回の記事では、Webアプリにおけるトランザクションとコネクション管理について簡単に解説したあと、PostgreSQLにデータを書いたり消したりする実装例を紹介しました。

そして、これまで6回の記事にわたった、Express.jsアプリでPostgreSQLを扱う方法の紹介は終わりました。

意外と奥深いものだったでしょう。これまで超特急でゼロからプログラミング技術を紹介してきたこの連載で、ただExpress.jsとPosgreSQLをつなげるだけの話に6回ぶんもの記事が必要になるとは、私自身、全く思いもよらないことでした。このことは、私にとっても大きな発見でした。

次回予告です。PosgreSQLを使ったこのExpress.jsアプリを、Render.comで公開してみたいと思います。

#コラム #プログラミング #JavaScript #node -postgres


この記事が気に入ったらサポートをしてみませんか?