2021年9月9日木曜日

表の内容をNDJSON形式で出力する

 以前に改行で区切ったJSON形式(Newline Delimited JSON)のファイルをロードする記事を書きました。今度は逆に、指定した表の内容をNDJSON形式で出力してみます。

アプリケーション作成ウィザードを起動し、空のアプリケーションを作成します。アプリケーションの名前NDJSON生成としました。アプリケーションの作成を実行します。

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

Content Bodyにリージョンを作成します。識別タイトルNDJSON生成タイプ静的コンテンツとします。

NDJSON形式でデータを出力する表を指定するページ・アイテムを作成します。識別名前P1_TABLE_NAMEタイプテキスト・フィールドとします。ラベルテーブル名とします。

ページ・アイテムP1_TABLE_NAMEに変更があった時点で、その値をセッション・ステートに保存するよう、動的アクションの作成を行います。

識別名前テーブル名の保存とします。P1_TABLE_NAMEにたいして動的アクションの作成を実行していると、タイミングはデフォルトで、イベント変更選択タイプアイテムアイテムP1_TABLE_NAMEとなっています。

TRUEアクション識別アクションサーバー側のコードを実行を選択します。設定PL/SQLコードとしてはnull;を記述します。サーバー側では実際には何も実行されませんが、送信するアイテムとしてP1_TABLE_NAMEを指定することにより、このページ・アイテムがセッション・ステートに保存されます。


NDJSONの生成を実行するボタンを作成します。

識別ボタン名B_CALLとします。ラベルNDJSON生成とします。動作アクションとしてこのアプリケーションのページにリダイレクトを選びます。


ターゲットの設定としては、ページ&APP_PAGE_ID.を指定します。&APP_PAGE_ID,は現在開いているページ番号に置き換えられます。リクエストとしてAPPLICATION_PROCESS=GEN_NDJSONを設定します。Ajaxコールバックとして作成されたプロセスGEN_NDJSONを呼び出します。


NDJSONを生成するプロセスをAjaxコールバックの位置に作成します。以下のコードを記述します。

declare
l_table_name varchar2(128);
l_sql varchar2(800);
l_line varchar2(32767);
l_raw raw(32767);
l_len integer;
l_blob blob;
type t_ndjson is ref cursor;
c_ndjson t_ndjson;
begin
l_table_name := dbms_assert.sql_object_name(:P1_TABLE_NAME);
l_sql := 'select json_object(*) as l from ' || l_table_name;
-- 生成したNDJSON形式のデータはBLOBに書き込む。
dbms_lob.createtemporary(l_blob, TRUE, dbms_lob.session);
dbms_lob.open(l_blob, dbms_lob.lob_readwrite);
-- 一行ずつJSONオブジェクトとして取り出す。
open c_ndjson for l_sql;
loop
fetch c_ndjson into l_line;
exit when c_ndjson%notfound;
l_line := l_line || chr(10); -- 改行の追加
l_raw := utl_raw.cast_to_raw(l_line);
l_len := utl_raw.length(l_raw);
dbms_lob.writeappend(l_blob, l_len, l_raw); -- BLOBに追記
end loop;
close c_ndjson;
dbms_lob.close(l_blob);
-- NDJSONの書き込みの終了。
-- 書き込んだLOBの内容をダウンロード。
sys.htp.init;
sys.htp.p('Content-Length: ' || dbms_lob.getlength(l_blob));
sys.htp.p('Content-Disposition: attachment; filename=' || l_table_name || '.json');
sys.owa_util.http_header_close;
sys.wpg_docload.download_file(l_blob);
apex_application.stop_apex_engine;
dbms_lob.freetemporary(l_blob);
end;

ページ・アイテムP1_TABLE_NAMEを使ってSQLを構築するため、SQLインジェクションの対応としてDBMS_ASSERT.SQL_OBJECT_NAMEで囲み、安全な文字列であることを保証しています。またSQLの中でjson_object(*)という記法を使っています。これはOracle Database 19c以降で利用可能です。それ以前(例えば現行のapex.oracle.comは18cなので、このままでは動作しません)のバージョンでは、この部分を書き換える必要があります。


以上でアプリケーションの完成です。実行すると記事の先頭のGIF動画のように動きます。

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

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

Autonomous Database上のAPEXアプリケーションでSQLトレースを取得する

 Autonomous DatabaseでSQLトレースを取得することができるようになったようです。APEXのアプリケーションのSQLトレースを取る方法を確認してみました。

ルート・コンパートメント直下に、SQLトレースのファイルを保存するバケットを作成することにしました。もちろん、バケットはどこに作成しても構いません。

オブジェクト・ストレージの管理画面を開いて、バケットの作成をクリックします。


バケット名sql-traceとします。デフォルト・ストレージ層として標準を選んで、作成をクリックします。


SQLトレースの出力が確認できればよいだけなので、事前承認リクエストの作成を行います。


事前承認済リクエスト・ターゲットとしてバケットを選びます。アクセス・タイプとしてオブジェクトの書込みを許可を選択します。名前有効期限をそれぞれ設定し、事前承認済リクエストの作成をクリックします。


生成された事前承認済リクエストのURLをクリップボードにコピーしておきます。閉じるをクリックして、ダイアログを閉じます。


これでオブジェクト・ストレージの用意は完了です。

ユーザーADMINにてデータベース・アクションに接続し、開発SQLを開きます。以下のSQLを実行します。

set define off;
ALTER DATABASE PROPERTY SET DEFAULT_LOGGING_BUCKET = '事前承認済リクエストのURL';


Oracle APEXに作成したワークスペースのスキーマがAPEXDEVと仮定して、そのスキーマでALTER SESSION文の実行ができるように権限を与えます。

grant alter session to apexdev;


APEXのアプリケーション・ビルダーに移り、SQLトレースを取得したいアプリケーションのアプリケーション定義セキュリティのタブを開きます。

データベース・セッション初期化PL/SQLコードで、SQL_TRACEを有効にします。

execute immediate 'alter session set sql_trace = true';

PL/SQLコードのクリーンアップで、SQL_TRACEを無効にします。

execute immediate 'alter session set sql_trace = false';

以上を記述して、変更の適用を行います。


以上の設定を行なった後APEXアプリケーションを実行すると、SQLトレースがオブジェクト・ストレージに保存されます。オブジェクト・ストレージ上のファイルを確認してみます。


Oracle APEXのアプリケーションによって、データベースのセッションにクライアントIDモジュールが設定されるため、開発者がそれらを設定する必要はありません。

マニュアルによるとclientIDの後にmoduleNameが続くことになっていますが、APEXのアプリケーションから生成されたSQLトレースでは、その間にデータベース・ユーザーが入っています。これは、APEX固有になるのかマニュアルの記載が違うのかは分かりません。データベース・ユーザー名がパスに含まれていても、特に問題は無いでしょう。

APEXアプリケーションでSQLトレースを取得する場合は、ビューSESSION_CLOUD_TRACEを検索する機会はほとんどないと思います。APEXの場合は、ページをリクエストするごとに異なるデータベース・セッションが割り当たるためです。

以上でAutonomous DatabaseでのOracle APEXアプリケーションで、SQLトレースを取得する方法の紹介は終了です。

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

CSVをつなげて1つのファイルとしてダウンロードする

 1ページに対話モード・レポートを2つ作り、両方を1つのCSVファイルとしてダウンロードしてみます。元にした実装は、Ajaxコールバックを使ったCSVのダウンロードです。

以下、実装手順になります。

サンプル・データセットEMP/DEPTがインストール済みとします。それに含まれる表EMPを対話モード・レポートのソースとします。

最初に空のアプリケーションを作成します。アプリケーションが作成されたら、ホーム・ページに対話モード・レポートを2つ作成します。

最初の対話モード・レポートの静的IDとして、emp1を設定します。

次の対話モード・レポートの静的IDは、emp2とします。


Ajaxコールバックとして、プロセスCSVEXPORTを作成します。PL/SQLコードとして以下を記述します。

declare
l_ctx_emp1 apex_exec.t_context;
l_ctx_emp2 apex_exec.t_context;
l_rid_emp1 number;
l_rid_emp2 number;
l_exp_emp1 apex_data_export.t_export;
l_exp_emp2 apex_data_export.t_export;
begin
-- Generate CSV of IR region emp1
select region_id into l_rid_emp1 from apex_application_page_regions
where application_id = :APP_ID and page_id = 1.
and static_id = 'emp1';
apex_debug.info('region_id of emp1 is ' || l_rid_emp1);
l_ctx_emp1 := apex_region.open_query_context (
p_page_id => 1,
p_region_id => l_rid_emp1 );
l_exp_emp1 := apex_data_export.export (
p_context => l_ctx_emp1,
p_format => apex_data_export.c_format_csv,
p_file_name => 'emp1' );
apex_exec.close(l_ctx_emp1);
-- Generate CSV of IR region emp1
select region_id into l_rid_emp2 from apex_application_page_regions
where application_id = :APP_ID and page_id = 1
and static_id = 'emp2';
apex_debug.info('region_id of emp2 is ' || l_rid_emp2);
l_ctx_emp2 := apex_region.open_query_context (
p_page_id => 1,
p_region_id => l_rid_emp2 );
l_exp_emp2 := apex_data_export.export (
p_context => l_ctx_emp2,
p_format => apex_data_export.c_format_csv,
p_file_name => 'emp2' );
apex_exec.close(l_ctx_emp2);
-- merge BLOB output into 1
dbms_lob.append(l_exp_emp1.content_blob, l_exp_emp2.content_blob);
-- download as a single file.
apex_data_export.download( p_export => l_exp_emp1 );
end;

対話モード・レポートに設定されている検索条件でSQL問合せ行うコンテキストをapex_region.open_query_contextを呼び出して取得します。その問合せからCSVのデータをapex_data_export.exportを呼び出して生成します。

l_exp_emp1には対話モード・レポートemp1のデータ、l_exp_emp2には対話モード・レポートemp2のデータが保持されています。dbms_lob.appendを呼び出して、l_exp_emp2の出力をl_exp_emp1に追記したのち、apex_data_export.downloadを呼び出して、ファイルとしてl_exp_emp1の内容を出力しています。

画面上にAjaxコールバックCSVEXPORTを呼び出すボタンを追加します。

ブレッドクラムのリージョンでボタンの作成を実行します。識別ボタン名B_EXPORTラベルCSVエクスポートとします。動作アクションとして、このアプリケーションのページにリダイレクトを選択します。


ターゲットページには&APP_PAGE_ID. (今回の例では1に置き換わります)、詳細リクエストとして、APPLICATION_PROCESS=CSVEXPORTを設定します。


以上でアプリケーションは完成です。アプリケーションを実行すると、最初のGIF動画の動作を確認できます。

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

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

2021年9月8日水曜日

APEXの一時表を使った画像ビューワー・アプリの作成

 Oracle APEXのページ・アイテムのタイプファイル参照...というのがあります。このページ・アイテムを使って、選択した画像ファイルを表示するアプリケーションを作成してみます。

最初に空のアプリケーションを作成します。アプリケーション作成ウィザードを起動します。名前画像ビューワーとして、アプリケーションの作成を実行します。

アプリケーションが作成されたら、ページ・デザイナにてホーム・ページを開きます。ホーム・ページに画像のアップロードと表示の機能を実装します。


アプリケーションとしてはホーム・ページしか含まれないため、ナビゲーション・メニューやブレッドクラムは不要です。ページ・プロパティ外観ページ・テンプレートとしてMinimail (No Navigation)を選択し、ブレッドクラムのリージョン画像ビューワーを削除します。


Content Bodyでリージョンの作成を実行します。識別タイトル画像ビューワーとし、タイプ静的コンテンツとします。リージョンとしての装飾は不要なので、外観テンプレートとしてBlank with Attributesを選択します。


表示するファイルをローカルのファイル・システムから選択するためのページ・アイテムを作成します。

識別名前P1_FILEタイプとしてファイル参照...を選択します。重要な設定は設定記憶域タイプで、これにはTable APEX_APPLICATION_TEMP_FILESを選択します。この記憶域タイプが選択されている場合、ローカルのファイル・システムからアップロードされるファイルは、Oracle APEXが用意している表APEX_APPLICATION_TEMP_FILESに書き込まれます。記憶域タイプとしてBLOB column specified in Item Source attributeを選んだ場合は、BLOB列を持つ表をあらかじめ作成しておく必要があります。アップロードするファイルを永続的に保存する場合は、後者の設定を行った方がよいでしょう。

それ以外の設定は操作方法や見かけに関するものなので、必ずしもこの通りではなくても構いません。今回の手順では、ラベルとしてファイル設定表示形式Block Dropzoneドロップ・ゾーンのタイトルとして画像ファイルの選択ドロップ・ゾーンの説明として「画像ファイルをドラッグ&ドロップします。」を設定しています。


ページ・アイテムP1_FILEで選択されたファイルは、ページの送信を行うまではアップロードされません。ページ・アイテムP1_FILEの値の変更が行われたとき(ファイルが選択されたときになります)に、動的アクションによってページの送信を実行します。

ページ・アイテムP1_FILEで動的アクションの作成を実行します。識別名前ファイルの選択とします。タイミングはデフォルトで、イベント変更選択タイプアイテムアイテムP1_FILEになります。


TRUEアクション識別アクションとして、ページの送信を選択します。これで選択したファイルのアップロードが行われます。今回のアプリケーションでは使用しませんが、設定リクエスト/ボタン名UPLOADとしています。この名前がリクエストの値として送信されるので、サーバー側に作成する検証プロセスサーバー側の条件で使用することができます。


サーバー側の条件タイプには、リクエストを参照するものがあります。これらの条件として上記で設定したリクエスト/ボタン名を使うことができます。


以上でファイルを選択すると、サーバー側に選択されたファイルがアップロードされるようになりました。アップロードされたファイルは表APEX_APPLICATION_TEMP_FILESより参照できます。

アップロードされたファイルを表示するページ・アイテムを作成します。

識別名前P1_IMAGEとします。タイプとしてイメージの表示を選択します。ラベルラベル画像設定基準としてBLOB Column returned by SQL statementを選択し、SQL文として以下を記述します。表APEX_APPLICATION_TEMP_FILESよりアップロードされたファイルの内容であるBLOB列を検索しています。

select blob_content
from apex_application_temp_files
where name = :P1_FILE

外観テンプレートHiddenを選択し、CSSクラスとしてmy-imageを設定しておきます。CSSクラス自体の記述は後ほど、ページ・プロパティに含めます。


アップロードされたファイルの、ファイル名を表示するページ・アイテムを作成します。

識別名前P1_FILENAMEタイプとして表示のみを選択します。ラベルラベルファイル名とします。ソースタイプとしてSQL問合せ(単一の値を返す)を選択し、SQL問合せとして以下を記述します。

select filename
from apex_application_temp_files
where name = :P1_FILE


同様にアップロードされたファイルの、MIMEタイプを表示するページ・アイテムを作成します。

識別名前P1_MIME_TYPEタイプとして表示のみを選択します。ラベルラベルMIMEタイプとします。ソースタイプとしてSQL問合せ(単一の値を返す)を選択し、SQL問合せとして以下を記述します。

select mime_type
from apex_application_temp_files
where name = :P1_FILE


画像の表示を画面にフィットさせるため、ページ・プロパティCSSインラインに以下を記述します。

.my-image div {
display: flex;
justify-content: center;
}

.my-image img {
width: 30%;
height: auto;
}


以上で完成です。ページを実行し画像ファイルをアップロードすると、以下のように表示されます。


画像の表示にカード・リージョンを使うと、複数の画像を表示することができます。

ページ・アイテムP1_FILE設定複数ファイルの許可ONに変更します。ページ・アイテムP1_IMAGE、P1_FILENAME、P1_MIME_TYPEは削除します。


カード・リージョンを作成します。

識別名前画像とします。タイプカードです。ソース位置ローカル・データベースで、タイプSQL問合せとします。SQL問合せとして以下を記述します。

select id, blob_content, filename, mime_type
from apex_application_temp_files
where name in
(
select column_value
from apex_string.split(:P1_FILE, ':')
)


カード・リージョンのAttributesとして、カード主キー列1IDタイトルFILENAMEサブタイトルとしてMIME_TYPEを指定します。


メディアソースBLOB列とし、BLOB列BLOB_CONTENTを指定します。BLOB属性MIMEタイプ列MIME_TYPEを指定します。


以上で複数ファイルのアップロードと表示を行うアプリケーションは作成できました。ページを実行し、複数の画像ファイルを選択すると、以下のように表示されます。


APEXの一時表APEX_APPLICATION_TEMP_FILESの使い方の紹介は以上になります。

ひとつの画像を表示するアプリケーションのエクスポートは以下です。
https://github.com/ujnak/apexapps/blob/master/exports/image-viewer-single.sql

複数の画像を表示するアプリケーションのエクスポートは以下です。
https://github.com/ujnak/apexapps/blob/master/exports/image-viewer-multi.sql

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

2021年9月7日火曜日

Universal Themeの安全なカスタマイズ方法について

 Oracle APEXのArchitectであるShakeeb Rahmanさんが、先日のOffice Hour - Deep Dive: Universal Theme in APEX 21.1にて、Universal Themeのカスタマイズ方法について紹介しています。

Recommended Approach - 推奨する方法 - として、以下を挙げています。

  • Theme Roller - テーマ・ローラー
  • Template Options - テンプレート・オプション
  • Overriding CSS variables - CSS変数の上書き
  • Scoped CSS snippets where necessary - スコープ付きCSSスニペット - 必要に応じて
テーマ・ローラーおよびテンプレート・オプションはOracle APEXの標準機能であり、サポートされる変更手順です。CSS変数の上書きやCSSスニペットの記述(テーマ・ローラーのカスタムCSSとしての記述を含め)は、開発者によって書かれたコードになります。ですので、そのコード自体はサポートの対象にはなりません。それらの方法によるUIのカスタマイズは、Oracle APEXのバージョンアップの影響が最小限である(もしくはほとんどない)ため、カスタマイズ手順として推奨されています。

推奨されている手順について、Tim Kimberlさんがビデオの中で説明しています。サンプル・データセットのEMP/DEPTから作成されるアプリケーションを使って、ビデオで紹介されている手順を確認してみます。

Oracle APEX 21.1からHTML要素のclass属性としてpage-ページ番号およびapp-アプリケーション別名が指定されるようになりました。

例えば以下のページ番号3、ファセット検索のページのHTML要素は以下から始まります。

<html class="page-3 app-DEMO-EMP-DEPT">


テーマ・ローラーを開いてカスタムCSSとして以下のコードを設定します。ヘッダーのバックバウラウンド色を定義しているCSS変数を赤に変更します。

.page-3 {
--ut-header-background-color: red;
}

.page-3クラスの定義とすることで、ページ番号3に適用範囲を限定しています。


部門ページを開いてみます。部門ページはページ番号5なので、この設定による影響はありません。


また、CSS変数の設定では、他のCSS変数を参照することができます。--ut-palette-successをヘッダーのバックラウンド色に設定します。

.page-3 {
--ut-header-background-color: var(--ut-palette-success);
}

ヘッダーの色が緑になります。


続いて、レポートとして表示される文字の色を変更してみます。ページ・デザイナを開いて、レポート・リージョンのEmployee外観CSSクラスmy-regionを設定します。


テーマ・ローラーのカスタムCSSに以下の記述を追加します。.page-3と.my-regionクラスに設定することにより、ページ番号3およびEmployeeのリージョンに適用範囲を限定しています。

.page-3 .my-region {
--ut-region-text-color: var(--ut-palette-success);
}

リージョンの文字の色が緑になっています。


CSS変数の上書きとスコープ付きCSSスニペットの例は以上になります。

Office HourではOracle APEX 21.1の新しい機能である行CSSクラスについても紹介されていました。

ダッシュボードのチャートが次のように配置されています。


行CSSクラスを定義して、以下のようなレイアウトにしてみます。


最初に開発者ツール・バーよりクイック編集を実行し、4つのチャート・リージョンすべてでライブ・テンプレート・オプションを呼び出し、Body Heightを320pxからAuto - Defaultに変更します。


部署ごとの総給与のリージョンの、レイアウト新規行の開始OFFに変更します。すべてのチャート・リージョンが横一列に並びます。


ページ・プロパティCSSインラインに以下を記述します。同じ行に並んでいるリージョンの高さを一致させます。

.equal-row-height .col {
display: flex;
}

.equal-row-height .t-Region {
flex-grow: 1;
}

定義したCSSクラスequal-row-heightを、行の先頭となるリージョン部門ごとの従業員レイアウト行CSSクラスとして設定します。同じ行に配置される4つのリージョンの高さが同じになります。


以上で行CSSクラスの設定はできました。設定結果の表示は以下になります。

もともとBody Heightはすべて320pxとして設定されていたので、リージョンの高さは一致していました。ここでチャートの職種ごとの合計給与の棒グラフを縦方向に広げてみます。

チャートのAttributesレイアウト高さ500ピクセルとします。


チャートは以下の表示になります。職種ごとの合計給与のチャートは縦長になりますが、同じ行のすべての(チャート)リージョンも同じ高さに変更されます。そのため、Body Heightをそれぞれのリージョンで個別に設定する必要がありません。


行CSSクラスはフォーム上のページ・アイテムの配置でも活用できます。従業員の編集フォームのSalaryとCommisionのページ・アイテムを以下のように強調してみます。


Employeeのフォーム・ページのCSSインラインに以下を記述します。パディングとして20ピクセル、バックグラウンド色を警告色にしています。

.highlight-form-row {
padding: 20px;
background-color: var(--ut-palette-warning-shade);
}


フォームの作成直後は、ページ・アイテムP4_COMMはP4_SALの右隣には配置されていません。ページ・アイテムP4_COMMレイアウト新規行の開始OFFにします。


ページ・アイテムP4_SALレイアウト行CSSクラスとして、highlight-form-rowを設定します。


以上でSalaryとCommissionの入力項目を強調することができました。行CSSクラスが導入される前はサブリージョンを使うなど、非常に手間がかかりました。

Tim Kimberlさんがデモで使用していたアプリケーションを以下からアクセスできるようにしています。
https://apex.oracle.com/pls/apex/japancommunity/r/ut211-demo/generic-component-styles

Universal Themeにて定義されているCSS変数の一部が記載されていますが、リファレンスとして使えるようになるまでは、まだ時間がかかるようです。

以上で今回の記事は終了です。

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