2025年2月19日水曜日

セッション・ステートのストレージの永続設定について

Oracle APEXのページ・アイテムの属性として、セッション・ステートストレージがあります。この属性には、以下のどれかを設定できます。
  • リクエストごと(メモリーのみ)
  • セッションごと(永続)
  • ユーザーごと(永続)
アプリケーションに多数のページ・アイテムがあり、かつ、それらのセッション・ステートストレージ永続が選択されていると、(程度の違いはありますが)APEXのページ表示のパフォーマンスに影響します。

原則として、ページ・アイテムの値をリクエストをまたがって保存する必要がなければ、セッション・ステートストレージリクエストごと(メモリーのみ)を選択します。

セッション・ステートストレージ永続が設定された場合、ページ・アイテムの値はページが送信されたとき(一般に動作アクションページの送信であるボタンをブラウザ上でクリックしたとき = データベースがHTTPのPOSTリクエストに含まれるフォームのデータを受信したとき)にデータベースに保存されます。データベースに保存されているセッション・ステートは、ページのリクエストまたはAjaxのリクエストを受け付けたときに、データベースからメモリにロードされます。この際、リクエストを受け付けたページに含まれるページ・アイテムだけではなく、アプリケーションに含まれるすべてのページ・アイテムの値がメモリへロードされます。アプリケーションのページ数が多い、永続に設定されたページ・アイテムが多い、および、テキスト領域のページ・アイテムでデータ型にCLOBを設定していたりすると、リクエストごとに発生するデータベースからメモリへのセッションの復元コストが高くなります。特にデータ型がCLOBのページ・アイテムには注意が必要です。

セッション・ステートストレージリクエストごと(メモリーのみ)のページ・アイテムの値は、セッション・ステートとしてデータベースに保存されません。また、ページやAjaxコールバックの呼び出し時にデータベースからメモリにロードされることもありません。

動作確認のためのAPEXアプリケーションを作成し、セッション・ステートに関して説明します。

確認に使用するAPEXアプリケーションのエクスポートを以下に置きました。APEX 24.2で作成しています。
https://github.com/ujnak/apexapps/blob/master/exports/flow-data.zip

セッション・ステートストレージ永続が設定されているページ・アイテムの値は、APEXのスキーマ以下の表WWV_FLOW_DATAに保存されます。APEX 24.2であれば、APEX_240200.WWV_FLOW_DATAになります。この表の内容をSQLclで接続して確認します。Autonomous DatabaseではAPEXのスキーマは保護されていてアクセスできない(管理者ADMINでもREAD権限はありません)ため、SYSDBAで接続できるローカルのAPEX環境で作業します。

確認に使用するアプリケーションについて簡単に説明します。アプリケーションは2枚のページを含みます。

ページ番号1の画面は以下になります。


ボタン同じセッションでページ2を開くには、ボタンのクリックで実行される動的アクションを設定しています。以下のJavaScriptコードを実行します。
apex.navigation.openInNewWindow(`${prefix}/flow-data/page-2?session=${apex.env.APP_SESSION}`);

prefixページ・プロパティJavaScriptファンクションおよびグローバル変数の宣言で定義しています。アプリケーションをインストールした環境に合わせて設定を調整します。
const prefix = '/ords/r/apexdev';


ボタンクローンしたセッションでページ2を開くでは、以下のJavaScriptコードを実行します。引数requestAPEX_CLONE_SESSIONを与えているので、セッションがクローンされます。
apex.navigation.openInNewWindow(`${prefix}/flow-data/page-2?session=${apex.env.APP_SESSION}&request=APEX_CLONE_SESSION`);

ページ・アイテムP1_SESSION_IDはAPEXのセッションIDを表示する、タイプ表示のみのページ・アイテムです。ソースタイプアイテムを選択し、アイテムとしてAPP_SESSIONを設定します。

表示のみのアイテムでもセッション・ステートストレージの設定があります。セッション・ステートに保存する必要はないので、リクエストごと(メモリーのみ)にします。設定ページの送信時に送信オフではなく、セッション・ステートストレージ永続が設定されていると、タイプ表示のみのページ・アイテムの値もデータベースに保存されます。


ページ・アイテムP1_TEXTは、タイプテキスト・フィールドのページ・アイテムです。セッション・ステートストレージセッションごと(永続)を設定します。

ソース使用としてセッション・ステートの値がNULLの場合のみを指定します。この設定により、ページ・ロード時に設定されるP1_TEXTの値は、セッション・ステートに保存されている値が優先されます。セッション・ステートにP1_TEXTの値が保存されていない場合に、ソースに定義された値がP1_TEXTに設定されます。今回の例ではタイプNULLなので、セッション・ステートにP1_TEXTの値が設定されていないときは、P1_TEXTの値はNULLになります。


よくある設定ミスですが、例えばP1_TEXTソースタイプとしてSQL問合せ(単一の値を返す)を選択し、SQL問合せに以下を記述します。
select to_char(sysdate, 'YYYY-MM-DD HH24:MI:SS') from dual
使用セッション・ステートの値がNULLの場合のみとし、セッション・ステートストレージセッションごと(永続)とします。


ページ・アイテムP1_TEXTを空白にして、ページを送信します。


ページ・アイテムP1_TEXTの値はNULLとして送信され、セッション・ステートにNULLが保存されます。その後、ページが再描画されます。ページの再描画では、ページ・アイテムP1_TEXTのソースはセッション・ステートがNULLであるため、ソースに設定したSQLが評価されます。結果としてto_char(sysdate,'YYYY-MM-DD HH24:MI:SS')の値がP1_TEXTに設定されます。同時にこの値がセッション・ステートに保存されます。ページの再描画が完了すると、P1_TEXTにto_char(sysdate,'YYYY-MM-DD HH24:MI:SS')の値が表示されます。


この後はボタン送信を何度クリックしてもP1_TEXTの時刻は変わりません。ページ・アイテムP1_TEXTに設定されている時刻が送信され、セッション・ステートに保存されます。ページの再描画時は、セッション・ステートの値が優先され、ソースのSQL文は評価されません。


セッション・ステートの値よりソースの値を優先する場合は、セッション・ステートストレージリクエストごと(メモリーのみ)に設定し、そもそもセッション・ステートに値を保存しないか、または、ソース使用セッション・ステートの既存の値を常に置換に変更します。このように設定すると、セッション・ステートよりもソースの値が優先されます。


ページ・アイテムP1_CLOBは、タイプテキスト領域のページ・アイテムです。セッション・ステートデータ型CLOBストレージセッションごと(永続)を設定します。


ボタン送信 - セッション・ステートへの保存は、動作アクションとしてページの送信を実行します。ページ・アイテムの値のセッション・ステートへの保存は、プロセスを設定しなくても行われます。


ページ描画時に保存されているセッション・ステートの値を、静的コンテンツのリージョンに出力します。

静的コンテンツのリージョンを作成し、ソースHTMLコードとして以下を記述します。ページ2に作成したページ・アイテムの値も表示対象にします。
<b>ページ1のVARCHAR2:</b> &P1_TEXT.
<br>
<b>ページ1のCLOB:</b> &P1_CLOB.
<br>
<b>ページ2のVARCHAR2:</b> &P2_TEXT.
<br>
<b>ページ2のCLOB:</b> &P2_CLOB.

Ajaxコールバックを呼び出してセッション・ステートの値を取り出すボタンAjaxコールバックの呼び出しを作成します。ボタンをクリックしたときに、以下のサーバー側のコードを実行します。PL/SQLコードを実行し、出力をページ・アイテムP1_RESPONSEに出力します。

ページ1で実行しますが、ページ2に作成したページ・アイテムのセッション・ステートも取り出します。
declare
    l_response clob;
begin
    l_response := l_response || 'ページ1のVARCHAR2: ' || :P1_TEXT || apex_application.LF;
    l_response := l_response || 'ページ1のCLOB: ' || :P1_CLOB || apex_application.LF;
    l_response := l_response || 'ページ2のVARCHAR2: ' || :P2_TEXT || apex_application.LF;
    l_response := l_response || 'ページ2のCLOB: ' || :P2_CLOB || apex_application.LF;
    :P1_RESPONSE := l_response;
end;

Ajaxコールバックの出力を保持するページ・アイテムとしてP1_RESPONSEを作成します。タイプテキスト領域セッション・ステートデータ型CLOBストレージリクエストごと(メモリーのみ)を設定します。ストレージが永続ではないため、このページ・アイテムの値はサーバーに送信されますが、セッション・ステートには保存されません。


ページ2はページ・アイテムの名前のプレフィックスがP1_からP2_に置き換えられているだけで、構成はページ1と同じです。ページ2なので、ページ2を開くボタンはありません。


動作確認に使用するアプリケーションの説明は以上です。

実際にアプリケーションを操作して、セッション・ストレージの永続設定について確認します。

ページ1を開き、VARCHAR2CLOBに適当な文字列を入力して送信します。


データベースにSYSで接続し、カレント・スキーマをAPEX_240200に設定します。

alter session set current_schema = apex_240200;

SQL> alter session set current_schema = apex_240200;


Sessionが変更されました。


SQL>


表WWV_FLOW_DATAを検索します。FLOW_INSTANCEにセッションIDを指定して検索範囲を限定します。

select * from wwv_flow_data where flow_instance = [セッションID] and item_id > 0 order by item_name asc;

ページ・アイテムP1_TEXTP1_CLOBの値が列ITEM_VALUE_VC2に保存されていることが確認できます。ページ・アイテムP1_CLOBのデータ型はCLOBですが、列 
ITEM_VALUE_VC2に収まる長さであれば、列ITEM_VALUE_VC2に保存されます。


ページ・アイテムの値は列ITEM_VALUE_VC2に保存されています。IS_ENCRYPTYなので、値は暗号化されています。

セッション・ステートの暗号化は、ページ・アイテムセキュリティセッション・ステートに暗号化された値を保存で制御します。デフォルトはオン(IS_ENCRYPT = Y)です。


同じセッションでページ2を開きます。


ページ2で、VARCHAR2CLOBに適当な文字列を入力して送信します。今回はCLOBに長い文字列を入力します。

ページ2の静的コンテンツで、ページ1のページ・アイテムであるP1_TEXTおよびP1_CLOBの値が参照できています。アプリケーション全体のセッション・ステートがメモリにロードされているためです。


表WWV_FLOW_DATAの内容を確認します。

ページ・アイテムP2_CLOBの値が列ITEM_VALUE_CLOBに保存されていることが確認できます。SET LONGに80と設定されているため、CLOBとして保存されている値は先頭80文字までが表示されています。


ページ1に戻り、送信を実行します。静的コンテンツにページ2に作成されているP2_TEXTおよびP2_CLOBの値が表示されます。セッション・ステートのメモリへのロードは、ページ呼び出し毎に実行されます。


ページ1でAjaxコールバックの呼び出しを実行します。

ページ・アイテムP1_RESPONSEにページ・アイテムP1_TEXTP1_CLOBP2_TEXTP2_CLOBの値が出力されます。


ページ2に移り、同様にAjaxコールバックの呼び出しを実行します。

ページ2でも同様に、ページ・アイテムP2_RESPONSEにページ・アイテムP1_TEXTP1_CLOBP2_TEXTP2_CLOBの値が出力されます。


最後にページ1から、クローンしたセッションでページ2を開きます。


ページ2が新しいタブで開きます。このとき、セッションをクローンしているため、新しいセッションIDが割り当たります。


元のセッションIDとクローン後のセッションIDの両方を検索対象として、表WWV_FLOW_DATAを検索します。

select * from wwv_flow_data where flow_instance in ([元のセッションId],[クローンしたセッションID]) and item_id > 0 order by flow_instance,item_name asc;

元のセッションIDで保存されていたセッション・ステートが丸ごと複製(クローン)されていることが確認できます。


デバッグ・レベル完全トレースまであげると、セッション・ステートがメモリにロードされる状況がログに出力されます。


ログには次のように出力されます。これはページ・ビューとAjaxコールバックで共通です。


ここまで、ページ・アイテムのセッション・ステートのストレージを永続にしたときの動作について説明してきました。

セッション・ステートはこのような動作をするため、以下のような状況について気を付ける必要があります。

突然、そのページは何も変更していないのに、ページの表示やAjaxコールバックが遅くなった。

以下を確認する必要があります。
  1. アプリケーションにページを大量に追加していませんか?
  2. 追加したページにセッション・ステートストレージ永続になっているページ・アイテムが沢山ありませんか?
  3. セッション・ステートストレージ永続になっている、データ型CLOBになっているページ・アイテムはありませんか?
  4. 上記のCLOBのページ・アイテムに極端に大きいデータを保存していませんか?
  5. 上記の事象はユーザーの操作に依存します。そのユーザーが別のページで巨大なCLOBをセッション・ステートに保存していると発生します。同じページにアクセスしていて同じようなデータを操作していても、操作しているページだけに注目していると、原因が分かりません。
  6. セッションのクローンを多用していませんか?
  7. クローンするごとに表WWV_FLOW_DATAに保存されるデータは増加します。クローンを使わない場合は、ユーザーがログインした分のデータがセッション・ステートに保存されますが、セッションのクローンは注意深く実装していないと、表WWV_FLOW_DATAに保存するデータが極端に増加する可能性があります。表WWV_FLOW_DATAに保存されているセッション・ステートのデータはユーザーがログアウトしたりセッションがタイムアウトしたりした時点では削除されず、日時で実行されるメンテナンス・タスクが削除します。そのため、表WWV_FLOW_DATAに保存されているデータは、1日程度は維持されます。
  8. セッションをクローンするアプリケーションは、ページ数を少なくしたり、セッション・ステートのストレージに永続を設定したページ・アイテムを少なくする必要があります。
  9. 沢山のページを含む巨大なアプリケーションよりは、セッション共有を使って小さなアプリケーションを集めてひとつのアプリケーションに見せる方が、パフォーマンス面では有利です。
ページ作成ウィザードによる場合ではなく、ページ・アイテムを単独で作成したときのセッション・ステートストレージのデフォルトの設定はセッションごと(永続)です。

そのため、ページ・アイテムを作成したときは、必ずセッション・ステートのストレージの設定を確認すべきです。


今回の記事は以上になります。

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