🙄

コレクショングループとは何か?

2023/05/30に公開

名前が同じコレクションを全ての階層から取ってくる

Firestoreには、collectionGroupなるものがあります。これの使い方がわからないので調べてみた。公式のドキュメントを見つつ技術記事も見て、コードを書いてみましたが、UIにFirestoreから取得したデータを表示できなかった。
orderByやwhereをつけてなければ、できるのですが、条件をつけてデータを読み込まないといけませんので、Firebaseのコンソールに設定が必要なので躓いた!
https://firebase.google.com/docs/firestore/query-data/queries?hl=ja

Firestoreの構造

chatsコレクションは、2箇所に存在していて、トップレベルのコレクションとusersコレクションのサブコレクションとして、存在しています。

- users
    - ドキュメントID
        - chats
            - ドキュメントID
                - createdAt
                - isMe
                - text
                
- chats
    - createdAt
    - isMe
    - text

トップレベルのコレクションIDとサブコレクションのコレクションIDの内容は一緒です。

データの型 フィールド名 内容
timestamp createdAt 作成時間
string text チャットで投稿するテキスト
boolen isMe trueかfalseで色が変わりチャットのメッセージの位置も変わる

Firestoreは、このようになっております。

こちらが問題のソースコード

コード自体は正しいので問題ないが、Firebaseの設定が必要なので、エラーで苦しんだ!
Firestoreから取得しているデータを見ると、Firebaseコンソールで自動でクエリの設定をする場所へ誘導してくれるリンクが出てくるので、こちらをたどってエラーを解消した。

main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:collection_group_example/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ChatScreen(),
    );
  }
}

class ChatScreen extends StatelessWidget {
  const ChatScreen({super.key});

  
  Widget build(BuildContext context) {
    final snap = FirebaseFirestore.instance
        .collectionGroup('chats')
        .orderBy('createdAt', descending: false)
        .snapshots();
    return Scaffold(
      appBar: AppBar(
        title: const Text('All Chats'),
      ),
      body: StreamBuilder<QuerySnapshot>(
        stream: snap,
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError) {
            print('Error: 😄${snapshot.error}');
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          return ListView(
            children: snapshot.data!.docs.map((DocumentSnapshot document) {
              Map<String, dynamic> data =
                  document.data()! as Map<String, dynamic>;
              return ListTile(
                title: Text(data['text']),
              );
            }).toList(),
          );
        },
      ),
    );
  }
}

エラー対応

こちらが問題のエラー
![](https://storage.googleapis.com/zenn-user-upload/30ee8ba7c02f-20230529.png
=250x)

ログにエラーを解消するように、Firebaseの設定をしてくれるページへ誘導してくれるリンクが出てくるので、そのページへ行って設定をします。

https://firebase.google.com/docs/firestore/query-data/queries?hl=ja


こんなダイアログが表示されるので、指示通りに設定をする

有効になるまで、時間がかかりますのでお茶でも飲んで待ちましょう。

有効にすると画面に表示できました🙌

チャットのUIはLINE風に変えてみました。isMeのプロパティがtrueかfalseで変化します。

修正したソースコード

main.dart
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:collection_group_example/firebase_options.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const ChatScreen(),
    );
  }
}

class ChatScreen extends StatelessWidget {
  const ChatScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    final snap = FirebaseFirestore.instance
        .collectionGroup('chats')
        .orderBy('createdAt', descending: false)
        .snapshots();
    return Scaffold(
      appBar: AppBar(
        title: const Text('All Chats'),
      ),
      body: StreamBuilder<QuerySnapshot>(
        stream: snap,
        builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
          if (snapshot.hasError) {
            print('Error: 😄${snapshot.error}');
            return Center(child: Text('Error: ${snapshot.error}'));
          }
          if (snapshot.connectionState == ConnectionState.waiting) {
            return Center(child: CircularProgressIndicator());
          }

          return ListView.builder(
            reverse: true,
            itemCount: snapshot.data!.docs.length,
            itemBuilder: (context, index) {
              DocumentSnapshot chat = snapshot.data!.docs[index];
              Map<String, dynamic> data = chat.data()! as Map<String, dynamic>;
              final isMe = data['isMe']; // TODO: replace this with your logic to check if the message is sent by the current user
              return Container(
                padding: EdgeInsets.symmetric(vertical: 10.0, horizontal: 20.0),
                child: Column(
                  crossAxisAlignment:
                      isMe ? CrossAxisAlignment.end : CrossAxisAlignment.start,
                  children: <Widget>[
                    Container(
                      constraints: BoxConstraints(
                          maxWidth: MediaQuery.of(context).size.width * 0.75),
                      padding: EdgeInsets.symmetric(
                        vertical: 10.0,
                        horizontal: 15.0,
                      ),
                      decoration: BoxDecoration(
                        color: isMe ? Colors.blue[200] : Colors.grey[200],
                        borderRadius: BorderRadius.circular(20.0),
                      ),
                      child: Text(
                        data['text'],
                        style: TextStyle(
                          color: isMe ? Colors.white : Colors.black54,
                        ),
                      ),
                    ),
                  ],
                ),
              );
            },
          );
        },
      ),
    );
  }
}

まとめ

コレクショングループについては、全くわからなくて、使ったことがある うしさんというエンジニアさんにレクチャーしていただきました!

セキュリティールールを書くとしたらこんな感じになると思います。

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

    // 'chats' collection in the root of the database
    match /chats/{chatId} {
      allow read: if true;
      allow write: if request.auth != null;
    }

    // 'chats' subcollection under 'users' collection
    match /users/{userId}/chats/{chatId} {
      allow read: if request.auth.uid == userId;
      allow write: if request.auth.uid == userId;
    }
  }
}

このルールだと、トップレベルのchatsを見るのは誰でもできて、書き込みはログインしているユーザーだけになります。usersコレクションのサブコレクションのchatsは、ログインしているユーザーのみ、書き込み、読み取りができます。

Discussion