Firestore为noSQL和Flutter复制SQL Join

时间:2019-02-12 12:35:02

标签: firebase dart flutter nosql google-cloud-firestore

我意识到在复制NoSql文档数据库(如FireStore)的联接方面存在很多问题,但是我无法找到将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"

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

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

很显然,它仅从一个集合中返回。如何以多对多关系加入收藏?

4 个答案:

答案 0 :(得分:2)

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

因此,我能想到的最简单的解决方案是查询数据库,以从uid集合中获取用户的profile。获得该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。

此外,在复制数据时,需要记住一件事。用与添加数据相同的方式,您需要对其进行维护。换句话说,如果您想更新/删除项目,则需要在它存在的每个位置进行。

答案 1 :(得分:1)

我做了这样的事情,将两个对象和类别的结果结合起来。

我做了两个StreamBuilder来显示在一个列表中,在第一个中,我获得了类别并放入了地图,然后我查询了对象并使用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),
                                        ),

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

我不确定您想要的还是明确的,但希望对您有所帮助。

答案 2 :(得分:0)

我认为不应该使用宗派命名法,因为要保持该名称,您必须对Firestore进行额外的写入

相反,jorge vieira 是正确的,因为与写入相比,您可以进行两次读取

因此,最好两次读取而不是两次写入数据,并且记住大型项目中每一个沮丧的事物也是不切实际的

答案 3 :(得分:0)

假设您要使用依赖于某些Stream对象的Future

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;
}