seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

PL/SQLで動的に関数を呼び出す(実行時に呼び出す関数を決定する)方法

動的SQL(execute immediate文)の利用

PL/SQLにはJAVAのリフレクションのような機能はないが、文字列として書かれたSQLを実行する「動的SQL」という機能がある。
動的SQLにはSELECT/UPDATE/INSERT/DELETEのようなDML以外にDDLもかけるし、無名PL/SQLブロックも書くことができる。
無名PLSQLブロックとは、BEGIN ... ENDで囲まれたPLSQLのコードのことである。
当然、ファンクションやプロシージャ、パッケージの呼び出しも可能である。
したがって、sqlplus上で試してみると、

SQL> set serveroutput on
SQL> begin
  2  execute immediate 'begin dbms_output.put_line(''ok''); end;';
  3  end;
  4  /
ok

PL/SQLプロシージャが正常に完了しました。

のように、文字列として渡したコードを実行することが可能なわけである。

これは単なる文字列であるから、呼び出すパッケージ名やプロシージャ名を実行時に決定することが可能である。

たとえば、関数ポインタのように使うには以下のようにすることができるだろう。

SQL> create or replace function func1(p_value in number) return number is
  2  begin
  3      return p_value * 10;
  4  end;
  5  /

ファンクションが作成されました。

SQL> create or replace function func2(p_value in number) return number is
  2  begin
  3      return p_value * 100;
  4  end;
  5  /

ファンクションが作成されました。

これらの2つの関数は、名前によって呼び分けられるものとする。

呼ぶ側のファンクションは、以下のとおり。

SQL> create or replace function func(p_type in number, p_value in number) return number is
  2      v_ret number;
  3  begin
  4      execute immediate 'begin :1 := func' || p_type || '(:2); end;'
  5          using out v_ret, p_value;
  6      return v_ret;
  7  end;
  8  /

ファンクションが作成されました。

これにより、引数v_typeが1であるか2であるかによって関数を呼び分けることができる。

動的SQLのexecute immediate文はusing句によってパラメータを渡したり受け取ったりすることができる。
暗黙ではinモードであるため、パラメータを渡せるが戻すことはできない。
結果を受け取る場合はoutを指定する。(また、in outと記述することで受け渡しが可能となる。)*1
(DML文を相手にする場合はバインド変数名が同じでも出現回数分、変数を指定する必要があるが、
無名PL/SQLブロックを相手にする場合は同じバインド変数名は最初の1回だけ指定する必要がある。)

これを実行すると、以下のようになる。

SQL> var ret number;
SQL> execute :ret := func(1, 123);

PL/SQLプロシージャが正常に完了しました。

SQL> print ret;

       RET
----------
      1230

SQL> execute :ret := func(2, 123);

PL/SQLプロシージャが正常に完了しました。

SQL> print ret;

       RET
----------
     12300

ちなみに、定義されていないファンクションを動的SQLから呼び出すと以下のようになる。

SQL> execute :ret := func(3, 123);
BEGIN :ret := func(3, 123); END;

*
行1でエラーが発生しました。:
ORA-06550: line 1, column 13:
PLS-00201: identifier 'FUNC3' must be declared
ORA-06550: line 1, column 7:
PL/SQL: Statement ignored
...

普通のPLSQLならばコンパイル時点で判明するエラーであるから、動的SQL特有の実行時エラーだといえる。
なお、ORA-06550は「コンパイルエラー」である。

動的SQLのスコープにおける制約

動的SQLで実行されるコードは、グローバルなスコープ上で実行されているのに等しく、それ以外はusing句で指定された変数のみ見えている。
もちろん、パッケージで定義されていればグローバルに見えるので、それは使える。
あるいはパッケージ変数にアクセスすることも可能である。
しかし、execute immediate文があるPLSQLコードのブロックにあるローカル変数やネストした関数/プロシージャはもとより、
あるいはパッケージボディ内でのみ定義されている変数や関数/プロシージャも見えない制約がある。

結論

動的SQLによる実行時の呼び出し関数の決定は、本当に原始的な、状態を持たない関数ポインタのようなものとして使えるものの、
レキシカルスコープを使った関数オブジェクトのようなものを作るのは、この方法では無理そうである。
しかし、使いようによってはコードをコンパクト、あるいは、より柔軟に書ける素材として活用できる可能性はありそうである。

*1:returning intoはDMLだけで無名PL/SQLブロックでは使えない。