2026年8月26日水曜日

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

Oracle Backend for Firebase(Fusabase)ではCloud Firestoreと同様にスナップショット・リスナーを実装できます。Oracle Backend for FirebaseのLiveLabsとして、JavaScriptiOSAndroidの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 IDDOCUMENT本文が得られます。

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を選択します。

へ進みます。


主キー列1ID (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アプリケーションに戻りページを下スクロールし、レシピに評価を追加します。

Authortest@example.comRating5Created At本日の日付CommentYum!を入力します。

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


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


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


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

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

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