見出し画像

なぜ住民票の誤交付を防ぐのは難しいのか 〜例外処理, フェイルファスト, throw, catch〜

プログラム自学案内の34回目です。今回は、Express.jsアプリによる PostgreSQLへの書き込み操作と、そのために必要な例外処理についての案内をします。おまけで、例外処理に関連した話題として、住民票の誤交付問題を取り上げたいと思います。これまでの記事はこちらからどうぞ。


今回やることの説明:PostgreSQLへのデータ追加・削除

前回作った磯野家画面に、追加、削除ボタンを足してあげます。

追加ボタン、削除ボタン

削除ボタンを押したときの動きの例です。

ミケさん去る
ミケさんが消えた

さっそく、挑戦してみてください。

ポイントは3つ!

前回すでにテーブル内容の読み取りが出来ているので、あとはSQLを SELECTから INSERT, DELETE に変えて同じような処理を作るだけ? 訳もないように思えますが、じつは結構面倒くさい話題が残っています。

ちゃんとした プログラムにするには、じつは前回のコードでは考える必要のなかった、気をつけなければならない点が 3つ もあるのです。

  • 例外処理

  • トランザクション制御とコネクションの解放

  • SQLのパラメーター化

その3つ全てにちゃんと気をつけた、削除処理のサンプルコードをお見せしますね。たかが一行削除するだけのために、ウンザリするほど盛りだくさんのコードが必要なのが分かると思います。

models/family.js (のうち追加される一部)

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();
    }
}

今回の記事では例外処理の解説のみを行います。

例外処理入門

上掲のサンプルで、例外処理のキーワードが使われている部分を抜き出すと、こんな感じになります。

    try {
        throw new Error(...)
    } catch (e) {
        throw e;
    } finally {
    }

いろいろなキーワードがでてきました。try, catch, finally, throw。全部理解する必要があるのですが、最初に理解すべき語は何でしょうか。じつは、throw なのですね。

そのまえに、そもそも例外とは何? ってところから説明します。ちなみにこの例外という考え方は1980年代あたりに普及しはじめたものでして、老舗言語のC言語とかには存在しません。

例外とは

まず、普通の日本語の 例外 (Exception) の意味を思い出してみましょう。

通例にあてはまらないこと。一般原則の適用を受けないこと。また、そのもの。

小学館『デジタル大辞泉』(https://dictionary.goo.ne.jp/word/%E4%BE%8B%E5%A4%96/#jn-234052)

具体例をあげてみます。

  • 学校を休むと欠席日数が増える。忌引きは例外。

  • 赤信号は止まらなければいけない。救急車は例外。

  • 警察は容疑者を逮捕できる。容疑者が国会議員の場合は例外。

忌引きや救急車はなぜ例外なのでしょうか。これらは、決まりごとを適用している場合ではないような状況だから、例外として扱われるわけです。(ここで、例外は定義しつくせない という点をちょっと心にとめておいて下さい。学校を休んでも欠席日数が増えないケースは、忌引き以外にもいろいろありますよね)

さてこれを踏まえて、プログラミングの世界での例外はどういう意味でしょう。プログラムそのものが、プログラマがコンピュータに対して指示する決まりごとです。つまり、「プログラムを続行している場合ではないような状況」のことを、例外と言います。

例外の発生:throw と return の違いを知る

「プログラムを続行している場合ではないような状況」の時に、例外が発生します。例外は、プログラマが throw というコードで発生させることもあれば、node-postgresなどのライブラリが発生させることもあります(後者の方が多いです)。

具体的に、例外が起きたときの動きを見てみましょう。次のコードを動かして、例外発生時の動き、throwとreturnとの違いを確認してください。プログラムを続行している場合ではないので、呼出し元もろとも処理を中断させてしまう のが、throw の効果です。

error.js

function main() {
    sub1();
    sub2();
    sub3();
}

function sub1() {
    sub1_1();
    sub1_2();
    sub1_3();
}

function sub2() {
    console.log("sub2");
}

function sub3() {
    console.log("sub3");
}


function sub1_1() {
    console.log("sub1_1");
}

function sub1_2() {
    console.log("sub1_2a");
    if (process.argv[2] === "RETURN") {
        return;
    }
    if (process.argv[2] === "THROW") {
        throw new Error("例外発生!");
    }
    console.log("sub1_2b");
}

function sub1_3() {
    console.log("sub1_3");
}

main();

次の3パターンでそれぞれ違う出力をします。

node error.js
node error.js RETURN
node error.js THROW

例外の捕捉:try, catch, finally を知る

try-catch は、例外が発生したときに、やっておきたい処理があるときに使う構文です。冒頭のサンプルだとこれですね。

await client.query('ROLLBACK');

try-finally は、例外が発生しようがしまいが、どうしてもやっておきたい処理があるときに使う構文です。冒頭のサンプルだとこれがその処理に該当します。

client.release();

さきほどの error.jsの main()sub1_2() を次のように書きかえて、try, catch, finally の動きを確かめてみてください。


function main() {
    try {
        sub1();
        sub2();
        sub3();

    } catch (e) {
        console.log("main catch");
    } finally {
        console.log("main finally");
    }
    console.log("main after finally");
}

//(中略)

function sub1_2() {
    try {
        console.log("sub1_2a");
        if (process.argv[2] === "RETURN") {
            return;
        }
        if (process.argv[2] === "THROW") {
            throw new Error("例外発生!");
        }
        console.log("sub1_2b");
        
    } finally {
        console.log("sub1_2 finally")
    }
    console.log("sub1_2 after finally");
}

次の3パターンでそれぞれ違う出力をします。

node error.js
node error.js RETURN
node error.js THROW

けっこう複雑な順序で処理が動くことが分かるかと思います。この順序が理解出来たら、例外処理入門はいったん卒業として良いでしょう。

なぜ住民票の誤交付を防ぐのは難しいのか

ここで時事ネタに触れます。この春(2023年春)から夏にかけて、あるシステムで住民票の誤交付が止まらないことが、話題になっています。この記事では、「例外処理」の観点でこのシステムの問題を考えてみます。

住民票交付プログラムのしくみ(私の想像ですが)

このシステムでは、「印刷イメージファイル」というものが鍵を握っています。 https://www.fujitsu.com/jp/group/fjj/imagesgig5/topics20230407.pdf

私の理解では、このシステムは概ね、こんな感じで処理が行われるものと推測しています。

function 交付申請(個人番号){

    // 申請開始時点では、前の人に交付した
    // 印刷イメージファイルが残っている

    印刷処理管理();
    住民票データを取得(個人番号);
    印刷イメージファイルを作る();

    // 上記の処理が全てうまくいっていれば、
    // 印刷イメージファイルが申請者のものになっているはず!
    // さもないと誤交付になってしまうのでやばい
    // (祈るような気持ち)

    印刷イメージファイルを送信(); 
    最後に必ずすべき処理();
}

function 印刷処理管理(){
    あんな処理(); //例外的に、うまく行かないこともある
    こんな処理();
    ...;
}

function 住民票データを取得(個人番号){
    別の処理();
    また別の処理(); //例外的に、うまく行かないこともある
    ...;
}

...

それぞれの自治体での異なる誤交付の事象は、「印刷イメージファイルを送信」 の前段階の様々な処理で、自治体ごとに様々な例外的な状況が発生し、前の人のイメージファイルが残ったまま、そのまま印刷イメージファイルの送信に突入しているということなのだと思います。

さて、例外的な状況は列挙しきれません。うむ、これはツライですね。

どうする?

誤交付を防ぐ方法を考えます。

    // 申請開始時点では、前の人に交付した
    // 印刷イメージファイルが残っている

本来ならこの前提条件を手当するべき(申請ごとに印刷イメージファイルのファイル名を変えるなど)ですが、この記事ではせっかく例外処理を勉強したので、この観点でどうすべきか考えてみましょう。

すると、問題は様々な例外的な状況で、「印刷イメージファイルが作れない」ことではなく、そのような状況でも、「印刷イメージファイルの送信処理に突入してしまうこと」にあります。つまり、誤交付を避けるためには、何かがあれば例外を発生させ、送信処理に突入させなければ良いのです。

function 交付申請(個人番号) {

    // この時点では、前の人に交付した
    // 印刷イメージファイルが残っている

    try {
        印刷処理管理();
        住民票データを取得(個人番号);
        印刷イメージファイルを作る();

        // 上記の処理のうちどこかうまくいかないときは
        // 必ずその時点で例外が発生し、処理は中断するので
        // 次の処理までたどりつかない
        // ここにたどり着くときは必ず、印刷イメージファイルが申請者のものになっている!

        印刷イメージファイルを送信();
        
    } finally {
        最後に必ずすべき処理();
    } 
}

function 印刷処理管理(){
    あんな処理(); //ちょっとでもうまく行かないときには 例外がthrowされる
    こんな処理();
    ...;
}

function 住民票データを取得(個人番号){
    別の処理();
    また別の処理(); //ちょっとでもうまく行かないときには 例外がthrowされる
    ...;
}

function あんな処理(){
    ...;
    if (どうもうまくいってない) {
        throw new Error("どうもうまくいってない");
    }
    ...;
}

function また別の処理(){
    ...;
    if (どうもうまくいってない) {
        throw new Error("どうもうまくいってない");
    }
    ...;
}

全てをうまく行かせようとするのではなく、とにかく、何かあったらすぐ、例外をthrowして処理を中断させてしまう。その考え方を徹底させていれば、誤交付を完全に無くすことはできなくても、いまニュースになっているほどに、ここまでたくさん、あちこちで誤交付が起きることはなかったはずだと、私は思うのですね。

フェイルファスト――プログラムは、スキーのようなもの

何かあったらすぐ、例外を発生させてプログラムを中断、または異常終了させてしまう。この方針を フェイルファスト(fail fast) と言います。プログラムの異常終了(俗にコケるといいます)というもの、これ、一見最悪の結果のように見えますが、じつは最悪ではないのですね。

むしろ、安全のためには、とにかく早くコケることが大切。スキーと似ていますね。スキーでスピードが出てしまったとき、コケて止まれないと命の危険に繋がるように、例外的な状況では、プログラムが停止しないと、取返しがつかないことが起こる可能性があります。

取り返しがつかないこと

取返しがつかないこと、とは具体的にどういうことでしょう。

  • DBに誤ったデータを書き込んでしまう

  • DBのデータを誤って消去してしまう

  • 別人の住民票を交付してしまう

  • etc...

連載34回目のこのタイミングで、例外処理を紹介した理由は、データの追加・削除・更新、すなわちDBへのトランザクションこそが、間違えると取り返しのつかない過ちの典型だからなのですね。この過ちは、取り返しがつかないニュアンスをこめて、よくDBを汚してしまうと言います。

DBが汚れるのを防ぐためにはどうすればいいでしょう。try句の一番最後でトランザクションのCOMMITを行い、catch句でROLLBACKを行う。 この構造が必然であることが納得できれば、この記事の目的は達成されたことになるのです。

    try {
        await client.query('BEGIN');
        ...
        ...
        ...
        await client.query('COMMIT');
    } catch (e) {
        await client.query('ROLLBACK');
    }

まとめと次回予告

今回の記事では、例外処理について紹介しました。なぜか、例外処理というとcatchの説明になりますが、むしろthrowされてプログラムが止まることのほうが大切だということを、時事ネタと絡めて紹介したのでした。

次回予告です。追加・削除時に留意しなければいけない残りの2つのポイントと、追加ボタン・削除ボタンの実装例を紹介したいと思います。

#コラム #プログラミング #JavaScript #フェイルファスト #例外処理


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