エクスポートはそれほど危険ではありませんが、インポートについては既存のアプリケーションが上書きされる可能性があります。あくまで一括でエクスポートやインポートを行うPL/SQLコードのサンプルとして取り扱うようお願いします。
本記事は、Oracle Corporationの公式ブログにOracle APEXのアーキテクトのCarsten Czarskiさんが投稿した、以下の記事を参考にしています。
Building a REST API to Deploy APEX Apps
記事のタイトルの通り、APEXのアプリケーションのインポートとエクスポートを、ORDSのRESTサービスとして実装しています。Oracle APEXにはリモート・デプロイメントが機能として含まれていますが、この機能では、デプロイ先となるAPEXワークスペースでREST対応SQLが有効になっている必要があります。セキュリティ上の理由などでREST対応SQLを有効にできない場合、APEXアプリケーションのインポート/エクスポートを行うORDS RESTサービスを作成することでリモート・デプロイメントを実行できるようになります。
今回の記事で作成するアプリケーションのエクスポートを以下に置いてあります。
https://github.com/ujnak/apexapps/blob/master/exports/bulk-app-exporter.zip
以下、アプリケーションの作り方を紹介します。
アプリケーションの名前はBulk App Exporterとしました。
アプリケーションの作成をクリックし、アプリケーションを作成します。
アプリケーション定義の置換に、アプリケーションのエクスポート先となるオブジェクト・ストレージのリージョン、ネームスペース、バケット名とWeb資格証明を設定します。
置換文字列G_REGIONにリージョン(本記事ではus-ashburn-1)、G_NAMESPACEにネームスペース、G_BUCKETにバケット名(同app-exports)、G_CREDENTIALにOCIのAPI呼び出しに使用するWeb資格証明の静的ID(同OCI_API_ACCESS)を置換値として設定します。
Oracle Cloudのオブジェクト・ストレージの準備については、少し古いですがこちらの記事が参考になります。
エクスポートするアプリケーションを複数選択し、エクスポートを実行するページを作成します。アプリケーションの一覧表示と選択を行うために、対話グリッドを使用します。
ページの作成を開始し、対話グリッドを選択します。
ページの名前はExportとします。データ・ソースのソース・タイプとしてSQL問合せを選択し、SQL SELECT文を入力に以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
select | |
application_id | |
,'N' as export | |
,'N' as overwrite | |
,application_name | |
,alias | |
,owner | |
,application_group | |
,version | |
,created_by | |
,created_on | |
,last_updated_by | |
,last_updated_on | |
,application_comment | |
,is_working_copy | |
from apex_applications | |
where workspace_id = :workspace_id |
編集が有効をオンにします。
次へ進みます。
主キー列1としてAPPLICATION_ID (Number)を選択します。
ページの作成を実行します。
編集可能な対話グリッドのリージョンが作成されますが、実際にはこの対話グリッドのデータ・ソースは更新できません。今回実装するエクスポートの処理が必要としていない、対話グリッドの操作を無効化します。
対話グリッドを選択し、プロパティ・エディタの属性を開きます。
編集の実行可能な操作の内、行の追加と行の削除のチェックを外します。行の更新のみを実行可能な操作にします。
ツールバーのコントロールから保存ボタンのチェックを外します。対話グリッドの保存ボタンの代わりに、エクスポートを実行するボタンを後ほど作成します。
この後に列自体をコメント・アウトするのですが、対話グリッドの列APEX$ROW_SELECTORを選択し、設定のコントロールの非表示をオンにし、機能を無効化しておきます。
列EXPORTとOVERWRITEの識別のタイプをチェックボックスに変更します。EXPORTにチェックが入っているアプリケーションが一括エクスポートの対象となります。また、OVERWRITEにチェックが入っていると、バケット内に同じ名前のファイルがあれば上書きします。今回の実装では、同じ名前のファイルがありOVERWRITEにチェックが入っていない場合は、エラーにせず単にエクスポートをスキップします。
列APPLICATION_ID、EXPORT、OVERWRITE、ALIAS以外はエクスポート処理には使わないため、ソースの問合わせのみをオンにします。列の値がサーバーへの送信対象から外されます。
Button Containerのリージョンに、アプリケーションの一括エクスポートを実行するボタンEXPORTを作成します。ラベルはExportとします。動作のアクションはデフォルトのページの送信です。
プロセス・ビューを開き、デフォルトで作成されている対話グリッドの行の自動処理を行うプロセスを選択します。
識別の名前をExport - アプリケーションのアップロードに変更します。設定のターゲット・タイプをPL/SQLコードに変更し、挿入/更新/削除するPL/SQLコードとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
declare | |
l_app_export apex_t_export_files; | |
l_zip_export blob; | |
l_request_url varchar2(4000); | |
l_response clob; | |
e_upload_failed exception; | |
begin | |
if :APEX$ROW_STATUS <> 'U' then | |
/* 更新行のみのはずだが念のため他の状態は除外する。 */ | |
return; | |
end if; | |
if :EXPORT <> 'Y' then | |
/* EXPORTがチェックされていないアプリケーションはエクスポートしない。 */ | |
return; | |
end if; | |
/* アップロード先となるURLを作成する。 */ | |
l_request_url := apex_string.format('https://objectstorage.%s.oraclecloud.com/n/%s/b/%s/o/%s.zip', | |
:G_REGION, :G_NAMESPACE, :G_BUCKET, utl_url.escape(lower(:ALIAS), false, 'AL32UTF8') | |
); | |
/* OVERWRITEがチェックされていない場合、上書きを回避する。 */ | |
if :OVERWRITE <> 'Y' then | |
apex_web_service.clear_request_headers(); | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_request_url | |
,p_http_method => 'HEAD' | |
,p_credential_static_id => :G_CREDENTIAL | |
); | |
if apex_web_service.g_status_code = 200 then | |
apex_debug.info('Export exists, %s, id %s', :ALIAS, :APPLICATION_ID); | |
/* すでに存在するのでアップロードせずに処理を終了する。 */ | |
return; | |
end if; | |
end if; | |
/* | |
* ZIP形式でエクスポートする。 | |
* Ref: https://docs.oracle.com/en/database/oracle/apex/23.2/aeapi/GET_APPLICATION_Function.html | |
* | |
* 引数の指定によりエクスポートに含まれる内容が変わるため、要件に合わせて変更する。 | |
*/ | |
apex_debug.info('Start export application %s, id %s', :ALIAS, :APPLICATION_ID); | |
l_app_export := apex_export.get_application( | |
p_application_id => :APPLICATION_ID | |
,p_split => TRUE | |
,p_with_date => FALSE /* エクスポート日を除外 */ | |
,p_with_ir_public_reports => FALSE /* ユーザーが保存したパブリック・レポートを除外 */ | |
,p_with_ir_private_reports => FALSE /* ユーザーのプライベート・レポートも除外 */ | |
,p_with_ir_notifications => FALSE | |
,p_with_translations => TRUE /* 翻訳は含める */ | |
,p_with_original_ids => TRUE | |
,p_with_no_subscriptions => TRUE /* サブスクリプションは除外する */ | |
,p_with_comments => TRUE | |
,p_with_supporting_objects => 'Y' | |
,p_with_acl_assignments => TRUE | |
); | |
/* | |
* 分割されてエクスポートされたファイルをZIP(BLOB)に固める。 | |
*/ | |
l_zip_export := apex_export.zip( | |
p_source_files => l_app_export | |
); | |
/* | |
* オブジェクト・ストレージにアップロードする。 | |
*/ | |
apex_debug.info('Upload export to %s, bytes %s', l_request_url, dbms_lob.getlength(l_zip_export)); | |
apex_web_service.clear_request_headers(); | |
apex_web_service.set_request_headers('Content-Type', 'application/zip', p_reset => false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => l_request_url | |
,p_http_method => 'PUT' | |
,p_body_blob => l_zip_export | |
,p_credential_static_id => :G_CREDENTIAL | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_upload_failed; | |
end if; | |
apex_debug.info('End export application %s', :ALIAS); | |
end; |
データベースへの操作ではないため、失われた更新の防止はオフ、行のロックはいいえを選択します。
サーバー側の条件のボタン押下時にEXPORTを設定します。
以上でエクスポート自体は実装できたのですが、実際にアプリケーションがエクスポートされると、以下のようにUnexpected end of JSON inputのエラーが発生します。
エクスポートの実行が対話グリッドのリフレッシュに何かの影響を与えているようなので、エクスポートが完了した後に明示的にページをリロードさせます。
プロセスの後にブランチを作成します。
識別の名前はページのリロードとします。動作のタイプとしてページまたはURL(リダイレクト)を選択し、ターゲットはタイプがこのアプリケーションのページ、ページ番号に2、つまり同じページを指定します。
サーバー側の条件のボタン押下時にEXPORTを指定します。
以上でAPEXアプリケーションをオブジェクト・ストレージにエクスポートする処理が実装できました。
今回のアプリケーションは、アプリケーションを一括でエクスポートすることを目的にしています。その操作を容易にするために、すべての行の列EXPORTにチェックを入れるボタンを作成します。
ページ・プロパティのJavaScriptのページ・ロード時に実行に以下を記述し、対話グリッドの指定した列のチェックボックスに、すべての行に同じ値を設定するAPEXアクションset-column-value-allを定義します。ただし、JavaScriptのコードからでも画面からでも同じですが、対話グリッドで操作できる行は画面に表示されている行に限られます。APEXアクションset-column-value-allで値が設定される行も、表示中の行に限定されます。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
apex.actions.add( | |
[ | |
{ | |
name: "set-column-value-all", | |
action: (event, element, args) => { | |
const grid = apex.region(args.grid).call("getViews","grid"); | |
const model = grid.model; | |
let rec; | |
/* **表示されている** すべての行で繰り返す */ | |
model.forEach( (row) => { | |
/* 先頭列は主キー、つまりAPPLICATION_ID */ | |
rec = model.getRecord(row[0]); | |
model.setValue(rec, args.column, args.value); | |
}); | |
} | |
} | |
] | |
); |
Button Containerのリージョンに、すべての行のEXPORTをチェックするボタンを作成します。
識別の名前はCHECK_ALL_EXPORT、ラベルはCheck All Exportとします。レイアウトの新規行の開始をオフにし、ボタンEXPORTの隣に配置します。
動作のアクションに動的アクションで定義を選択し、詳細のカスタム属性に以下を記述します。
data-action="#action$set-column-value-all?grid=export-grid&column=EXPORT&value=Y"
ボタンUNCKECK_ALL_EXPORT、ラベルはUncheck All Export、カスタム属性は以下。
data-action="#action$set-column-value-all?grid=export-grid&column=EXPORT&value=N"
ボタンCHECK_ALL_OVERWRITE、ラベルはCheck All Overwrite、カスタム属性は以下。
data-action="#action$set-column-value-all?grid=export-grid&column=OVERWRITE&value=Y"
ボタンUNCHECK_ALL_OVERWRITE、ラベルはUnheck All Overwrite、カスタム属性は以下。
data-action="#action$set-column-value-all?grid=export-grid&column=OVERWRITE&value=N"
バケット内のファイル一覧を取得するRESTデータ・ソースを作成します。
RESTデータ・ソースのデータ・プロファイルを自動的に作成するため、一つ以上のアプリケーションのエクスポートを実行し、あらかじめバケット内にファイルを配置しておいてください。
共有コンポーネントのRESTデータ・ソースを開きます。
作成をクリックします。
RESTデータ・ソースの作成は最初から行います。
次へ進みます。
RESTデータ・ソース・タイプにOracle Cloud Infrastructure (OCI)を選択します。名前はList Exported Applicationsとします。
URLエンドポイントとして以下を指定します。<リージョン>の部分は環境に合わせて置き換えます(本記事はus-ashburn-1)。:namespaceおよび:bucketがパラメータとして認識されます。
https://objectstorage.<リージョン>.oraclecloud.com/n/:namespace/b/:bucket/o?fields=name,size,etag,timeCreated,md5,timeModified
URLパラメータ1がnamespaceとなるので、値にネームスペースを設定します。URLパラメータ2がbucketとなるので、値にバケット名を設定します。
次へ進みます。
次へ進みます。
認証が必要ですをオンにし、資格証明としてオブジェクト・ストレージのアクセスに使用するWeb資格証明を選択します。
検出をクリックします。
バケット内のファイルがデータとして表示されます。
RESTデータ・ソースの作成をクリックします。
RESTデータ・ソースList Exported Applicationsが作成されます。
このRESTデータ・ソースをデータ・ソースとした対話グリッドのページを作成します。バケット上のファイルの一覧よりインポートするファイルを複数選択するために、対話グリッドを使用します。
Exportのページと同様に、ページの作成を開始し対話グリッドを選択します。
ページ名はImportとします。データ・ソースとしてRESTデータ・ソースを選択し、RESTデータ・ソースとして先ほど作成したList Exported Applicationsを選びます。編集が有効はオンにします。
次へ進みます。
主キー列1は必ず指定が必要なので、列NAMEを除く列から一意であると思われる列を選択します。列NAMEは一意ですが、主キー列として選択するとデフォルトで非表示列になるため、他の列を選択します。今回は一意だと思われるMD5(Varchar2)を主キー列1として選択しています。
ページの作成をクリックします。
対話グリッドのページが作成されます。
対話グリッドのリージョンを選択し、ローカル後処理のタイプをSQL問合せに変更し、SQL問合せとして以下を記述します。インポート対象として選択するための列IMPORTを追加しています。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
select MD5, | |
'N' as IMPORT, | |
'N' as OVERWRITE, | |
ETAG, | |
NAME, | |
SIZE_, | |
TIMECREATED, | |
TIMEMODIFIED | |
from #APEX$SOURCE_DATA# |
対話グリッドの属性を、Exportと同様に設定します。
行セレクタAPEX$ROW_SELECTORのコントロールの非表示をオンにします。
列APEX$ROW_SELECTORとAPEX$ROW_ACTIONをコメント・アウトします。
列IMPORTとOVERWRITEのタイプをチェックボックスに変更します。
列IMPORTとOVERWRITE以外の表示列は変更不可なので、詳細のカスタム属性にreadonlyを設定します。
インポートについては、アプリケーションを全て選択して一度にインポートすることはなさそうなので、Exportで行ったJavaScriptに関する設定は省略します。
対話グリッドの設定は以上になります。
Exportのページと同様に、ボタンを配置するButton Containerのリージョンを作成します。
そのリージョンにボタンIMPORTを作成します。
プロセス・ビューを開き、デフォルトで作成されている対話グリッドの行の自動処理を行うプロセスを選択します。
識別の名前をImport - アプリケーションのインポートに変更します。設定のターゲット・タイプをPL/SQLコードに変更し、挿入/更新/削除するPL/SQLコードとして以下を記述します。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Ref: Building a REST API to Deploy APEX Apps | |
* https://blogs.oracle.com/apex/post/building-a-rest-api-to-deploy-apex-apps | |
*/ | |
declare | |
l_request_url varchar2(400); | |
l_zip_file blob; | |
l_app_files apex_zip.t_files; | |
l_files apex_t_export_files := apex_t_export_files(); | |
l_new_app_id number; | |
e_download_object_failed exception; | |
begin | |
if :APEX$ROW_STATUS <> 'U' then | |
/* 更新行のみのはずだが念のため他の状態は除外する。 */ | |
return; | |
end if; | |
if :IMPORT <> 'Y' then | |
/* IMPORTがチェックされていないアプリケーションは除外する。 */ | |
return; | |
end if; | |
/* | |
* Object StorageからZIPファイルを取得する。 | |
*/ | |
l_request_url := apex_string.format('https://objectstorage.%s.oraclecloud.com/n/%s/b/%s/o/%s', | |
:G_REGION, :G_NAMESPACE, :G_BUCKET, utl_url.escape(lower(:NAME), false, 'AL32UTF8') | |
); | |
apex_debug.info('Download application export from %s', l_request_url); | |
apex_web_service.clear_request_headers(); | |
l_zip_file := apex_web_service.make_rest_request_b( | |
p_url => l_request_url | |
,p_http_method => 'GET' | |
,p_credential_static_id => :G_CREDENTIAL | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise e_download_object_failed; | |
end if; | |
apex_debug.info('Download success: %s, size %s', :NAME, dbms_lob.getlength(l_zip_file)); | |
/* | |
* ZIPファイルを展開する。 | |
*/ | |
l_app_files := apex_zip.get_files( | |
p_zipped_blob => l_zip_file, | |
p_only_files => true | |
); | |
l_files.extend( l_app_files.count ); | |
for i in 1 .. l_app_files.count | |
loop | |
l_files(i) := apex_t_export_file( | |
l_app_files(i), | |
apex_util.blob_to_clob( | |
apex_zip.get_file_content( | |
p_zipped_blob => l_zip_file, | |
p_file_name => l_app_files(i) | |
) | |
) | |
); | |
end loop; | |
/* | |
* 未使用のアプリケーションIDを取得する。 | |
*/ | |
select max(application_id) + 1 into l_new_app_id | |
from apex_applications; | |
/* | |
* 予約されている番号であれば、9000にする。 | |
*/ | |
if l_new_app_id between 3000 and 8999 then | |
l_new_app_id := 9000; | |
end if; | |
/* | |
* アプリケーションをインポートする。 | |
* | |
* SET_APPLICATION_ALIAS, SET_APPLICATION_NAMEの呼び出しも要検討。 | |
*/ | |
apex_application_install.set_application_id( | |
p_application_id => l_new_app_id); | |
apex_application_install.install( | |
p_source => l_files, | |
/* | |
* アプリケーションには必ず空いているアプリケーションIDが割り当たる | |
* ため、上書きをYにする必要がない。 | |
*/ | |
p_overwrite_existing => (:OVERWRITE = 'Y') ); | |
end; |
データベースへの操作ではないため、失われた更新の防止はオフ、行のロックはいいえを選択します。
サーバー側の条件のボタン押下時にIMPORTを設定します。
以上でアプリケーションをインポートするページは完成です。
実際はアプリケーションをインポートする際に、アプリケーションIDとして未割り当てのIDを選んで割り当てているため、OVERWRITEにチェックが必要な状況は発生しないはずです。複数の人が同時にインポートを実行するといった場合に、同じアプリケーションIDでアプリケーションをインポートしようとする状況が発生する可能性はあります。
インポート処理については、実際の要件からもっと実装を調整する必要はあるでしょう。
APEXアプリケーションよりインポートを実行するには、アプリケーション定義のセキュリティの詳細のランタイムAPIの使用状況に含まれる他のアプリケーションを変更にチェックを入れる必要があります。
データベースにsqlplusやSQLclなどで直接接続してAPEXアプリケーションをインストールする場合は、接続ユーザーにロールAPEX_ADMINISTATOR_ROLEが割り当たっている必要があります。詳しくはOracle APEXの管理ガイドを参照してください。
以下より、アプリケーションを若干調整します。
作成したRESTデータ・ソースのパラメータbucketとnamespaceのデフォルト値を削除し、必須をはいに切り替えます。
Importの対話グリッドのパラメータbucketの値のタイプを静的値に変更し、静的値として&G_BUCKET.を指定します。
パラメータnamespaceの静的値に&G_NAMESPACE.を指定します。
以上でアプリケーション定義G_NAMESPACEとG_BUCKETの設定が、Import対象に一覧表示に反映されるようになります。
リージョンはホスト名に含まれるため、パラメータとしては定義できません。ホスト部分の切り替えは、RESTデータ・ソースのリモート・サーバーを変更することで対応します。リモート・サーバーによる切り替えは、こちらの記事「リモート・サーバーを入れ替えてRESTデータ・ソースの実行環境を切り替える」で紹介しています。
今回の記事は以上になります。
Oracle APEXのアプリケーション作成の参考になれば幸いです。
完