2026年9月2日水曜日

Oracle Databaseを操作するiOSアプリをOracle Backend for FirebaseとOpenAI Codexで作成する

Oracle Databaseに保存されているデータを操作するiOSアプリを、Oracle Backend for Firebase(Fusabase)とOpenAI Codexを使って作成します。本記事の著者もそうですが、技術者としての背景がバックエンドだったりDBAだったりすると、iOSアプリやAndroidといったモバイル・アプリケーションの作成はハードルが高く、取り組むのに躊躇してしまいます。

本記事では、Oracle Backend for Firebase(Fusabase)をiOSアプリケーションに組み込んで、Oracle Databaseを操作するアプリケーションを作成してみます。iOSアプリケーションはSwiftで記述しますが、コードはすべてOpenAI Codexが生成します。

(同様の手順でCodexで生成したAndroidとWebアプリについて、記事の末尾に追記しました。)

OpenAI Codexは以下のようなiOSアプリケーションを作成しました。


Oracle APEXのサンプル・データセットに含まれるプロジェクト・データを、本アプリケーションが使用するデータセットとして使用しています。


サンプル・データセットのプロジェクト・データはすべてリレーショナル・データベースの(複数の列を持つ行の集合)です。Oracle Backend for Firebase(Fusabase)では、Relational to collection mappingを定義して、これらの表をコレクション(JSONドキュメントの集合)としてアクセスできるようにします。

Codexにアプリケーションの要件として、以下の文書を与えています。


スキーマ定義として与えているfusabase-schema.mdは以下です。この文書は、Codexにfusabase-cliを使ってコレクションにアクセスさせて生成させています。


スキーマ定義の本文にinspect_fusabase_schema.shによりと記載されている、fusabase-cliを再帰的に呼び出し、コレクションやサブコレクション、およびそれらに含まれているドキュメントを取り出しているスクリプトです。こちらもCodexで生成しています。


Codexに要件を与えてiOSアプリケーションを生成するまでの準備を含め、以下の作業を行なっています。
  1. APEXのサンプル・データセットをOracle Backend for Firebaseのプロジェクトを構成したスキーマにインストールした上で、APEXのサンプル・アプリケーションを作成します。
  2. SQL Developer Extension for VS Codeを使って、インストールされたサンプル・スキーマのER図を作成し親子関係を確認します。Oracle Backend for FirebaseのRelational to collection mappingでは、サブ・コレクションの親コレクションは1つだけです。表の参照制約では参照先となる表(親)はいくつあっても良いため、サブ・コレクションの親コレクションは定義されている参照制約だけでは決められません。
  3. Oracle Backend for Firebaseのコンソールより、ステップ2で決めたRelational to collection mappingを定義します。
  4. Relational to collection mappingが定義されると、コレクションに対応するJSON Duality Viewが生成されます。生成されたJSON Duality Viewはトリガーによるサーバー側の更新を考慮せずにメタデータETAGを生成するため、ドキュメントの更新時に同時実行制御のエラーが発生します(ORA-42698: Cannot updateJSON Relational Duality View, Concurrent modification detected)。そのため、トリガーでアップデートされる列をGENERATED USINGで囲んでETAGの計算対象外にするよう、JSON Duality Viewの定義を変更します。
  5. 以上の準備を行なった後に、iOSアプリケーションを作成するフォルダをプロジェクトのフォルダとして、Codexのプロジェクトを作成します。プロジェクトのフォルダにアプリケーションの要件を配置します。
  6. 定義されたRelational to collection mappingにfusabase-cliでアクセスし取り出したコレクションのデータを元にして、参照するスキーマ情報を記述したファイルをプロジェクトのフォルダ以下に生成します。
  7. CodexにiOSアプリケーションの要件とスキーマ情報を元にして、iOSアプリケーションを生成するよう指示します。
  8. 生成されたiOSアプリケーションをXcodeで開き、アプリケーションを実行します。思った動きと違っていたり、こうして欲しいという要求をCodexに伝えてアプリケーションを改良します。
以下より、それぞれの作業について紹介します。


サンプル・データセットのインストール



作業環境は以前の記事「Oracle APEXが構成済みのデータベースにOracle Backend for Firebaseを構成する」にそって作成済みで、Oracle Backend for Firebaseのプロジェクトが構成されたスキーマがAPEXのワークスペースに割り当てられていることを前提とします。

APEXワークスペースへのスキーマの割り当ては、管理サービスワークスペースの管理に含まれる、ワークスペースとスキーマの割当ての管理から実施します。


APEXワークスペースとしてAPEXDEV、Oracle Backend for Firebaseのプロジェクトが構成されているスキーマがTESTUSERである場合、ワークスペースがAPEXDEV、スキーマがTESTUSERの行が存在しています。


この状態でAPEXのワークスペースにサインインし、スキーマTESTUSERにサンプル・データセットをインストールします。

ユーティリティサンプル・データセットを開きます。


プロジェクト・データインストールをクリックします。


サンプル・データセットの管理のダイアログが開きます。

データセットのインストール先のスキーマとして、Oracle Backend for Firebaseのプロジェクトが構成されているスキーマを選択します。今回の例ではTESTUSERです。

Oracle Backend for FirebaseのRelational to collection mappingでは、対象がプロジェクトが構成されているスキーマに存在している表に限定されています。そのため、Oracle Backend for Firebaseでアクセスする表は、スキーマAPEXDEVに配置できません。

へ進みます。


データセットのインストールを実行します。


サンプル・データセットがスキーマTESTUSERにインストールされます。そのデータセットを扱うAPEXアプリケーションを作成します。

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


アプリケーション作成ウィザードが開きます。機能はすべて不要なので、すべてチェックをクリックして、チェックを外します。

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

作成するアプリケーションには、表EBA_PROJECT_TASK_TODOS、EBA_PROJECT_TASK_LINKS、EBA_PROJECT_COMMENTSの一覧と編集を行うページが含まれていません。これらは必要に応じて追加することとし、本記事の手順には含めません。


アプリケーションDemonstration - Projectsが作成されます。


アプリケーションを実行し、Tasksのファセット検索のページを表示します。インストールされたサンプル・データセットの内容を確認できます。



SQL Developer Extension for VS Codeによるスキーマの確認



SQL Developer Extension for VS CodeのER図描画機能を使って、インストールされたサンプル・データセットの構造を確認します。

Oracle Backend for Firebaseのプロジェクトが構成されたスキーマへの接続を作成します。その作成された接続(以下のスクリーンショットではtestuser)の上でコンテキスト・メニューを表示し、Open Diagramを実行します。


左のツリーより、今回の対象である表EBA_PROJECTSEBA_PROJECT_COMMENTSEBA_PROJECT_MILESTONESEBA_PROJECT_STATUSEBA_PROJECT_TASKSEBA_PROJECT_TASK_TODOSEBA_PROJECT_TASK_LINKSの7つの表を選択します。


選択した表をDiagramまでドラッグし、Shiftキーを押し、そしてShiftキーを押したままDiagram上でドロップします。

ドロップした表のER図が生成されます。


見やすくなるように、表の配置を調整します。


スキーマの構造を見ると、表EBA_PROJECT_TASK_TODOSおよびEBA_PROJECT_TASK_LINKSにはそれぞれ、EBA_PROJECTSとEBA_PROJECT_TASKSを対象とした外部キー(列PROJECT_IDとTASK_ID)が定義されています。TODOおよびLinkは、同一プロジェクトの異なるタスクに付け替えることを可能にしているのかもしれませんが、Relational to collection mappingではどちらかを親コレクションとして選択する必要があります。

今回は表EBA_PROJECT_TASKSに表EBA_PROJECTSへの外部キーが定義されていることより、表EBA_PROJECT_TASK_TODOS、EBA_PROJECT_TASK_LINKSの親はEBA_PROJECT_TASKSとします。

表EBA_PROJECTSは表EBA_PROJECT_STATUSを参照していますが、親コレクションとはいえないので、それぞれ独立したコレクションとして定義します。

以上より、コレクションとサブコレクションは以下のように設定します。
  • eba_projects
    • eba_project_milestones
    • eba_project_comments
    • eba_project_tasks
      • eba_project_task_todos
      • eba_project_task_links
  • eba_project_status


Relational to collection mappingの定義



Oracle Backend for Firebaseのコンソールを開きます。

DatabaseRelational to collection mappingのタブを選択し、Link existing tablesをクリックします。


Link to existing relational tableにて、リレーショナル表を元にしたコレクションを作成します。

コレクションとサブ・コレクションの関係を間違えないように、親となる表にあるサブ・コレクションの追加ボタンをクリックし、サブ・コレクションとなる表を追加します。
  • 最上位のコレクションであるeba_projectsとeba_project_statusはAdd tableをクリックして追加します。
  • eba_project_milestones、eba_project_comments、eba_project_tasksは、eba_projectsのサブ・コレクションの追加ボタンをクリックして追加します。
  • eba_project_task_todos、eba_project_task_linksは、eba_project_tasksのサブ・コレクションの追加ボタンをクリックして追記します。
7つの表をコレクションとして登録して、Saveをクリックします。


リレーショナル表がコレクションとして作成されます。Expand allをクリックして、作成されたコレクションの階層が想定通りになっているかを確認します。




JSON Duality Viewの更新



Relational to collection mappingにより作成されたJSON Duality Viewの定義を確認します。Relational to collection mappingにより作成されたコレクションは、プロジェクトが構成されているスキーマに作成された表BAAS_COLLECTION_METADATAに定義されています。

select table_name, path from baas_collection_metadata where table_type = 'rel';

SQL> select table_name, path from baas_collection_metadata where table_type = 'rel';


TABLE_NAME                     PATH                                                                    

______________________________ _______________________________________________________________________ 

EBA_PROJECT_COMMENTS$BAAS      /eba_projects/_docId/eba_project_comments                               

EBA_PROJECT_MILESTONES$BAAS    /eba_projects/_docId/eba_project_milestones                             

EBA_PROJECT_TASKS$BAAS         /eba_projects/_docId/eba_project_tasks                                  

EBA_PROJECT_TASK_LINKS$BAAS    /eba_projects/_docId/eba_project_tasks/_docId/eba_project_task_links    

EBA_PROJECT_TASK_TODOS$BAAS    /eba_projects/_docId/eba_project_tasks/_docId/eba_project_task_todos    

EBA_PROJECT_STATUS$BAAS        /eba_project_status                                                     

EBA_PROJECTS$BAAS              /eba_projects                                                           


7行が選択されました。 


SQL> 


TABLE_NAMEとなっていますが、PATHでアクセスされるコレクションの実体となるJSON Duality Viewの名前です。

このJSON Duality Viewを作成したDDLは、列CONDITIONに記載されています。

select condition from baas_collection_metadata where table_type = 'rel';

SQL> select condition from baas_collection_metadata where table_type = 'rel';


CONDITION                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    

____________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________ 

create or replace json relational duality view "EBA_PROJECT_COMMENTS$BAAS" as  select json {'project_id':"PROJECT_ID", 'comment_text':"COMMENT_TEXT", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid': GENERATED USING ( select '/' ||SYS_MAKE_OID_FROM_PK("EBA_PROJECTS"."ID") from "EBA_PROJECTS" where "EBA_PROJECTS"."ID" = "EBA_PROJECT_COMMENTS"."PROJECT_ID")} from "EBA_PROJECT_COMMENTS" with insert delete update                                                                                                                                                                                                                                                                                       

create or replace json relational duality view "EBA_PROJECT_MILESTONES$BAAS" as  select json {'project_id':"PROJECT_ID", 'name':"NAME", 'description':"DESCRIPTION", 'due_date':"DUE_DATE", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid': GENERATED USING ( select '/' ||SYS_MAKE_OID_FROM_PK("EBA_PROJECTS"."ID") from "EBA_PROJECTS" where "EBA_PROJECTS"."ID" = "EBA_PROJECT_MILESTONES"."PROJECT_ID")} from "EBA_PROJECT_MILESTONES" with insert delete update                                                                                                                                                                                                                                             

create or replace json relational duality view "EBA_PROJECT_TASKS$BAAS" as  select json {'project_id':"PROJECT_ID", 'milestone_id':"MILESTONE_ID", 'name':"NAME", 'description':"DESCRIPTION", 'assignee':"ASSIGNEE", 'start_date':"START_DATE", 'end_date':"END_DATE", 'cost':"COST", 'is_complete_yn':"IS_COMPLETE_YN", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid': GENERATED USING ( select '/' ||SYS_MAKE_OID_FROM_PK("EBA_PROJECTS"."ID") from "EBA_PROJECTS" where "EBA_PROJECTS"."ID" = "EBA_PROJECT_TASKS"."PROJECT_ID")} from "EBA_PROJECT_TASKS" with insert delete update                                                                                                                         

create or replace json relational duality view "EBA_PROJECT_TASK_LINKS$BAAS" as  select json {'project_id':"PROJECT_ID", 'task_id':"TASK_ID", 'link_type':"LINK_TYPE", 'url':"URL", 'application_id':"APPLICATION_ID", 'application_page':"APPLICATION_PAGE", 'description':"DESCRIPTION", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid': GENERATED USING ( select '/' ||SYS_MAKE_OID_FROM_PK("EBA_PROJECTS"."ID") || '/' || SYS_MAKE_OID_FROM_PK("EBA_PROJECT_TASKS"."ID") from "EBA_PROJECTS","EBA_PROJECT_TASKS" where "EBA_PROJECTS"."ID" = "EBA_PROJECT_TASKS"."PROJECT_ID" and "EBA_PROJECT_TASKS"."ID" = "EBA_PROJECT_TASK_LINKS"."TASK_ID")} from "EBA_PROJECT_TASK_LINKS" with insert delete update    

create or replace json relational duality view "EBA_PROJECT_TASK_TODOS$BAAS" as  select json {'project_id':"PROJECT_ID", 'task_id':"TASK_ID", 'name':"NAME", 'description':"DESCRIPTION", 'assignee':"ASSIGNEE", 'is_complete_yn':"IS_COMPLETE_YN", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid': GENERATED USING ( select '/' ||SYS_MAKE_OID_FROM_PK("EBA_PROJECTS"."ID") || '/' || SYS_MAKE_OID_FROM_PK("EBA_PROJECT_TASKS"."ID") from "EBA_PROJECTS","EBA_PROJECT_TASKS" where "EBA_PROJECTS"."ID" = "EBA_PROJECT_TASKS"."PROJECT_ID" and "EBA_PROJECT_TASKS"."ID" = "EBA_PROJECT_TASK_TODOS"."TASK_ID")} from "EBA_PROJECT_TASK_TODOS" with insert delete update                                           

create or replace json relational duality view "EBA_PROJECT_STATUS$BAAS" as  select json {'code':"CODE", 'description':"DESCRIPTION", 'display_order':"DISPLAY_ORDER", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid':'_docId'} from "EBA_PROJECT_STATUS" with insert delete update                                                                                                                                                                                                                                                                                                                                                                                                                              

create or replace json relational duality view "EBA_PROJECTS$BAAS" as  select json {'status_id':"STATUS_ID", 'name':"NAME", 'description':"DESCRIPTION", 'project_lead':"PROJECT_LEAD", 'budget':"BUDGET", 'completed_date':"COMPLETED_DATE", 'created':"CREATED", 'created_by':"CREATED_BY", 'updated':"UPDATED", 'updated_by':"UPDATED_BY", 'OID' : SYS_MAKE_OID_FROM_PK("ID"),'_id':"ID",'parent_oid':'_docId'} from "EBA_PROJECTS" with insert delete update                                                                                                                                                                                                                                                                                                                                                             


7行が選択されました。 


SQL> 


このDDLに含まれるトリガーで更新される監査列(CREATED、CREATED_BY、UPDATED、UPDATED_BY)と、念の為parent_oid: '_docId'となっている列定義をGENERATED USINGで囲みETAGの計算対象外とします。トークンがもったいないとも思いましたが、これも上記の検索結果をファイルにスプールして、Codexに指示して監査列と_docIdをGENERATED USINGで囲ませています。

修正したDDLは以下になります。

このDDLをOracle Backend for Firebaseのプロジェクトを構成したスキーマで実行します。

SQL> @json-duality-views.sql


View "EBA_PROJECT_COMMENTS$BAAS"は作成されました。



View "EBA_PROJECT_MILESTONES$BAAS"は作成されました。



View "EBA_PROJECT_TASKS$BAAS"は作成されました。



View "EBA_PROJECT_STATUS$BAAS"は作成されました。



View "EBA_PROJECTS$BAAS"は作成されました。



View "EBA_PROJECT_TASK_LINKS$BAAS"は作成されました。



View "EBA_PROJECT_TASK_TODOS$BAAS"は作成されました。


SQL> 




スキーマ情報の生成



以前の記事「Oracle Backend for FirebaseのCLIインターフェースfusabase-cliを使用する」にそって、fusabase-cliを実行できる環境を作成します。

前掲のinspect_fusabase_schema.shを実行します。

このスクリプトは以下のプロンプトを与えてCodexに作成させたものです。プロンプトに例となるgetコマンドやqueryコマンドを与えていますが、すでにコレクションの内容を取り出すスクリプトが作成できているため、これらの内容については説明を割愛します。

スクリプトinspect_fusabase_schema.shはbashで実行します。zshでは動作しないようです。デフォルトでは各コレクションから3件ずつドキュメントを取得します。もっとサンプルを増やしたい場合はSCHEMA_SAMPLE_LIMITの指定を上げます。

SCHEMA_SAMPLE_LIMIT=100 bash inspect_fusabase_schema.sh

生成されたドキュメントを元にCodexにスキーマ情報を生成させるため、スクリプトもCodexに指示して実行させた方が良いかもしれません。


出力されたドキュメントから、Codexにスキーマ情報を生成させます。


以上でiOSアプリケーションが扱うスキーマ情報の準備もできました。


CodexによるiOSアプリケーションの生成



CodexがiOSアプリケーションをビルドできるように、あらかじめホストであるMacにXcodeとCommand Line Toolをインストールしておきます。Oracle Backend for FirebaseのiOS向けのLiveLabsを実施していると必要な準備はできているので、最初にLiveLabsに取り組んでおくことをお勧めします。

Oracle Backend for Firebaseのコンソールを開き、iOSアプリケーションを登録します。


App NicknameProjectsとします。Register Appを実行します。


SDKのインストール方法の説明を表示するだけなので、どれを選択しても登録されるアプリケーションに違いはありませんが、今回の作業で使用するSDK installationの方法であるGithubを選択します。

Nextをクリックします。


画面に表示されたSDK構成をfusabasa-config.jsonの値として、iOSアプリケーションの要件に含めます。

Doneをクリックして、アプリケーションの登録を終了します。


作成するiOSアプリケーションの要件にはユーザーを登録する機能は含んでいません。Oracle Backend for Firebaseのコンソールより、あらかじめ、テスト用のアカウントを作成しておきます。


今回はとりあえずiOSアプリケーションの作成だけが目的なので、DatabaseSecurity rulesでは制限をかけません。

match /{document=**} { allow read, write: if true;}


StorageSecurity rulesについても同様です。

match /{document=**} { allow read, write: if true;}


Codexでプロジェクトを作成します。ローカルを選択して次へ進みます。


プロジェクト名を設定し、iOSアプリケーションを作成するフォルダをプロジェクトに紐づけます。

Codexのプロジェクトを作成します。


作成したフォルダにアプリケーションの要件を記述したファイル(今回はspec.md)とスキーマ情報を取り出すスクリプトinspect_fusabase_schema.shを配置します。

最初にスキーマ情報を生成させました。スキーマ情報を出力するにあたって、アプリケーションの要件を与えておくと、生成される情報の精度が上がるようです。


スキーマ情報が出力されたら、iOSアプリケーションを作成するように指示しました。


推論過程を見てみると、私の作業環境にダウンロードされていたiOSアプリケーションのLiveLabsとiOSのFusabase SDKのコードを参照していました。なので、これらをダウンロードしてプロジェクトで参照できるようにしていると、より精度が高いiOSアプリケーションのアプリケーションが生成されると思われます。

git clone https://github.com/KillianLynch/fusabase-livelabs-ios.git
git clone https://github.com/oracle/fusabase-ios-sdk.git



iOSアプリケーションのデバッグと更新



Codexが生成したiOSアプリケーションを、Xcodeで開いて実行します。


iOSシミュレータが起動し、作成したアプリケーションが実行されます。


こうじゃないな、と思うところを指示してアプリケーションを更新します。


この作業を繰り返し、iOSアプリケーションの完成度を上げていきます。

本記事では、Oracle Databaseを操作するiOSアプリをOracle Backend for FirebaseとOpenAI Codexで作成する手順を紹介しました。

本記事は以上になります。

 追記

ほとんど同じ手順でspec.mdにiOSの代わりにAndroidを指定して、CodexにAndroidアプリを生成させてみました。spec.mdの変更部分は以下です。
# Projects Androidアプリケーション要件

## 概要

Oracle Backend for Firebase(Fusabase)をバックエンドとして使用する、プロジェクト管理用の Android アプリケーション。

アプリケーション名: `Projects`  
対象プラットフォーム: Android / Java
認証方式: Basic 認証

AndroidアプリケーションはAndroid Studioで扱える形式で作成してください。

## Fusabase 接続設定

app/fusabase-config.jsonとして以下を記述します。

```json
{
    "schema": "testuser",
    "app_name": "com.oracle.fusabase.projects",
    "app_type": "ANDROID",
    "app_id": "5A79FA609C221B92E063020012ACF003",
    "objs_type": "dbfs",
    "project_id": "59C1663EAB6F13DFE063020012AC5621",
    "storage_bucket": "dbfs_BQYKZXSFPIUUWSH",
    "auth_type": "base",
    "auth_id": "59C1663EAB7313DFE063020012AC5621",
    "ords_host": "http://10.0.2.2:8181/ords/testuser/"
}
```

Fusabase iOS SDK は以下のリポジトリから取得する。

https://github.com/oracle/fusabase-android-sdk.git
出来上がったアプリケーションは以下です。アプリの見かけについて、きちんと指示を出せばもう少し見栄えの良いアプリケーションになると思います。


Webアプリケーションはspec.mdの変更を以下に変更して、Codexでアプリケーションを生成させてみました。
# Projects Webアプリケーション要件

## 概要

Oracle Backend for Firebase(Fusabase)をバックエンドとして使用する、プロジェクト管理用の Web アプリケーション。

アプリケーション名: `Projects`  
対象プラットフォーム: Web / JavaScript
認証方式: Basic 認証

Webアプリケーションとして扱える形式で作成してください。

## Fusabase 接続設定

fusabase-config.jsとして以下を記述します。

```json
{
    "schema": "testuser",
    "app_name": "ProjectWeb",
    "app_type": "WEB",
    "app_id": "5A7B034D62F4B85DE063020012ACC847",
    "objs_type": "dbfs",
    "project_id": "59C1663EAB6F13DFE063020012AC5621",
    "storage_bucket": "dbfs_BQYKZXSFPIUUWSH",
    "auth_type": "base",
    "auth_id": "59C1663EAB7313DFE063020012AC5621",
    "ords_host": "http://localhost:8181/ords/testuser/"

}
```

Fusabase Web SDK は以下のリポジトリから取得する。

https://github.com/oracle/fusabase-js-sdk.git
出来上がったアプリケーションは以下です。こちらもアプリの見かけについて、きちんと指示を出せばもう少し見栄えの良いアプリケーションになると思います。


どちらにしても、Oracle Backend for Firebase側でコレクションを準備してくれていれば、アプリケーションを作成する側は、バックエンドがOracle Databaseかどうかを意識することはありません。UIのデザインについては、巷で評判の高い方法を採用することができます。

2026年8月28日金曜日

APEXアプリのレポートとフォームの操作を行うWebMCPツールを作成する

先日の記事「APEXアプリケーションのフォームをWebMCPに対応させる」にて、宣言型APIを使って従業員を登録するWebMCPツールを作成しました。その際に、APEXアプリケーション向けのWebMCPツールを作成するには、宣言型APIでは制限が多いことが分かりました。

本記事ではWebMCPの命令型APIを使って、従業員の検索と従業員の作成、変更、削除を行う4つのWebMCPツールを作成します。

以下のGIF動画で、作成した4つのWebMCPツールを実行しています。
  1. WebMCPツールsearch_employeesを呼び出し、ACCOUNTING部門の従業員を一覧します。
  2. WebMCPツールsearch_employeesを呼び出し、SALES部門の従業員を一覧します。
  3. 作成ボタンをクリックし、従業員の作成フォームを開きます。
  4. WebMCPツールregister_employeeを呼び出し、従業員を一人作成します。
  5. レポートの画面に戻ります。
  6. 作成した従業員を編集するフォームを開きます。
  7. WebMCPツールupdate_employeeを呼び出し、従業員の給与を8000に更新します。
  8. レポートの画面に戻ります。
  9. 作成した従業員を編集するフォームを開きます。
  10. WebMCPツールdelete_employeeを呼び出し、従業員を削除します。
  11. レポートの画面に戻ります。

WebMCPツールを実装するにあたって、以下の制約がありました。
  • WebMCPツールはdocumentに登録されるため、ツール内の処理でページを遷移するのは難しい(documentが変わるため)です。できないわけではないようですが、ツールの実行結果がnullになることがあるようです。その場合、ツール実行が成功したのか失敗したのか、WebMCPツールを呼び出したAIに伝わりません。そのため、従業員の編集フォームを開き、WebMCPツールによって従業員の作成、変更、削除を行った後にレポートのページに戻っていません。WebMCPツールの呼び出し元であるAIが、ページのナビゲーションを行ってくれることを期待しています。
  • 一般的に、APEXでは編集フォームはモーダル・ダイアログやドロワーとして実装します。モーダル・ダイアログやドロワーは親ページであるレポートのページとdocumentが同じになります。WebMCPツールの登録をフォームのページのページ・ロード時に実行すると、親ページのドキュメントにフォームを開けるたびに同じWebMCPツールが登録されます。回避策はあるようですが手間なので、単純にフォームのページを標準ページとして作成しています。レポートとフォームは別のページとなり、documentもそれぞれのページが持ちます。
WebMCPツールを登録するレポートとフォームのページは、先日の記事で作成したAPEXアプリケーションWebMCP Testに追加します。

ページの作成を開始します。


クラシック・レポートを選択します。

WebMCPツールでの検索に、対話モード・レポートや対話グリッドが持つ機能を実装するのはあまり現実的ではありません。ここでは、WebMCPツールの検索結果と画面上のレポートの表示を一致させることができるクラシック・レポートを選択します。


WebMCPツールのコードにページ番号が含まれています。レポートページ番号5フォームページ番号6としておくと、後でコードを変更しなくて済みます。

レポートページ名Employees - WebMCPとして、フォーム・ページを含めるオンにします。フォームページ名Employee - WebMCPとします。レポートもフォームもページ・モード標準を選択します。

データ・ソース表/ビューの名前EMPを指定します。

へ進みます。


主キー列1EMPNO(Number)です。ページの作成を実行します。


以上でレポートとフォームのページが作成されます。

作成されたレポートのページは以下です。


フォームのページは以下です。


最初にレポートのページにWebMCPツールsearch_employeesを作成します。

従業員名または部門名による完全一致を検索の条件とします。検索条件となる従業員名および部門名を入力するページ・アイテムと、検索を実行するボタンを作成します。従業員名のページ・アイテムはP5_ENAME、部門名のページ・アイテムはP5_DNAME、検索ボタンはSEARCHとします。

ページ・アイテムとボタンを配置するリージョンをWebMCP Search Toolとして作成します。タイプ静的コンテンツです。

作成したリージョンにページ・アイテムP5_ENAMEP5_DNAMEを作成します。タイプテキスト・フィールドです。両方のページ・アイテムで、詳細保存されていない変更の警告無視セッション・ステートストレージリクエストごと(メモリーのみ)に変更します。AIエージェントがページ・アイテムへ検索条件の設定した後、ページ遷移しようとすると警告されます。検索条件は無条件で破棄して良いので、警告が発生しないように無視します。

セッション・ステートストレージは、ページ・アイテムの値を永続化する必要がない場合は、無駄にリソースを使わないようにできるだけリクエストごと(メモリーのみ)にします。


ボタンSEARCHを作成し、トリガー・アクションとしてクラシック・レポートのリフレッシュを作成します。

識別アクションリフレッシュを選択します。影響を受ける要素選択タイプとしてリージョンを選び、リージョンとしてクラシック・レポートのリージョンEmployees - WebMCPを選びます。


クラシック・レポートに検索条件が反映されるようにします。

ソースタイプSQL問合せに変更し、SQL問合せとして検索条件を追加した以下のSELECT文を記述します。
select
    e.empno,
    e.ename,
    e.job,
    e.mgr,
    e.hiredate,
    e.sal,
    e.comm,
    e.deptno
from emp e
where (:P5_ENAME is null or e.ename = :P5_ENAME)
  and (
    :P5_DNAME is null
    or exists (
        select 1
        from dept d
        where d.deptno = e.deptno
            and d.dname = :P5_DNAME
    )
)
送信するページ・アイテムP5_ENAMEおよびP5_DNAMEを設定します。


以上で、従業員または部門名を指定して従業員を検索するページが作成できました。

ページを実行すると、手作業による検索動作を確認できます。


WebMCPツールsearch_employeesを作成します。

最初に検索結果をJSONで返すAjaxコールバックを作成します。名前はSEARCH_EMPLOYEESとします。ソースPL/SQLコードに以下を記述します。

クラシック・レポートと同じ検索結果(列FORM_URLは追加)をJSON配列で返します。
declare
  l_result json_object_t := json_object_t();
  l_employees clob;
begin
    select json_arrayagg(
        json_object(
            'EMPNO'    value e.empno,
            'ENAME'    value e.ename,
            'JOB'      value e.job,
            'MGR'      value e.mgr,
            'HIREDATE' value to_char(e.hiredate,'YYYY/MM/DD'),
            'SAL'      value e.sal,
            'COMM'     value e.comm,
            'DEPTNO'   value e.deptno,
            'FORM_URL' value apex_page.get_url(
                p_page      => 6,
                p_clear_cache => '6',
                p_items     => 'P6_EMPNO',
                p_values    => e.empno,
                p_plain_url => true
            )
            returning clob
        )
        returning clob
    )
into l_employees
from emp e
where (:P5_ENAME is null or e.ename = :P5_ENAME)
  and (
    :P5_DNAME is null
    or exists (
        select 1
        from dept d
        where d.deptno = e.deptno
            and d.dname = :P5_DNAME
    )
);

l_result := json_object_t();
l_result.put('success', true);
if l_employees is null then
    l_result.put('search_result', json_array_t());
else
    l_result.put('search_result', json_array_t(l_employees));
end if;

htp.p(l_result.to_clob());

exception
    when others then
        l_result := json_object_t();
        l_result.put('success', false);
        l_result.put('message', sqlerrm);

        htp.p(l_result.to_clob());
end;

このAjaxコールバックを呼び出すWebMCPツールsearch_employeesを、ページに登録します。ページ・ロード時に実行される動的アクションで、以下のJavaScriptを実行します。
(async () => {
  await document.modelContext.registerTool({
    name: 'search_employees',
    description: 'Lists employee information according to the specified conditions.',
    inputSchema: {
      type: 'object',
      properties: {
        ename: {
            type: 'string',
            description: 'Employee name to search for'
        },
        dname: {
            type: 'string',
            description: 'Department to which the employee belongs' 
        }
      }
    },
    execute: async ({ ename,dname }) => {
      try {
        apex.item('P5_ENAME').setValue(ename ?? '');
        apex.item('P5_DNAME').setValue(dname ?? '');
        apex.region('employees-report').refresh();

        return await apex.server.process(
          'SEARCH_EMPLOYEES',
          {
            pageItems: [
                'P5_ENAME',
                'P5_DNAME'
            ]
          }
        );
      } catch (error) {
        return {
          success: false,
          errorCode: 'REQUEST_ERROR',
          message: error.message || 'Employee search request failed.'
        };
      }
    }
  });
})();

WebMCPツールのJavaScriptコードの中より、クラシック・レポートのリフレッシュを実行しています。そのリフレッシュ先をemployees-reportとしているため、クラシック・レポートの詳細HTML DOM IDemployees-reportを設定します。


以上でWebMCPツールsearch_employeesが作成できました。Chrome拡張機能のWebMCP Toolを開き、search_employeesを実行できます。


次にフォームのページにWebMCPツールとして、register_employeeupdate_employeedelete_employeeを作成します。

AjaxコールバックとしてREGISTER_EMPLOYEEを作成します。ソースPL/SQLコードは以下です。
declare
  l_empno   emp.empno%type;
  l_result  json_object_t;
begin
  insert into emp (
    ename,
    job,
    mgr,
    hiredate,
    sal,
    comm,
    deptno
  )
  values (
    upper(:P6_ENAME),
    :P6_JOB,
    :P6_MGR,
    :P6_HIREDATE,
    :P6_SAL,
    :P6_COMM,
    :P6_DEPTNO
  )
  returning empno into l_empno;

  :P6_EMPNO := l_empno;

  l_result := json_object_t();
  l_result.put('success', true);
  l_result.put('empno', l_empno);
  l_result.put('message', 'Employee registered successfully.');

  htp.p(l_result.to_clob());

exception
  when others then
    l_result := json_object_t();
    l_result.put('success', false);
    l_result.put('message', sqlerrm);

    htp.p(l_result.to_clob());
end;

AjaxコールバックとしてUPDATE_EMPLOYEEを作成します。ソースPL/SQLコードは以下です。

APEXの標準プロセスによるアップデート処理では、チェックサムによる同時実行制御やロスト・ライトの対応が行われています。今回はそこまでの実装はせず、単純に受け取った値でUPDATE文を実行しています。
declare
    l_result  json_object_t;
begin
    update emp set
        ename = :P6_ENAME,
        job   = :P6_JOB,
        mgr   = :P6_MGR,
        hiredate  = :P6_HIREDATE,
        sal   = :P6_SAL,
        comm  = :P6_COMM,
        deptno = :P6_DEPTNO
    where empno = :P6_EMPNO;

    l_result := json_object_t();
    l_result.put('success', true);
    l_result.put('empno', :P6_EMPNO);
    l_result.put('message', 'Employee updated successfully.');

    htp.p(l_result.to_clob());

exception
    when others then
        l_result := json_object_t();
        l_result.put('success', false);
        l_result.put('message', sqlerrm);

        htp.p(l_result.to_clob());
end;

AjaxコールバックとしてDELETE_EMPLOYEEを作成します。ソースPL/SQLコードは以下です。
declare
    l_result  json_object_t;
begin
    delete from emp where empno = :P6_EMPNO;

    l_result := json_object_t();
    l_result.put('success', true);
    l_result.put('empno', :P6_EMPNO);
    l_result.put('message', 'Employee deleted successfully.');

    htp.p(l_result.to_clob());

exception
    when others then
        l_result := json_object_t();
        l_result.put('success', false);
        l_result.put('message', sqlerrm);

        htp.p(l_result.to_clob());
end;

これらのAjaxコールバックを呼び出すWebMCPツールをページに登録します。

従業員を作成するWebMCPツールregister_employeeは、ページ・アイテムP6_EMPNOがnullのときに作成します。そのため、アクションのクライアント側の条件タイプアイテムはnullを選択し、アイテムとしてP6_EMPNOを指定します。

ページ・ロード時に実行される動的アクションで、以下のJavaScriptを実行します。
(async () => {
  await document.modelContext.registerTool({
    name: 'register_employee',
    description: 'Register new employee information.',
    inputSchema: {
      type: 'object',
      properties: {
        ename: { type: 'string', description: 'employee name' },
        job: { type: 'string', description: 'job' },
        mgr: { type: 'string', description: 'empno of the manager' },
        hiredate: { type: 'string', description: 'hire date' },
        sal: { type: 'string', description: 'salary' },
        comm: { type: 'string', description: 'commission' },
        deptno: { type: 'string', description: 'departement number' }
      },
      required: [ 'ename','job','hiredate','sal','deptno' ]
    },
    execute: async ({ ename,job,mgr,hiredate,sal,comm,deptno }) => {
      try {
        apex.item('P6_ENAME').setValue(ename);
        apex.item('P6_JOB').setValue(job);
        apex.item('P6_MGR').setValue(mgr ?? '');
        apex.item('P6_HIREDATE').setValue(hiredate);
        apex.item('P6_SAL').setValue(sal);
        apex.item('P6_COMM').setValue(comm ?? '');
        apex.item('P6_DEPTNO').setValue(deptno);

        return await apex.server.process(
          'REGISTER_EMPLOYEE',
          {
            pageItems: [
                'P6_ENAME',
                'P6_JOB',
                'P6_MGR',
                'P6_HIREDATE',
                'P6_SAL',
                'P6_COMM',
                'P6_DEPTNO'
            ]
          }
        );
      } catch (error) {
        return {
          success: false,
          errorCode: 'REQUEST_ERROR',
          message: error.message || 'Employee registration request failed.'
        };
      }
    }
  });
})();

従業員情報を更新するWebMCPツールupdate_employeeと、従業員を削除するWebMCPツールdelete_employeeは、ページ・アイテムP6_EMPNOがnullではないのときに作成します。そのため、アクションのクライアント側の条件タイプアイテムはnullではないを選択し、アイテムとしてP6_EMPNOを指定します。

ページ・ロード時に実行される動的アクションで、以下のJavaScriptを実行します。
(async () => {
    /*
     * This tool updates the information for the employee already selected, 
     * so P6_EMPNO should not be included as a parameter.
     */
  await document.modelContext.registerTool({
    name: 'update_employee',
    description: 'Update existing employee information.',
    inputSchema: {
      type: 'object',
      properties: {
        ename: { type: 'string', description: 'employee name' },
        job: { type: 'string', description: 'job' },
        mgr: { type: 'string', description: 'empno of the manager' },
        hiredate: { type: 'string', description: 'hire date' },
        sal: { type: 'string', description: 'salary' },
        comm: { type: 'string', description: 'commission' },
        deptno: { type: 'string', description: 'departement number' }
      }
    },
    execute: async ({ ename,job,mgr,hiredate,sal,comm,deptno }) => {
      try {
        ename    != null && apex.item('P6_ENAME').setValue(ename);
        job      != null && apex.item('P6_JOB').setValue(job);
        mgr      != null && apex.item('P6_MGR').setValue(mgr);
        hiredate != null && apex.item('P6_HIREDATE').setValue(hiredate);
        sal      != null && apex.item('P6_SAL').setValue(sal);
        comm     != null && apex.item('P6_COMM').setValue(comm);
        deptno   != null && apex.item('P6_DEPTNO').setValue(deptno);

        return await apex.server.process(
          'UPDATE_EMPLOYEE',
          {
            pageItems: [
                'P6_EMPNO',
                'P6_ENAME',
                'P6_JOB',
                'P6_MGR',
                'P6_HIREDATE',
                'P6_SAL',
                'P6_COMM',
                'P6_DEPTNO'
            ]
          }
        );
      } catch (error) {
        return {
          success: false,
          errorCode: 'REQUEST_ERROR',
          message: error.message || 'Employee update request failed.'
        };
      }
    }
  });
  /*
   * This tool removes the employee currently selected in P6_EMPNO,
   * so no parameters need to be specified.
  */
  await document.modelContext.registerTool({
    name: 'delete_employee',
    description: 'delete existing employee information.',
    execute: async ({}) => {
      try {
        return await apex.server.process(
          'DELETE_EMPLOYEE',
          {
            pageItems: [
                'P6_EMPNO'
            ]
          }
        );
      } catch (error) {
        return {
          success: false,
          errorCode: 'REQUEST_ERROR',
          message: error.message || 'Employee delete request failed.'
        };
      }
    }
  });
})();

以上で、予定していた4つのWebMCPツールの作成は完了です。この記事の先頭のGIF動画で行っている操作を、WebMCPツールを使って実施できます。

今回作成したAPEXアプリケーションの、APEXlang形式のエクスポートを以下に置きました。
https://github.com/ujnak/APEXlang-exports/tree/main/webmcp-test

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