【PL/SQL】動的SQLのコーディング方法
※必要があれば「静的SQLと動的SQLの概要」ページを参照下さい。
※必要があれば【動的SQLからバルクバインドを使用する】記事を読む > 【PL/SQL】バルク・バインドもどうぞ。
「動的SQL」は条件によってSQLが変化する場合に柔軟に対応する為や、DDL(CREATE TABLE 等)の発行に使用します。
特にDDLは静的SQLでは記述できません。
PL/SQL上の静的SQLでは、コンパイル時にSQLで指定したオブジェクトが存在している必要がある為です。
※ 例えば、PL/SQL上の静的SQLで「CREATE TABLE」を記述するとコンパイルエラーとなります。指定したテーブル(オブジェクト)がその時点で存在しない為です。
動的SQLには、ORACLE8以前からある「DBMS_SQLパッケージ」を使う方法、ORACLE8i以降からある「システム固有の動的SQL」の記述方法があります。
それぞれメリット・デメリットがあるので、順に説明して行きます。
目次
システム固有の動的SQL
(INSERT、UPDATE、DELETE、DDL)
ORACLE8i以降からある方法です。
「:」を付けた項目名は「プレースホルダ」(バインド変数)となります。
「パフォーマンス」や「SQLインジェクション対策」の為に積極的に使用していきたいです。
「プレースホルダ」は、EXECUTE IMMEDIATE の USING句に並べた値の順番にバインド(紐付け)されます。
ただ、USING句の並びでバインドされる為、プレースホルダの個数が未確定(※)の場合は「USING」では記述できません。後記の「DBMS_SQLパッケージ」を使用します。
※未確定とは、例えば下記の場合だと、条件によって「DDD」の項目が有ったり無かったりする事です
DECLARE
sql_stmt VARCHAR2(32767);
BEGIN
sql_stmt := 'INSERT INTO TBL(AAA,' ||
'BBB,' ||
'CCC' ||
') VALUES(' ||
':AAA,' ||
':BBB,' ||
':CCC' ||
')';
FOR I IN 1..8
LOOP
EXECUTE IMMEDIATE sql_stmt USING I, 'A', 'B'; -- I変数の値が1~8で繰返し実行される
END LOOP;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END
上記を実行した「TBL」テーブルの結果は下記となります。
AAA | BBB | CCC |
1 | A | B |
2 | A | B |
3 | A | B |
4 | A | B |
5 | A | B |
6 | A | B |
7 | A | B |
8 | A | B |
システム固有の動的SQL(SELECT INTO)
ORACLE8i以降からある方法です。
「SELECT INTO」を動的SQLで記述すると下記となります。SELECT結果が2行以上になるとエラーとなるので注意が必要です。(よっぽどの理由がない限りはカーソルを定義しましょう)
プレースホルダの個数が未確定の場合は「USING」では記述できません。
DECLARE
sql_stmt VARCHAR2(32767);
DB_TBL TBL%ROWTYPE;
BEGIN
sql_stmt := 'SELECT * ' ||
'FROM TBL ' ||
'WHERE (AAA = :AAA) AND (BBB = :BBB)';
EXECUTE IMMEDIATE sql_stmt INTO DB_TBL USING 1, 'A';
END
カーソル変数を使用した動的SQL(SELECT)
ORACLE8i以降からある方法です。
「IS REF CURSOR」でカーソル変数を定義し、OPEN ~ FOR で文字列として記述したSQLを発行します。
プレースホルダの個数が未確定の場合は「USING」では記述できません。
DECLARE
sql_stmt VARCHAR2(32767);
TYPE typ_TEST IS REF CURSOR;
csrTEST typ_TEST;
DB_TEST TEST%ROWTYPE;
BEGIN
sql_stmt := 'SELECT * ' ||
'FROM TBL ' ||
'WHERE (AAA = :AAA) AND (BBB = :BBB)';
OPEN csrTEST FOR sql_stmt USING 1, 'B';
LOOP
FETCH csrTEST INTO DB_TEST;
EXIT WHEN csrTEST%NOTFOUND;
END LOOP;
CLOSE csrTEST;
EXCEPTION
WHEN OHTERS THEN
IF csrTEST%ISOPEN THEN
CLOSE csrTEST;
END IF;
END
DBMS_SQLパッケージ
(INSERT、UPDATE、DELETE、DDL)
ORACLE8以前からある方法です。システム固有の動的SQL(EXECUTE IMMEDIATE)では実施する事ができない、プレースホルダの個数が未確定なSQLを記述できます。逆にプレースホルダの個数が決まっている場合は、パフォーマンスの問題やコーディング量が少なくなるのでシステム固有の動的SQLを使用します。
下記は、「SFLG」の条件によって「DDD」項目が追加される例です。
DECLARE
sql_stmt VARCHAR2(32767);
cid INTEGER;
result INTEGER;
SFLG BOOLEAN := FALSE;
BEGIN
sql_stmt := 'INSERT INTO TBL(AAA,' ||
'BBB,' ||
'CCC';
IF NOT SFLG THEN
sql_stmt := sql_stmt || ', DDD';
END IF;
sql_stmt := sql_stmt || ') VALUES(' ||
':AAA,' ||
':BBB,' ||
':CCC';
IF NOT SFLG THEN
sql_stmt := sql_stmt || ', :DDD';
END IF;
sql_stmt := sql_stmt || ')';
cid := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(cid, sql_stmt, dbms_sql.native);
FOR I IN 1..8
LOOP
DBMS_SQL.BIND_VARIABLE(cid, ':AAA', I);
DBMS_SQL.BIND_VARIABLE(cid, ':BBB', 'B');
DBMS_SQL.BIND_VARIABLE(cid, ':CCC', 'C');
IF NOT SFLG THEN
DBMS_SQL.BIND_VARIABLE(cid, ':DDD', 'D');
END IF;
result := DBMS_SQL.EXECUTE(cid);
END LOOP;
DBMS_SQL.CLOSE_CURSOR(cid);
COMMIT;
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(cid) THEN
DBMS_SQL.CLOSE_CURSOR(cid);
END IF;
ROLLBACK;
END
「DBMS_SQL」で「INSERT」等を発行する手順は下記の通りです
1.文字列でSQLを記述する
2.「DBMS_SQL.OPEN_CURSOR」でカーソルのハンドル番号を取得する
3.「DBMS_SQL.PARSE」でSQLの解析を行う
4.「DBMS_SQL.BIND_VARIABLE」でプレースホルダに値を渡す。項目名で指定するので並びは関係ないです。
5.「DBMS_SQL.EXECUTE」でSQLを発行する。戻り値はSQLで処理された件数
6.「DBMS_SQL.CLOSE_CURSOR」でカーソルのハンドルを閉じる
※値を数回渡す場合は「4. BIND_VARIABLE」と「5. EXECUTE」を繰り返し実行します
DBMS_SQLパッケージ(SELECT)
ORACLE8以前からある方法です。システム固有の動的SQL(EXECUTE IMMEDIATE)では実施する事ができない、プレースホルダの個数が未確定なSQLを記述できます。
ただ、SELECTで取得する項目を個別に番号で指定する必要があるので、項目数が多いと死にますw
DECLARE
sql_stmt VARCHAR2(32767);
cid INTEGER;
ret INTEGER;
fetch INTEGER;
L_AAA DBMS_SQL.Number_Table;
L_BBB DBMS_SQL.Varchar2_Table;
SFLG BOOLEAN := FALSE;
BEGIN
sql_stmt := 'SELECT AAA, BBB ' ||
'FROM TBL ' ||
'WHERE (AAA = :AAA) AND ' ||
'(BBB = :BBB) AND ' ||
'(CCC = :CCC)';
IF NOT SFLG THEN
sql_stmt := sql_stmt || ' AND (DDD = :DDD)';
END IF;
cid := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(cid, sql_stmt, dbms_sql.native);
DBMS_SQL.BIND_VARIABLE(cid, ':AAA', 1);
DBMS_SQL.BIND_VARIABLE(cid, ':BBB', 'B');
DBMS_SQL.BIND_VARIABLE(cid, ':CCC', 'C');
IF NOT SFLG THEN
DBMS_SQL.BIND_VARIABLE(cid, ':DDD', 'D');
END IF;
ret := DBMS_SQL.EXECUTE(cid);
LOOP
fetch := DBMS_SQL.FETCH_ROWS(cid);
EXIT WHEN fetch <= 0;
DBMS_SQL.COLUMN_VALUE(cid, 1, L_AAA);
DBMS_SQL.COLUMN_VALUE(cid, 2, L_BBB);
END LOOP;
DBMS_SQL.CLOSE_CURSOR(cid);
EXCEPTION
WHEN OTHERS THEN
IF DBMS_SQL.IS_OPEN(cid) THEN
DBMS_SQL.CLOSE_CURSOR(cid);
END IF;
END
「DBMS_SQL」で「SELECT」を発行する手順は下記の通りです
1.文字列でSQLを記述する
2.「DBMS_SQL.OPEN_CURSOR」でカーソルのハンドル番号を取得する
3.「DBMS_SQL.PARSE」でSQLの解析を行う
4.「DBMS_SQL.BIND_VARIABLE」でプレースホルダに値を渡す。項目名で指定するので並びは関係ないです。
5.「DBMS_SQL.EXECUTE」でSQLを発行する
6.「DBMS_SQL.FETCH_ROWS」で行を読み込む
7.「DBMS_SQL.COLUMN_VALUE」で項目の値を取得する。項目名ではなく、何番目の項目かで指定する
※値を代入する変数は「DBMS_SQL.Number_Table」、「DBMS_SQL.Varchar2_Table」で宣言していますが通常通り「NUMBER」、「VARCHAR2」で宣言しても問題ありません
8.「DBMS_SQL.CLOSE_CURSOR」でカーソルのハンドルを閉じる
DBMS_SQLパッケージ
(【SELECT】DBMS_SQL.TO_REFCURSOR)
ORACLE11g以降からある方法です。DBMS_SQLパッケージでSELECTを発行すると、値の取得に各項目を個別に指定しなければいけませんが、「DBMS_SQL.TO_REFCURSOR」でカーソルハンドルを変換する事で通常のカーソルFETCHにする事ができます。
DBMS_SQLパッケージのプレースホルダ指定の柔軟性と、カーソル変数でのデータ取得と良いとこ取りができます。
DECLARE
sql_stmt VARCHAR2(32767);
cid INTEGER;
ret INTEGER;
fetch INTEGER;
TYPE typ_TBL IS REF CURSOR;
csrTBL typ_TBL;
DB_TBL TBL%ROWTYPE;
BEGIN
sql_stmt := 'SELECT AAA, BBB ' ||
'FROM TBL ' ||
'WHERE (AAA = :AAA) AND ' ||
'(BBB = :BBB) AND ' ||
'(CCC = :CCC)';
cid := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(cid, sql_stmt, dbms_sql.native);
DBMS_SQL.BIND_VARIABLE(cid, ':AAA', 1);
DBMS_SQL.BIND_VARIABLE(cid, ':BBB', 'B');
DBMS_SQL.BIND_VARIABLE(cid, ':CCC', 'C');
ret := DBMS_SQL.EXECUTE(cid);
csrTBL := DBMS_SQL.TO_REFCURSOR(cid);
LOOP
FETCH csrTBL INTO DB_TBL;
EXIT WHEN csrTBL%NOTFOUND;
END LOOP;
CLOSE csrTBL;
EXCEPTION
WHEN OTHERS THEN
IF csrTBL%ISOPEN THEN
CLOSE csrTBL;
END IF;
END
「DBMS_SQL.TO_REFCURSOR」を使用する手順は下記の通りです
1.文字列でSQLを記述する
2.「DBMS_SQL.OPEN_CURSOR」でカーソルのハンドル番号を取得する
3.「DBMS_SQL.PARSE」でSQLの解析を行う
4.「DBMS_SQL.BIND_VARIABLE」でプレースホルダに値を渡す。項目名で指定するので並びは関係ないです。
5.「DBMS_SQL.EXECUTE」でSQLを発行する
6.「DBMS_SQL.TO_REFCURSOR」でカーソルハンドルをカーソル変数に変換する
7.通常のFETCHで値を取得する事ができる
8.通常のCLOSEでカーソルを閉じる
【動的SQLからバルクバインドを使用する】記事を読む >> 【PL/SQL】バルク・バインド