なぜ住民票の誤交付を防ぐのは難しいのか 〜例外処理, フェイルファスト, 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) の意味を思い出してみましょう。
具体例をあげてみます。
学校を休むと欠席日数が増える。忌引きは例外。
赤信号は止まらなければいけない。救急車は例外。
警察は容疑者を逮捕できる。容疑者が国会議員の場合は例外。
忌引きや救急車はなぜ例外なのでしょうか。これらは、決まりごとを適用している場合ではないような状況だから、例外として扱われるわけです。(ここで、例外は定義しつくせない という点をちょっと心にとめておいて下さい。学校を休んでも欠席日数が増えないケースは、忌引き以外にもいろいろありますよね)
さてこれを踏まえて、プログラミングの世界での例外はどういう意味でしょう。プログラムそのものが、プログラマがコンピュータに対して指示する決まりごとです。つまり、「プログラムを続行している場合ではないような状況」のことを、例外と言います。
例外の発生: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 #フェイルファスト #例外処理
この記事が気に入ったらサポートをしてみませんか?