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アプリに反映されます。

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

2026年8月14日金曜日

Oracle Backend for FirebaseのStorageの構成をDBFSからOCI Object Storageに切り替える

Oracle Backend for Firebase(Fusabase)ではStorageに保存するファイルの実際の保存先として、DBFS(Database File System - つまりOracle Database)またはOCI Object Storageを選択できます。デフォルトではDBFSが構成されます。

一般にファイルとして保存するデータは、PowerPointなどのファイルや画像、音声などサイズが大きいデータです。Oracle Backend for FirebaseのStorageの保存先がDBFSで構成されていると、これらのサイズの大きいデータがデータベースに保存されます。

Fusabase SDKによるStorageへのデータのアップロードでは、クライアント(Web、iOSアプリ、Androidアプリ)からORDSへデータが送信され、次にORDSからデータベースにデータが送信されます。ダウンロードの場合は、データベースからORDSにデータが送信され、次にORDSからクライアントにデータが送信されます。通常、クライアントとORDSの間のネットワークの速度はそれほど速くないため、そこで律速されます。

問題は、ダウンロードが完了するまではORDSとデータベースの間の接続が占有されることです。処理時間はクライアントとORDS間のネットワーク速度に依存します。データベースに負荷はほとんどかかりませんが、ファイルのアップロードまたはダウンロードが完了するまではORDSとデータベース間のセッションが使用中となるため、利用できる接続を確保するにはORDSのコネクション・プールが保持する接続の上限を増やす必要があります。また、データベースからデータを取り出し、クライアントに送信する(またはその逆)間に、転送するデータをORDSが保持するため、ORDSが扱えるメモリの上限も上げる必要があります。Oracle Backend for FirebaseのStorageをDBFSで構成するのはお手軽ですがスケールしないため、利用者が少ないなど負荷がかからない状況でなければ実運用は難しいのではないかと思います。

Oracle Backend for FirebaseではStorageの保存先としてOCI Object Storageを構成することができます。この場合、クライアントからのファイルのアップロードは、クライアント、ORDS、データベース、Object Storageという経路で、最終的にObject Storageにファイルが配置されます。ダウンロードについては、Object Storageから直接クライアントにダウンロードされ、ORDSおよびデータベースは経由しません。ファイルのアップロードよりもダウンロードの方が頻度は高く、その経路からORDSやデータベースが外れるため、ORDSが必要とするリソースも大幅に少なくなります。

本記事ではOracle Backend for FirebaseのStorageの構成を、OCI Object Storageに切り替える手順を紹介します。

検証した範囲では、StorageとしてOCI Object Storageを構成すると、Relational to collection mapping(実体はJSON Duality View)によって作成したコレクションにStorageを紐づけられませんでした。Storageを紐づけるコレクションは、通常のコレクションに限定されます。

構成方法は、以下の公式ドキュメントに記載されています。

Oracle Backend for Firebase 26.1
Developer's Guide
Part IV Storage
25 Storage Setup and Configuration

動作確認に使用するアプリケーションとして、以下のLiveLabsで作成したWebアプリケーションを使用します。

Build a Recipe Web App with Oracle Backend for Firebase
https://livelabs.oracle.com/ords/r/dbpm/livelabs/view-workshop?wid=4404

OCI Object Storageの構成を行う前に、デフォルトで構成されるDBFSについて確認します。


DBFSの構成



Oracle Backend for Firebaseのコンソールに接続し、StorageFile systemを開きます。


Storageの保存先(File system)のTypeDBFSRegiondbfsBucket namedbfs_<任意の文字列>として構成されています。

SQLclでプロジェクトのスキーマに接続し、以下のSELECT文を実行しDBFSに保存されているファイルを確認します。

select store, pathname from dbfs_content;

SQL> select store, pathname from dbfs_content;


STORE                   PATHNAME                                 

_______________________ ________________________________________ 

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ                    

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs/tools         

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs/snapshots     

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs/content       

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs/attributes    

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs/RECYCLE       

dbfs_FDNSTBCAVUSCPHZ    /dbfs_FDNSTBCAVUSCPHZ/.sfs               


7行が選択されました。 


SQL> 


ビューDBFS_CONETNTのSTORE列の値が、Bucket nameとなっています。Storageにアップロードされたファイルは、このStoreに保存されます。

ファイルが参照される場合は、このStoreからファイルが取り出されます。

すでに作成済みのDBFSのFile systemも、ビューDBFS_CONTENTのSTORE列から確認できます。


OCI Object Storageの準備



OCIのオブジェクト・ストレージにバケットを作成します。Oracle Backend for Firebase向けの特別なバケットというものはありませんが、ポリシーとしてバケットへのファイルの読み書きと事前承認済リクエストの作成を許可する必要があります。

バケットの準備として、以下の作業を行います。
  1. コンパートメントFusabaseの作成
  2. バケットfusabase-storageの作成
  3. APIユーザーfusabase-storage-managerの作成
  4. グループFusabaseStorageManagersの作成
  5. ポリシーFusabaseStoragePolicyの作成
  6. APIユーザーfusabase-storage-userのAPIキーの作成

コンパートメントFusabaseの作成



Oracle Cloudのコンソールに接続し、アイデンティティとセキュリティコンパートメントを開きます。

コンパートメントの作成を実行します。


作成するコンパートメントの名前Fusabaseとします。親コンパートメントルートを選択し、コンパートメントを作成します。


コンパートメントFusabaseが作成されました。



バケットfusabase-storageの作成



作成したコンパートメントFusabaseに、オブジェクト・ストレージのバケットとしてfusabase-storageを作成します。

ストレージバケットを開き、コンパートメントFusabaseを選択します。

バケットの作成を実行します。


作成するバケット名fusabase-storageとします。それ以外はデフォルトのまま、バケットの作成を実行します。


バケットfusabase-storageが作成されました。



APIユーザーfusabase-storage-managerの作成



Object Storageの操作に使用するユーザーとしてfusabase-storage-managerを作成します。ユーザーはルート・コンパートメントに作成済みのDefaultドメインに作成します。

アイデンティティとセキュリティドメインを開き、ルート・コンパートメントを選択します。

作成済みのドメインDefaultを開きます。


ユーザー管理タブを開き、ユーザーの作成を実行します。


ユーザー名として電子メール・アドレスを使用チェックを外します。

Fusabaseユーザー名fusabase-storage-managerとします。電子メールを設定し、ユーザーを作成します。


ユーザーfusabase-storage-managerが作成されました。APIキーの生成は最後に実施します。



グループFusabaseStorageManagersの作成




ポリシーのサブジェクトとして使うグループとしてFusabaseStorageManagersを作成します。

Defaultドメインのユーザー管理より、グループの作成を実行します。


作成するグループの名前FusabaseStorageManagersとします。ユーザーとして先ほど作成したfusabase-storage-managerチェックし、グループに割り当てます。

以上で作成します。


グループFusabaseStorageManagersが作成されました。



ポリシーFusabaseStoragePolicyの作成




グループFusabaseStorageManagersに所属しているユーザーによる、コンパートメントFusabaseにあるバケットfusabase-storageへのオブジェクトの読み書き、および事前認証済リクエストの生成を許可するポリシーを設定します。

アイデンティティとセキュリティポリシーを開き、ルート・コンパートメントを選択します。

ポリシーの作成を実行します。


作成するポリシーの名前FusabaseStoragePolicyとします。コンパートメントルートを選択し、手動エディタに切り替えます。

ポリシーとして以下の3行を記述します。
Allow group FusabaseStorageManagers to read buckets in compartment Fusabase
Allow group FusabaseStorageManagers to manage objects in compartment Fusabase where target.bucket.name = 'fusabase-storage'
Allow group FusabaseStorageManagers to manage buckets in compartment Fusabase where all { request.permission = 'PAR_MANAGE', target.bucket.name = 'fusabase-storage' }
以上で作成します。


ポリシーFusabaseStoragePolicyが作成されました。



APIユーザーfusabase-storage-userのAPIキーの作成




作成済みのユーザーfusabase-storage-managerAPIキーを開きます。

APIキーの作成を実行します。


APIキー・ペアの生成を選択し、秘密キーのダウンロードを実行します。


秘密キーがファイルとしてダウンロードされると、APIキーの追加ができるようになります。

秘密キーのダウンロードだけではAPIキーとして登録されないため、必ず追加をクリックします。


構成ファイルとして、Oracle Backend for FirebaseのStorageをOCI Object Storageとして構成するために必要なパラメータの値が表示されます。

構成ファイルの内容をコピーし保管しておきます。

保管したのち、ドロワーを閉じます


以上でユーザーfusabase-storage-managerAPIキーが作成されました。


最後にプロファイルからテナンシ詳細を開き、オブジェクト・ストレージ・ネームスペースを確認します。


以上でOCI Object Storageへの切り替えに必要な情報がすべて揃いました。


OCI Object Storageへの切り替え



Oracle Backend for Firebaseのコンソールに接続し、StorageFile systemを開きます。

Manage configurationをクリックします。


Link to OCI Object Storageをクリックすると、切り替えに必要な設定値の入力フォームが表示されます。

OCI Object Storageの準備作業で集めた以下の情報を設定します。

Namespace、User OCID、Tenancy OCID、Fingerprint、Private key(ダウンロードしたファイルに記載)、Bucket name(これはfusabase-storage)、Region


Bucket nameとRegionの入力するために、フォームを下にスクロールさせます。

Private keyの入力フィールドに、Must start with "-----BEGIN PRIVATE KEY-----" and end with "-----END PRIVATE KEY-----"と表示されています。

秘密キーの入力は-----BEGIN PRIVATE KEY-----から始まり、-----END PRIVATE KEY-----で終わるように入力しますが、形式の認識に問題があり警告が表示されます。


最初の行に続く行を、以下のように連結します。
-----BEGIN PRIVATE KEY-----MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDnxhnPjsuFbOYU
また、-----END PRIVATE KEY-----の後に空行がないことも確認します。

Private keyの入力フィールドからフォーカスが移動すると、入力した秘密キーの検証が行われます。上記の対応をすると、警告が消えます。

以上の入力を行いSaveを実行します。


StorageのFile systemがOCI Object Storageに切り替わると、TypeOCIBucket namefusabase-storageRegionOCIのリージョンNamespaceにテナントのオブジェクト・ストレージ・ネームスペースが表示されます。




動作確認



Oracle Backend for FirebaseのLiveLabsで作成したWebアプリケーションを使用します。

Storageの構成が変わると、Webアプリケーションの構成ファイルの内容も変わります。

Project settingsのApplicationのSDK setup and configurationを確認します。

構成のJSONドキュメントの属性objs_typeocistorage_bucketがfusabase-storageに変更されています。


Webアプリケーションのfusabase-config.jsの内容を更新します。


以上で、WebアプリケーションもOCI Object Storageを使用するように切り替わりました。

Webアプリケーションから写真付きのレシピを作成してみます。

LiveLabsのLab 6: Photo Upload, Task 3: Upload a photo and verifyの操作です。


レシピが登録され、写真も表示されます。


画像がどこから取得されているか確認します。

パスタの画像の上でコンテキスト・メニューを開き、画像アドレスをコピーを実行します。


コピーしたアドレスを確認すると、以下のようにOCI Object Storageから直接画像をダウンロードしていることがわかります。このURLは、Object Storageにアップロードされたファイルlemon-herb-pasta.pngの事前承認済リクエストのURLです。

https://namespace.objectstorage.ca-toronto-1.oci.customer-oci.com/p/6lGeRgyKCYbAe5pXAnEQAUG3UjmnA40G2g0-z4QzxIhcUJopxg3pW06S8CCmxURJ/n/yz2dlxjrvfsc/b/fusabase-storage/o/recipes/58FEF49CF3EA7CCAE063020012ACA499/lemon-herb-pasta.png

バケットfusabase-storageの可視性はプライベートなので、少なくてもオブジェクトのリード権限を持つユーザーで認証していないとファイルは読めません。事前承認済リクエストを生成することにより、認証無しでファイルにアクセスできるようにしています。

写真ファイルのアップロードは、scripts/app.jsの以下のコードで行われています。
        const photoFile = fd.get("photo");
        if (photoFile && photoFile.size > 0) {
          // ── Lab 6 TODO: Upload the new recipe photo with the SDK ──
          // After adding this code, remove hidden from #modalPhotoField in index.html.
          
          storage = getStorage(app);
          const photoRef = ref(storage, `recipes/${recipeRef.id}/${photoFile.name}`);
          await uploadBytes(photoRef, photoFile, { contentType: photoFile.type || "image/png" });
          const photoURL = await getDownloadURL(photoRef);
          await updateDoc(doc(collection(db, "recipes"), recipeRef.id), { photoURL });
        }
        el.recipeForm.reset();
        closeModal();
        await refreshRecipes(recipeRef.id);
      });
uploadBytesで写真をStorageに保存(StorageがOCI Object Storageの場合、Object Storageへのアップロード)した後、getDownloadURLを呼び出してアップロードされたオブジェクトの事前承認済リクエストのURLを取得しています。

このURLは、コレクションrecipesのドキュメントにphotoURLとして保存されます。

Oracle Backend for FirebaseのコンソールのDatabaseより、該当のドキュメントのphotoURLを確認すると、アップロードされたオブジェクトの事前承認済リクエストのURLが保持されていることが確認できます。


事前承認済みリクエストには有効期限があります。

Oracle Cloudのコンソールよりバケットfusabase-storage管理を開きます。

管理のページには作成済みの事前承認済リクエストが一覧されます。この一覧に、先ほどアップロードした写真の事前承認済リクエストも含まれています。

写真の事前承認済リクエストの詳細を表示し、有効期限を確認します。


有効期限は、生成日から7日間のようです。


つまり、コレクションrecipesのドキュメントにphotoURLとして保存されているURLは、生成した7日後には無効(404 Not Foundが発生する)なURLになります。

事前承認済リクエストを自動で更新する機能があるとは思えないため、photoURLの生成日のタイプスタンプも保存し、画面に写真を表示するためにphotoURLが取り出される際には有効期限を超えてないかどうか確認し、超えていたらgetDownloadURLを発行してphotoURLを置き換えるといった処理をWebアプリケーションに組み込む必要があるかもしれません。アプリケーションに組み込むのが大変な場合は、バッチ処理で更新するなども対応も考えられます。

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