【IRIS/Cache】例外クラス(%Exception)について

本記事は、IRIS/Cacheにおける例外クラス(%Exception)について解説したいと思います。

※この記事は下記の方向けになります。
  • Try-Catch 構文の基本は理解している方
  • 例外クラスについて興味がある方

はじめに

IRIS/Cacheでは、Try-Catch構文とともに%Exceptionパッケージのクラス群を使うことで、エラーハンドリングをより柔軟に設計できます。

全ての例外クラスは、「%Exception.AbstractException.cls」を継承しており、エラーに関する情報は、統一された構造を持つオブジェクトとして処理する事が可能です。

また、従来の$ztrapや$etrapによるエラー処理では難しかった、状況に応じたメッセージを含ませる事が出来るのも大きな特徴です。

「そこで実際に何が起こったのか?」を離れたcatchコード・ブロックで取得/解析できるのは、従来のエラー処理と比較して、強力なアドバンテージだと思います。

では、例外クラスを見ていきましょう。

例外クラスの種類

先ずは、例外クラスの種類を確認します。
 ※PythonException(Python用), CPPException(IS社内用)は使用できないので除外

例外クラス一覧
  • %Exception.AbstractException
  • %Exception.General
  • %Exception.SystemException
  • %Exception.StatusException
  • %Exception.SQL

%Exception.AbstractException

例外クラス全ての親クラスになります。
ただし、クラスキーワードに「Abstract」が付与されているので、このクラスを使用する事はできません。

%Exception.General

一般的な例外クラスを作成する際に利用します。
使い方は後述します。

%Exception.SystemException

<DIVIDE>や<UNDEFINED>等の、システムレベルのエラー発生時に自動生成されます。

エラー状況の切り分けの為にも、このクラスをインスタンス化してthrowする事は、やめた方が良いかと思います。

%Exception.StatusException

使用頻度が高い例外クラスです。

%Stasusを生成して、関数「CreateFromSTatus()」の引数にセットしてthrowします。
 →%Statusの生成方法に関しては、別の記事で記載します。

サンプルです

ClassMethod StatusError()
{
	try {
		throw ##class(%Exception.StatusException).CreateFromStatus(
			$$$ERROR($$$TooManyErrors)
		)
	} catch e {		
		w $system.Status.GetErrorText(e.AsStatus()),!
		w e.%ClassName(1)
	}
}

ファイル操作系は、関数の戻り値が%Boolean型が多いので、戻り値「0」を受け取った際は、%Statusを生成してthrowしたりします。

ロジックレベルのエラー表現を行うのに最適です。

%Exception.SQL

SQL実行時のエラーをthrowする際に利用します。

埋め込みSQLでは「SQLCODE, %msg」、ダイナミックSQLでは「res.%SQLCODE, res.%Message」を引数にして、関数「CreateFromSQLCODE」から例外クラスを取得します。

サンプルです

ClassMethod SQLError()
{
	try {
		&sql(select className into:クラス名 from developer_data.HogeHoge)
		throw ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE, $g(%msg))
	} catch e {		
		w $system.Status.GetErrorText(e.AsStatus()),!
	}
}

例外クラスの使い方

基本的には「%Exception」クラスを生成し、throwでポイッとcatchコード・ブロックに投げるだけです。

今回は、一般的な例外クラス「%Exception.General」を利用して、エラー処理を行ってみたいと思います。

%Exception.General

クラス「%Exception.General.cls」を使用して、例外クラスを作成してみます。

引数説明
pNameエラーの名称
例)<DIVIDE>、<UNDEFINED>等々
pCodeエラーコード
%occErrors.inc参照
pLocationエラー発生個所
pData追加情報
 → 一番重要。詳しい情報を設定する
pInnerException他のエラーを抱える事が可能

サンプル

ClassMethod errGeneral()
{
	try {
		// サブエラー
		s subE = ##class(%Exception.General).%New(
			"<inErr>", $$$CacheError, , "インナーエラー"
		)
		
		// メインエラー
		throw ##class(%Exception.General).%New(
			"<SAMPLE>", $$$GeneralError, $$$info(3), "サンプルエラー", subE
		)
	} catch e {
		d $system.Status.DisplayError(e.AsStatus())
	}
}

【実行結果】
エラー #5035: 一般例外 名前 ‘<SAMPLE>’ コード ‘5001’ データ ‘サンプルエラー’
> エラー #5035: 一般例外 名前 ‘<inErr>’ コード ‘5002’ データ ‘インナーエラー’
errGeneral+3^developer.err.Sample

エラー発生箇所を自動取得する

throwする度に「pLocation」を作成するのは大変です。
特にライン番号なんて、毎回数えるのはとてもしんどいです。

サンプルの「$$$info」は、マクロになります。
別の記事にて解説するので、しばしお待ちください。

そこで、システム設定のグローバル「callererrorinfo」を設定する事により、エラー箇所(pLocation)を自動で取得する事が可能になります。

s ^%oddENV("callererrorinfo", [ネームスペース名]) = 1
// 例) s ^%oddENV("callererrorinfo", "SAMPLE") = 1

先ほどの関数から、「pLocation」の引数を除去した形で実行してみたいと思います。

ClassMethod errGeneral()
{
	try {
		s subE = ##class(%Exception.General).%New(
			"<inErr>", $$$CacheError, , "インナーエラー"
		)
		
		throw ##class(%Exception.General).%New(
			"<SAMPLE>", $$$GeneralError, , "サンプルエラー", subE
		)
	} catch e {
		d $system.Status.DisplayError(e.AsStatus())
	}
}

では、実行してみます。

【実行結果】
エラー #5035: 一般例外 名前 ‘<SAMPLE>’ コード ‘5001’ データ ‘サンプルエラー’ [er rGeneral+5^developer.err.Sample.1:SAMPLE]
> エラー #5035: 一般例外 名前 ‘<inErr>’ コード ‘5002’ データ ‘インナーエラー’ [errGeneral+2^developer.err.Sample.1:SAMPLE]

おぉ!

黄色い下線が、今回のグローバル設定で追加された情報になります。

自動でlocationが取得できるのはいいですね。

では次は、値を2に変更します。

s ^%oddENV("callererrorinfo", [ネームスペース名]) = 2

【実行結果】
エラー #5035: 一般例外 名前 ” コード ‘5001’ データ ‘サンプルエラー’ [e^OnAsStatus+1^%Exception.General.1^1 e^AsStatus+1^%Exception.AbstractException.1^1 e^errGeneral+9^developer.err.Sample.1^1 d^^^0:SAMPLE]
> エラー #5035: 一般例外 名前 ” コード ‘5002’ データ ‘インナーエラー’[e^OnAsStatus+1^%Exception.General.1^1 e^AsStatus+5^%Exception.AbstractException.1^1 e^errGeneral+9^developer.err.Sample.1^1 d^^^0:SAMPLE]

location情報がガッツリと表示されています。

このグローバル設定を、本番環境に設定するかの判断は検討が必要だと思いますが、開発環境に関しては行ってよいと思います。

設定値に関しては、1 or 2お好みで!

例外クラスの拡張

PythonやJAVA等は、例外の種類によってエラー処理を細分化する時があります。
下記のような感じですね。

def err_sample():
    try:
        print('Let's GO')
    except FileNotFoundError as e:
        print('FileNotFoundError!!')
    except TypeError as e:
        print('TypeError!!')
    else:
        print('error!!')

IRIS/Cacheで同じように作成する事は難しいですが、近い形で実現したいと思います。

カスタム例外クラスを作成する

先ずは、Pythonの例外「FileNotFoundError」「TypeError」に相当するエラーハンドルを作成します。

エラーハンドルは、「%Exception.AbstractException」を継承する必要があります。
Extendsに指定してください。

先ずは手始めに「FileNotFoundError」から作成します。
 ※サンプルなので、必用最低限の機能しか実装していません。

Class developer.err.FileNotFoundException Extends %Exception.AbstractException
{
Method OnAsStatus() As %Status [ CodeMode = expression, Private ]
{
$$$ERROR($$$FileDoesNotExist,i%Name_i%Location_$select(i%Data'="":$select($extract(i%Data)="^":" ",1:" *")_i%Data,1:""),,,,,,,,,,$select(i%iStack="":$lb(""),1:i%iStack))
}
}

お次は「TypeError」を作成します。
「$$$PropertyTypeInvalid」は、それっぽいだけで選択している感じです。
特に深い意味はありません。

Class developer.err.TypeException Extends %Exception.AbstractException
{
Method OnAsStatus() As %Status [ CodeMode = expression, Private ]
{
$$$ERROR($$$PropertyTypeInvalid,i%Name_i%Location_$select(i%Data'="":$select($extract(i%Data)="^":" ",1:" *")_i%Data,1:""),,,,,,,,,,$select(i%iStack="":$lb(""),1:i%iStack))
}
}

エラーハンドルを作成したら、さっそく使ってみましょう
サンプルは↓です

ClassMethod exception(mode As %Integer)
{
	try {
		i (mode = 1){
			s e = ##class(developer.err.FileNotFoundException).%New(,, $$$methodName_"^"_$$$className, "〇×ファイル")
		}elseif(mode = 2){
			s e = ##class(developer.err.TypeException).%New(,, $$$methodName_"^"_$$$className, "%Integerではない")
		}else{
			s e = ##class(%Exception.General).%New("<SAMPLE>", $$$GeneralError, $$$methodName_"^"_$$$className, "サンプルテスト")
		}
		throw e
	} catch e {
		i (e.%IsA("developer.err.FileNotFoundException")){
			w !, $system.Status.GetErrorText(e.AsStatus())
		}elseif (e.%IsA("developer.err.TypeException")){
			w !, $system.Status.GetErrorText(e.AsStatus())
		}else{
			w !, $system.Status.GetErrorText(e.AsStatus())
		}
	}
}

%Exceptionの判別は、「e.%IsA([例外クラス名])」で行っていますが、「e.%ClassName()」でも可能です。

では、一先ず動作させてみましょう。

d ##class(developer.err.Sample).exception(1)
>> エラー #5012: ファイル 'exception^developer.err.Sample *〇×ファイル' が存在しません
d ##class(developer.err.Sample).exception(2)
>> エラー #5418: プロパティタイプに誤り: exception^developer.err.Sample *%Integerではない
d ##class(developer.err.Sample).exception(3)
>> エラー #5035: 一般例外 名前 '<SAMPLE>' コード '5001' データ 'サンプルテスト'

一先ず「それっぽく」動かせました。

ただ、エラー・コードでも同様に分岐できるので、エラー・コードだけでは細分化できないケースや特殊なケースで使用すると良いと思います。

導入に関しては、業務ルールや構築ルールを定めて、計画的に検討してください。

おわりに

いかがだったでしょうか。

%Exception関連のクラスを上手に活用する事により、明確で柔軟なエラーハンドリングが実現できると思います。

例外処理は、単なるエラー処理で留めるのは勿体ないと思います。
保守性の高いアプリケーションの一環として、設計に組み込むと良いと思います。

信頼性のあるコーディングを目指していきたいですね!