2023年1月11日 星期三

Flutter學習-firebase Cloud Firestore說明(2)

翻譯自:https://firebase.flutter.dev/docs/firestore/usage/ 

Writing Data#

Firebase 文件提供了一些很棒的示例,詳細講解了最佳的資料結構架構。建議您在構建資料庫之前先閱讀此指南。 想了解更多關於將資料寫入Firestore的可能性,請參考此文件。

Typing CollectionReference and DocumentReference#

預設的情況下,Firestore引用操作Map<String, dynamic>物件。缺點是我們失去了類型安全性。一種解決方案是使用withConverter,它將修改CollectionReference.add或Query.where等方法,使其具有類型安全性。 與可序列化類一起使用withConverter的一般用法如下:

class Movie {
Movie({required this.title, required this.genre});
Movie.fromJson(Map<String, Object?> json)
: this(
title: json['title']! as String,
genre: json['genre']! as String,
);
final String title;
final String genre;
Map<String, Object?> toJson() {
return {
'title': title,
'genre': genre,
};
}
}

我們可以使用 withConverter 來操作一個電影集合,就像這樣:

final moviesRef = FirebaseFirestore.instance.collection('movies').withConverter<Movie>(
fromFirestore: (snapshot, _) => Movie.fromJson(snapshot.data()!),
toFirestore: (movie, _) => movie.toJson(),
);
Future<void> main() async {
// 取得科幻電影
List<QueryDocumentSnapshot<Movie>> movies = await moviesRef
.where('genre', isEqualTo: 'Sci-fi')
.get()
.then((snapshot) => snapshot.docs);
// Add a movie
await moviesRef.add(
Movie(
title: 'Star Wars: A New Hope (Episode IV)',
genre: 'Sci-fi'
),
);
/取得id為我的資料
Movie movie42 = await moviesRef.doc('42').get().then((snapshot) => snapshot.data()!);
}

Adding Documents#

要將新文件新增到集合中,請使用 CollectionReference 上的 add 方法:

class AddUser extends StatelessWidget {
final String fullName;
final String company;
final int age;
AddUser(this.fullName, this.company, this.age);
Widget build(BuildContext context) {
// 建立一個名為users的CollectionReference,它引用firestore的集合
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
// 呼叫使用者的CollectionReference以新增新使用者
return users
.add({
'full_name': fullName, // John Doe
'company': company, // Stokes and Sons
'age': age // 42
})
.then((value) => print("User Added"))
.catchError((error) => print("Failed to add user: $error"));
}
return FlatButton(
onPressed: addUser,
child: Text(
"Add User",
),
);
}
}
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
return users
.doc('ABC123')
.set({
'full_name': "Mary Jane",
'age': 18
})
.then((value) => print("User Added"))
.catchError((error) => print("Failed to add user: $error"));
}
CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> addUser() {
return users
// existing document in 'users' collection: "ABC123"
.doc('ABC123')
.set({
'full_name': "Mary Jane",
'age': 18
},
SetOptions(merge: true),
)
.then(
(value) => print("'full_name' & 'age' merged with existing data!")
)
.catchError((error) => print("Failed to merge data: $error"));
}

Updating documents#(更新文件資料)

有時候你可能希望更新一個文件,而不是取代所有的資料。上述的set方法會取代所給予DocumentReference上的任何現有資料。如果你想要更新而不是取代文件,請使用update方法:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> updateUser() {
return users
.doc('ABC123')
.update({'company': 'Stokes and Sons'})
.then((value) => print("User Updated"))
.catchError((error) => print("Failed to update user: $error"));
}

此方法也提供支援,藉由點符號來更新深層嵌套的值:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> updateUser() {
return users
.doc('ABC123')
.update({'info.address.zipcode': 90210})
.then((value) => print("User Updated"))
.catchError((error) => print("Failed to update user: $error"));
}

Field values#

Cloud Firestore 支援儲存和操作您資料庫中的值,例如時間戳記、地理點、Blob (二進位大型物件(Binary Large Object))和陣列管理。

為了儲存地理點值,請將緯度和經度提供給 GeoPoint 類別:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> updateUser() {
return users
.doc('ABC123')
.update({'info.address.location': GeoPoint(53.483959, -2.244644)})
.then((value) => print("User Updated"))
.catchError((error) => print("Failed to update user: $error"));
}


要儲存的Blob是圖片的 Blob,請提供 Uint8List。下面的例子展示了如何從你的 assets 目錄取得一張圖片,並將它嵌入 Firestore 中的 info 物件中。


CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> updateUser() {
return rootBundle
.load('assets/images/sample.jpg')
.then((bytes) => bytes.buffer.asUint8List())
.then((avatar) {
return users
.doc('ABC123')
.update({'info.avatar': Blob(avatar)});
})
.then((value) => print("User Updated"))
.catchError((error) => print("Failed to update user: $error"));
}

Removing Data#

使用Cloud Firestore的delete方法可以將文件刪除:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> deleteUser() {
return users
.doc('ABC123')
.delete()
.then((value) => print("User Deleted"))
.catchError((error) => print("Failed to delete user: $error"));
}

如果您需要刪除文件內部的特定屬性而不是整個文件,您可以使用FieldValue類別的delete方法:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> deleteField() {
return users
.doc('ABC123')
.update({'age': FieldValue.delete()})
.then((value) => print("User's Property Deleted"))
.catchError((error) => print("Failed to delete user's property: $error"));
}

Transactions#

Transactions 是確保只使用伺服器上最新的資料進行寫入操作的一種方法。Transactions 絕不會部分應用寫入,並且在成功交易結束時執行寫入。 當您想要根據其當前值或其他字段的值更新字段時,Transactions 很有用。如果您想要在不使用文檔當前狀態的情況下寫入多個文檔,應該使用批量寫入。

使用交易時請注意: 讀取操作必須在寫入操作之前 當客戶端離線時Transactions 將會失敗,它們無法使用緩存數據

一個可用於transaction的例子就是在一個應用程式中,一個用戶可以訂閱一個頻道。當用戶按下訂閱按鈕時,文件中的「訂閱者」字段會增加。如果不使用交易,我們就需要先讀取現有的值,然後使用兩個單獨的操作來進行增加。 在一個高流量的應用程式中,服務器上的值可能已經改變,直到寫入操作設置新值,導致數字不一致。 transaction可以消除這個問題,通過原子性地更新服務器的值。如果價值在交易執行期間改變,它將重試,確保服務器上的值被使用,而不是客戶端的值。 要執行交易,請調用runTransaction方法:

// Create a reference to the document the transaction will use
DocumentReference documentReference = FirebaseFirestore.instance
.collection('users')
.doc(documentId);
return FirebaseFirestore.instance.runTransaction((transaction) async {
// Get the document
DocumentSnapshot snapshot = await transaction.get(documentReference);
if (!snapshot.exists) {
throw Exception("User does not exist!");
}
//根據目前的count更新follower
//注意:可以通過使用FieldValue.increment()更新人口,而不使用transaction來完成此操作
int newFollowerCount = snapshot.data()['followers'] + 1;
//對文件進行更新
transaction.update(documentReference, {'followers': newFollowerCount});
// 回傳新的數量
return newFollowerCount;
})
.then((value) => print("Follower count updated to $value"))
.catchError((error) => print("Failed to update user followers: $error"));

在上述範例中,如果文檔在transaction期間更改,則最多重試五次。 您不應該在交易中直接修改應用程序狀態,因為處理程序可能會多次執行。您應該在處理程序的末尾返回一個值,在交易完成後更新應用程序狀態。 如果在處理程序中拋出異常,則整個交易將被中止。

Batch write#

Firestore支援將多個寫入操作結合為單個批次,其中可包含任意組合的設定、更新或刪除操作。 首先,使用batch方法創建一個新的批次實例,然後對批次進行操作,然後在準備就緒時提交它。 下面的示例展示了如何在單個操作中刪除集合中的所有文檔:

CollectionReference users = FirebaseFirestore.instance.collection('users');
Future<void> batchDelete() {
WriteBatch batch = FirebaseFirestore.instance.batch();
return users.get().then((querySnapshot) {
querySnapshot.docs.forEach((document) {
batch.delete(document.reference);
});
return batch.commit();
});
}

Data Security#

在你的Firebase控制台中撰寫規則,以確保數據安全是非常重要的。請按照Firebase Firestore的安全文檔來操作。

Access Data Offline#

Configure Offline Persistence#


Cloud
Firestore提供出箱即用的離線功能。在讀取和寫入資料時,Firestore使用本地資料庫,該資料庫會自動與伺服器同步。使用者離線時,Cloud Firestore功能仍然會繼續,並在重新連接時自動處理資料遷移。 此功能預設啟用,但在執行任何Firestore交互之前可以停用它:

// Web.
await FirebaseFirestore.instance.enablePersistence();
// All other platforms.
FirebaseFirestore.instance.settings =
Settings(persistenceEnabled: false);

如果您想要清除任何持久化的資料,可以呼叫clearPersistence() 方法。

await FirebaseFirestore.instance.clearPersistence();

在使用 Firestore 之前,必須先執行更新設定或清除持續性的呼叫。如果之後呼叫,它們將在下一次 Firestore 索取中生效(例如,重新啟動應用程式)。

Configure Cache Size#

當啟用持續性時,Firestore將會將每個文件設定以供離線存取。在超出快取大小後,Firestore將會嘗試移除較舊、未使用的資料。你可以設定不同的快取大小,或是停用移除程序:

//預設值為40 MB,門檻必須設定為至少1 MB,並可以設定為Settings.CACHE_SIZE_UNLIMITED 以停用垃圾收集。
FirebaseFirestore.instance.settings =
Settings(cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED);

Disable and Enable Network Access#

可以為您的Firestore客戶端禁用網絡訪問。 當網絡訪問被禁用時,所有Firestore請求均從緩存中獲取結果。 任何寫操作都將排隊,直到重新啟用網絡訪問為止。

await FirebaseFirestore.instance.disableNetwork()

要重新啟用網路存取,請呼叫enableNetwork方法:

await FirebaseFirestore.instance.enableNetwork()

Emulator Usage#

如果您正在使用本地Firestore模擬器,則可以使用useFirestoreEmulator方法連接到它。確保您傳遞Firebase模擬器正在運行的正確端口。 確保您已經按照每個操作系統中FlutterFire安裝說明中的模擬器使用說明,對應用程序啟用了對模擬器的網絡連接。

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
// Ideal time to initialize
FirebaseFirestore.instance.useFirestoreEmulator('localhost', 8080);
//...
}

Data Bundles#

如果您已經設置了數據包,並且正在提供它,您可以將數據包加載到您的應用程序中。

// Use a package like 'http' to retrieve bundle.
import 'package:http/http.dart' as http;
final response = await http.get(url);
// Convert the 'bundle.txt' string in the response to an Uint8List instance.
Uint8List buffer = Uint8List.fromList(response.body.codeUnits);
// Load bundle into cache.
LoadBundleTask task = FirebaseFirestore.instance.loadBundle(buffer);
// Use .stream API to expose a stream which listens for LoadBundleTaskSnapshot events.
task.stream.listen((taskStateProgress) {
if(taskStateProgress.taskState == LoadBundleTaskState.success){
//bundle is loaded into app cache!
}
});
// If you do not wish to .listen() to the stream, but simply want to know when the bundle has been loaded. Use .last API:
await task.stream.last;
// Once bundle is loaded into cache, you may query for data by using the GetOptions() to specify data retrieval from cache.
QuerySnapshot<Map<String, Object?>> snapshot = await FirebaseFirestore.instance
.collection('cached-data')
.get(const GetOptions(source: Source.cache));

Named Query#

如果您已經加載了一個帶有命名查詢的bundle (包),您可以通過調用namedQueryGet()API來檢索查詢快照(snapshot):

// Assuming you've loaded a data bundle that includes a named query called 'sports-teams':
// Query snapshot loaded from cache.
QuerySnapshot<Map<String, Object?>> snapshot = await FirebaseFirestore.instance.namedQueryGet('sports-teams', options: const GetOptions(source: Source.cache));

Distributed Counters#

為了支持更頻繁的計數器更新,創建一個分散的計數器。每個計數器是一個具有分片子集合的文檔,計數器的值是分片的值的總和。 寫入吞吐量隨著分片數量的線性增加,因此具有10個分片的分散計數器可以處理10倍於傳統計數器的寫入數量。 注意 另一個解決方案是使用Firebase擴展,請參閱Ditributed Counters擴展以及如何在此處設置。 一個分散的計數器集合看起來像這樣:

// counters/${ID}
class Counter {
final int numShards;
Counter(this.numShards);
}
// counters/${ID}/shards/${NUM}
class Shard {
final int count;
Shard(this.count);
}

以下程式碼初始化一個分散式計數器:

Future<void> createCounter(DocumentReference ref, int numShards) async {
WriteBatch batch = FirebaseFirestore.instance.batch();
// Initialize the counter document
batch.set(ref, {'numShards': numShards});
// Initialize each shard with count=0
for (var i = 0; i < numShards; i++) {
final shardRef = ref.collection('shards').doc(i.toString());
batch.set(shardRef, {'count': 0});
}
// Commit the write batch
await batch.commit();
}

選擇一個隨機分片並增加計數,以增加計數器的值。

Future<void> incrementCounter(DocumentReference ref, int numShards) async {
// Select a shard of the counter at random
final shardId = Random().nextInt(numShards).toString();
final shardRef = ref.collection('shards').doc(shardId);
// Update count
await shardRef.update({'count': FieldValue.increment(1)});
}


查詢所有分片並將它們的計數欄位相加以取得總計數:


Future<int> getCount(DocumentReference ref) async {
// Sum the count of each shard in the subcollection
final shards = await ref.collection('shards').get();
int totalCount = 0;
shards.docs.forEach(
(doc) {
totalCount += doc.data()['count'] as int;
},
);
return totalCount;
}

請參閱有關分散計數器的官方文件以獲得更多詳細資訊。

沒有留言:

張貼留言