Database
技術記事
PL/SQL 101
2011年7/8月 |
PL/SQLの理解と利用に関するシリーズ記事のパート2
新参のPL/SQL開発者がこの言語を最大限に活用できるように、Oracle Magazineから、PL/SQL初心者向けのシリーズを執筆しないかという話をいただきました。この記事は、そのパート2にあたります。経験豊富なPL/SQL開発者でも、PL/SQLの基本を手軽に思い出すためにこのシリーズをご利用いただけるでしょう。
このシリーズは、ある程度のプログラミング経験があり、SQLに慣れているPL/SQL初心者の方を対象としています。また、シリーズ全体を通じて、開発者ができる限りすぐにPL/SQLで生産的に開発できるようになるアプローチをとっていきます。
PL/SQLには、開発者が通常利用するあらゆるプログラミング言語と同じところが1つあります。それは、PL/SQL(PL/SQLランタイム・エンジン)は、開発者が指示したことだけをそのまま実行するということです。開発者が記述するPL/SQLコードの各ブロックは、複雑なビジネス・ルールやプロセスを実装した1つ以上の文により構成されます。そのコード・ブロックを、無名ブロックまたはスクリプトとして実行するか、あるいはすべてのロジックを含むストアド・プログラム・ユニットをコールすることによって実行すると、Oracle Databaseはそのブロック内で開発者が示した指示内容に従って動作します。
したがって、どの文を、どのような状況下で、どれほどの頻度で実行するのかを指定する方法を知ることが重要です。そのために、Oracle Databaseでは、条件分岐および反復のための構成メンバーを提供しています。この記事では、PL/SQLでサポートされるIF文、CASE文とCASE式、およびさまざまなタイプのループについて概要を説明します。
開発者が記述するほぼすべてのコードで、条件制御が必要になります。条件制御とは、条件に基づいてプログラム内の実行フローを指示することです。条件制御のために、IF-THEN-ELSE文とCASE文を使用します。
また、CASE式というものもあります。CASE式はCASE文とは異なりますが、CASE式を使用することでIF文やCASE文がすべて不要になる場合があります。
IF:IF文は、プログラムで条件分岐ロジックを実装するために使用できます。IF文を使用すれば、次のような要件を実装できます。
| IF文の種類 | 特性 |
| IF THEN END IF; | もっとも単純なIF文の形式。IFとTHENの間に記述する条件により、THENとEND IFの間に記述する一連の文を実行すべきかが判定される。条件がFALSEまたはNULLと評価される場合は、コードは実行されない。 |
| IF THEN ELSE END IF; | この組合せでは二者択一のロジックを実装する。IFキーワードとTHENキーワードの間に記述する条件に基づいて、THENとELSEの間のコードと、ELSEとEND IFの間のコードのいずれかが実行される。文で構成される2つのセクションのうち1つが実行される。 |
| IF THEN ELSIF ELSE END IF; | IF文の最後の形式であり、もっとも複雑な形式。相互に排他的な一連の条件からTRUEとなる条件が選択され、その条件に関連付けられた一連の文が実行される。Oracle9i Database Release 1以降のOracle Databaseリリースでこの形式のIF文を記述している場合は、代わりに検索CASE文を使用することが推奨される。 |
表1:IF文の種類
IF-THEN:次の文は、2つの数値を比較します。これらの2つの値のいずれかがNULLの場合、この式全体がNULLを返すことに注意してください。次の例では、給与がNULLの場合、ボーナスは付与されません。
IF l_salary > 40000 THEN give_bonus (l_employee_id,500); END IF;
この、ブール式にNULLが含まれる場合は結果がNULLになる、というルールには例外があります。結果がNULLではなくTRUEまたはFALSEになるようにNULLを処理するための専用の演算子やファンクションがあります。たとえば、IS NULLを使用して、NULLであるかを検査できます。
IF l_salary > 40000 OR l_salary IS NULL THEN give_bonus (l_employee_id,500); END IF;
この例では、給与に値がない場合に"給与がNULL"がTRUEと評価され、それ以外の場合はFALSEと評価されます。これで、給与のデータがない従業員にもボーナスが付与されます(そもそも雇用者が軽率で従業員への支払いを追跡管理できていないことを考慮すれば、確かにこのような対応も必要でしょう)。
IF-THEN-ELSE:次に、IF-THEN-ELSE構成メンバー(IF-THEN構成メンバーの発展形)の例を示します。
IF l_salary <= 40000 THEN give_bonus (l_employee_id, 0); ELSE give_bonus (l_employee_id, 500); END IF;
この例では、給与が$40,000を超える従業員には$500のボーナスが付与され、それ以外の従業員にはボーナスが付与されません。しかし、本当にそうでしょうか。理由は何であれ、ある従業員の給与がNULLの場合はどうなるでしょう。その場合は、ELSEの後の文が実行され、その従業員にボーナスが付与されます。しかし、ボーナスは高い給与を得ている従業員にのみ支払うことになっています。そのため、給与がNULLとなりうる場合は、NVLファンクションを使用することで、この問題を回避できます。
IF NVL(l_salary,0) <= 40000 THEN give_bonus (l_employee_id, 0); ELSE give_bonus (l_employee_id, 500); END IF;
給与がNULLであればNVLファンクションは0を返します。そのため、給与がNULLの従業員にはボーナスが付与されません(かわいそうな従業員ですね)。
覚えておくべき重要なこととして、文の2つのシーケンスのいずれかが、かならず実行されます。IF-THEN-ELSEは二者択一を表す構成メンバーであるからです。適切な一連の文が実行された後は、END IFの直後の文に制御が渡されます。
IF-ELSIF:次に、ELSIFの使い方を見てみましょう。この最後のIF文形式は、二者択一の状況ではなく、多くの選択肢があるロジックを実装する必要がある場合に便利です。IF-ELSIF式は、1つのIF文の中で複数の条件に対処する方法を提供します。一般的には、相互に排他的な選択肢に対して(つまり、IF文を実行するとかならず1つの条件のみがTRUEになる場合に)ELSIFを使用すべきです。
それぞれのELSIF句には、条件の後にTHENを置く必要があります(ELSEキーワードに限り、THENキーワードは必要ありません)。IF-ELSIFのELSE句は、文の"上記以外の条件"を示します。いずれの条件もTRUEと評価されない場合に、ELSE句の文が実行されます。ただし、ELSE句はオプションです。IF句とELSIF句のみから成るIF-ELSIFをコーディングすることもできます。その場合、いずれの条件もTRUEと評価されなければ、IFブロック内の文は実行されません。
IF-ELSIF文の例を次に示します。この例では、3種類の条件を検査します。また、これらのいずれの条件もTRUEと評価されない場合に備え、ELSE句も含まれています。
IF l_salary BETWEEN 10000 AND 20000 THEN give_bonus(l_employee_id, 1000); ELSIF l_salary > 20000 THEN give_bonus(l_employee_id, 500); ELSE give_bonus(l_employee_id, 0); END IF;
CASE文とCASE式は、条件分岐を実装するための別の手法を提供します。多くの場合はIFの代わりにCASEを使用することで、条件をより明確に表現し、コードをシンプルにすることもできます。CASE文には次の2つの形式があります。
単純CASE文:1つ以上のPL/SQL文のシーケンスを、それぞれ1つの値に関連付けます。単純CASE文は、これらの値のいずれかを返す式に基づいて、実行すべき文のシーケンスを選択します。
検索CASE文:ブール条件のリストを評価することで、実行すべき1つ以上のPL/SQL文のシーケンスを選択します。TRUEと評価された最初の条件に関連付けられている文のシーケンスが実行されます。
CASE文に加えて、PL/SQLではCASE式もサポートされます。CASE式は形式的にはCASE文に似ていて、評価する1つ以上の式を選択できます。CASE式の結果は単一の値です。一方、CASE文では結果として、PL/SQL文のシーケンスが実行されます。
単純CASE文:単純CASE文は、1つの式の結果に基づいて、PL/SQL文の複数のシーケンスの中から実行すべきシーケンスを選択するために使用できます。単純CASE文は次のような形式です。
CASE expression WHEN result1 THEN statements1 WHEN result2 THEN statements2 ... ELSE statements_else END CASE;
単純CASE文の例を次に示します。この例では、従業員のタイプに基づいて、適切なボーナスのアルゴリズムを選択しています。
CASE l_employee_type
WHEN 'S'
THEN
award_bonus (l_employee_id);
WHEN 'H'
THEN
award_bonus (l_employee_id);
WHEN 'C'
THEN
award_commissioned_bonus (
l_employee_id);
ELSE
RAISE invalid_employee_type;
END CASE;
このCASE文には明示的なELSE句がありますが、ELSEはオプションです。開発者がELSE句を明示的に指定しない場合は、PL/SQLで次のELSE句が暗黙的に使用されます。
ELSE RAISE CASE_NOT_FOUND;
つまり、ELSE句を指定せず、WHEN句のいずれの結果もCASE式の結果に一致しない場合は、PL/SQLでCASE_NOT_FOUNDエラーが発生します。この動作はIF文の動作とは異なります。IF文にELSE句がなく、条件が満たない場合には何も起きません。CASEを使用する場合は、そのような状況ではエラーになります。
検索CASE文:検索CASE文はブール式のリストを評価し、TRUEと評価される式が見つかった場合に、その式に関連付けられた文のシーケンスを実行します。検索CASE文は次のような形式です。
CASE WHEN expression1 THEN statements1 WHEN expression2 THEN statements2 ... ELSE statements_else END CASE;
検索CASE文は、次のようなボーナスのロジック実装問題に最適です。
CASE
WHEN l_salary
BETWEEN 10000 AND 20000
THEN
give_bonus(l_employee_id, 1500);
WHEN salary > 20000
THEN
give_bonus(l_employee_id, 1000);
ELSE
give_bonus(l_employee_id, 0);
END CASE;
ヒント:WHEN句は順に評価されるため、もっとも可能性の高いWHEN句を先に記述することで、コードの実行効率を少し向上できる場合があります。また、"コストの高い"式(つまり、多くのCPUサイクルとメモリが必要となる式)を含むWHEN句がある場合は、最後に記述することで、評価される機会を最小限に抑えることができます。
実行すべき一連の文を識別する根拠としてブール式を使用する場合には、検索CASE文を使用します。1つの式の結果に基づいて判断できる場合は、単純CASE文を使用します。
CASE式の使用:CASE式は1つの値を返し、いずれかの結果式の結果が選択されます。それぞれのWHEN句は1つの式(文ではない)と関連付ける必要があります。CASE式の終了を示すためにセミコロンやEND CASEを使用しないでください。CASE式は単純なENDだけで終了します。
検索CASE式を使用すれば、ボーナスを付与するためのコードがシンプルになります。give_bonusを3回コールするIF文またはCASE文を記述する代わりに、give_bonusを1回だけコールし、第2引数の代わりにCASE式を使用できます。
give_bonus(l_employee_id,
CASE
WHEN l_salary
BETWEEN 10000 AND 20000
THEN 1500
WHEN l_salary > 40000
THEN 500
ELSE 0
END);
CASE文とは異なり、CASE式でWHEN句がまったく選択されない場合でもエラーは発生しません。満たされるWHEN条件がない場合は、単純にCASE式によりNULLが返されます。
コード内でループを使用すれば、コードの同じ本体部分を複数回実行できます。ループは非常によく使用されるプログラミング要素です。プログラムによってモデル化する実際のアクティビティのほとんどで、繰り返し処理が実行されるからです。たとえば、前年の各月に対して操作を実行する必要があります。あるいは、オラクルの有名な従業員表のよくある例を引用すると、ある部門に所属するすべての従業員の情報を更新する必要があります。
PL/SQLでは、3種類のループ構成メンバーを提供しています。
FORループ(数値またはカーソル)
単純(無限)ループ
WHILEループ
ループの種類ごとに目的は異なり、それぞれに微妙な違いや利用ルール、高品質な構成を行うためのガイドラインがあります。
各ループの問題解決方法の違いについて感覚を掴むために、次の3つのループ例について考えてみます。それぞれの例で、プロシージャは、開始の引数値から終了の引数値までの年ごとに、その年のdisplay_total_salesをコールします。
FORループ:Oracle Databaseは、数値によるFORループとカーソルによるFORループの両方を提供しています。数値によるFORループでは、開始と終了の整数値を開発者が指定すると、後はPL/SQLにより、その開始から終了までのそれぞれの値について繰り返し処理が実行され、最後にループが終了します。
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
,end_year_in IN PLS_INTEGER
)
IS
BEGIN
FOR l_current_year
IN start_year_in .. end_year_in
LOOP
display_total_sales
(l_current_year);
END LOOP;
END display_multiple_years;
カーソルによるFORループも基本構造は同じですが、整数の最大値と最小値による範囲の代わりに、明示的なカーソルまたはSELECT文を指定します。
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
,end_year_in IN PLS_INTEGER
)
IS
BEGIN
FOR l_current_year IN (
SELECT * FROM sales_data
WHERE year
BETWEEN start_year_in
AND end_year_in)
LOOP
display_total_sales
(l_current_year);
END LOOP;
END display_multiple_years;
数値によるFORループでもカーソルによるFORループでも、Oracle Databaseにより、イテレータ(この例の場合はl_current_year)が整数またはレコードとして暗黙的に宣言されます。開発者は次のように同じ名前で変数を宣言する必要はありません(むしろ宣言しないでください)。
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
,end_year_in IN PLS_INTEGER
)
IS
l_current_year
INTEGER; /* NOT NEEDED */
BEGIN
FOR l_current_year
IN start_year_in
.. end_year_in
実際、このような変数を宣言した場合でも、この変数はFORループで使用されません。正確に言えば、この明示的な変数は、Oracle Databaseによって暗黙的に宣言されループの本体内で使用される変数とは異なる整数変数となります。
単純ループ:単純と呼ばれるのには理由があります。単純ループは、単純にLOOPキーワードから開始し、END LOOP文で終了します。ループの本体内でEXIT、EXIT WHEN、またはRETURNを実行した場合(あるいは例外が発生した場合)、ループは終了します。
リスト1に単純ループを示します。また、リスト2に、カーソルの行に対して繰り返し処理を実行する単純ループを示します(論理的には前項のカーソルによるFORループと同じものです)。
コード・リスト1:単純ループ
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
,end_year_in IN PLS_INTEGER
)
IS
l_current_year PLS_INTEGER := start_year_in;
BEGIN
LOOP
EXIT WHEN l_current_year > end_year_in;
display_total_sales (l_current_year);
l_current_year := l_current_year + 1;
END LOOP;
END display_multiple_years;
リスト2のカーソルベースの単純ループをカーソルによるFORループと比較しましょう。リスト2では、カーソルを明示的にオープンし、次のレコードをフェッチし、フェッチが終了したかどうかを%NOTFOUNDカーソル属性を使用して判定し、ループの終了後にはカーソルをクローズする必要がある点に注意してください。
コード・リスト2:カーソルの行に対して繰り返し処理を実行する単純ループ
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
, end_year_in IN PLS_INTEGER)
IS
CURSOR years_cur
IS
SELECT *
FROM sales_data
WHERE year BETWEEN start_year_in AND end_year_in;
l_year sales_data%ROWTYPE;
BEGIN
OPEN years_cur;
LOOP
FETCH years_cur INTO l_year;
EXIT WHEN years_cur%NOTFOUND;
display_total_sales (l_year);
END LOOP;
CLOSE years_cur;
END display_multiple_years;
カーソルによるFORループの場合はこれらの手順は一切不要です。Oracle Databaseによりすべての手順(オープン、フェッチ、終了、クローズ)が暗黙的に実行されます。
単純ループは、カーソルのデータセットを1行ずつフェッチするためには使用すべきではなく、次の場合に使用すべきです。(1)条件に基づいて、EXIT文を使用してループを終了する必要がある場合。(2)ループの本体を1回以上実行したい場合。
WHILEループ:WHILEループは単純ループと非常によく似ていますが、決定的な違いがあります。それは、WHILEループでは終了条件が事前に検査されることです。つまり、WHILEループでは本体が1回も実行されない場合があります。リスト3に、WHILEループの例を示します。
コード・リスト3:WHILEループ
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
,end_year_in IN PLS_INTEGER
)
IS
l_current_year PLS_INTEGER := start_year_in;
BEGIN
WHILE (l_current_year <= end_year_in)
LOOP
display_total_sales (l_current_year);
l_current_year := l_current_year + 1;
END LOOP;
END display_multiple_years;
WHILEループは、条件(ブール式)とループ本体により構成されます。本体のそれぞれの反復を実行する前に、Oracle Databaseによって条件が評価されます。条件がTRUEと評価された場合は、ループ本体が実行されます。条件がFALSEまたはNULLと評価された場合は、ループが終了します。
そのため、条件の評価に影響を及ぼすコードをループ本体に含める必要があります。そうすれば、いつかはループが停止します。リスト3のプロシージャでは、次のコードがそのようなコードにあたります。
l_current_year := l_current_year + 1
つまり、指定されたすべての年の売上合計が表示されるまで、次の年に移ることになります。
WHILEループで条件の評価方法が変更されなければ、そのループはいつまでも終了しません。
前項のすべての例で、必要なコード量がもっとも少ないのは明らかにFORループです。一般的には、要件に応じたもっとも単純で可読性の高い実装が求められます。では、常にFORループを使用すべきということになるでしょうか。決してそうではありません。
説明したシナリオにとってFORループを使用するのが最適な解決策であったのは、ループ本体を実行する回数が固定されていたためです。その他の多くの状況で、ループの実行回数は、アプリケーション内のデータの状態によって変わります。また、特定の条件が満たされたときにループを終了する必要がある場合もあります。そのような場合は、FORループは適していません。
構造化プログラミングの重要な基本原則の1つは、"入口1つ、出口1つ"です。つまり、プログラムには単一のエントリ・ポイント(入口点)と単一のエグジット・ポイント(出口点)を作成すべきです。単一のエントリ・ポイントについては、PL/SQLでは問題になりません。使用するループの種類を問わず、常にループへのエントリ・ポイントは1つしかないからです。それは、LOOPキーワード直後の最初の実行可能文です。しかし、複数の終了パスが存在するループを構成することは十分に可能です。このプラクティスは避けてください。ループを終了する方法が複数あると、そのような方法が1つしかない場合と比較してコードのデバッグや保守が格段に難しくなります。
ループの終了については、特に次の2つのガイドラインに従ってください。
FORループとWHILEループの内部でEXIT文またはEXIT WHEN文を使用しないこと。
FORループは、範囲内で指定したすべての値(整数またはレコード)に対して繰り返し処理を実行する場合にのみ使用してください。FORループの中にEXITを記述するとこのプロセスが中断され、その構造の意図が阻害されます。一方、WHILEループでは、WHILE文自体に終了条件を指定します。
リスト4に、条件付きの終了を含むFORループの例を示します。この例では、指定した範囲内の年ごとに売上合計を表示したいのですが、売上のない年があれば(total_sales_for_yearファンクションで計算)、ループを停止する必要があります。
コード・リスト4:条件付きの終了を含むFORループ
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
, end_year_in IN PLS_INTEGER)
IS
BEGIN
FOR l_current_year IN start_year_in .. end_year_in
LOOP
display_total_sales_for_year (l_current_year);
EXIT WHEN total_sales_for_year (l_current_year) = 0;
END LOOP;
END display_multiple_years;
この例には、ループを"出る"方法が2つあります。このような状況では、FORループをWHILEループとして記述し直します。そのため、リスト5のように、イテレータも宣言し、ループ内に追加する必要があります。
ループ内ではRETURN文またはGOTO文を使用しないでください。構造化されていない未熟なループの終了につながるからです。
コード・リスト5:終了が1つのみのWHILEループ
PROCEDURE display_multiple_years (
start_year_in IN PLS_INTEGER
, end_year_in IN PLS_INTEGER)
IS
l_current_year PLS_INTEGER := start_year_in;
BEGIN
WHILE ( l_current_year <= end_year_in
AND total_sales_for_year (l_current_year) > 0)
LOOP
display_total_sales_for_year (l_current_year);
l_current_year := l_current_year + 1;
END LOOP;
END display_multiple_years;
リスト6に、RETURNを含むFORループの例を示します。total_salesファンクションは、指定した数年間の売上合計を返しますが、売上が$0の年があれば、ループを終了して現在の売上合計を返します。
コード・リスト6:2つのRETURNインスタンスを含むFORループ
FUNCTION total_sales (
start_year_in IN PLS_INTEGER
, end_year_in IN PLS_INTEGER)
RETURN PLS_INTEGER
IS
l_return PLS_INTEGER := 0;
BEGIN
FOR l_current_year IN start_year_in .. end_year_in
LOOP
IF total_sales_for_year (l_current_year) = 0
THEN
RETURN l_return;
ELSE
l_return :=
l_return + total_sales_for_year (l_current_year);
END IF;
END LOOP;
RETURN l_return;
END total_sales;
このループは、2つの方法のいずれかにより終了します。開始年から終了年までのすべて整数値に対して繰り返し処理を実行した場合、またはループ内部でRETURNを実行した場合です。さらにこれに関連して、このファンクションには2つのRETURNインスタンスがあります。そのため、ファンクションを"出る"方法が2つあることになります。これも、推奨されないファンクション設計方法です。実行可能セクション内には、RETURNを1つだけ、ファンクションの最終行にのみ配置すべきです。
このファンクションは、リスト7のように、ループもファンクションも"出口が1つ"だけ含まれるように再構成できます。
コード・リスト7:出口が1つだけの改良版ループ
FUNCTION total_sales (
start_year_in IN PLS_INTEGER
, end_year_in IN PLS_INTEGER)
RETURN PLS_INTEGER
IS
l_current_year PLS_INTEGER := start_year_in;
l_return PLS_INTEGER := 0;
BEGIN
WHILE (l_current_year <= end_year_in
AND total_sales_for_year (l_current_year) > 0)
LOOP
l_return := l_return + total_sales_for_year (l_current_year);
l_current_year := l_current_year + 1;
END LOOP;
RETURN l_return;
END total_sales;
次のステップ その他の記事
|
ループを終了するために必要なすべてのロジックがWHILE条件に収まり、ループの終了後は、ファンクションによって売上合計の値を返す1つのRETURNが実行されます。この2つ目の実装の方がシンプルで理解しやすくなっています。これは、それ自体がすでにかなりシンプルであるプログラム向けです。より複雑なアルゴリズムを使用する場合は、"入口1つ、出口1つ"のガイドラインに従うことで、コードの可読性が格段に向上します。
この記事では、PL/SQLコンパイラに対して、ブロック内の文を条件付きおよび反復的に実行するように指示する方法を説明しました。このような制御によって、ユーザーによって定義されたビジネス・プロセス・フローを忠実に反映したストアド・プログラム・ユニットを記述できます。
次回について:このシリーズの次の記事(パート3)では、PL/SQLプログラムでの文字列操作について取り上げます。