Google Firestore Bundle數據包功能調研
簡介
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 http://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!