2022年4月29日金曜日

動的アクションのJavaScriptコードで使用するthisについて

 動的アクションのTrueアクションをJavaScriptでコーディングする際に、this.triggeringElement、this.browserEventなどが使用できます。この使い方を紹介します。

アクションとしてJavaScriptコードの実行を選択したときの、設定のコードのオンライン・ヘルプには、以下の記載があります。

  • this.triggeringElement
    • 動的アクションをトリガーした要素のDOMオブジェクトへの参照。
  • this.affectedElements
    • 影響を受けるすべての要素を含むjQueryオブジェクト。
  • this.action
    • アクション名と追加の属性値などの詳細を含むアクション・オブジェクト。
  • this.browserEvent
    • イベントをトリガーしたイベントのイベント・オブジェクト。ノート: ロード時、これは'load'と同等になります。
  • this.data
    • イベント・ハンドラから渡すことができるオプションの追加データ。
一般にOracle APEXでアプリケーションを開発している方は、HTML、CSS、JavaScriptはそれほど経験がない方が多いといわれています。なので、この説明だけではピンとこないようです。

先日作成したアプリケーションを使って、これらの属性を使ってみます。

そもそも、このthisは何か、から確認します。

TRUEアクションに以下のコードが記述されています。(ページ・アイテムへの値の移入を題材にとります。列への移入はコメント・アウトします。)


このように作成した動的アクションは、HTMLのソースとして以下のように出力されます。

<script type="text/javascript">
apex.da.initDaEventList = function(){
    apex.da.gEventList = [{
        "triggeringElementType":"COLUMN",
        "triggeringElement":"C11489677485938601",
        "triggeringRegionId":"test_grids",
        "isIGRegion":true,
        "bindType":"bind",
        "bindEventType":"click",
        "anyActionsFireOnInit":false,
        actionList [{
            "eventResult":true,
            "executeOnPageInit":false,
            "stopExecutionOnError":true,
            javascriptFunction:function (){
                $s("P1_CELL_VALUE",$v(this.triggeringElement));
            }
,
            "action":"NATIVE_JAVASCRIPT_CODE"
        }]
    }];
}
</script>

設定コードのJavaScriptの記述が、ファンクションとしてactionListに登録されています。thisはこのファンクションの実行コンテキストになります。

<script>から</script>の間に記載されている内容は、概ねページに作成された動的アクションの定義を転記しています。動的アクションの定義リストがapex.da.initDaEventListになります。このリストを受け取って、動的アクションを実行するコードはdynamic_actions_core.jsに含まれています。


dynamic_actions_core.jsは、すべてのAPEXのインストールに含まれています。(実際に、ページにロードされているのはミニファイされたdesktop_all.min.jsです。)

JavaScriptのコードを実行する部分は、以下のようにコーディングされています。
    /**
     * doAction function
     * Executes the action (pAction) on certain elements (pSelector)
     * @ignore
     */
    da.doAction = function( pContext, pSelector, pAction, pDynamicActionName, pResumeCallback ) {
        var lContext = {
            triggeringElement : pContext.triggeringElement,
            affectedElements  : $( pSelector, apex.gPageContext$ ),
            action            : pAction,
            browserEvent      : pContext.browserEvent,
            data              : pContext.data,
            resumeCallback    : pResumeCallback
        };

        // Call the javascript function if one is defined and pass the lContext object as this
        if ( pAction.javascriptFunction ) {

            // Log details of dynamic action fired out to the console (only outputs when running in debug mode)
            apex.debug.log( "Dynamic Action Fired: " + pDynamicActionName + " (" + pAction.action + ")", lContext );
            return pAction.javascriptFunction.call( lContext );
        }
    }; // doAction

変数IContextが、実行コンテキストになっていることが分かります。

では、IContextに含まれるtriggeringElementやbrowserEventは何か?ということになります。

動的アクションは以下のように定義されています。


指定したHTML要素(リージョンTest Gridsの列A)が、クリックされたときにアクションが実行されます。

よって、this.triggeringElementにはクリックされた列Aのセルへの参照が含まれます。this.browserEventにはクリックであるMouseEventが含まれます。MouseEventはEventから派生したインターフェースで、イベントの種類によってインターフェースは変わります。

TRUEアクションのコードにconsole.log(this.triggeringElement);を追加して、内容をJavaScriptコンソールに出力してみます。
console.log(this.triggeringElement);
$s("P1_CELL_VALUE",$v(this.triggeringElement));

対話グリッドの列Aをクリックすると、JavaScriptコンソールにクリックしたセルのHTML要素(inputタグ)が印刷されます。


this.triggeringElementとしてHTML要素、this.browserEventとしてMouseEventが渡されていることがわかったので、これを使ったコードを追加してみます。

Shiftキーを押してクリックしたときだけ、値をコピーするようにします。

this.triggeringElementはinputのHTML要素ですから、APEXが提供しているファンクション$vの代わりに.valueでも値を取ることができます。

コードは以下に変更します。

$s("P1_CELL_VALUE",this.triggeringElement.value);

クライアントの条件タイプJavaScript式を選択し、JavaScript式に以下を記述します。シフト・キーを押した状態でMouseEventが発生したときにtrueになります。

this.browserEvent.shiftKey


this.actionthis.affectedElementsthis.dataは、主にOracle APEXに依存した情報になります。

this.actionからは、呼び出されているTRUEアクションの定義を参照できます。プラグインを作成するようなケースを除き、参照することはないと思います。

this.affectedElementsは、アクション影響を受ける要素として設定されたHTML要素が(配列として)渡されます。今回のTRUEアクションは、影響を受ける要素P1_CELL_VALUEを設定して、次のように書き直すことができます。
let elem = this.affectedElements[0];
$s(elem,this.triggeringElement.value);

this.dataとして渡される値は動的アクションによって異なります。

例えば、対話グリッド選択を変更したときに発生するイベントで、動的アクションを作成します。


TRUEアクションとしてconsole.log(this.data);を実行し、内容を確認します。


this.dataには、対話グリッドのmodelと選択されたレコードが含まれていることが確認できます。

最後に、アプリケーション開発中であればthisの内容を確認するためにconsole.logを埋め込む必要はありません。開発者ツール・バーが開いている状態で、JavaScriptコンソールを開いていれば、動的アクションが実行される都度、thisの内容がコンソールに出力されます。


以上で、動的アクションのJavaScriptコードで使用するthisの紹介は終了です。

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

2022年4月28日木曜日

対話グリッドで選択したセルの値を別の場所に移入する

 対話グリッド上で選択したセルの値を、別の列にコピーする方法、および、ページ上のアイテムにコピーする方法を実装してみました。

準備として対話グリッドを含むアプリケーションを作成します。

クイックSQLの以下のモデルを使用します。

# prefix: test
grids  /insert 10
    a num
    b num
SQLの生成SQLスクリプトを保存レビューおよび実行を行い、表TEST_GRIDSを作成します。


アプリケーション作成ウィザードを起動し、対話グリッドを含んだアプリケーションを作成します。

名前Copy Valueとします。


対話グリッドのページは、以下の設定で作成します。

ページ名Copy Value表またはビュー編集を許可を選択します。表またはビューとしてTEST_GRIDSを選択します。


以上で、アプリケーションの作成を実行します。

最初に選択したセルの値を、ページ・アイテムにコピーする機能を実装します。

ページ・アイテムP1_CELL_VALUEを、ページCopy ValueのリージョンTest Gridsに作成します。

識別名前P1_CELL_VALUEタイプテキスト・フィールドラベルセルの値とします。


対話グリッドの列A動的アクションを作成します。

識別名前クリックしてコピータイミングイベントクリック選択タイプリージョンTest GridsAになります。


Trueアクションを選択し、アクションとしてJavaScriptコードの実行を選択します。設定コードとして、以下を記述します。

$s("P1_CELL_VALUE",$v(this.triggeringElement));

設定コードを選択した状態で中央ペインのヘルプを開くと、コードで使用できる属性の説明が表示されます。


以上の設定でアプリケーションを実行します。以下のような動作になります。


イベントクリックの場合、セルにフォーカスが当たった後にクリックして初めてイベントが発生する動作になっています。期待通りの動作でない場合、フォーカスの取得といった他のイベントの使用も検討できます。

次にページ・アイテムではなく、他のセルにコピーします。

対話グリッドに静的IDを設定します。

対話グリッドTest Gridsを選択し、詳細静的IDとしてtest_gridsを設定します。


TrueアクションJavaScriptコードを以下に変更します。
/*
 * setValueを呼び出すためにIGのmodelを取得する。
*/
let grid = apex.region("test_grids").widget(),
    model = grid.interactiveGrid("getViews", "grid").model;
/* 選択されたレコードを取得する */
let elem = $(this.triggeringElement);
let rowId = elem.closest('tr').data('id');
let record = model.getRecord(rowId);
/* 設定元となる値を取り出す。 */
// let sal = model.getValue(record,"A");
let val_a = $v(this.triggeringElement);
/* 値を設定する */
model.setValue(record,"B",val_a);

この変更を行なった上でアプリケーションを実行します。

動作が分かりにくいので、列Bはnullに更新しました。
update test_grids set b = null ;

以下の動作になります。


以上で、実装の説明は終了です。

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/ig-cell-copy-value.sql

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

Autonomous Databaseで動作しているAPEXアプリのSQLトレースを取得する

以前にも同じテーマで記事を書いているのですが、 SQLトレースの保存先に事前承認リクエストではなくクリデンシャルを使う、APEXのアプリケーションでSQLトレースを有効にするために、セキュリティの設定ではなくプロセスを作成するようしました。

以下、SQLトレースを取得する手順を紹介します。


認証トークンの生成


手順を簡素にするため、グループAdministratorsに含まれているユーザーで認証トークンを生成します。できれば、ユーザー、グループ、ポリシーの作成を検討しましょう。

OCIコンソールよりアイデンティティユーザーから、ユーザーの詳細を開きます。リソースより認証トークンを選択します。

トークンの生成を実行します。

説明を入力し、トークンの生成を実行します。

トークンが生成されるので、コピーします。この値はAutonomous Databaseにクリデンシャルを作成する際に使用します。

閉じるをクリックします。


以上で、認証トークンの生成はできました。


オブジェクト・ストレージのバケットの作成

OCIコンソールよりオブジェクト・ストレージバケットを開きます。バケットの作成を実行します。


バケット名を指定して、作成を実行します。今回はバケット名をsql_traceとしています。


バケットsql_traceが作成されます。作成されたバケットを開きます。


作成したバケットのパスを確認します。小さなファイルをバケットにアップロードして、オブジェクト詳細の実行を行います。


URLパス(URI)として表示されているパスの、ファイルの名前を除いた/o/までをコピーします。コピーしたら取消をクリックして、ドロワーを閉じます。


以上で、SQLトレースの出力先の準備はできました。


Autonomous Databaseの設定



管理者ユーザーADMINにてデータベース・アクションに接続し、SQLを開きます。

以下のコマンドを実行しクリデンシャルDEF_CRED_NAMEを作成します。

BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'DEF_CRED_NAME',
username => 'ユーザー名',
password => '生成した認証トークン'
);
END;
/


SQLトレースの出力先を、データベース・プロパティDEFAULT_LOGGING_BUCKETとして設定します。

SET DEFINE OFF;
ALTER DATABASE PROPERTY SET
DEFAULT_LOGGING_BUCKET = 'https://objectstorage.リージョン.oraclecloud.com/n/ネームスペース/b/sql_trace/o/';


デフォルトで使用するクリデンシャルとして、先ほど作成したDEF_CRED_NAMEを設定します。スキーマ名のADMINをつける必要があります。

ALTER DATABASE PROPERTY SET DEFAULT_CREDENTIAL = 'ADMIN.DEF_CRED_NAME';


APEXのワークスペース・スキーマでSQLトレースを有効にできるよう、ALTER SESSIONを実行する権限を与えます。ワークスペース・スキーマがAPEXDEVの例です。

grant alter session to apexdev;


Autonomous Databaseの設定は以上で完了です。


APEXアプリでのSQLトレースの有効化



APEXアプリケーションでSQLトレースを取る処理の前に、以下のコードを実行するプロセスを作成します。

execute immediate 'alter session set sql_trace = true';

ページのレンダリング全体のSQLトレースを取得する場合は、レンダリング前にプロセスを配置します。


データベースのセッションは別のページ処理で再利用されるため、かならずページ処理が終了する前に、SQLトレースを停止します。

execute immediate 'alter session set sql_trace = false';

レンダリング前にSQLトレースを開始した場合は、レンダリング後にSQLトレースを停止します。


セッションのClient IdentifierModuleの情報は、APEXによって設定されているので、設定しない方がよいでしょう。

変更したAPEXのアプリケーションを実行すると、SQLトレースがオブジェクト・ストレージに出力されます。サーバー側の条件などを組み合わせると、より効果的にSQLトレースが取得できると思います。


出力されたSQLトレースの確認



SQLトレースの出力先として指定したバケットの内容を確認します。

サインインしたユーザー、セッションID、ワークスペース、アプリケーションIDやページ番号で分類されたフォルダの下に、SQLトレースが出力されています。


今回の記事は以上です。

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

2022年4月27日水曜日

接触追跡のファンクションを表関数から呼び出す

 以前の記事で、Oracle Spatialの接触追跡のファンクションを呼び出す方法について説明しました。その際にAPEXのワークスペースから呼び出すと行が返されないため(sdo_obj_tracing.get_all_durationsの引数track_table_nameとして与える表名を所有者で修飾することにより、異なるユーザーから呼び出すことができました)、RESTサービスにラップしています。

RESTサービスで実装する前に、表関数による実装も試していたので、その作業を記録として残しておきます。

データベース・ユーザーSPATIALUSERで以下のコードを実行し、表関数contact_tracingを作成しました。

データベース・アクションにて実行しています。

表関数contact_tracingを使った接触追跡は、以下のSELECT文で行います。

select * 
from table(contact_tracing(
    7
    , to_date('2020-05-31 00:00:00','YYYY-MM-DD HH24:MI:SS')
    , to_date('2020-05-31 23:59:59','YYYY-MM-DD HH24:MI:SS')
    , 15
    , 5
    , 20
))

座標値を除いた結果を確認してみます。

select distinct contact_id, in_user_id, out_user_id, duration_in_minutes, start_time, end_time, num_contact_times 
from table(contact_tracing(
    7
    , to_date('2020-05-31 00:00:00','YYYY-MM-DD HH24:MI:SS')
    , to_date('2020-05-31 23:59:59','YYYY-MM-DD HH24:MI:SS')
    , 15
    , 5
    , 20
))

続いて、ユーザーADMINから検索します。

ユーザーadminに表関数contact_tracingの実行権限を与えます。ユーザーSPATIALUSERにて、以下のコマンドを実行します。

grant execute on contact_tracing to admin;


SELECT文を変更しなくて済むように、シノニムを作成します。ユーザーADMINにて、以下のコマンドを実行します。

create synonym contact_tracing for spatialuser.contact_tracing;


ユーザーADMINにて、SELECT文を実行します。


ユーザーSPATIALUSERで実行した場合と同じ結果が得られます。

APEXのレポートでデータ・ソースが複雑になる場合に、表関数を利用する一例として紹介してみました。

以上になります。

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

レポートの行の文字数制限について

 レポートに表示される行のデータが長いとORA-6502が発生する、との相談がありました。Oracle APEXはデータベース・サーバーでHTMLを生成しています。その処理はPL/SQLで記述されていますが、PL/SQLのVARCHAR2の最大値が32767バイトであるため、いくつかの箇所で、32767バイトが扱えるデータの上限になっています。

クラシック・レポートと対話モード・レポートの1行の長さも、そのひとつです。

以下のようなエラーが発生します。

同じレポートでも対話グリッドは、ブラウザ側でJavaScriptのコンポーネントとして実装されていて、描画(HTMLの生成)はブラウザ側で行われています。そのため、このような制限はありません。

クラシック・レポートを例にとって、1行の長さがどのように決まるのか説明します。クラシック・レポートのテンプレートを新規に作成する、または編集するときの参考になると思います。

最初にテストに使用するデータを準備します。

表TEST_STRLENを作成し、1バイトから32767バイトまでの長さの文字列を挿入します。

create table test_strlen(str varchar2(32767));

declare
    l_string varchar2(32767);
begin
    for i in 1..32767
    loop
        l_string := l_string || mod(i,10);
        insert into test_strlen values(l_string);
    end loop;
end;

確認作業にはAlways FreeのAutonomous Databaseを使用しています。ADBは初期化パラメータのMAX_STRING_SIZEEXTENDEDになっているため、VARCHAR2の最大サイズが32767バイトになっています。そうでない場合は4000バイトが上限になるため、代わりに最大サイズの制限がないCLOBを使う必要があります。

作成した表TEST_STRLENには、1バイトから32767バイトまでの長さの文字列が挿入されています。全部で32767行になります。

確認に使用するAPEXアプリケーションを作成します。

アプリケーション作成ウィザードを起動します。

アプリケーションの名前文字数制限の確認とします。ページの追加をクリックし、クラシック・レポートのページを作成します。

追加ページを開いて、クラシック・レポートを選択します。

ページ名レポートとし、SQL問合せとして、以下のSQLを記述します。

select str
from test_strlen
where length(str) <= :P1_MAX
order by length(str) desc

検索条件に使用しているページ・アイテムP1_MAXは、アプリケーションの作成後に追加します。

ホーム・ページは使わないので編集をクリックして、削除を実行します。レポートのページだけを含めて、アプリケーションの作成を実行します。

アプリケーションが作成されたら、ページ・デザイナホーム・ページを開きます。リージョンレポートにページ・アイテムP1_MAXを作成します。

識別名前P1_MAX、タイプを数値フィールドラベル最大値とします。


以上で確認に使用するアプリケーションは完成です。

アプリケーションを実行し、表示できる文字数の上限を確認します。

エラーが発生せずにレポートが表示される上限は、32693バイトでした。32767バイトまでは扱えません。


クラシック・レポートでは、レポートの1行がどのように表示されるかは、レポートテンプレートで決められています。

使用しているテンプレートは、リージョンAttributes外観テンプレートにて、設定されています。今回の例ではStandardを使用しています。


レポート・テンプレートのStandardを確認します。

左ペインで共有コンポーネント・ビューを開き、テンプレート以下のクラシック・レポートに含まれるStandardを選択します。右ペインでコンポーネントの編集をクリックします。


テンプレートStandardの編集画面が開きます。


レポートのテンプレートに、各行の前<tr>と設定されています。


列テンプレートの設定は以下です。ここで#COLUMN_VALUE#がSQLの検索結果に置き換えられます。

<td class="t-Report-cell" #ALIGNMENT# headers="#COLUMN_HEADER_NAME#">#COLUMN_VALUE#</td>


各行の後</tr>です。


レポートの1行の長さ(バイト数)は、テンプレートに含まれている置換文字列が置き換えられた後の、以下の合計の長さになります。

各行の前 + 列テンプレート(複数の列がある場合は繰り返し) + 各行の後

列テンプレートをもっと簡単にして、上限を確認してみます。テンプレートStandardを編集します。標準のテンプレートを直接編集すると他のレポートに影響するため、コピーを作成して編集します。

共有コンポーネントテンプレートを開き、レポート・テンプレートStandardコピーします。


新規テンプレート名Standard-Simple新規テンプレート識別子STANDARD-SIMPLEとして、コピーを実行します。


レポート・テンプレートStandardのコピーがStandard-Simpleとして作成されます。Standard-Simpleを開いて、列テンプレートを変更します。


列テンプレートを以下に変更します。

<td>#COLUMN_VALUE#</td>

変更の適用をクリックします。


このテンプレートでは、1行の表示は以下になります。

<tr><td>#COLUMN_VALUE#</td></tr>

HTMLのタグは18バイトになるので、列のデータとしては32767 - 18 = 32749が上限になります。

実際にアプリケーションで確認してみます。クラシック・レポートのテンプレートStandard-Simpleに変更します。


アプリケーションを実行し、最大値32749を入力します。レポートは正常に表示されます。


最大値として32750を指定すると、エラーが発生します。


レポートの表示の仕組みと、1行の長さの上限値の説明は以上になります。

対話モード・レポートでも表示の仕組みは同様で、1行の上限値についても同様に適用されます。

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