关于 firebase-cloud-firestore
面向 Flutter 应用开发者的 Cloud Firestore 实施指南,涵盖数据结构设计、查询、实时更新、安全规则和扩展实践。
主要功能
- 01规划文档与子集合结构
- 02筛选查询与游标分页
- 03批量写入和事务
- 04实时监听器
- 05索引、热点与流量建议
- 06安全规则和输入验证
适用场景
- 01设计分层应用数据
- 02为筛选后的集合查询分页
- 03原子更新多个文档
- 04通过安全规则限制访问
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ evanca/ flutter-ai-rules --skill firebase-cloud-firestore安装后在 Claude Code 中输入/firebase-cloud-firestore确认已加载。
详细介绍
Firebase Cloud Firestore 技能
此技能介绍如何在 Flutter 应用中正确实现 Cloud Firestore,涵盖数据建模、查询、实时更新、安全规则和规模优化。
适用场景
在以下情况下使用此技能:
- 在 Flutter 项目中设置和配置 Cloud Firestore。
- 设计文档和集合结构,或规划子集合。
- 执行读取、写入、批量写入或事务操作。
- 实现实时监听器或分页查询。
- 针对扩展进行优化并避免写入热点。
- 编写或调试 Firestore 安全规则。
1. 数据库选择
当应用需要以下功能时,选择 Cloud Firestore:
- 包含子集合的丰富分层数据模型。
- 复杂查询:串联筛选条件,或组合按属性筛选和排序。
- 可从数据库任意位置以原子方式读取和写入数据的事务。
- 高可用性(典型正常运行时间为 99.999%)或关键级别的可靠性。
- 自动扩展以支持数百万并发用户。
如果只需要简单的数据模型、简单查找以及极低延迟的同步(典型响应时间低于 10 ms),则改用 Realtime Database。
2. 设置和配置
flutter pub add cloud_firestore
import 'package:cloud_firestore/cloud_firestore.dart';
final db = FirebaseFirestore.instance; // after Firebase.initializeApp()
位置:
- 选择距离用户和计算资源最近的数据库位置。
- 关键应用使用多区域位置(可获得最高可用性和持久性)。
- 需要降低成本和写入延迟时,使用区域位置。
iOS/macOS: 可考虑使用预编译框架来缩短构建时间:
pod 'FirebaseFirestore',
:git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git',
:tag => 'IOS_SDK_VERSION'
离线持久化默认在移动设备上启用。配置缓存大小:
FirebaseFirestore.instance.settings = const Settings(
persistenceEnabled: true,
cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);
3. 文档结构
- 避免使用
.和..作为文档 ID(它们在 Firestore 路径中有特殊含义)。 - 避免在文档 ID 中使用正斜杠(
/)(它是路径分隔符)。 - 不要使用单调递增的文档 ID(例如
Customer1、Customer2)——这会导致写入热点。 - 尽可能使用 Firestore 的自动文档 ID:
final docRef = await db.collection("users").add({
'name': 'Ada Lovelace',
'email': '[email protected]',
'created_at': FieldValue.serverTimestamp(),
});
print('Created document with ID: ${docRef.id}');
- 避免在字段名称中使用以下字符(需要额外转义):
.[]*` - 使用文档中的子集合来组织复杂的分层数据,而不是使用深度嵌套的对象。
4. 索引
- Firestore 查询默认会建立索引;查询性能取决于结果集大小,而非数据集大小。
- 设置集合级索引豁免,以降低写入延迟和存储成本。
- 对不需要降序或数组索引的字段禁用这些索引。
- 对不用于查询的长字符串字段设置索引豁免。
- 如果不在查询中使用具有连续值(例如时间戳)的字段,则对其设置索引豁免——这样可以避免每秒 500 次写入的索引限制。
- 对 TTL 字段添加单字段索引豁免。
- 对不用于查询的大型数组或映射字段设置索引豁免——这样可以避免每个文档 40,000 个索引条目的限制。
5. 读取和写入操作
读取集合中的所有文档
final querySnapshot = await db.collection("users").get();
for (var doc in querySnapshot.docs) {
print("${doc.id} => ${doc.data()}");
}
使用筛选条件查询
final query = db.collection("users")
.where("age", isGreaterThanOrEqualTo: 18)
.orderBy("age")
.limit(20);
final results = await query.get();
基于游标的分页
// First page
final first = db.collection("cities").orderBy("name").limit(25);
final firstSnapshot = await first.get();
// Next page using last document as cursor
final lastDoc = firstSnapshot.docs.last;
final next = db.collection("cities")
.orderBy("name")
.startAfterDocument(lastDoc)
.limit(25);
- 不要使用偏移量进行分页——使用游标,以避免检索被跳过的文档并为其付费。
使用服务器时间戳写入
await db.collection("users").doc("user_1").set({
'name': 'Grace Hopper',
'updated_at': FieldValue.serverTimestamp(),
});
批量写入(原子操作,最多 500 次操作)
final batch = db.batch();
batch.set(db.collection("cities").doc("LA"), {'name': 'Los Angeles'});
batch.update(db.collection("cities").doc("SF"), {'population': 860000});
batch.delete(db.collection("cities").doc("OLD"));
await batch.commit();
事务
await db.runTransaction((transaction) async {
final snapshot = await transaction.get(db.collection("counters").doc("visits"));
final currentCount = snapshot.get("count") as int;
transaction.update(snapshot.reference, {"count": currentCount + 1});
});
- 将独立操作(例如文档查找和查询)并行执行,而不是按顺序执行。
- 注意写入速率限制:每个文档每秒约 1 次写入。
- 需要写入大量文档时,使用批量写入器,而不是原子批量写入器。
6. 面向规模进行设计
- 避免以很高的读写频率访问按字典序相近的文档(热点问题)。
- 避免以很高的频率创建包含单调递增字段(例如时间戳)的新文档。
- 避免以很高的频率删除集合中的文档。
- 以很高的频率向数据库写入时,逐步增加流量——用 5 分钟逐渐提升。
- 避免查询跳过最近删除的数据——使用
start_at查找正确的起始点。 - 将写入分散到不同的文档路径,以避免争用。
- Firestore 会自动扩展至约 100 万个并发连接和每秒 10,000 次写入。
7. 实时更新
final subscription = db.collection("messages")
.where("room", isEqualTo: "general")
.orderBy("timestamp", descending: true)
.limit(50)
.snapshots()
.listen((querySnapshot) {
for (var change in querySnapshot.docChanges) {
switch (change.type) {
case DocumentChangeType.added:
print("New message: ${change.doc.data()}");
break;
case DocumentChangeType.modified:
print("Modified: ${change.doc.data()}");
break;
case DocumentChangeType.removed:
print("Removed: ${change.doc.id}");
break;
}
}
});
// Detach when no longer needed:
subscription.cancel();
- 限制同时运行的实时监听器数量。
- 不再需要时分离监听器,以避免内存泄漏和不必要的读取。
- 使用复合查询在服务器端筛选数据,而不是在客户端筛选。
- 对于大型集合,使用查询限制所监听的数据量——切勿监听整个大型集合。
8. 安全
- 始终使用 Firebase 安全规则保护 Firestore 数据。
- 除非使用通配符,否则安全规则不会级联。
- 如果查询结果可能包含用户无权访问的数据,整个查询都会失败。
用户自有文档的规则示例:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
- 在提交到 Firestore 之前验证用户输入,以防止注入攻击。
- 对需要原子性更新多个文档的操作使用事务。
- 为所有 Firestore 操作实施适当的错误处理。
- 未设置适当访问控制时,切勿在 Firestore 中存储敏感信息。
参考资料
评论
支持 Markdown