メニューボタン
IBMi海外記事2007.08.09

絶対に失敗しない組み込みSQLのエラー処理

ポール・コンテ 著

実運用のアプリケーションでSQLが今まで以上に広く使用されるようになるにつれ、組み込みSQLステートメントについて、堅牢なエラー・チェックやエラー処理をすることの重要性がますます高まってきています。組み込みSQLステートメントをILE RPGやILE Cobolなどといった高級言語で記述する際は、実行可能なSQLステートメントすべてに関してそれが正常に完了したか否かを常にチェックし、予期していない条件が発生したら適切に処理しなければなりません。本稿ではこうしたタスクを簡単に行い、しかも絶対に失敗しないためのアドバイス、テクニック、コーディング・パターンをご紹介します。

組み込みSQLのプログラミングには簡単なルールがあります。プログラム中の実行可能なすべてのSQLステートメントの実行直後にSQL状態値をチェックすべし、というものです。SQLランタイムがSQLステートメントを実行しようと試みた後に呼び出し側のプログラムに戻ってくると、SQLランタイムはSqlStateというプログラム変数をSQL状態値にセットします。(Declare Cursorなどの組み込み宣言文やSet Optionなどのプリコンパイラ・ディレクティブはSQLランタイムが実行するわけではないので、SQL状態値がセットされることはありません。)

SQL状態は5文字のコードでXXYYYという構造をしています。ここでXXとYYYは以下の通りです。

  • XXはクラスを示します。
  • YYYはサブクラスを示します。

クラス値の意味は以下の通りです。

  • 00 - ステートメントは無条件で正常に実行されました。
  • 01 - ステートメントは正常に実行されましたが警告があります。
  • 02 - ステートメントが処理したデータはありません。
  • 03~ZZ - エラーのためステートメントの実行に失敗しました。

これらのクラスについては、いくつか気をつけなければならない点があります。「00」クラスには「00000」というSQL状態値しか含まれませんので、クラスだけでなく文字列全体を調べることができます。
「01」クラスのSQL状態値のいくつかに対しては、ホスト変数に関連付けられたヌル・インジケータ変数のなかに正の値が入り、カラム・レベルのさまざまな警告やエラーが起きたことを示すことがあります。カラム・レベルのエラーとして起こりうるものの中には、文字列や日付の切り捨て、算術エラー、文字変換エラー、データ・マッピング・エラーなどがあります。SQLリファレンス・マニュアルの「ホスト変数の参照」というトピックには、インジケータ変数に関する全設定が記載されています。これらはカラム固有の条件であるため、本稿で説明するステートメント・レベルのエラー・チェックに加え、アプリケーションに適したテストをコーディングする必要があります。

クラス値の全リストを図1に示します。最新のV5のSQLメッセージとコードというマニュアルにはSQL状態値の総一覧が記載されています。SQL状態値はIBM DB2ファミリー全体に渡って首尾一貫したものであるように設計されており、SQL 1999という業界標準をベースとしています。このトピックの詳細については、後述の「SqlState値とSqlCode値の探し方」を参照してください。

SQL状態をチェックする

SQL状態値は常に5文字の文字列なので、値全体をテストしたり、クラスを示す部分の最初の2文字だけをテストしたりすることも容易にできます。プログラム中のSQL状態をチェックするための絶対に失敗しないRPG IVのパターンをいくつか紹介しますので、これを流用したり応用したりしてください。これらのパターンをILE Cobolプログラムに適用する際のアドバイスについては、後述する「ILE Cobolのエラー処理」を参照してください。

まず、SQL状態クラスのニーモニック(ILE RPGでいう名前付き定数)と、チェックの対象としたい値全体を宣言します。警告クラスであることを示す「01」という接頭辞を忘れずにつけてください。(Falseのニーモニックも含めておきました。これは後述するパターンの1つで使用します。)これらの宣言文を/Copyメンバーの中に入れることで、コードの再利用をしやすくできます。図2に例を示します。
状況によっては、上記のニーモニックといくつかの共通パターンを用いて、SQLステートメントの結果をチェックすることができます。「success」という結果だけに着目したいステートメントについては、以下の簡単なテストで十分でしょう。

If SqlState <> SqlStateOK
  ExSr SqlError
EndIf

たとえば、サブルーチンを実行する代わりにプロシージャを呼び出すことで、このパターンのバリエーションを使用できます。SQL状態と失敗したステートメントを記述している文字列を渡す方法を図3に示します。ループ中のFetchステートメントの後で、利用可能な行がこれ以上ないことや警告やエラーを検知するには、図4に示すテストを使用します。

このパターンは、正常に実行されたFetchという最も起こりうる場合をまず切り分けてくれます。次に、「02000」状態をテストすることで、「結果セットの終わり」条件を探します。こうした予期された状態をチェックした後に初めて、警告クラス中のSQL状態をチェックします。(SqlStateの最初の2文字は「01」です。)これ以外の結果はすべてエラーとして扱われます。「02」クラスにはSqlStateの値として「02000」以外に2つの値がありますので注意してください。ただし、その2つの値は通常のFetchステートメントでは出現しないので、このパターンでもまたエラーとして扱われます。

MoreRows論理変数(ILE RPGのインジケータ・タイプ)がFetchループ(コード例には載っていません)を制御します。MoreRowsは、ループに入る前にTrue ('1')に初期化されていなければなりません。SqlState自体を使用してFetchループを制御してはいけません。ループのスコープ中にある他のSQLステートメントやエラー処理ルーチンによって、SqlStateがリセットされる可能性があるからです。ここでも、サブルーチンではなくプロシージャを呼び出すことで、あるいは特定のクラスや状態用にテストを追加することで、このパターンを変更することができます。

InsertのようなSQLステートメントについては、特定のエラーを検出したいか、あるいは実行が正常に完了したのを知りたいだけであれば、図5に示すテストを使用してください。「Skip」の行は単なるコメント行であり、Insertが正常に実行されたときには、Selectの後の次のRPGステートメントに制御が単に移るという点に注意してください。 2番目のテストは、プライマリー・キーまたはユニークな制約、あるいはユニークなインデックスを持つ任意のカラムに対して、重複した値をインサートしようとすると、それを検知します。この特定の条件を検知することで、アプリケーションは使用しているユーザーに関連するフィードバックを提供することができ、ユーザーはトランザクションを回復させて完了させることができます。

3番目のテストはその他のすべての制約違反を検知し、アプリケーションが回復できない場合でも、より有用なフィードバックを提供できるようにします。重複したキー状態(「23505」)になっているか否かのテストは、特定の状態を含むクラス(「23」)のテストの前に実行しなければなりません。この順番が逆になると、「23505」という状態は「23」クラスのテストによって検知され、常に一般定数エラーとして処理されてしまいます。このパターンや2文字のクラス値と5文字の状態値を混ぜてテストする他のパターンでは、特定の状態値のいずれかを含むクラスをテストする前に、5文字の状態値をテストすることが重要です。

同様のアプローチを取ることで、When条件を追加してその他の特定のSQL状態やクラスを処理することができます。DeleteステートメントやUpdateステートメント用のパターンはInsertパターンをベースとすることができ、row-not-found(「02000」)やrow-locked(「57033」)などの関連するSQL状態のテストを含むことができます。

最後にちょっとしたアドバイスを。エラー処理にSQL Wheneverステートメントを使用しようなどとは、決して考えないでください。トラブルの元になります。これについては、「Wheneverは決して使用しない」(後述)で説明します。

応用編

本稿でご紹介したパターンをテンプレートとして使用することで、実行可能な各SQLステートメントの後に/Copy(あるいは/Include)できる部分的なコードの自分専用のコレクションを作成することができます。いくつかのバリエーションを検討したほうがよいSQLステートメントもあるでしょう。たとえば、ユーザーからのフィードバックやエラー回復処理がたくさんあるような対話型アプリケーションで使用されているFetchと、バッチ・ジョブ中のFetchに対しては、別の方法が必要になるかもしれません。
聡明な読者の中には、SqlState変数やその他の引数(SQLステートメント・タイプなど)を渡す汎用のプロシージャを呼び出し、そのプロシージャの中でさまざまなテストをコーディングするという単純な方法をなぜ推奨しないのかと、疑問に思われる方もおられるかもしれません。この方法でもうまくいくのですが、それ以降の実行フローを制御するためのアーキテクチャがずっと複雑にならざるを得ないのです。

私がご紹介したパターンに沿った/Copyコードのセクションの集合、サブルーチン中のアプリケーション固有のコードまたは標準名を持つサブ・プロシージャ、追加の診断情報の取得やログ・エントリの発行などといった共通の機能を提供する呼び出し可能なプロシージャ一式を結合する方が、より簡単であるし柔軟性も保てると思うのです。 またいずれかの機会に、SQL状態ばかりでなく追加の診断情報を取得する方法を調べ、SQLエラー処理を強化するテクニックをご紹介したいと思います。

TILE Cobolのエラー処理
本稿で紹介したパターンは、ILE RPGだけでなくILE Cobolでも実装可能です。エラーをチェックするCobol変数もSqlStateです。チェックをしたいSQL状態や、警告状態を示す「01」などのクラス値のニーモニックを宣言します。この宣言をCopyメンバーに置くことでコードの再利用を容易にします(図A)。

次に示す、本稿で紹介したパターンのILE Cobol版の3つのパターンを使用することができます。チェックの対象としたい結果が「success」だけである場合のSQLステートメントの後に使用するパターンは、次の通りです。

If SqlState Not = SqlStateOK
  Perform SqlError
End-If

必要があれば、この例のPerformステートメントの代わりに独自のアクションを記述することもできます。
ループ中のFetchステートメントについて、利用可能な行がこれ以上ないことを検知したり、警告やエラーを検知したりするには、図Bに示すテストを使用してください。
Insertステートメントなどのように、特定のエラーを処理したい場合や正常な完了となるようにしたい場合は、図Cに示すタイプのテストを使用してください。ContinueステートメントはCobolでは単なる「no-op」に過ぎないという点に注意してください。他のSQL状態を処理するときは、When条件を追加することができます。
SqlState値とSqlCode値の探し方
IBMはV5からSQLメッセージとコードという新しい有用なリファレンス・マニュアルを発行しています。このマニュアルには、SqlState値とそれに対応するSqlCode値の総一覧が記載されています。またこのマニュアルには、SQLメッセージ記述とそれに対応するSqlState値とSqlCode値の総一覧も記載されています。各SQLメッセージIDにはそれぞれ対応するSqlCode値がありますので、このリストをSqlCodeからSqlStateへのクロス・リファレンスとして使用することができます。このマニュアルはV5 iSeries Information CenterおよびSystem i Information Centerからオンラインで入手可能です。

またIBMは、上記のInformation Centerで便利なSQL Message Finderを提供しています。SQL Message Finderを使用すると、SqlState、SqlStateクラス、SqlCode、メッセージID別に関連情報を検索することができます。V5R4のSQL Message Finderは、[データベース]-[リファレンス]-[SQLメッセージとコード] にあります。
SqlStateをコピーして問題の発生を防ぐ
次に示すようなコードでSqlState変数をループの終了条件としてテストするループ中のSQL Fetchステートメントが組み込まれたHLLプログラムをよく見かけます。

Dow SqlState = SqlStateOK



Fetch Next From CustCursor



EndDo

この方法は、Fetchの後doブロックの終わりまでで実行されるすべてのSQLステートメントがSqlState変数の値をリセットするため、危険です。
他にSQLステートメントを含まない単純なループであれば、このアプローチを使用しても問題ないと思うかもしれません。しかし、誰か他のプログラマが後になって、エラーをログするためにInsertステートメントを使用するなど、コードを修正したときに何が起こるでしょうか。問題がなかったとしても、SqlStateを使用してループを制御することで、コードの修正がやりづらくなります。最悪の場合、そのプログラマがこの問題を認識せず、問題が発生したときに無限ループを起こす可能性のあるコードを追加してしまうかもしれません。
SqlStateに依存したコードを実装する最良の方法は、SqlState値をすぐに他の変数にコピーするか、SqlStateをすぐにテストして他の制御変数をセットするというものです。本稿で紹介した2番目のパターンは、このテクニックを使用して、結果セットの最後に、あるいは予期せぬ条件が発生したときに、MoreRows変数をFalseにセットしています。
SqlStateをコピーして問題の発生を防ぐ
SQLには、Wheneverというプリコンパイラ・ディレクトリがあります。新米のプログラマにとっては、Wheneverはちょっと見ただけでは良い速記コーディング手法と見えるかもしれません。SQLリファレンス・マニュアルでは、Wheneverステートメントは「特定の例外条件が発生したときに取るべきアクションを指定する」と記載されています。しかし、Wheneverステートメントがプログラムのロジックを台無しにしてしまう方法があまりにたくさんあるので、絶対に使用しないほうがいいステートメントの1つです。

Wheneverステートメントは扱いにくく、エラーを発生しやすいですし、コードの流れをわかりづらくします。Wheneverステートメントのスコープはソース・コードの実行の順番ではなく静的な順番に基づいているため、問題が発生します。Wheneverがコードに与える影響を誤解している場合、あるいはWheneverステートメントのスコープ内のサブルーチンやその他のコードを並べ替えるだけの場合、プログラムが不可解な動作を起こすのを目の当たりにするかもしれません。

本稿でも述べた通り、SQLプログラムが組み込まれたコードのフローの制御を適切に処理する方法は、実行可能な各SQLステートメントの後でSQL状態をチェックすることです。

あわせて読みたい記事

PAGE TOP