Google Firestore Bundle資料包功能調研

語言: CN / TW / HK

簡介

Cloud FireStore是Firebase提供的一種託管於雲端的NoSQL資料庫方案。

資料結構為Collection(集合)和Document(文件),文件中儲存鍵值對,整個資料庫是由小型文件組成的大型集合。

  • 支援子集合、複雜分層
  • 支援單索引、複合索引
  • 支援複合查詢
  • 支援事務讀寫
  • 支援水平擴充套件、自動擴容

Bundle

Bundle是以查詢條件為維度,將一系列查詢結果快取在一起的資料體。比如對“overlay_categories”的全表查詢,對“overlay_materials”的分頁查詢。

優勢:

  • 生成查詢快照,避免多次查詢Firestore造成的費用消耗。
  • 結合CDN,在網路資料查詢時直接返回查詢快照,加快響應速度。
  • 可以作為客戶端本地的初始化資料,後續使用API查詢時會增量更新資料快取。
  • Bundle在更新快取時也會通知addSnapshotListener的監聽器,可與線上資料更新邏輯合併處理。
  • Bundle資料生成的快取與API更新的快取資料屬於同一種快取,統一管理。

劣勢:

  • 資料會以近似JSON的格式明文儲存,不可儲存敏感資訊。
  • Bundle資料包的匯入需要經歷:資料讀取,解析,入庫的步驟,比較耗時。
  • 效能隨集合量級上升而下降,不推薦長期離線使用。
  • 快取沒有索引,快取的查詢不如API查詢快

服務端

SDK生成Bundle資料包

``` from google.cloud import firestore from google.cloud.firestore_bundle import FirestoreBundle ​ db = firestore.Client() bundle = FirestoreBundle("latest-stories") ​ doc_snapshot = db.collection("stories").document("news-item").get() query = db.collection("stories")._query() ​

Build the bundle

Note how query is named "latest-stories-query"

bundle_buffer: str = bundle.add_document(doc_snapshot).add_named_query(    "latest-stories-query", query, ).build() ​ ```

Cloud Function生成Bundle資料包

index.js

執行時 : Node.js 12 入口點 : createBundle

需要新增執行時環境變數 GCLOUD_PROJECT = "ProjectID" BUCKET_NAME = "xxxx"

const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); ​ const bundleNamePrefix = process.env.BUNDLE_NAME_PREFIX const bucketName = process.env.BUCKET_NAME ​ const collectionNames = [  "doubleexposure_categories",  "materials" ] ​ exports.createBundle = functions.https.onRequest(async (request, response) => { ​  // Query the 50 latest stories  // Build the bundle from the query results  const store = admin.firestore();  const bucket = admin.storage().bucket(`gs://${bucketName}`); ​  for (idx in collectionNames) {    const name = collectionNames[idx]    const bundleName = `${bundleNamePrefix}_${name}_${Date.now()}`;    const bundle = store.bundle(bundleName);    const queryData = await store.collection(name).get(); ​    // functions.logger.log("queryData name:", name);    // functions.logger.log("queryData:", queryData);    bundle.add(name, queryData)    const bundleBuffer = bundle.build();    bucket.file(`test-firestore-bundle/${bundleName}`).save(bundleBuffer); }  // Cache the response for up to 5 minutes;  // see https://firebase.google.com/docs/hosting/manage-cache  // response.set('Cache-Control', 'public, max-age=300, s-maxage=600');  response.end("OK"); });

package.json

{  "name": "createBundle",  "description": "Uppercaser Firebase Functions Quickstart sample for Firestore",  "dependencies": {    "firebase-admin": "^10.2.0",    "firebase-functions": "^3.21.0" },  "engines": {    "node": "16" } }

客戶端

離線快取配置

func setupFirestore() {        let db = Firestore.firestore()        let settings = db.settings        /**         對於 Android 和 Apple 平臺,離線持久化預設處於啟用狀態。如需停用持久化,請將 PersistenceEnabled 選項設定為 false。         */        settings.isPersistenceEnabled = true        /**         啟用持久化後,Cloud Firestore 會快取從後端接收的每個文件以便離線訪問。         Cloud Firestore 會為快取大小設定預設閾值。         超出預設值後,Cloud Firestore 會定期嘗試清理較舊的未使用文件。您可以配置不同的快取大小閾值,也可以完全停用清理功能         */        settings.cacheSizeBytes = FirestoreCacheSizeUnlimited        /**         如果您在裝置離線時獲取了某個文件,Cloud Firestore 會從快取中返回資料。         查詢集合時,如果沒有快取的文件,系統會返回空結果。提取特定文件時,系統會返回錯誤。         */        db.settings = settings }

載入Bundle資料

// Loads a remote bundle from the provided url. func fetchRemoteBundle(for firestore: Firestore, from url: URL, completion: @escaping((Result<LoadBundleTaskProgress, Error>) -> Void)) {        guard let inputStream = InputStream(url: url) else {            let error = self.buildError("Unable to create stream from the given url: (url)")            completion(.failure(error))            return        } ​        // The return value of this function is ignored, but can be used for more granular bundle load observation.        let _ = firestore.loadBundle(inputStream) { (progress, error) in            switch (progress, error) {                case (.some(let value), .none):                    if value.state == .success {                        completion(.success(value))                    } else {                        let concreteError = self.buildError("Expected bundle load to be completed, but got (value.state) instead")                        completion(.failure(concreteError))                    }                case (.none, .some(let concreteError)):                    completion(.failure(concreteError))                case (.none, .none):                    let concreteError = self.buildError("Operation failed, but returned no error.")                    completion(.failure(concreteError))                case (.some(let value), .some(let concreteError)):                    let concreteError = self.buildError("Operation returned error (concreteError) with nonnull progress: (value)")                    completion(.failure(concreteError))            }        } }

使用提示

  • Bundle載入一次後就會合併入Firestore快取,不需要重複匯入。
  • Bundle在建立時,會以Query為維度增加Name標籤,可以通過QueryName查詢匯入的Bundle資料集合。

- (void)getQueryNamed:(NSString *)name completion:(void (^)(FIRQuery *_Nullablequery))completion

  • Bundle由非同步載入完成,在載入完成之後才能查到資料,可以用監聽器來查詢。

- (id<FIRListenerRegistration>)addSnapshotListener:(FIRQuerySnapshotBlock)listener    NS_SWIFT_NAME(addSnapshotListener(_:))

效能測試

小資料量樣本

| 資料大小 | 文件數量 | | ----- | ---- | | 22 KB | 33 |

| 次數 | iPhone 6s 載入耗時 | iPhone XR 載入耗時 | | --- | -------------- | -------------- | | 第一次 | 1066 ms | 627 ms | | 第二次 | 715 ms | 417 ms | | 第三次 | 663 ms | 375 ms | | 第四次 | 635 ms | 418 ms | | 第五次 | 683 ms | 578 ms |

大資料量樣本

| 資料大小 | 文件數量 | | ----- | ---- | | 2.7 M | 359 |

| 次數 | iPhone 6s 載入耗時 | iPhone XR 載入耗時 | | --- | -------------- | -------------- | | 第一次 | 4421 ms | 2002 ms | | 第二次 | 698 ms | 417 ms | | 第三次 | 663 ms | 375 ms | | 第四次 | 635 ms | 418 ms | | 第五次 | 683 ms | 578 ms |

參考連結

Firestore Data Bundles-A new implementation for cached Firestore documents

Load Data Faster and Lower Your Costs with Firestore Data Bundles!

Why is my Cloud Firestore query slow?