ラベル Firebase の投稿を表示しています。 すべての投稿を表示
ラベル Firebase の投稿を表示しています。 すべての投稿を表示

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_PROJECTS、EBA_PROJECT_COMMENTS、EBA_PROJECT_MILESTONES、EBA_PROJECT_STATUS、EBA_PROJECT_TASKS、EBA_PROJECT_TASK_TODOS、EBA_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のコンソールを開きます。

DatabaseのRelational 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 NicknameはProjectsとします。Register Appを実行します。


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

Nextをクリックします。


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

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


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


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

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


StorageのSecurity 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月26日水曜日

Oracle Backend for Firebaseのスナップショット・リスナーを使用する

Oracle Backend for Firebase(Fusabase)ではCloud Firestoreと同様にスナップショット・リスナーを実装できます。Oracle Backend for FirebaseのLiveLabsとして、JavaScript、iOS、AndroidのSDKを使用したワークショップが提供されていますが、この中のAndroid SDKを使ったワークショップは、スナップショット・リスナーを実装しています。

そのため、Androidのワークショップを行うとスナップショット・リスナーを実装したクライアントが作成されます。本記事では、Androidクライアントを使用してスナップショット・リスナーの動作を確認します。Android SDKのスナップショット・リスナーについては、Andorid SDKのドキュメントのSnapshot listenersで紹介されています。

LiveLabsのワークショップでは使用されていませんが、JavaScript SDK、iOS SDKでもスナップショット・リスナーを実装できます。これらもCloud Firestore APIと同様の形式で、JavaScript SDKのドキュメントではLive listeners、iOS SDKのドキュメントではSnapshot listenersとして紹介されています。

Oracle Backend for Firebase(Fusabase)のスナップショット・リスナーを使用するにあたって、以下の点に注意が必要でした。

使用しているSDKはWeb, iOS, Androidともに、バージョンv26.2.0です。
  • Oracle Backend for Firebase(Fusabase)はローカル・キャッシュを実装していません。DocumentSnapshotおよびQuerySnapshotのメタデータのisFromCacheまたはfromCacheはつねにfalseを返します。
  • スナップショット・リスナーが受け取る更新イベントを発生させる機構として、ロング・ポーリングとWebSocketの2種類があります。設定ファイルfusabase-config.jsonで、useSocket(またはuse_socket - SDKによって異なります)をtrueにするとWebSocketになります。falseのときはロング・ポーリングです。
  • ロング・ポーリング(long polling)は名称の通り、クライアント側から定期的にサーバーに問い合わせを行い、変更を見つけた時にスナップショット・リスナーに更新イベントを送ります。
  • ロング・ポーリングの場合、設定ファイルfusabase-config.jsonで、long_polling_intervalによりポーリング間隔を調整できます。単位は秒です。省略時は29秒、JavaScript SDKでは設定可能な値は5から300秒に制限されています。iOS SDKに制限はないようです。Android SDKでは10秒で固定されています。
  • WebSocket場合はサーバー側の表にOracle Continuous Query Notification(CQN)が構成され、CQNで検知された更新がWebSocket経由でクライアントに送られます。useSocket(use_socket)がtrueの場合、ORDSとクライアント間でWebSocketによって接続されます。
  • useSocketがtrueすなわち、Oracle CQNとWebSocketが通知に使用される場合、データ更新からクライアントへの通知までにかかる時間は1秒前後です。ロング・ポーリングの場合は、long_polling_intervalの設定に依存します。間隔を短くすると通知も速くなりますが、サーバー側への負荷は高くなります。
WebSocketによるサーバーからの変更分のプッシュ通知は魅力的ですが、現時点で色々な不備が見つかっています。そのため、設定としてはロング・ポーリングを選択することをお勧めします。ロング・ポーリングの場合は、新たに取得したコレクションとメモリに残っているスナップショットを比較して差分を生成しています。スナップショット・リスナーが有効である限り、繰り返しサーバーに問い合わせが発行されることに注意が必要です。サーバーへの負荷を考慮すると、スナップショット・リスナーの活用は慎重に行う方が良いでしょう。

LiveLabsのAndroid SDKでのワークショップで、スナップショット・リスナーを実装しているのは、Lab 4: Read Recipe DataのTask 2: Subscribe to the recipes collectionの以下のコードです。コレクションrecipesにスナップショット・リスナーを構成しています。
    /**
     * Subscribes to the {@code recipes} collection. When {@code category}
     * is non-null, applies a {@code whereEqualTo("category", ...)} filter.
     * Sorts by {@code createdAt} descending and limits to 24 results.
     *
     * <p>TODO Lab 4 — Task 1: Build the query and call
     * {@code addSnapshotListener(...)}. Save the returned
     * {@link ListenerRegistration} into {@link #recipesListener} so it can
     * be removed later in {@link #stopListeningToRecipes()}.
     */
    public void startListeningToRecipes(@Nullable String category) {
        // TODO Lab 4
        Query q = db.collection("recipes");
        if (category != null) {
            q = q.whereEqualTo("category", category);
        }
        q = q.orderBy("createdAt", Query.Direction.DESCENDING).limit(24);

        recipesListener = q.addSnapshotListener((snapshot, error) -> {
            if (error != null) {
                errorMessage.postValue(error.getMessage());
                return;
            }
            if (snapshot == null) return;
            List<Recipe> mapped = new ArrayList<>();
            for (QueryDocumentSnapshot doc : snapshot) {
                Recipe r = doc.toObject(Recipe.class);
                if (r != null) {
                    r.setId(doc.getId());
                    mapped.add(r);
                }
            }
            recipes.postValue(mapped);
        });
    }
Lab 4のTask 4: Subscribe to a single recipe and its ratingsでは、単一のレシピとそれに紐づくサブ・コレクションratingsにスナップショット・リスナーを構成しています。
    /**
     * Subscribes to a single recipe document. Drives the recipe detail
     * screen.
     *
     * <p>TODO Lab 4 — Task 3: Implement using
     * {@code db.collection("recipes").document(recipeId).addSnapshotListener(...)}.
     * Also start the ratings subcollection listener so the detail screen
     * stays in sync when ratings are added in Lab 5.
     */
    public void startListeningToRecipe(String recipeId) {
        // TODO Lab 4
        DocumentReference doc = db.collection("recipes").document(recipeId);

        recipeDetailListener = doc.addSnapshotListener((snapshot, error) -> {
            if (error != null) {
                errorMessage.postValue(error.getMessage());
                return;
            }
            if (snapshot == null || !snapshot.exists()) return;
            Recipe r = snapshot.toObject(Recipe.class);
            if (r != null) {
                r.setId(snapshot.getId());
                currentRecipe.postValue(r);
            }
        });

        ratingsListener = doc.collection("ratings").addSnapshotListener((snapshot, error) -> {
            if (error != null) {
                errorMessage.postValue(error.getMessage());
                return;
            }
            if (snapshot == null) return;
            List<Rating> mapped = new ArrayList<>();
            for (QueryDocumentSnapshot d : snapshot) {
                Rating r = d.toObject(Rating.class);
                if (r != null) {
                    r.setId(d.getId());
                    mapped.add(r);
                }
            }
            currentRatings.postValue(mapped);
        });
    }
以下より、RecipeShareのAndroidアプリケーションに実装されたスナップショット・リスナーの動作を確認します。


通常のコレクションでの確認



通常のコレクション(Relational to collection mapping - JSON Duality Viewを使わないコレクション)での更新通知を確認します。

app/fusabase-config.jsonにuseSocket: falseとenableLogging: trueの設定を加えます。
{
    "schema": "testuser",
    "app_name": "com.oracle.fusabase.recipeshare",
    "app_type": "ANDROID",
    "app_id": "59C27E98507C546FE063020012ACBFD3",
    "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/",
    "useSocket": false,
    "enableLogging": true
}
Build a Recipe App with Oracle Backend for FirebaseのAndroid SDKのワークショップのLab 5: Write Recipe Data, Task 3: Run and verifyを実施し、Garlic Butter Shrimpのレシピが登録された状態です。


コレクションを操作するクライアントとして、fusabase-cliを使用します。fusabase-cliの構成方法については記事「Oracle Backend for FirebaseのCLIインターフェースfusabase-cliを使用する」で紹介しています。

タイトルがGarlic Butter Shrimpのレシピを使用して、fusabase-cliで以下の操作を行います。Androidアプリはデータの変更を見つけて、自動で画面を更新します。
  1. Garlic Butter Shrimpを検索しドキュメントIDとJSON本文を取得する。
  2. Garlic Butter Shrimpのレシピを削除する。AndroidアプリからGarlic Butter Shrimpが削除されることを確認する。
  3. Garlic Butter ShrimpのJSON本文を使って、fusabase-cliよりレシピを登録する。AndroidアプリにGarlic Butter Shrimpが表示されることを確認する。
  4. AndroidアプリでGarlic Butter Shrimpを選択する。
  5. Garlic Butter ShrimpのJSON本文を変更し、fusabase-cliよりレシピを更新する。AndroidアプリのGarlic Butter Shrimpの表示が更新されることを確認する。
レシピGarlic Butter Shrimpを検索します。以下の内容のファイルをquery.jsonとして作成します。
{
  "path": [ "recipes" ],
  "conditions": [
    { "field": "title", "op": "=", "value": "Garlic Butter Shrimp" }
  ],
  "limit": 1,
  "explicitOrder": [
    { "field": "createAt", "direction": "asc" }
  ],
  "aggregate": []
}
fusabase-cliを使って検索します。

npx fusabase database query --path=query.json

Document IDとDOCUMENT本文が得られます。

fusabase % npx fusabase database query --path=query.json                        

⠋ Trying to fetch document..{ ret: [ { osons: [Object] } ] }

✔ List of OID within the collection:

  Document ID                        VERSION   DOCUMENT                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  

  59D961B5641AE3F8E063020012ACEA5D             {"ingredients":["1 lb large shrimp","4 tbsp butter","4 cloves garlic","1/4 cup parsley","1 lemon"],"title":"Garlic Butter Shrimp","averageRating":0,"instructions":"Melt butter in a skillet over medium-high heat. Add minced garlic and cook for 30 seconds. Add shrimp and cook 2-3 minutes per side until pink. Finish with chopped parsley and a squeeze of lemon.","ratingCount":0,"description":"Quick weeknight dinner with shrimp tossed in garlic butter and parsley.","category":"Dinner","ownerId":"","createdAt":1787635746671,"prepTime":20,"createdBy":""} 

fusabase % 


得られたDocument IDを指定して、レシピGarlic Butter Shrimpを削除します。

npx fusabase database delete recipes/[Document ID]

fusabase % npx fusabase database delete recipes/59D961B5641AE3F8E063020012ACEA5D


Path is : recipes/59D961B5641AE3F8E063020012ACEA5D

✔ Collection delete Successfully:

{"message":"document deleted successfully!"}

fusabase % 


Androidアプリのレシピの一覧よりGarlic Butter Shrimpが削除されます。


Garlic Butter Shrimpのレシピを登録するファイルrecipe.jsonを作成します。dataには、検索結果のDOCUMENTを与えます。
{
  "path": [
    "recipes"
  ],
  "data": {
    "ingredients": [
      "1 lb large shrimp",
      "4 tbsp butter",
      "4 cloves garlic",
      "1/4 cup parsley",
      "1 lemon"
    ],
    "title": "Garlic Butter Shrimp",
    "averageRating": 0,
    "instructions": "Melt butter in a skillet over medium-high heat. Add minced garlic and cook for 30 seconds. Add shrimp and cook 2-3 minutes per side until pink. Finish with chopped parsley and a squeeze of lemon.",
    "ratingCount": 0,
    "description": "Quick weeknight dinner with shrimp tossed in garlic butter and parsley.",
    "category": "Dinner",
    "ownerId": "",
    "createdAt": 1787635746671,
    "prepTime": 20,
    "createdBy": ""
  }
}
fusabase-cliを使ってレシピを追加します。

npx fusabase database add --path=recipe.json

fusabase % npx fusabase database add --path=recipe.json

{"path":["recipes"],"data":{"ingredients":["1 lb large shrimp","4 tbsp butter","4 cloves garlic","1/4 cup parsley","1 lemon"],"title":"Garlic Butter Shrimp","averageRating":0,"instructions":"Melt butter in a skillet over medium-high heat. Add minced garlic and cook for 30 seconds. Add shrimp and cook 2-3 minutes per side until pink. Finish with chopped parsley and a squeeze of lemon.","ratingCount":0,"description":"Quick weeknight dinner with shrimp tossed in garlic butter and parsley.","category":"Dinner","ownerId":"","createdAt":1787635746671,"prepTime":20,"createdBy":""}}

✔ Document Added Successfully!!

{"OID":"59D961B5641BE3F8E063020012ACEA5D","VERSION":1}

fusabase % 


Androidアプリのレシピの一覧に、再度Garlic Butter Shrimpが表示されます。


Garlic Butter Shrimpのレシピを開きます。


Garlic Butter Shrimpのレシピを更新するために、以下のファイルrecipe-upd.jsonを作成します。pathに更新対象のドキュメントIDを追加し、dataを更新します。今回はtitleにUPDATEを追加しています。
{
  "path": [
    "recipes",
    "59D961B5641BE3F8E063020012ACEA5D"
  ],
  "data": {
    "ingredients": [
      "1 lb large shrimp",
      "4 tbsp butter",
      "4 cloves garlic",
      "1/4 cup parsley",
      "1 lemon"
    ],
    "title": "Garlic Butter Shrimp - UPDATE",
    "averageRating": 0,
    "instructions": "Melt butter in a skillet over medium-high heat. Add minced garlic and cook for 30 seconds. Add shrimp and cook 2-3 minutes per side until pink. Finish with chopped parsley and a squeeze of lemon.",
    "ratingCount": 0,
    "description": "Quick weeknight dinner with shrimp tossed in garlic butter and parsley.",
    "category": "Dinner",
    "ownerId": "",
    "createdAt": 1787635746671,
    "prepTime": 20,
    "createdBy": ""
  }
}
fusabase-cliを使ってレシピを更新します。

npx fusabase database upd --path=recipe-upd.json

fusabase % npx fusabase database upd --path=recipe-upd.json 

{"path":["recipes","59D961B5641BE3F8E063020012ACEA5D"],"data":{"ingredients":["1 lb large shrimp","4 tbsp butter","4 cloves garlic","1/4 cup parsley","1 lemon"],"title":"Garlic Butter Shrimp - UPDATE","averageRating":0,"instructions":"Melt butter in a skillet over medium-high heat. Add minced garlic and cook for 30 seconds. Add shrimp and cook 2-3 minutes per side until pink. Finish with chopped parsley and a squeeze of lemon.","ratingCount":0,"description":"Quick weeknight dinner with shrimp tossed in garlic butter and parsley.","category":"Dinner","ownerId":"","createdAt":1787635746671,"prepTime":20,"createdBy":""}}

✔ Document Updated Successfully!!

{"OID":"59D961B5641BE3F8E063020012ACEA5D","VERSION":3}

fusabase %


表示されているレシピのタイトルが更新されます。


以上で、異なるアプリケーションによるデータの変更に追従して、Androidアプリケーションの画面の表示が更新されることが確認できました。


Relational to collection mappingによるコレクションでの確認



通常のコレクションの代わりにRelational to collection mappingで作成したコレクションを使用して、スナップショット・リスナーの動作を確認します。

表RECIPESおよびRATINGSの作成およびRelational to collection mappingによるコレクションの作成とAndroidアプリケーションの変更手順については、記事「Oracle Backend for FirebaseのRelational to collection mappingを使ってJSON Duality Viewをコレクションとして使用する」にて紹介しています。

今回はデータがリレーショナル表であるRECIPESとRATINGSに保存されるため、データの変更にfusabase-cliの代わりに、これらの表を操作するAPEXアプリケーションを作成します。APEXとOracle Backend for Firebaseを共存させる環境については、記事「Oracle APEXが構成済みのデータベースにOracle Backend for Firebaseを構成する」で紹介しています。

通常のコレクションのときと同様に、Build a Recipe App with Oracle Backend for FirebaseのAndroid SDKのワークショップのLab 5: Write Recipe Data, Task 3: Run and verifyを実施し、Garlic Butter Shrimpのレシピが登録された状態まで作業を進めます。


データベースにはAPEXワークスペースとしてAPEXDEV、ワークスペースに紐づくスキーマとしてAPEXDEVが作成済みとします。Oracle Backend for Firebaseのプロジェクトが構成されているスキーマ(これまでの手順通りであればTESTUSER)に表RECIPESと表RATINGSがあります。これらの表を操作する権限を、スキーマAPEXDEVに与えます。

SQLclでスキーマTESTUSERに接続し、以下のコマンドを実行します。

grant all on recipes to apexdev;
grant all on ratings to apexdev;

SQL> grant all on recipes to apexdev;


Grantが正常に実行されました。


SQL> grant all on ratings to apexdev;


Grantが正常に実行されました。


SQL> 


APEXのワークスペースにサインインし、空のアプリケーションを作成します。

アプリケーションの名前はRecipeShareとします。


アプリケーションRecipeShareが作成されます。ページの作成を開始します。


表RECIPESとRATINGSを編集するページを作成します。2つの表の編集画面を一度で作成できる、マスター・ディテールを選択します。


スタイルとしてドリルダウンを選択します。


マスターページ定義の名前はRecipesとします。マスターデータ・ソースの表/ビューの所有者にOracle Backend for Firebaseのプロジェクトを構成したスキーマ、今回の作業ではTESTUSERを選択します。表/ビューの名前にマスター表にあたる表RECIPESを選択します。

次へ進みます。


主キー列1にID (Number)、フォームのナビゲーションの順序にCREATED_AT(Date)を選択します。

次へ進みます。


ディテール・ページ定義の名前はRatingsとします。表/ビューの所有者は表RACIPESと同じくTESTUSER、表/ビューの名前はRATINGSを選択します。

次へ進みます。


ディテール主キーの主キー列1としてID(Number)を選択します。マスタ・ディテール外部キーはID -> RECIPE_IDを選択します。

以上で、ページの作成を実行します。


表RECIPESとRATINGSの編集を行う、マスター・ディテールのページが作成できました。


ページを実行し、レシピGarlic Butter Shrimpの編集画面を開きます。


レシピGarlic Butter Shrimpの編集画面が開きます。


AndroidアプリケーションでレシピGarlic Butter Shrimpを開いておきます。


APEXアプリケーションに戻りページを下スクロールし、レシピに評価を追加します。

Authorにtest@example.com、Ratingに5、Created Atに本日の日付、CommentにYum!を入力します。

以上で、変更の適用をクリックします。


表RATINGSに1行追加されました。


Androidアプリケーションに戻ると、APEXアプリから入力した評価が表示されていることが確認できます。


Oracle Backend for Firebaseのアプリケーションはコレクションrecipesとratingsを操作していますが、Relational to collection mappingにより、それらのコレクションはJSON Duality Viewを介してリレーショナル表RECIPESとRATINGSの操作になります。

APEXアプリケーションはそれらの実体である表RECIPESとRATINGSを操作します。両方のアプリケーションが同じ表を操作することにより、APEXアプリで操作したデータがネイティブのAndroidアプリに反映されます。

今回の記事は以上になります。

完