2021年3月3日水曜日

エディションベース再定義をAPEXで使用する - クロスエディション・トリガー編

エディション・ベースの再定義の記事の3番目で、最後の記事になります。前回はエディショニング・ビューを扱いました。今回はエディショニング・ビューに加えて、クロスエディション・トリガーを実装することにより、以前のバージョンと新バージョンのアプリケーションの間で、データの同期を行います。

0. 行う作業の説明。

前回の記事で作成したエディションed02を一旦削除します。基底となる表に列FIRST_NAME、LAST_NAMEを追加し、再度エディションed02を作成し、列ENAMEの代わりに列FIRST_NAME、LAST_NAMEを使用するエディショニング・ビューEMPを作成します。

現行のアプリケーションが列ENAMEを更新する際に、FIRST_NAMEとLAST_NAMEを設定するフォワード・クロスエディション・トリガーを作成します。

さらに、新しいアプリケーションが列FIRST_NAMEとLAST_NAMEを更新する際に、ENAMEを設定するリバース・クロスエディション・トリガーも実装します。

1. エディションed02を削除する。

エディションを削除せず新規にED02の子エディションとしてED03を作成してもよいのですが、エディションの削除手順の確認の意味を含めて、ED02を削除してみます。

ユーザーADMINで接続し、エディションED02を削除します。

SQL> drop edition ed02;


次のコマンドの開始中にエラーが発生しました : 行 1 -

drop edition ed02

エラー・レポート -

ORA-38811: need CASCADE option to drop edition that has actual objects


SQL>


オブジェクトが残っているため、エラーとなりました。存在するオブジェクトをビューDBA_OBJECTS_AEを検索して確かめます。

SQL> select owner, object_type, object_name from dba_objects_ae where edition_name = 'ED02';


     OWNER    OBJECT_TYPE    OBJECT_NAME 

__________ ______________ ______________ 

APEXDEV    VIEW           EMP            


SQL>


スキーマAPEXDEVにエディショニング・ビューEMPが存在します。こちらをドロップするとエディションED02を削除できそうです。実施してみます。エディショニング・ビューはエディションED02に存在するため、削除前にセッション・エディションをED02へ変更します。また、エディションED02を削除する際には、エディションをED02以外に設定します。

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> drop view apexdev.emp;


View APEXDEV.EMPが削除されました。


SQL> alter session set edition = ora$base;


Sessionが変更されました。


SQL> drop edition ed02;


Edition ED02が削除されました。


SQL>


2. エディションed02を作成し、スキーマを更新する。

ユーザーADMINで接続し、エディションed02を作成します。作成したエディションed02を、ユーザーAPEXDEVで利用できるように権限を与えます。

SQL> create edition ed02 as child of ed01;


Edition ED02は作成されました。


SQL> grant use on edition ed02 to apexdev;


Grantが正常に実行されました。


SQL>


ユーザーAPEXDEVで接続し、表EMP_Bに列FIRST_NAME、LAST_NAMEを追加します。

SQL> alter table emp_b add (first_name varchar2(80));


Table EMP_Bが変更されました。


SQL> alter table emp_b add (last_name varchar2(80));


Table EMP_Bが変更されました。


SQL> 


エディションED02で、エディショニング・ビューEMPを作成します。列ENAMEの代わりにFIRST_NAME、LAST_NAMEを含めます。前回表EMP_Bに追加した、列WORK_LOCATIONも除外しています。

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> create or replace editioning view emp

  2  as

  3  select empno, first_name, last_name, job, mgr, hiredate, sal, comm, deptno from emp_b;


View EMPは作成されました。


SQL>


アプリケーション・ビルダー(ページ・デザイナ含む)にて、同じ定義のエディショニング・ビューを参照できるように、エディションORA$BASEにエディショニング・ビューEMPを作成します。

SQL> alter session set edition = ora$base;


Sessionが変更されました。


SQL> drop view emp;


View EMPが削除されました。


SQL> create view emp

  2  as

  3  select empno, first_name, last_name, job, mgr, hiredate, sal, comm, deptno from emp_b;


View EMPは作成されました。


SQL>


クロスエディション・トリガーを作成する前段階まで完了しました。

3. アプリケーションを更新する。

作成済みのアプリケーションデモ - 従業員 / 部門 第2版を、更新したエディショニング・ビューに対応させます。

ページ・デザイナにてPage 3を開き、クラシック・レポートのリージョンEmployeesの列の同期化を実行します。


列WORK_LOCATIONが無くなり、FIRST_NAMEとLAST_NAMEが追加されます。


続いてフォームのページを開いて、フォーム・リージョンに含まれるページ・アイテムの同期化を実行します。


レポート列と同様にページ・アイテムP4_WORK_LOCATIONが無くなり、P4_FIRST_NAMEとP4_LAST_NAMEが追加されます。


スポットライト・サーチを使用し、列ENAMEを参照している部分を探します。このアプリケーションでenameを検索を実行します。


ファセット検索のP3_SEARCHと共有コンポーネントのLOV、EMP.ENAMEで使用されていることがわかります。


それぞれビューをクリックし、修正していきます。

ファセット検索のP3_SEARCHソースデータベース列よりENAMEを除き、FIRST_NAMEとLAST_NAMEに置き換えます。


続いて共有コンポーネントのEMP.ENAMEを編集します。ソースソース・タイプSQL問合せに変更し、以下のSQLを指定します。その後、列のマッピングとして戻り値R表示DデフォルトのソートDソート方向昇順(Nulls Last)を設定し、変更の適用をクリックします。
select last_name || ' ' || first_name d,
    empno r
from emp

スポットライト・サーチを再度行い、変更漏れが無いことを確認します。

アプリケーションの更新は完了したので、アプリケーションを実行して動作を確認します。

従業員のページを開くと列ENAMEが無くなり、First NameとLast Nameが追加されていることが確認できます。


作成をクリックし、フォームを開きます。フォームにはFirst NameとLast Nameのページ・アイテムが追加され、また、ENAMEがなくなっていることが確認できます。

適当な値を入力し、従業員をひとり登録します。


First Name、Last Nameの値とともに従業員が追加されました。Manager列が空白ですが、これは既存の列ENAMEと違い、FIRST_NAME、LAST_NAMEの列に何も設定されていないためです。


以上でアプリケーションの更新は完了です。

これから列ENAMEからFIRST_NAME、LAST_NAMEを設定するフォワード・クロスエディション・トリガー、列FIRST_NAME、LAST_NAMEからENAMEを設定するリバース・クロスエディション・トリガーを設定します。

4. クロスエディション・トリガーを設定する。

ユーザーAPEXDEVで接続し、セッション・エディションをED02にします。

最初にフォワード・クロスエディション・トリガーを設定します。

トリガーは一旦無効化(disable)で作成し、コンパイルが成功したことを確認してから有効化(enable)します。
create or replace editionable trigger ename_to_first_last
before insert or update
on emp_b
for each row
forward crossedition
disable
begin
    :new.first_name := substr(:new.ename,instr(:new.ename,' ')+1);
    :new.last_name  := substr(:new.ename,1,instr(:new.ename,' ')-1);
end;
/

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> create or replace editionable trigger ename_to_first_last

  2  before insert or update

  3  on emp_b

  4  for each row

  5  forward crossedition

  6  disable

  7  begin

  8      :new.first_name := substr(:new.ename,instr(:new.ename,' ')+1);

  9      :new.last_name  := substr(:new.ename,1,instr(:new.ename,' ')-1);

 10  end;

 11  /


Trigger ENAME_TO_FIRST_LASTがコンパイルされました


SQL> alter trigger ename_to_first_last enable;


Trigger ENAME_TO_FIRST_LASTが変更されました。


SQL>


次にリバース・クロスエディション・トリガーを作成します。
create or replace editionable trigger first_last_to_ename
before insert or update
on emp_b
for each row
reverse crossedition
disable
begin
   :new.ename := :new.last_name || ' ' || :new.first_name;
end;
/

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> create or replace editionable trigger first_last_to_ename

  2  before insert or update

  3  on emp_b

  4  for each row

  5  reverse crossedition

  6  disable

  7  begin

  8      :new.ename := :new.last_name || ' ' || :new.first_name;

  9  end;

 10  /


Trigger FIRST_LAST_TO_ENAMEがコンパイルされました


SQL> alter trigger first_last_to_ename enable;


Trigger FIRST_LAST_TO_ENAMEが変更されました。


SQL>


作成したトリガーを確認します。

SQL> select trigger_name, trigger_type, status,edition_name, crossedition from user_triggers_ae;


          TRIGGER_NAME       TRIGGER_TYPE     STATUS    EDITION_NAME    CROSSEDITION 

______________________ __________________ __________ _______________ _______________ 

DEPT_TRG1              BEFORE EACH ROW    ENABLED                    NO              

EMP_TRG1               BEFORE EACH ROW    ENABLED                    NO              

ENAME_TO_FIRST_LAST    BEFORE EACH ROW    ENABLED    ED02            FORWARD         

FIRST_LAST_TO_ENAME    BEFORE EACH ROW    ENABLED    ED02            REVERSE         


SQL>


エディションED02より親になるエディションにて表EMP_Bの操作が行われる際にフォワード・クロスエディション・トリガーが起動されます。エディションED02からの操作では、リバース・クロスエディション・トリガーが起動されます。


5. アプリケーションを実行して動作を確認する。

変更前のアプリケーションを使用して、従業員を一名追加します。作成をクリックします。


Employee Nameオラクル 花子として従業員を追加します。作成をクリックします。


Employee Nameがオラクル 花子である行がレポートに含まれます。


新しいアプリケーションを実行して、新規に作成された従業員を確認します。

First Nameが花子、Last Nameがオラクルの行を見つけることができます。フォワード・クロスエディション・トリガーに記載した処理によって、Employee NameがFirst NameとLast Nameに分解されて登録されていることを確認できます。


作成をクリックし、新しいアプリケーションから従業員を一名登録します。First Nameは二郎、Last Nameはオラクルとしました。


従業員が登録されたことが確認できます。


この従業員を旧バージョンのアプリケーションから確認します。

リバース・クロスエディション・トリガーの処理によって、First NameとLast Nameを連結した名前がEmployee Nameとして表示されています。


以上でクロスエディション・トリガーの設定と動作確認が完了しました。

6. クロスエディション・トリガーを呼び出して既存のデータを変換する。

クロスエディション・トリガーの設定だけでは、トリガーを設定する以前から存在するデータは更新されません。作成済みのフォワード・クロスエディション・トリガーを明示的に指定して呼び出すことで、一括でデータの変換を行うことができます。

トリガーの呼び出しは、プロシージャDBMS_SQL.PARSEもしくはDBMS_PARALLEL_EXECUTE.RUN_TASKから行うことができます。双方ともapply_crossedition_trigger、fire_apply_triggerの引数を持ち、トリガー名と実行フラグの指定を行えます。

更新中のアップデートが失われないよう、DBMS_UTILITY.WAIT_ON_PEINDING_DMLプロシージャを呼び出して、更新待ちのトランザクションが無いことを確認します。
declare
   out_scn number := null;
begin
   if not dbms_utility.wait_on_pending_dml('EMP_B', 2, out_scn) then
      raise_application_error(-20105, 'Wait on Pending DML timed out due to transactions started before ' || out_scn);
   end if;
end;
/
DBMS_SQL.PARSEでの実行は以下になります。
declare
  c     number := dbms_sql.open_cursor();
  retval number;
begin
    dbms_sql.parse(
         c => c,
         statement => 'update emp_b set empno = empno',
         language_flag => dbms_sql.native,
         edition => null,
         apply_crossedition_trigger => 'ENAME_TO_FIRST_LAST',
         fire_apply_trigger => TRUE
     );
     retval := dbms_sql.execute(c);
     dbms_sql.close_cursor(c);
     commit;
end;
/

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> declare

  2      c     number := dbms_sql.open_cursor();

  3      retval number;

  4  begin

  5      dbms_sql.parse(

  6          c => c,

  7          statement => 'update emp_b set empno = empno',

  8          language_flag => dbms_sql.native,

  9          edition => null,

 10          apply_crossedition_trigger => 'ENAME_TO_FIRST_LAST',

 11          fire_apply_trigger => TRUE

 12      );

 13      retval := dbms_sql.execute(c);

 14      dbms_sql.close_cursor(c);

 15      commit;

 16  end;

 17  /


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


SQL> 


更新する行が多い場合はDBMS_PARALLEL_EXECUTE.RUN_TASKを使用します。今回の例に対応するコードは以下になります。
DECLARE
 l_sql_stmt VARCHAR2(512);
 l_try      NUMBER := 0;
 l_status   NUMBER;
BEGIN
  dbms_parallel_execute.create_task('EMPEBR');

  dbms_parallel_execute.create_chunks_by_rowid(
    task_name => 'EMPEBR',
    table_owner => 'APEXDEV',
    table_name => 'EMP_B',
    by_row =>  TRUE,
    chunk_size => 100000
  );

  l_sql_stmt := 'UPDATE apexdev.emp_b SET empno =  empno WHERE rowid BETWEEN :start_id AND :end_id';

  dbms_parallel_execute.run_task(
    task_name => 'EMPEBR',
    sql_stmt => l_sql_stmt,
    language_flag => dbms_sql.native,
    edition => null,
    apply_crossedition_trigger => 'ENAME_TO_FIRST_LAST',
    fire_apply_trigger => TRUE,
    parallel_level => 1
  );

  l_status := dbms_parallel_execute.task_status('EMPEBR');
  WHILE(l_try < 2 and l_status != dbms_parallel_execute.finished) LOOP
    l_try := l_try + 1;
    dbms_parallel_execute.resume_task('EMPEBR');
    l_status := dbms_parallel_execute.task_status('EMPEBR');
  END LOOP;

  dbms_parallel_execute.drop_task('EMPEBR');
END;
/
更新されたアプリケーションの従業員一覧を確認します。Manager、First Name、Last Nameのすべてでデータが更新されています。



7. エディショニング・ビューを一時的に読取専用にする。

アップグレードに関連して、エディションング・ビューを読取専用(および読み書き可能)に設定するプロシージャが用意されています。

プロシージャdbms_editions_utilities.set_editioning_views_read_onlyです。以下に実行例を示します。読取専用にしたのちにINSERT文を実行するとORA-42399というエラーが発生していることが確認できます。

SQL> alter session set edition = ed02;


Sessionが変更されました。


SQL> begin

  2      dbms_editions_utilities.set_editioning_views_read_only(

  3          table_name => 'EMP_B',

  4          owner => 'APEXDEV',

  5          read_only => TRUE

  6      );

  7  end;

  8  /


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


SQL> insert into emp(empno) values(100);


次のコマンド行の開始中にエラーが発生しました : 1 -

insert into emp(empno) values(100)

コマンド行 : 1 列 : 13 でのエラー

エラー・レポート -

SQLエラー: ORA-42399: cannot perform a DML operation on a read-only view

42399.0000 - "cannot perform a DML operation on a read-only view"

SQL> begin

  2      dbms_editions_utilities.set_editioning_views_read_only(

  3          table_name => 'EMP_B',

  4          owner => 'APEXDEV',

  5          read_only => FALSE 

  6      );

  7  end;

  8  /


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


SQL> insert into emp(empno) values(100);


1行挿入しました。


SQL> commit;


コミットが完了しました。


SQL>


アプリケーションのアップグレード中にすべてのエディショニング・ビューからの更新を禁止したい場合に利用できるでしょう。

以上でOracle Databaseの機能であるエディションベースの再定義を、Oracle APEXで使用する方法の紹介記事は終了です。

難しそうで敬遠してきた機能ですが、実際に動かして確認してみると、思った以上にシンプルで使い勝手が高そうです。とはいえ、最初からエディションの利用を前提としてスキーマ設計をする必要がありそうで、導入のハードルは低くは無いな、という印象を持ちました。

Oracle APEXのアプリケーション作成の一助になれば幸いです。