2020年6月25日木曜日

特定のページへの直リンクによるアクセス(1) - はじめに

Oracle APEXのアプリケーションでは、アプリケーションに含まれるページに直接アクセスすることは、デフォルトでは許可されていません。いくつかの属性を変更することによりセキュリティ上の保護を解除することで、直リンクを有効にする方法について説明します。

以下のトピックを予定しています。
  1. はじめに - 検証に使用するアプリケーションの作成
  2. ページの保護について
  3. 直リンクの少し凝った設定
  4. 補足
これらの保護はデフォルトですべて有効なので、セキュリティ上の保護を緩めるつもりがなければ、設定を変更する必要はありません。

以下のようなタスク詳細のページを、タスクのIDを引数として含んだURLから開く実装を行います。


直接アクセスをするURLは以下を想定しています。

簡易URLがONの場合
簡易URLがOFFの場合

Oracle APEX 20.1から簡易URL(Friendly URLs)の機能が追加され、直リンクのためのURLの形式がより一般的になりました。新規アプリケーションの場合デフォルトでONですが、特に直リンクを扱うアプリケーションでは積極的に利用したい機能です。簡易URLの設定はアプリケーション設定のプロパティに含まれています。


承認などを求める電子メールの通知に、その作業を行うページへのリンクを埋め込みたい、という要件より、直リンクの実装が求められることが多いです。

現在の簡易URLの実装には、ひとつ問題が見つかっています。ディープ・リンクを有効にするとチェックサムによる保護が設定にかかわらず無効になります。ディープ・リンクを機能させるにはチェックサムによる保護を解除する必要があるので実用上の問題はありませんが、注意が必要です。実際には、チェックサムの保護が有効のまま運用していて、Oracle APEXのバージョンが上がって問題が修正されると、今まで動いていた直リンクでエラーが発生するようになる、というのが発生しそうなケースとして考えられます。

それでは、これから直リンクの確認に使うアプリケーションを作成します。

クイックSQLを使って表を作成します。SQLワークショップからユーティリティに含まれるクイックSQLを実行します。左の簡易表定義に以下を指定します。
tasks
  name
  detail
  start_date
  end_date
  status
DDLの生成オプション設定を開いて指定します。オブジェクト接頭辞としてtest主キートリガーおよび順序経由を選択します。その後、変更の保存を行います。


挿入の生成は、表ディレクティブ/insertが指定されているときに機能し、ONにすると/insertの指定にしたがってサンプル・データを投入するinsert文を生成します。今回は表デイレクティブとして/insertの指定は含んでいないため、ONでもOFFでも結果は変わりません。

設定を行なってから、SQLの生成を実行し、生成されたSQLのスクリプトの保存を行なった後、レビューおよび実行を行います。


SQLスクリプトの保存時に指定するファイル名は任意です。後の手順で参照されることはありません。レビューおよび実行をクリックしたのちに表示される画面ではアプリケーションの作成ではなく、実行をクリックし表、順序およびトリガーの作成のみを行います。


次に表示される確認画面では即時実行をクリックします。スクリプトの実行結果として3行成功していれば、実装に使用する表は完成です。


次に空のアプリケーションを作成します。アプリケーション・ビルダーの画面を開き、作成をクリックします。


新規アプリケーションをクリックします。


名前だけを指定して(ここではサンプルタスク管理としています)、アプリケーションの作成を実行します。


これで空のアプリケーションが作成されました。先ほど作成した表TEST_TASKSの内容を一覧するレポートとそれぞれのタスクの詳細を表示するフォームを生成します。ページの作成を実行します。


コンポーネントフォームをクリックすると、次に進みます。


フォーム付きレポートをクリックすると、次に進みます。

フォーム付きレポートの作成を行うウィザードが開始されます。ページ属性として、レポート・タイプ対話モード・レポートレポート・ページ名タスク一覧フォーム・ページ名タスク詳細フォーム・ページ・モード標準とします。フォーム・ページ・モードがモーダル・ダイアログであるページは直リンクでのアクセスは不可ですので、必ず標準にします。レポート・ページ番号、フォーム・ページ番号はそれぞれ2、3になるはずですが、一度ウィザードをキャンセルしているとページ番号が進むことがあります。ページ番号は後続の説明で参照されるので、その場合は2、3に変更してください。ブレッドクラムBreadcrumbを選択し、エントリ名はデフォルトでタスク一覧が設定されます。以上を設定して、へ進みます。


ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選択すると、新規ナビゲーション・メニュー・エントリとしてタスク一覧が設定されます。この設定により、アプリケーションの左に表示されるメニューに、今作成しているレポート・ページを呼び出すエントリが追加されます。に進みます。


データ・ソースがローカル・データベース、ソース・タイプが表であること(これらがデフォルト)を確認し、表/ビューの名前としてTEST_TASKSを選択します。デフォルトですべての列が表示する列に選択されるので、そのままへ進みます。


フォームの設定です。こちらもデフォルトですべての列が表示する列に選択されるので、それは変更せず、主キー型主キー列の選択とし、主キー列ID(Number)を選びます。ウィザードによる設定はこれで完了です。作成をクリックします。


レポートのページとフォームのページが作成され、編集画面であるページ・デザイナが開きます。右上のページの保存と実行をクリックし、作成したアプリケーションを実行します。


ログイン画面が表示されるので、ワークスペースのサインイン時に使用したユーザー名とパスワードでサインインします。


作成したレポート画面が表示されます。別の画面が表示される場合は、左上にあるハンバーガーのアイコンをクリックしナビゲーション・メニューを開いて、タスク一覧を選択します。データを一行挿入するために作成をクリックします。


フォームの内容を埋めて(任意で構いません)、作成をクリックします。


データが挿入されるとレポートに表示が戻ります。主キーであるIDは 1 が設定されていることが確認できます。(何度も操作していると1より大きくなります)

編集アイコンをクリックすると、対象タスクの詳細画面を表示されます。この画面を直接URLで指定して開こう、というのがこの記事のテーマです。


その前に2点ほど、設定の調整をします。

アプリケーション定義の編集を開き、URLに日本語が含まれないように、アプリケーションの別名を英数字に変更します。ここではdemo-tasks(ハイフンはOK)としています。


また、タスク詳細のページを開き、ページの別名taskへ変更します。


このように変更したのち、再度、登録したタスクの詳細を開きます。ブラウザに表示されるURLは以下のようになります。
https://apex.oracle.com/pls/apex/japancommunity/r/demo-tasks/task?p3_id=1&clear=RP&session=708433516570720&cs=3uY1cC2Q1Zgb3GRwlPd5NNHX49MTHJ7kZvXJo4JihNORTgbDCzh6E5nHK52QifSBjZjrXzhpvTeOqJLU2Ou7zZA
URLには4つのパラメータが含まれています。p3_idは表示対象となるタスクのIDです。(p3_idではなくidという引数名にあとで変更します)これは必要なパラメータです。clearはフォームを初期化しなさい、という指定です。直リンクの場合は常にフォームは初期状態でアクセスされることより、指定は不要なパラメータです。

sessionはセッションが開始して初めて有効な値です。直リンクによるアクセスではセッションは開始していないため、有効なsessionをあらかじめ与えることはできません。cs(チェックサム)には生成方法にいくつかの種類がありますが、p3_idはチェックサムによって保護されているので、p3_idに与える値を変更するときはチェックサムも同時に更新する必要があります。

これからは、これらの設定によって行われているセキュリティ上の保護の説明と、それをどのように外すかについて説明していきます。

2020年6月23日火曜日

Oracle APEXにおける楽観的並行性制御の実装について

Oracle APEXでは、データベースに保存されているデータを更新するための標準コンポーネントとして、対話グリッドフォームが提供されています。これらの標準コンポーネントに組み込まれている楽観的並行性制御(Optimistic Concurrency Control)について説明します。

難しい話に聞こえるかもしれませんが、この機能はデフォルトで有効になっています。そのため、Oracle APEXのアプリケーションでは、楽観的並行性制御を意識して実装する必要はありません。

今回はフォームを例にとって説明します。対話グリッドにも同様の設定があります。正確には対話グリッドに実装されていた機能が、その後に開発されたフォーム・リージョンにも含まれました。

フォームまたは対話グリッドの操作を実行するプロセス、行の自動処理(DML)設定にある失われた更新の防止ONにすることで楽観的並行性制御が有効になります。先ほども説明しましたが、これはデフォルトでONになっています。


アプリケーションを作って動作を確認してみます。SQLワークショップSQLコマンドから以下のDDLを実行して、表TEST_EMPを作成します。
create table test_emp (
id number generated by default on null as identity
constraint test_emp_id_pk primary key,
ename varchar2(80),
job varchar2(40),
sal number
);
表を作成した後、オブジェクト・ブラウザから作成した表TEST_EMPを選択し、アプリケーションの作成を実行します。


アプリケーションの作成画面では、任意の名前を設定し、それ以外はウィザードに任せて、アプリケーションの作成を実行します。今回の確認で使用するのは、Empレポート(フォーム付き対話モード・レポート)のみです。


アプリケーションが作成されますが、実行する前に少々調整をします。最初に、アプリケーション・プロパティの編集を開いて、アプリケーションの別名を変更します。次に、共有コンポーネント認証スキームを開いて、認証スキーム公開資格証明にします。


アプリケーションの別名は英数字と一部の記号に限定します。


認証スキームは検証作業を楽にするために、ユーザー名のみで認証できる公開資格証明をカレント・スキームにします。


対話モード・レポートから開かれるDMLフォームをページ・デザイナで開き、失われた更新の防止の設定を確認します。


左ペインにてプロセス・ビューを表示させ、フォームのプロセスを選択します。失われた更新の防止ONになっています。


準備は以上で完了です。これからアプリケーションを実行し、動作の確認をしていきます。

最初にアプリケーションを実行し、ユーザーaliceとしてログインします。


Empレポートを開きます。


作成をクリックして、確認に使用するデータを登録します。


EnameCarolJob営業Sal(Salary)を1000として、一行登録します。


AliceとBobがそれぞれ、CarolのSal+100することを依頼されたとします。それぞれ+100ですので、両人が処理を実行した結果は1200になることが想定されます。

AliceのアカウントでCarolのデータの編集画面を開きます。Sal1100に変更しますが、まだ、変更の適用は行いません。


別のブラウザで同じアプリケーションにBobのアカウントでアクセスします。こちらは、1000
と表示されているSal1100に変更し、変更の適用を実行します。


変更は適用されて、Salは1100として保存されます。


Aliceの画面に戻って、変更の適用をクリックします。データベースから行を読み出した後に、保存されている行が変更されたため、エラーが発生して変更の適用が出来ません。エラー・メッセージは「ユーザーが更新処理を開始してから、データベース内の現行バージョンのデータが変更されています。」です。


一旦このフォームを取消をクリックして閉じ、再度、更新処理をやり直す必要があります。次にフォームを開いた時は、Salは1100になるため、+100して1200に更新することになり、想定した結果になります。

失われた更新の防止をOFFにして同様の操作を行うと、エラーにならずにSalが保存されることが確認できますので、試してみてください。これは大抵の場合で問題の起こる動作ですので、この設定をOFFにすることはないでしょう。Webアプリケーションを一から作成する場合は、このようなコーディングを自分で行う必要があります。

失われた更新の防止の仕組みですが、2種類の方法から選ぶことができます。フォーム・リージョンの属性に、その切り替えがあります。失われた更新タイプがその設定です。


それぞれについて、ヘルプでは以下のように説明されています。

行の値
データを最初に問い合せる場合、チェックサム値が各行に計算されます。 チェックサムは、すべての更新可能な列を文字列に連結し、一意の値を生成して計算されます。 更新されたレコードをコミットすると、このチェックサムが現在のデータベース・レコードのチェックサム値と比較されます。 同じでない場合、エラーが発生します。
行バージョン列
データベース表にデータベース・トリガー(可能な場合)によってレコードが更新されるたびに増える列が含まれている場合、チェックサムを計算するかわりにこの列を使用できます。 対話グリッドで複数の表のデータを更新する場合、このオプションを使用しないでください。

注意 - 行バージョン列をリージョンのSQLソースに含める必要があります。
失われた更新タイプ行の値のときは、 データベースからそれぞれの列の情報を取り出してフォームに表示する際に、それらのすべての列を連結して生成したチェックサムも同時にフォームに含めます。このチェックサムは画面には表示されませんが、フォームがサブミットされる際にページ・アイテムと一緒にサーバーへ送信されます。サブミットされたフォームを処理するプロセスは、データを更新する前に変更の対象となっている行をデータベースから読み出し、チェックサムを計算します。受信したチェックサムと一致している場合(つまり行が変更されていない)のみ、受け取ったページ・アイテムの情報で既存の行を更新します。

行バージョン列はちょっと馴染みがないかもしれません。クイックSQLを使って表のDDLを生成するときに指定可能なオプションに行バージョン番号というものがあります。行バージョン列というのはそれを指します。

SQLワークショップユーティリティからクイックSQLを実行します。左の画面には以下の定義を指定します。
test_emp
  ename vc80
  job       vc40
  sal        number
設定を開いて、追加列行バージョン番号にチェックを入れ、変更の保存を行います。


結果として以下のようなDDLが生成されます。

-- create tables
create table test_emp (
    id                             number generated by default on null as identity  
                                   constraint test_emp_id_pk primary key,
    row_version                    integer not null,
    ename                          varchar2(80),
    job                            varchar2(40),
    sal                            number
)
;

-- triggers
create or replace trigger test_emp_biu
    before insert or update 
    on test_emp
    for each row
begin
    if inserting then
        :new.row_version := 1;
    elsif updating then
        :new.row_version := nvl(:old.row_version,0) + 1;
    end if;
end test_emp_biu;
/
追加されたrow_version列はトリガーによってアップデートが実行されるたびに、1づつ数値がインクリメントされます。そのためチェックサムの代わりにrow_versionを読み出し、データの更新時にはrow_versionを比較することで行の変更を検知することができます。

Oracle APEXのアプリケーションではチェックサムを生成するコードを記述する必要はないため、デフォルトの設定である行の値を使用するケースがほとんどでしょう。行バージョン列はチェックサムより実装が容易なので、Oracle APEX以外の処理がある場合は採用を検討することになるかと思います。例えば、Oracle REST Data Servicesを使ったRESTサービスによる更新などです。

最後に行のロックについて説明します。失われた更新の防止の下にあり、ヘルプの記載だけではその意味が非常に掴みにくい設定です。


先ほど、チェックサムの一致を確認する手順を記述しました。フォームの送信に含まれるチェックサムをプロセスが受け取ると、
  1. 更新対象の行を読み出して、チェックサムを計算する。
  2. 受信したチェックサムと計算したチェックサムを比較する。
  3. 一致していれば、受信したページ・アイテムで行を更新する。
という順番で処理が行われます。行のロックYesである場合、1の処理でチェックサムを計算するためにデータベースからデータを取り出す際に"SELECT FOR UPDATE"を実行し、行を排他ロックします。非常に短い瞬間ですが、チェックサムを計算してから実際に行のアップデートを行う間に行が更新されないように保護しています。ですので、できる限りYesにします。

フォームのソースが単純な表やSQLではなく、SELECTは実行できるがFOR UPDATEをつけるとエラーが発生する(例えばソースとなるSQLが複数の表をJOINしている)場合、またはデータ・ソースがリモート・データベースなどの場合にNoを設定します。Noにする弊害が明確な場合は、PL/SQL Codeを選択し、排他制御を行うためのコードを記述することができます。

Oracle APEXの楽観的並行性制御の実装についての説明は以上です。Oracle APEXの良いところは、Oracle APEXでアプリケーションを作成すると、意識しなくても、今まで説明してきた機能が実装されている、というところでしょう。

2020年6月22日月曜日

対話グリッドの自動リフレッシュ


対話グリッドを定期的かつ自動的にリフレッシュしたいんですけど、と聞かれてググったところ、以下のような動的アクションのプラグインを見つけました。オラクルが提供しているもので、含まれているタイムスタンプをみると2010年9月13日でした。ITの時間では骨董品に近いものですが、機能しました。

http://www.oracle.com/technetwork/developer-tools/apex/apex-timer-171713.zip

上記ファイルをダウンロードし展開した後、プラグインをインポートします。

共有コンポーネント他のコンポーネントに含まれる、プラグインを開きます。


インポートを実行します。(以下の画面ではすでにTimerプラグインがインポートされています)


ファイルの選択として、展開したプラグインのフォルダに含まれるdynamic_action_plugin_com_oracle_apex_timer.sqlを選択します。ファイル・タイププラグインとして、へ進みます。共有コンポーネントのプラグインからインポートを実行すると、ファイル・タイプのデフォルトはプラグインになります。


次に表示される確認画面はそのままへ進み、インストール画面にてインストールするアプリケーションを確認して、プラグインのインストールを実行します。共有コンポーネントのプラグインからインポートを実行すると、インストールするアプリケーションは、デフォルトでインポートを開始したアプリケーションになります。


これでTimerのプラグインがインストールされました。

次にTimerプラグインを構成して、対話グリッドのリフレッシュを自動的に行う動的アクションを定義します。

最初にTimerを初期化する動的アクションを設定します。左ペインに動的アクション・ビューを表示します。そしてページのロード上で(いわゆるマウスの右ボタンをクリックし)コンテキスト・メニューを表示し、動的アクションの作成を実行します。名前は任意ですが、ここではタイマーの開始、としています。タイミングイベントページのロードになります。


ページのロードが行われたときに実行されるアクション(Trueアクション)として、先ほどインストールしたTimer[プラグイン]を選び、ActionとしてAdd TimerTimer Nameは(任意ですが)ここではIG refresh(20バイトが名前の長さの上限です)、Expire in x Milliseconds5000(つまり5秒)、OccurrenceとしてInfinite(繰り返し - これ以外にOnce - 一度だけもあります)を指定します。そもそもページロード時の動的アクションなので、初期化時に実行OFFです。



タイマー自体の初期化はこれで出来たので、次にリフレッシュを行う動的アクションを設定します。動的アクション・ビューにあるイベントの項目でコンテキスト・メニューを表示させ、動的アクションの作成を実行します。名前は(任意ですが)ここでは定期リフレッシュタイミングイベントTimer Expired[Timer]選択タイプリージョンリージョンはリフレッシュを行う対話グリッドのリージョン名を指定します。ここでは従業員グリッドを指定しています。


最後にタイマーによって実行されるアクションを設定します。アクションとしてリフレッシュを選び、影響を受ける要素は、選択タイプリージョンで、リージョンとしては従業員グリッドになります。こちらも初期化時に実行OFFです。


以上で設定は完了です。ページを実行すると、最初の画面のように定期的に対話グリッドがリフレッシュされます。

Ronny Weissが開発した、より新しいタイマーの実装は、こちらにあります。"If you like my stuff, donate me a coffee"とありますので、コーヒー代の支払いを厭わなければ、こちらのプラグインの方の利用をお勧めします。