Firestore 为 noSQL 和 Flutter 复制 SQL 连接

Posted

技术标签:

【中文标题】Firestore 为 noSQL 和 Flutter 复制 SQL 连接【英文标题】:Firestore replicating a SQL Join for noSQL and Flutter 【发布时间】:2019-07-06 02:38:59 【问题描述】:

我意识到在使用 FireStore 等 NoSql 文档数据库复制连接方面存在许多问题,但是我无法找到将 Dart/Flutter 与 FireStore 结合使用的彻底解决方案。

我做了一些研究,我觉得在下面的例子中我会寻找一个“多对多”的关系(如果这是错误的,请纠正我)因为将来可能需要查看所有配置文件作为所有连接。

在 firebase 中,我有两个根级集合(配置文件和连接):

profile
    > documentKey(Auto Generated)
         > name = "John Smith"
         > uid = "xyc4567"

    > documentKey(Auto Generated)
         > name = "Jane Doe"
         > uid = "abc1234"

    > documentKey(Auto Generated)
         > name = "Kate Dee"
         > uid = "efg8910"



connection
    > documentKey(Auto Generated)
         > type = "friend"
         > profileuid = "abc1234"
         > uid = "xyc4567"

    > documentKey(Auto Generated)
         > type = "family"
         > profileuid = "abc1234"
         > uid = "efg8910"

对于此示例,假设用户 John Smith (uid: xyc4567) 连接到 Jane Doe (uid: abc1234) 和 Kate Dee (uid: efg8910) 时创建了“连接”文档。

这是我要复制的关系 SQL,以显示 John Smith 已连接的配置文件列表:

Select * FROM profile, connection 
WHERE profile.uid = connection.profileuid 
AND profile.uid = "xyc4567"

在颤振我的颤振应用程序中,我有一个 fireStore 查询起点:

stream: Firestore.instance.collection('profile')
.where('uid', isEqualTo: "xyc4567").snapshots(),

显然它只从一个集合返回。我如何以多对多关系加入集合?

【问题讨论】:

【参考方案1】:

很遗憾,Cloud Firestore 和其他 NoSQL 数据库中都没有 JOIN 子句。在 Firestore 中,查询很浅。这意味着他们只能从运行查询的集合中获取项目。无法在单个查询中从两个***集合中获取文档。 Firestore 不支持一次性跨不同集合进行查询。单个查询只能使用单个集合中文档的属性。

所以我能想到的最简单的解决方案是查询数据库以从profile 集合中获取用户的uid。获得该 ID 后,进行另一个数据库调用(在回调中),并使用以下查询从 connection 集合中获取您需要的相应数据:

stream: Firestore.instance.collection('connection').where('uid', isEqualTo: "xyc4567").snapshots(),

另一种解决方案是在每个用户下创建一个名为connection 的子集合,并在其下添加所有connection 对象。这种做法称为denormalization,是 Firebase 的常见做法。如果您是 NoQSL 数据库的新手,我建议您观看此视频,Denormalization is normal with the Firebase Database 以便更好地理解。它适用于 Firebase 实时数据库,但同样的规则适用于 Cloud Firestore。

另外,当您复制数据时,需要牢记一件事。与添加数据的方式相同,您需要对其进行维护。换句话说,如果你想更新/删除一个项目,你需要在它存在的每个地方都这样做。

【讨论】:

【参考方案2】:

我做了一些这样的事情来连接来自两个集合对象和类别的结果。

我做了两个 StreamBuilders 来显示在一个列表中,在第一个中我得到了类别并放入了一个地图,然后我查询对象并使用 categoryID 从地图中获取类别对象:

StreamBuilder<QuerySnapshot>(
              stream: Firestore.instance
                  .collection('categoryPath')
                  .snapshots(),
              builder: (BuildContext context,
                  AsyncSnapshot<QuerySnapshot> categorySnapshot) 
                //get data from categories

                if (!categorySnapshot.hasData) 
                  return const Text('Loading...');
                

                //put all categories in a map
                Map<String, Category> categories = Map();
                categorySnapshot.data.documents.forEach((c) 
                  categories[c.documentID] =
                      Category.fromJson(c.documentID, c.data);
                );

                //then from objects

                return StreamBuilder<QuerySnapshot>(
                  stream: Firestore.instance
                      .collection('objectsPath')
                      .where('day', isGreaterThanOrEqualTo: _initialDate)
                      .where('day', isLessThanOrEqualTo: _finalDate)
                      .snapshots(),
                  builder: (BuildContext context,
                      AsyncSnapshot<QuerySnapshot> objectsSnapshot) 
                    if (!objectsSnapshot.hasData)
                      return const Text('Loading...');

                    final int count =
                        objectsSnapshot.data.documents.length;
                    return Expanded(
                      child: Container(
                        child: Card(
                          elevation: 3,
                          child: ListView.builder(
                              padding: EdgeInsets.only(top: 0),
                              itemCount: count,
                              itemBuilder: (_, int index) 
                                final DocumentSnapshot document =
                                    objectsSnapshot.data.documents[index];
                                Object object = Object.fromJson(
                                    document.documentID, document.data);

                                return Column(
                                  children: <Widget>[
                                    Card(
                                      margin: EdgeInsets.only(
                                          left: 0, right: 0, bottom: 1),
                                      shape: RoundedRectangleBorder(
                                        borderRadius: BorderRadius.all(
                                            Radius.circular(0)),
                                      ),
                                      elevation: 1,
                                      child: ListTile(
                                        onTap: () ,
                                        title: Text(object.description,
                                            style: TextStyle(fontSize: 20)),
//here is the magic, i get the category name using the map 
of the categories and the category id from the object
                                        subtitle: Text(
                                          categories[object.categoryId] !=
                                                  null
                                              ? categories[
                                                      object.categoryId]
                                                  .name
                                              : 'Uncategorized',
                                          style: TextStyle(
                                              color: Theme.of(context)
                                                  .primaryColor),
                                        ),

                                      ),
                                    ),
                                  ],
                                );
                              ),
                        ),
                      ),
                    );

我不确定是否是您想要的或是否清楚,但我希望它对您有所帮助。

【讨论】:

【参考方案3】:

我认为不应该首选宗派,因为要维护它,您必须对 firestore 进行额外的写入

而不是jorge vieira 是正确的,因为与写入相比,您可以进行双重读取

所以最好读两次而不是写两次数据,而且记住大型项目中每件令人沮丧的事情也是非常不切实际的

【讨论】:

【参考方案4】:

假设,您想使用依赖于一些 Future 对象的 Stream

Stories
     Document ID (Auto Generated) //Suppose, "zddgaXmdadfHs"
       > name = "The Lion & the Warthog"
       > coverImage = "https://...."
       > author = "Furqan Uddin Fahad"
       > publisDate = 123836249234
Favorites
     Document ID (Auto Generated)
       > storyDocID = "zddgaXmdadfHs" //Document ID of a story
       > userId = "adZXkdfnhoa" //Document ID of a user

Sql 等效查询应该是这样的

SELECT * FROM Favorites AS f, Stories AS s
WHERE f.storyDocID = s.DocumentID 
AND f.userId = user.userId

和这样的 Firestore 查询

final _storeInstance = Firestore.instance;

Stream <List<Favorite>> getFavorites() async*  
  final user = await _getUser(); //_getUser() Returns Future<User>
  yield* _storeInstance
      .collection('Favorites')
      .where('userId', isEqualTo: user.userId)
      .snapshots()
      .asyncMap((snapshot) async 
        final list = snapshot.documents.map((doc) async 
          final story = await _getStory(doc['storyDocID']);
          return Favorite.from(doc, story); //Favorite.from(DocumentSnapshot doc, Story story) returns an instance of Favorite
        ).toList(); //List<Future<Favorite>>
        return await Future.wait(list); //Converts List<Future<Favorite>> to Future<List<Favorite>>
      );


Future<Story> _getStory(String storyDocID) async 
  final docRef = _storeInstance
      .collection('Stories')
      .document(storyDocID);
  final document = await docRef.get();
  final story = Story.from(document);
  return story;

【讨论】:

以上是关于Firestore 为 noSQL 和 Flutter 复制 SQL 连接的主要内容,如果未能解决你的问题,请参考以下文章

谷歌Cloud Firestore NoSQL数据库正式迎来通用版本

期望一个“用户”类型的值,但在尝试将 Firestore 数据获取到 Flutter Web 上的 PaginatedDataTable 时得到一个“Null”类型的值?

模型Firestore数据

如何使用 Flutter 防御性地从 Firestore 请求数据

如何在 NoSQL 环境中创建以下数据结构

某些用户的 NoSQL 建模字段可见性