< 前へ | 次へ >

演習 7: 例外処理およびロールバック

データ・ソースにアクセスするアプリケーションでエラーを適切に処理することは、誤ったデータの取得や挿入を避けるためにも、アプリケーションとデータ・ソース間で不整合が発生するのを避けるためにも、特に重要です。この演習では、リレーショナル・データベースへのアクセス時に発生するエラーに対処するために使用可能な、いくつかのツールについて学習します。

SQL データ・アクセスでの例外処理

このチュートリアルの例では、try ブロック内にデータ・アクセス・ステートメントを置くケースが多かったことにお気付きだと思います。

function execDropTable(status StatusRec inOut)
  try  
    execute #sql{
      DROP TABLE EGL.CUSTOMERTEMP
      } ;
  onException (exception SQLException)
    ConditionHandlingLib.HandleException(status, exception);
  end
end
try ブロックにコードを置くと、EGL はこのコードの実行を通常どおりに試みます。EGL でエラーが発生した場合、EGL は try ブロック内でコードの実行を停止し、try ブロックに残っているステートメントはすべてスキップして、直接 onException ブロックに移動します。onException ブロック内に、エラーからのリカバリー、問題の修正、またはログ・ファイルへのエラー情報の書き込みを行うためのコードを含めることができます。

try ブロック内で EGL にエラーが生じると、エラー情報を提供する例外レコードも作成されます。このレコードは onException ブロックに渡されます。 onException ブロック内で、そのレコード内の情報を使用して、エラーの原因を判別し、リカバリーできます。 上記の例で、onException ブロックは、例外レコードを、エラーを処理する関数に渡します。

例外レコードは、エラー・タイプに応じて、いくつかのステレオタイプの 1 つを持っている可能性があります。 上記の例で、onException ブロックは、SQLException ステレオタイプを持つ例外レコードを必要とします。このステレオタイプは、SQL エラーおよびその他のリレーショナル・データベース用です。NULL 値の参照などの、異なるタイプのエラーが EGL で生じた場合は、異なるタイプの例外レコード (NULL 値の参照の場合は、NullValueException ステレオタイプを持つ例外レコード) が作成されます。

次の例のように、異なるタイプのエラーを異なる方法で処理するために、単一の try ブロックの後に複数の onException ブロックを置くことができます。
function execDropTable(status StatusRec inOut)
  try  
    execute #sql{
      DROP TABLE EGL.CUSTOMERTEMP
      } ;
  onException (exception SQLException)
    ConditionHandlingLib.HandleException(status, exception);
  onException (exception AnyException)
		HandleOtherError(exception);
  end
end
この例では、最初の onException ブロックで SQLException レコードが必要になり、2 番目のブロックで AnyException ステレオタイプが必要になります。この場合、EGL で SQL エラーが生じると、最初の onException ブロックが実行されます。別の種類のエラーであれば、2 番目のブロックが実行されます。AnyException ステレオタイプは、一般ユーザーの例外レコードのステレオタイプで、ステレオタイプに関係なく、どのようなエラーに応答する場合にも使用されます。

例外レコードにはすべて、最低 2 つのフィールドがあります。 1 つはエラーのエラー・コードを示す messageID フィールドで、もう 1 つは問題の簡単な説明を示すメッセージ・フィールドです。ステレオタイプに応じて、例外レコードは他のフィールドを持つことがあります。例えば、SQLRecord ステレオタイプには、ステートメントのステータスについての CHAR(5) 値を保持する sqlState フィールドと DBMS からの INT 戻りコードを保持する sqlCode フィールドがあります。

無効な情報の受け渡し、存在しないレコードの削除または更新、あるいは、同じ主キーを持つレコードの追加を行って、例外処理を試してみることができます。次の例では、同じ ID 番号を持つ新規顧客レコードを、データベースに既存の行として追加することを試みます。この結果、主キーとの競合が発生し、EGL が SQLException 例外をスローします。

  1. programs パッケージ内に、exceptionHandlingTest という名前の新規プログラムを作成します。
  2. 新規プログラムの main 関数に、顧客レコード変数を作成し、データベース内に既に存在している ID 番号などの情報をそれに割り当てます。
    package programs;
    
    import eglderbyr7.data.Customer;
    
    program exceptionHandlingTest type BasicProgram {}
      
      function main()
        customer Customer;
        customer.FirstName = "John";
        customer.LastName = "Doe";
        customer.CustomerId = 1;
      end
    
    end
  3. try ブロックで、データベースにレコードを追加します。
    try
      add customer;
      SysLib.writeStdout("Customer added successfully.");
    end
    この try ブロックは、エラーに応答するための対応した onException ブロックがないため、あまり有用ではありません。
  4. try ブロックを閉じる end の前に、SQLException を処理する onException ブロックを追加します。この例外は、EGL が try ブロックでコードの実行を試みる際に発生します。
    try
      add customer;
      SysLib.writeStdout("Customer added successfully.");
    onException(ex SQLException)
      SysLib.writeStdout("SQL exception "::ex.messageID);
      SysLib.writeStdout("message = "::ex.message);
      SysLib.writeStdout("SQLCode = "::ex.sqlCode);
      SysLib.writeStdout("SQLState = "::ex.sqlState);
    end
    完成したプログラムは次のようになります。
    package programs;
    
    import eglderbyr7.data.Customer;
    
    program exceptionHandlingTest type BasicProgram {}
      
      function main()
        customer Customer;
        customer.FirstName = "John";
        customer.LastName = "Doe";
        customer.CustomerId = 1;
      
        try
          add customer;
          SysLib.writeStdout("Customer and order added successfully.");
        onException(ex SQLException)
          SysLib.writeStdout("SQL exception "::ex.messageID);
          SysLib.writeStdout("message = "::ex.message);
          SysLib.writeStdout("SQLCode = "::ex.sqlCode);
          SysLib.writeStdout("SQLState = "::ex.sqlState);
        end
        
            
      end
      
    end
  5. プログラムを保存し、生成して実行します。 コンソールに、onException ブロックからの出力が表示されます。
    SQL exception EGL0504E
    message = EGL0504E ADD: The statement was aborted 
      because it would have caused a duplicate key value 
      in a unique or primary key constraint or unique 
      index identified by 'SQL070307095259210' defined 
      on 'CUSTOMER'.[sqlstate:23505][sqlcode:20000]
    EGL0002I The error occurred in the 
      exceptionHandlingTest program processing the main function.
    SQLCode = 20000
    SQLState = 23505

データベースへの変更のロールバック

ほとんどのリレーショナル・データベースは、Derby も含め、リカバリー可能リソースです。 リカバリー可能リソースに変更を行う場合、コミットを発行し、その変更を保存してはじめて変更が永続的なものになります。例外が生じた場合も、このように、それらの変更をコミットする前にデータベースへの変更を元に戻すことができます。

sqlCommitControl ビルド記述子オプションを NOAUTOCOMMIT に設定することができます。これにより、変更が行われるたびに EGL がそれを自動的にコミットすることがなくなります。自動コミットをオフにした場合、手動で sysLib.commit() を呼び出して変更をコミットし、sysLib.rollback() を呼び出して、最後のコミットから行われた変更を元に戻すことができます。 sysLib.commit() で変更をコミットしないと、メインプログラムの終わりなどの実行単位の終わりに、変更が必ずコミットされます。

例えば、新規顧客を追加し、顧客からの最初のオーダーに関するオーダー情報を入力するなど、データベースにいくつかの変更を行っているとします。これらの操作の一部が成功した後で別の部分が失敗した場合は、顧客が入力されてオーダーが入力されなかったり、オーダーが入力されて顧客が入力されなかったりすることがあります。この場合、次のようにして、ロールバックを onException ブロックに含めて、変更を元に戻し、データベースに不完全な情報が含まれないようにします。

  1. sqlCommitControl ビルド記述子オプションを NOAUTOCOMMIT に設定します。
    1. 「プロジェクト・エクスプローラー」ビューで、EGLSQL プロジェクトのビルド記述子をダブルクリックして EGL ビルド・パーツ・エディターで開きます。 このファイルは、EGLSQL/EGLSource/EGLSQL.eglbld にあります。
    2. ビルド・パーツ・エディターで、「指定したオプションのみを表示」チェック・ボックスをクリアします。
    3. 「オプション」から、sqlCommitControl を見つけます。
    4. sqlCommitControl の値を NOAUTOCOMMIT に設定します。
      注: 編集するために sqlCommitControl オプションを開くには、横にある「値」列で 2 回ゆっくりとクリックします。「値」列で、3 回素早くクリックしてもかまいません。
    5. ビルド記述子を保管して、閉じます。
  2. 前のセクションで作成した exceptionHandlingTest プログラムを開きます。
  3. プログラムに既にある顧客レコードにオーダー・レコードを追加します。
    program exceptionHandlingTest type BasicProgram {}
      
      function main()
        customer Customer;
        customer.FirstName = "John";
        customer.LastName = "Doe";
        customer.CustomerId = 1;
        
        customerOrder Orders;
        customerOrder.CustomerId = 1;
        customerOrder.OrderId = 50;
        customerOrder.OrderAmount = 0;
        customerOrder.OrderDetails = 
          "This is my first order, so get it right!";
        
        ...
        
      end
  4. try ブロック内で、最初にオーダーを、次に顧客を追加します。
    try
      add customerOrder;
      SysLib.writeStdout("Order added successfully.");
      add customer;
      SysLib.writeStdout("Customer added successfully.");
    end
    この場合、オーダーは追加されますが、顧客は主キーが重複するため、追加されません。したがって、onException ブロック内でロールバックを呼び出すことで、ORDERS テーブルへの変更を元に戻すことができます。
  5. try ブロックの後に、ロールバックを含めた onException ブロックを追加します。
    try
      add customerOrder;
      SysLib.writeStdout("Order added successfully.");
      add customer;
      SysLib.writeStdout("Customer added successfully.");
    onException(ex SQLException)
      SysLib.writeStdout("SQL exception "::ex.messageID);
      SysLib.writeStdout("message = "::ex.message);
      SysLib.writeStdout("Performing rollback.");
      SysLib.rollback();
    end
    これで、どちらかの add ステートメントが失敗した場合、コード onException ブロックが実行されて、データベースへの変更を元に戻そうとします。
  6. onException ブロックの後に、例外が生じる前に追加されたオーダーがロールバックで正常に除去されたかをテストするコードを追加します。 オーダー・レコードがまだデータベース内にあるか確認するために、それを noRecordFound と比較できます。
    tempOrder Orders;
    tempOrder.OrderId = 50;
    get tempOrder;
    
    if (tempOrder is noRecordFound)
      SysLib.writeStdout("The order was rolled back successfully.");
    else
      SysLib.writeStdout("The order is still in the database");
    end
    データベースへの呼び出し状況を調べる別の方法は、sqlLib.sqlData システム変数の値をテストすることです。このシステム変数は 1 つのレコードであり、最後の SQL データベース操作が成功したか失敗したかを示す情報がそのフィールドに保管されます。具体的に言うと、フィールド sqlLib.sqlData.sqlcode には、操作が正常に完了し、結果が返った場合は 0、操作は正常に完了したが、SELECT ステートメントによる結果が帰らなかった場合は 100 が含まれます。したがって、次のコードにより、行が検出されたかどうかもわかります。
    if (sqlLib.sqlData.sqlcode == 0)
    	SysLib.writeStdout("The order is still in the database");
    else
    	if (sqlLib.sqlData.sqlcode == 100)
    		SysLib.writeStdout("The order was rolled back successfully.");
    	end
    end
  7. プログラムを生成し、実行します。
プログラムの出力に、オーダーがデータベースに追加されていること、例外が発生していること、さらに、ロールバックの結果としてオーダーが正常に削除されたことが表示されます。
Order added successfully.
SQL exception EGL0504E
message = EGL0504E ADD: The statement was aborted 
  because it would have caused a duplicate 
  key value in a unique or primary key constraint 
  or unique index identified by 'SQL070307095259210' 
  defined on 'CUSTOMER'.[sqlstate:23505][sqlcode:20000]
EGL0002I The error occurred in the exceptionHandlingTest 
  program processing the main function.
Performing rollback.
The order was rolled back successfully.
これで、exceptionHandlingTest.egl ファイルのコードが完成しました。 このファイル内にエラーがある場合 (赤の X 記号でマークされている場合) は、演習 7 で完成した exceptionHandlingTest.egl ファイルに記載されているファイルのコードと、作成したコードが一致していることを確認してください。

演習のチェックポイント

この演習では、SQL 操作で生じたエラーの処理について基本的なことを学習しました。問題を予測し、アプリケーションをリカバリーできるよう例外処理を提供することは非常に重要です。

例外の原因となり得る実行時エラーについて詳しくは、「EGL Java™ ランタイムのエラー・コード」を参照してください。EGL で作成できる例外レコードのリストについては、「EGL コア例外レコード」を参照してください。