2020年4月20日月曜日

Emailキャンペーンを行うアプリケーションの作成(1) - OCI Email Deliveryの設定

Oracle APEXのプロダクト・マネージャーであるSalim Hlayelが、Creating Email Campaign App with Oracle APEXとして、Oracle APEXを使ってEmailキャンペーンを行うアプリケーションを作成する、という記事を書いています。記事に沿って自分でもアプリケーションを作ってみました。

元記事はOracle APEXが動いている環境を選んでいませんが、今回はAlways FreeのATP - Autonomous Transaction Processingのインスタンスを使ってアプリケーション開発を進めていきます。

Oracle Cloudの環境では、OCI Email Deliveryとして電子メールを送信するサービスが提供されています。このサービスではAlways Freeのリソースとして、1ヶ月あたり3,100通のメールを無料で送信できます。その他のサービスにかかる価格は、こちらのページに記載されています。

元の記事にはOracle CloudのOCI Email Deliveryの設定については記載がないので、最初にそちらから準備していきます。手順の説明は英語ではこちらのマニュアルの記載、日本語ではこちらの記載にしたがって作業を進めていきます。


SMTP接続エンドポイントを探す



使用しているリージョンにあるSMTP接続エンドポイントを見つけます。こちらのマニュアルにあるSMTP Connection Endpointsのリストから探します。東京リージョンであればSMTP接続エンドポイントsmtp.email.ap-tokyo-1.oci.oraclecloud.comです。大阪であれば、smtp.email.ap-osaka-1.oci.oraclecloud.comです。

以下より説明する作業では、北米のAshburnリージョンのエンドポイントsmtp.email.us-ashburn-1.oci.oraclecloud.comを使用します


ユーザーを作成する



マニュアルに記載されている推奨に従って新規にユーザーを作成します。そのユーザーにてSMTP資格証明を生成します。

Oracle Cloudのコンソールのハンバーガー・メニューから、アイデンティティとセキュリティに含まれるユーザーを開きます。
 

登録済みユーザーの一覧画面から、ユーザーの作成をクリックします。
 

ユーザー・タイプの選択としてIAMユーザーを選びます。

ユーザーの名前説明を入力し(電子メールはオプションなので入力不要)、作成をクリックします。これでユーザーが作成されます。

作成するユーザーの名前mail_agentとしました。
 


SMTP資格証明を生成する



ユーザーが作成され、ユーザーの詳細画面が開きます。

ユーザーの機能をSMTP資格証明に限定します。

ユーザー機能の編集をクリックします。


ユーザー機能の編集では、SMTP資格証明のみにチェックを入れます。

変更の保存をクリックし、ダイアログを閉じます。


機能SMTP資格証明だけが、はいと表示されます。

リソースから、SMTP資格証明をクリックします。クリックするとSMTP資格証明の領域が表示されるので、そこからSMTP資格証明の生成をクリックします。


確認画面がポップアップするので説明を入力し、再度SMTP資格証明の生成をクリックします。


SMTP資格証明が生成されたことを通知するポップアップが開きます。

ユーザー名パスワードコピーし、安全な場所に保管して後で参照できるようにします。ここでコピーを忘れて、ポップアップ・ウィンドウをクローズするとユーザー名とパスワードを後から参照する方法はないので、SMTP資格証明を再度生成しなければなりません。
 


グループを作成する



作成したユーザーの権限にて電子メールを送信するには、そのユーザーに対してOCI Email Deliveryへのアクセスが許可されている必要があります。そのため、グループの作成と、そのグループへのユーザーの追加、その後にグループへのポリシーの割り当てを行います。

最初にグループを作成します。

アイデンティティとセキュリティからグループを開きます。
 

登録済みグループの一覧が表示されます。グループの作成をクリックします。
 

グループの名前説明を入力し、作成をクリックします。

作成するグループの名前ApprovedSendersとしました。
 


グループにユーザーを追加する



グループが作成され、グループの詳細画面が開きます。

リソースグループ・メンバーより、ユーザーをグループに追加をクリックします。


先ほど作成したユーザーmail_agentを選択し、追加をクリックします。
 

グループ・メンバーに先ほど作成したユーザーが追加されていることを確認します。
 


ポリシーを定義する



作成したグループにOCI Email Deliveryへのアクセス権限を与えるポリシーを定義します。

アイデンティティとセキュリティより、ポリシーを開きます。
 

ポリシーの一覧画面から、ポリシーの作成をクリックします。
 

作成するポリシーの名前説明を入力します。

作成するポリシーの名前EmailFamilyPolicyとしました。

ポリシー・ステートメントを設定します。手動エディタの表示ONにします。

Allow group グループ名 to use email-family in tenancy

できれば"in tenancy"としてテナンシー全体で許可するより、コンパートメントに限定する方が推奨です。今回はAlways Freeのアカウントであり、コンパートメント単位の細かい管理は想定していないため、テナンシー全体で定義します。

グループはApprovedSendersとして作成しているため、今回の例で設定するポリシーは以下になります。

Allow group ApprovedSenders to use email-family in tenancy

以上を入力し、作成をクリックします。


ポリシーが作成されると、ポリシーの詳細画面が表示されます。
 


電子メールの承認済送信者を設定する



ハンバーガー・メニューより、電子メール配信を探して開きます。
 

このサービスまたはリソースを使用するには、有料アカウントにアップグレードする必要があります。と表示されますが、アップグレードはせずに作業を継続します。

送信者のメールアドレス(Fromに現れるアドレス)に許可を与えます。

電子メール配信承認済送信者の一覧から承認済送信者の作成をクリックします。


電子メール・アドレスとして送信するメールのFromに与える値を入力し、承認済送信者の作成をクリックします。
 

Due to domain policy, approved sender cannnot be created in this email domain until DKIM is provisioned. と表示され、承認済送信者の作成ができない場合があります。

このような電子メール・アドレスを承認済送信者として設定するには、DKIMを事前に設定する必要があります。DKIMの設定については、こちらのOracle Cloud Infrastructureチュートリアルが参考になります。

作成された電子メールの承認済送信者が一覧に表示されます。
 

以上で、Oracle Cloudのコンソールを使って行うOCI Email Deliveryに関する設定は完了です。


Oracle APEXのインスタンス設定を行う



データベース・アクションに管理ユーザーADMINでサインインします。

SQLの画面から以下のスクリプトを実行し、APEXの電子メール送信を構成します。

set define offはパスワードに'&'が含まれていると、置換する文字列と認識されるので、それを抑制するために指定しています。
set define off;
begin
APEX_INSTANCE_ADMIN.SET_PARAMETER('SMTP_HOST_ADDRESS', 'メールサーバーのアドレス');
APEX_INSTANCE_ADMIN.SET_PARAMETER('SMTP_USERNAME', 'SMTP資格証明を作成したときにコピーしたユーザー名');
APEX_INSTANCE_ADMIN.SET_PARAMETER('SMTP_PASSWORD', 'SMTP資格証明を作成したときにコピーしたパスワード');
COMMIT;
end;
  

以上で、Oracle APEXから電子メールを送信する設定は完了です。


テスト・メールを送信する



Oracle APEXのSQLワークショップに含まれる、SQLコマンドの画面を開いて、以下のSQLを実行します。
BEGIN
    APEX_MAIL.SEND(p_from => '承認済送信者として設定した電子メール・アドレス',
                   p_to   => '送信先の電子メール・アドレス',
                   p_subj => 'Email from Oracle Autonomous Database',
                   p_body => 'Sent using APEX_MAIL');
    APEX_MAIL.PUSH_QUEUE;
END;
/
  

サブジェクトが"Email from Oracle Autonomous Database"、本文が"Sent using APEX_MAIL"となっている電子メールの受信が確認できたら、OCI Email Deliveryにまつわる設定は完了です。
 

電子メールの送信履歴はビューAPEX_MAIL_LOGより確認できます。

select * from apex_mail_log order by last_updated_on desc;


次の記事で本題の、APEXのアプリケーション作成に取り掛かります。

続く

2020年4月14日火曜日

APEX_STRING.SPLITファンクションの呼び出し時の性能について

Twitterをみていたら、Martin D'Souzaによる"Optimizing MEMBER OF with APEX_STRING.SPLIT"という記事が目に留まりました。APEX_STRING.SPLITの性能の話は興味があるので内容を確認してみました。以下、その確認できたことを紹介します。

まず、APEX_STRING.SPLITというファンクションですが、日本語マニュアルの説明はこちらになります。例えば、こういった文字列"A:B:C"を、apex_t_varchar2という配列型の要素 A,B,C に分割します。色々な用途で使えます。APEXではページ・アイテムがチェックボックスの場合、選択した値は':'で区切られた複数の値として設定されます。例えば、選択肢がA, B, Cとあって、AとBがチェックされるとページ・アイテムとしての値は'A:B'になります。それを分割するために、APEX_STRING.SPLITを使うことができます。

もう少し具体的な例を挙げてみましょう。従業員のサンプル・データを使った対話グリッドに、選んだ職種のみ表示/編集の対象とするチェックボックスを付けました。

チェックボックスとなっているページ・アイテムの名前は、P8_JOB_SELECTIONSとしています。LOVの定義として、以下のSQL問合わせを設定しています。
select job as disp_value, job as ret_value from emp group by job
EMP表に存在するすべての職種が選択対象となります。スクリーンショット上で選択しているのは、アナリスト、社長、セールスなので、P8_JOB_SELECTIONSの値は"アナリスト:社長:セールス"になります。

このチェックボックスの選択結果を対話グリッドに反映させるために、対話グリッドのソースを以下のように変更します。

WHERE句に以下を追加しています。
job in (select column_value from apex_string.split(:P8_JOB_SELECTIONS, ':'))
そして、送信するページ・アイテムとしてP8_JOB_SELECTIONSを指定します。後は、ページ・アイテムP8_JOB_SELECTIONSの値が変更されたときに対話グリッドをリフレッシュする動的アクションを設定すると、チェックボックスの選択で対話グリッドの検索/編集対象を制限する機能の実装が完了です。

APEX_STRING.SPLITはPL/SQLで実装されているため、SQLから呼び出されるときはSQLからPL/SQLへのコンテキスト・スイッチ(結構重い)が発生します。WHERE句が評価される度にAPEX_STRING.SPLITが呼び出されるようだと、すごく遅くなります。では、それを避けるためには何を注意する必要があるのでしょうか。

先ほどの対話グリッドで実際に実行されているSQLは以下になります。
select * from emp
where job in (select column_value from apex_string.split('アナリスト:社長:セールス',':'));
APEX_STRING.SPLITが呼び出されたときにメッセージを表させるために、APEX_STRING.SPLITのラッパーとなるファンクションを定義します。
create or replace function wrapper_split(
  p_str in varchar2,
  p_sep in varchar2)
  return apex_t_varchar2
as
begin
  dbms_output.put_line('APEX_STRING.SPLIT CALLED at ' || systimestamp);
  return apex_string.split(
    p_str => p_str,
    p_sep => p_sep
  );
end wrapper_split;
定義したラーパー・ファンクションの動作を確認します。DBMS_OUTPUTの出力結果とSELECT文の実行結果が混らないように、双方ともDBMS_OUTPUTの出力結果として表示させます。
begin
  for r in
  (
      select * from wrapper_split('アナリスト:社長:セールス',':')
  )
  loop
    dbms_output.put_line(r.column_value);
  end loop;
end;
実行結果は以下になります。
APEX_STRING.SPLIT CALLED at 20-04-14 06:21:31.423595000 +00:00
アナリスト
社長
セールス
SELECT文1回の実行で、APEX_STRING.SPLITが一度だけ呼ばれていることがわかります。

次に対話グリッドで使われているSQLの結果を確認します。EMP表は全部で14行です。
begin
  for r in
  (
      select * from emp
      where job in (select column_value from wrapper_split('アナリスト:社長:セールス',':'))
  )
  loop
    dbms_output.put_line(r.job);
  end loop;
end;
実行結果は以下になります。選択された行は7行です。APEX_STRING.SPLITの呼び出しは一度だけで、14回ではありません。
APEX_STRING.SPLIT CALLED at 20-04-14 06:26:32.582635000 +00:00
アナリスト
アナリスト
社長
セールス
セールス
セールス
セールス
例えばこれが、以下のようなPL/SQLで記述されたファンクションの場合を考えてみます。UPPER()へのラッパー・ファンクションです。
create function my_upper(
    p_value varchar2
)
return varchar2
is
begin
   dbms_output.put_line('UPPER CALLED AT ' || systimestamp);
   return upper(p_value);
end;
このMY_UPPER()を検索条件に与えたSQLを実行します。
begin
  for r in
  (
      select * from emp
      where my_upper(job) = 'セールス'
  )
  loop
    dbms_output.put_line(r.job);
  end loop;
end;
実行結果は以下になります。
UPPER CALLED AT 20-04-14 06:35:06.321017000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321131000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321142000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321150000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321157000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321164000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321171000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321178000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321187000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321194000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321201000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321208000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321214000 +00:00
UPPER CALLED AT 20-04-14 06:35:06.321221000 +00:00
セールス
セールス
セールス
セールス
MY_UPPER()は14回呼び出されています。検索結果の行数(この結果では4行)ではなく、EMP表の行数だけ呼び出されます。その度にコンテキスト・スイッチが発生するため、パフォーマンス面での大きなデメリットになります。

さて、対話グリッドのSELECT文の検索条件にAPEX_STRING.SPLITを使った際に、呼び出し回数は1回でした。理由を理解するために、このSQLの実行計画を確認します。

EMP表とIN句に与えているSELECT文の結果で(HASH)JOINしているのがわかります。つまり、実際には、元ブログにあるような以下のSQLとして実行されています。
select e.* from emp e
  join  (select column_value from apex_string.split('アナリスト:社長:セールス',':')) j
  on e.job = j.column_value;
結果として、APEX_STRING.SPLITの呼び出しが1回になっています。

さて、member ofを使うと、もっとSQLがきれいに書ける、という意見がありました。次のようなSQLになります。
select * from emp
where job member of apex_string.split('アナリスト:社長:セールス',':');
確かにSQLは読みやすくなります。では、呼び出し回数を確認してみましょう。
begin
  for r in
  (
      select * from emp
      where job member of wrapper_split('アナリスト:社長:セールス',':')
  )
  loop
    dbms_output.put_line(r.job);
  end loop;
end;
実行結果は以下になります。
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.414940000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415131000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415164000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415197000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415229000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415245000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415260000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415275000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415291000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415306000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415321000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415336000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415351000 +00:00
APEX_STRING.SPLIT CALLED at 20-04-14 06:54:17.415366000 +00:00
社長
アナリスト
アナリスト
セールス
セールス
セールス
セールス
EMP表の行数だけAPEX_STRING.SPLITが呼ばれています。これは嬉しくないです。この検索条件をスカラ・サブクエリ・キャッシングを働かせるSQLに書き換えることにより、呼び出し回数を1回にすることができます。SQLは次のようになります。
select * from emp
where job member of (select apex_string.split('アナリスト:社長:セールス',':') from dual)
確認結果は省略しますが、APEX_STRING.SPLIITの呼び出し回数は1回になっています。実行計画は以下です。最初のSQLとは異なる実行計画で、さて、どちらが速いでしょうか。前者は社長、アナリスト、セールスを別々の値として扱った上で、それぞれのハッシュ値をとってジョインすることで検索対象を絞り込んでいます。後者は、社長、アナリスト、セールスをひとつの値としてキャッシュし、EMP表の行すべてで、JOBがこの3つの値に含まれるかどうか判別しています。確認は省きますが、ハッシュ値で絞り込む前者の方が高速だろうと思われます。

スカラ・サブクエリとは、1つの行から1つの列値のみを戻す副問合せです。オラクルのマニュアルでは、スカラ副問合わせ式として説明がされています。スカラ・サブクエリ・キャッシングはスカラ・サブクエリの結果、つまり1つの値ですが、これを与えられているバインド変数の値をキーとして(メモリ上に作成されたSQLが実行されている間だけ有効な)ハッシュ・テーブルに登録します。バインド変数として与えられる値をキーにして、メモリ上のハッシュ・テーブルから結果を取得することで、検索処理の実行を省きます。今回の例はSELECT文にバインド変数を含まないため、結果は1つだけであり、必ずキャッシュにヒットします。そのため、APEX_STRING.SPLITの呼び出しは1度だけです。かなり以前からある機能ですが、この仕組みを説明した文書は少なく、これくらいでしょうか。とはいえ、仕組みは難しいものではありません。

APEXが提供しているPL/SQL APIを効果的に使用するために、知識の引き出しにいれておきたい機能のひとつです。

追記


その1:元ブログのラッパー・ファンクションは内部で、Loggerを使用しています。元ブログの記事を書いたMartin D'SouzaがLoggerの作者です。

その2:最近知ったのですが、私はAPEX_STRING.SPLITを使用する際に以下のようにtable()で囲っていたのですが、18cからはtable()は省略できるようになっています。
job in (select column_value from table(apex_string.split(:P8_JOB_SELECTIONS, ':')))

2020年4月9日木曜日

APEXでOCIオブジェクト・ストレージ上のデータセットを使用する

OCIオブジェクト・ストレージ上のデータセット(CSVファイル)より、ファセット検索ページを作成するというブログエントリがありました。こういう要望は多いのかな、と思ったので自分でも実践してみます。元のブログ・エントリではブラジルにてオープン・データとして提供されている、労働災害と労働者の年齢区分のデータを使用しています。この記事は、その代わりに、日本で提供されているオープン・データを使用します。

使用するオープン・データ


東京都によって提供されている都内の最新感染動向のサイトより、
  • 東京都_新型コロナウイルス陽性患者発表詳細 - ここからダウンロード
  • 東京都 新型コロナコールセンター相談件数 - ここからダウンロード
  • 東京都_新型コロナ受診相談窓口相談件数 - ここからダウンロード

CSVファイルをオブジェクト・ストレージへ配置する


まず、オブジェクト・ストレージにバケットを作成します。そのあと、取得した3つのCSVファイルをアップロードします。

Oracle Cloudのコンソールより、オブジェクト・ストレージを開きます。

バケットの作成をクリックします。どのコンパートメントに作成してもかまいません。

バケット名opendataに設定し、バケットの生成をクリックします。

opendataというバケットが作成されたことを確認します。バケット名opendataをクリックし、バケットを開きます。

オブジェクトのアップロードをクリックし、アップロードするファイルを指定するフォームを開きます。

ファイルを選択をクリックし、ダウンロード済みのファイルを選択します。3つのファイルすべてを選択します。すべて選択した後、オブジェクトのアップロードをクリックします。

CSVファイルがアップロードされていることを確認します。

DBMS_CLOUDパッケージを使用するための準備


DBMS_CLOUDパッケージを使用するために、以下の4つの準備を行います。
  1. DBMS_CLOUDパッケージの実行権限をOracle APEXに与えます。
  2. DATA_PUMP_DIRの読み書き許可をOracle APEXに与えます。
  3. オブジェクト・ストレージにアクセスするための認証トークンを生成します。
  4. アップロードしたCSVファイルへのURLパスを取得します。

DBMS_CLOUDパッケージの実行権限、DATA_PUMP_DIRの読み書き許可をOracle APEXに与える


SQL Developer Webから管理者アカウントでログインし、GRANT文を実行するのが手順として容易でしょう。

Oracle APEXがホストされているAutonomous Databaseのサービス・コンソールの開発から、SQL Developer Webを開きます。

ユーザー名としてADMINパスワードはデータベース・インスタンスの管理者パスワードを入力します。(管理者パスワードはインスタンス作成時に指定します。または、サービス・コンソールの管理から、管理者パスワードの設定を呼び出して更新します。)

ワークシートに以下のSQLを入力し文の実行をクリックすることにより、DBMS_CLOUDパッケージの実行権限をOracle APEXに割り当てます。ワークスペース名は、それぞれ適切な値に置き換えてください。画面の例では、MYWORKSPACEをワークスペース名として与え、DBMS_CLOUDへの実行権限の割り当てを行っています。スクリーンショットは載せませんが、この後に、DATA_PUMP_DIRの読み書き権限の付与も実行します。
GRANT EXECUTE ON DBMS_CLOUD TO ワークスペース名;
GRANT READ,WRITE ON directory DATA_PUMP_DIR TO ワークスペース名;

認証トークンを生成する


Oracle Cloudのコンソールから、ユーザー設定を呼び出します。

ユーザー設定のリソースより認証トークンを選択し、トークンの生成をクリックします。

ダイアログが開き、説明の入力を求められます。説明を入力し、トークンの生成をクリックします。

トークンが生成されたことを通知する画面になります。コピーのリンクをクリックし、認証トークンをコピーします。コピーしたトークンは無くさないようにします。コピーした後、この画面は閉じます。

認証トークン自体が作成されていることはユーザー設定の画面に一覧されますが、認証トークン自体を再度取り出すことはできません。

これで、認証トークンの取得は完了です。

CSVファイルへのURLパスを取得する


先ほど作成したバケットopendataを開き、リソースからオブジェクトを選んで、含まれるオブジェクトの一覧を表示します。

オブジェクトの詳細を表示するため、オブジェクトの操作メニューを開き、オブジェクト詳細の表示を実行します。

それぞれ3つのCSVファイルについて、URLパス(URI)をコピーして、後で使用できるよう、どこかに保存しておきます。

これでDBMS_CLOUDパッケージを使って外部表を作成するために必要な情報がすべて揃いました。

オブジェクト・ストレージにアクセスするための認証情報をデータベースに登録する


最初にオブジェクト・ストレージのアクセスに使用する認証情報(クリデンシャル)を登録します。以下のコマンドをOracle APEXのSQLワークショップから実行します。画面上の例では、クリデンシャル名をcr_apex_datasetとしています。
begin
  dbms_cloud.create_credential(
      credential_name => 'クリデンシャル名',
      username => 'OCIユーザー名',
      password => '認証トークン'
  );
end; 

ここで私はちょっと引っ掛かったのですが、ユーザー名にoracleidentitycloudservice/で始まるものと、それが除かれているものの2種類登録されているはずです。oracleidentitycloudservice/で始まるユーザーに認証トークンを登録した(このブログの手順に従うと、oracleidentitycloudservice/で始まるユーザーに認証トークンをつけています)のであれば、OCIユーザー名もその名前を指定する必要があります。

次のSQLを実行することで、登録したクリデンシャルを確認することができます。
select * from user_credentials;
今回の手順は演習なのでユーザー登録はこのようにしていますが、ログインに使うアカウントをこのような用途で使うのは心配です。以前にオブジェクト・ストレージの操作について書いた記事で、API呼び出しのためのユーザーのセットアップについて書いています。この方法で登録したユーザーをDBMS_CLOUD.CREATE_CREDENTIALによってクリデンシャルとして登録する方法が、CREATE_CREDENTIAL Procedure (OCI Signing Key Credentials)として説明されているので、よりシリアスな用途のときは、そちらを検討した方が良いと思います。

オブジェクト・ストレージ上のCSVファイルを参照する外部表を作成する


オブジェクト・ストレージ上のCSVファイルを参照する外部表TOKYO_COVID19_PATIENTSを作成します。 元にしているデータは東京都_新型コロナウイルス陽性患者発表詳細です。SQLワークショップのSQLコマンドから実行します。
begin
    dbms_cloud.create_external_table(
        table_name      => 'tokyo_covid19_patients',
        credential_name => 'cr_apex_dataset',
        file_uri_list   => 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/xxxxxxxxx/b/opendata/o/130001_tokyo_covid19_patients.csv',
        format          => json_object(
            'type'             value 'csv',
            'skipheaders'      value '1',
            'recorddelimiter'  value 'newline',
            'dateformat'       value 'YYYY-MM-DD',
            'conversionerrors' value 'reject_record',
            'characterset'     value 'AL32UTF8',
            'trimspaces'       value 'lrtrim',
            'rejectlimit'      value '1'),
        column_list => 
            'LINE_NO number,
            全国地方公共団体コード number,
            都道府県名 varchar2(2000),
            市区町村名 varchar2(2000),
            公表_年月日 date,
            曜日 varchar2(8),
            発症_年月日 date,
            患者_居住地 varchar2(2000),
            患者_年代 varchar2(80),
            患者_性別 varchar2(80),
            患者_属性 varchar2(2000),
            患者_状態 varchar2(2000),
            患者_症状 varchar2(2000),
            患者_渡航歴の有無フラグ varchar2(2000),
            備考 varchar2(2000),
            退院済フラグ varchar2(8)'
    );
end;

表が作成されたら、内容を確認してみましょう。以下の単純なSELECT文を実行します。
select * from tokyo_covid19_patients;

次に東京都 新型コロナコールセンター相談件数の外部表TOKYO_COVID19_CALL_CENTERを作成します。
begin
    dbms_cloud.create_external_table(
        table_name      => 'tokyo_covid19_call_center',
        credential_name => 'cr_apex_dataset',
        file_uri_list   => 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/xxxxxxxxx/b/opendata/o/130001_tokyo_covid19_call_center.csv',
        format          => json_object(
            'type'             value 'csv',
            'skipheaders'      value '1',
            'recorddelimiter'  value 'newline',
            'dateformat'       value 'YYYY-MM-DD',
            'conversionerrors' value 'reject_record',
            'characterset'     value 'AL32UTF8',
            'trimspaces'       value 'lrtrim',
            'rejectlimit'      value '1'),
        column_list => 
            '全国地方公共団体コード number,
            都道府県名 varchar2(2000),
            市区町村名 varchar2(2000),
            受付_年月日 date,
            曜日 varchar2(8),
            相談件数 number'
    );
end;
最後に東京都_新型コロナ受診相談窓口相談件数の外部表TOKYO_COVID19_COMBINED_TELPHONE_ADVICE_CENTERを作成します。
begin
    dbms_cloud.create_external_table(
        table_name      => 'tokyo_covid19_combined_telephone_advice_center',
        credential_name => 'cr_apex_dataset',
        file_uri_list   => 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/xxxxxxxxx/b/opendata/o/130001_tokyo_covid19_combined_telephone_advice_center.csv',
        format          => json_object(
            'type'             value 'csv',
            'skipheaders'      value '1',
            'recorddelimiter'  value 'newline',
            'dateformat'       value 'YYYY-MM-DD',
            'conversionerrors' value 'reject_record',
            'characterset'     value 'AL32UTF8',
            'trimspaces'       value 'lrtrim',
            'rejectlimit'      value '1'),
        column_list => 
            '全国地方公共団体コード number,
            都道府県名 varchar2(2000),
            市区町村名 varchar2(2000),
            受付_年月日 date,
            曜日 varchar2(8),
            相談件数 number'
    );
end;

3つの外部表が登録できました。これ以降は、Oracle APEXのアプリケーション作成になります。参照だけが可能な表ですが、表を元にしたアプリケーション作成ですので、Oracle APEXとしては一般的なアプリケーション作成になります。

Oracle APEXアプリケーションを作成する


作成した3つの外部表を使って、以下のような画面を作ることができます。外部表といっても、書き込みができないことを除けば通常の表と同様に扱えます。また、実際のデータはオブジェクト・ストレージ上のCSVファイルなので、それを最新のデータに置き換えるとAPEX側から見ることができるデータも最新のものになります。

作成された表のDDLを確認してみたところ、以下でした。アクセス・ドライバはORACLE_LOADERで、オブジェクト・ストレージだからといって特別ではないようです。
CREATE TABLE  "TOKYO_COVID19_PATIENTS" 
   ( "LINE_NO" NUMBER, 
 "全国地方公共団体コード" NUMBER, 
 "都道府県名" VARCHAR2(80) COLLATE "USING_NLS_COMP", 
        “市区町村名" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "公表_年月日" DATE, 
 "曜日" VARCHAR2(3) COLLATE "USING_NLS_COMP", 
 "発症_年月日" DATE, 
 "患者_居住地" VARCHAR2(80) COLLATE "USING_NLS_COMP", 
 "患者_年代" VARCHAR2(20) COLLATE "USING_NLS_COMP", 
 "患者_性別" VARCHAR2(20) COLLATE "USING_NLS_COMP", 
 "患者_属性" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "患者_状態" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "患者_症状" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "患者_渡航歴の有無フラグ" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "備考" VARCHAR2(2000) COLLATE "USING_NLS_COMP", 
 "退院済フラグ" VARCHAR2(8) COLLATE "USING_NLS_COMP"
   )  DEFAULT COLLATION "USING_NLS_COMP" 
   ORGANIZATION EXTERNAL 
    ( TYPE ORACLE_LOADER
      DEFAULT DIRECTORY "DATA_PUMP_DIR"
      ACCESS PARAMETERS
      ( RECORDS IGNORE_HEADER=1 DELIMITED BY newline CHARACTERSET AL32UTF8 NOLOGFILE NOBADFILE NODISCARDFILE READSIZE=10000000 CREDENTIAL 'cr_apex_dataset' 
    FIELDS CSV WITHOUT EMBEDDED DATE_FORMAT DATE MASK 'YYYY-MM-DD' CONVERT_ERROR REJECT_RECORD LRTRIM 
  )
      LOCATION
       ( 'https://objectstorage.ap-tokyo-1.oraclecloud.com/n/xxxxxxxxxxx/b/opendata/o/130001_tokyo_covid19_patients.csv'
       )
    )
   REJECT LIMIT 1 
  PARALLEL
/
DBMS_CLOUDパッケージに関するマニュアルはこちらになります。日本語のマニュアルはこちらです。

補足


今回のような、オープン・データであるCSVファイルを認証なしで取得できる場合であれば、Oracle APEXが提供しているプロシージャを使用して以下のようにデータを取り込むことも可能です。参考までに方法を紹介します。

東京都_新型コロナウイルス陽性患者発表詳細のデータを取り込みます。最初にデータをロードする表を作成します。
CREATE TABLE "TOKYO_COVID19_PATIENTS"
(    "LINE_NO" NUMBER, 
    "全国地方公共団体コード" NUMBER, 
 "都道府県名" VARCHAR2(2000), 
 "市区町村名" VARCHAR2(2000), 
 "公表_年月日" DATE, 
 "曜日" VARCHAR2(8), 
 "発症_年月日" DATE, 
 "患者_居住地" VARCHAR2(2000), 
 "患者_年代" VARCHAR2(80), 
 "患者_性別" VARCHAR2(80), 
 "患者_属性" VARCHAR2(2000), 
 "患者_状態" VARCHAR2(2000), 
 "患者_症状" VARCHAR2(2000), 
 "患者_渡航歴の有無フラグ" VARCHAR2(2000), 
 "備考" VARCHAR2(2000), 
 "退院済フラグ" VARCHAR2(8)
);
次に以下のSQLを実行することで、CSVファイルからのデータロードを行います。実行しているのはinsert select文で、その中でapex_data_parser.parseとapex_web_service.make_rest_request_bを使用しています。
insert into tokyo_covid19_patients(
    LINE_NO, "全国地方公共団体コード","都道府県名","市区町村名","公表_年月日","曜日",
    "発症_年月日","患者_居住地","患者_年代","患者_性別","患者_属性","患者_状態",
    "患者_症状","患者_渡航歴の有無フラグ","備考","退院済フラグ")
select col001, col002, col003, col004, to_date(col005,'YYYY-MM-DD'), col006,
       to_date(col007,'YYYY-MM-DD'), col008, col009, col010, col011,
       col012, col013, col014, col015, col016
from table(apex_data_parser.parse(
    p_content => apex_web_service.make_rest_request_b(
        'https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_patients.csv',
        'GET'),
    p_file_name => '130001_tokyo_covid19_patients.csv',
    p_skip_rows => 1)
);
東京都 新型コロナコールセンター相談件数のロードは、以下の表とINSERT文で行うことができます。表のDDLです。
CREATE TABLE  "TOKYO_COVID19_CALL_CENTER" 
   ( "全国地方公共団体コード" NUMBER, 
 "都道府県名" VARCHAR2(2000), 
 "市区町村名" VARCHAR2(2000), 
 "受付_年月日" DATE, 
 "曜日" VARCHAR2(8), 
 "相談件数" NUMBER
   );
INSERT文は以下です。
insert into tokyo_covid19_call_center(
    "全国地方公共団体コード","都道府県名","市区町村名","受付_年月日","曜日","相談件数"
    )
select col001, col002, col003, to_date(col004,'YYYY-MM-DD'), col005, col006
from table(apex_data_parser.parse(
    p_content => apex_web_service.make_rest_request_b(
        'https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_call_center.csv',
        'GET'),
    p_file_name => '130001_tokyo_covid19_call_center.csv',
    p_skip_rows => 1)
);
東京都_新型コロナ受診相談窓口相談件数のロードは、以下の表とINSERT文で行うことができます。表のDDLです。
CREATE TABLE  "TOKYO_COVID19_COMBINED_TELEPHONE_ADVICE_CENTER" 
   ( "全国地方公共団体コード" NUMBER, 
 "都道府県名" VARCHAR2(2000), 
 "市区町村名" VARCHAR2(2000), 
 "受付_年月日" DATE, 
 "曜日" VARCHAR2(8), 
 "相談件数" NUMBER
   ) ;
INSERT文は以下です。
insert into tokyo_covid19_combined_telephone_advice_center(
    "全国地方公共団体コード","都道府県名","市区町村名","受付_年月日","曜日","相談件数"
    )
select col001, col002, col003, to_date(col004,'YYYY-MM-DD'), col005, col006
from table(apex_data_parser.parse(
    p_content => apex_web_service.make_rest_request_b(
        'https://stopcovid19.metro.tokyo.lg.jp/data/130001_tokyo_covid19_combined_telephone_advice_center.csv',
        'GET'),
    p_file_name => '130001_tokyo_covid19_combined_telephone_advice_center.csv',
    p_skip_rows => 1)
);
最後の最後に謝辞。Qiitaにある、こちらの記事はじめてのAutonomous Databaseへのデータロード(Object Storage経由の場合)も参考にさせていただきました。ありがとうございます。