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?