【Flutter】Cloud FireStoreで、ユーザーが作成した自分のデータのみ取得する方法

# はじめに
雑メモ的な内容になります。
Cloud FireStoreを使っていて、セキュリティルールを設け、ユーザーが作成した自身のデータのみ閲覧するという機能を実装しようと思いました。
ただ、思ったより時間がかかってしまったので、同じような機能を実装したい方向けに記事を書くことにしました。

**環境設定とか、細かな内容は省くので、環境設定は他の記事をご参照ください。**

# やること一覧
やることを大まかに並べると下記になります。
* Authenticationでユーザー登録、ログイン
* データ追加時に、userIdを含めて登録
* セキュリティルールでuserIdと合っているか確認
* 取得時whereでフィルタリングする

## Authenticationでユーザー登録、ログイン
ユーザーが作成した自分のデータのみ取得する機能の前提として、Firebase Authenticationでログインしている必要があります。
なので、まずAuthenticationでユーザー登録、ログインを実装しないといけないです。
もし、実装これからだという人は下記の記事を参考にFlutter側の実装をしてみてください。

https://qiita.com/MLLB/items/c4066203f6534e57370b

## データ追加時に、userIdを含めて登録
今回の例ではTodoを追加して、自分が追加したTodoしか見れないようにしようと思います。
追加するTodoは名前とユーザーIDを持ちます。

![スクリーンショット 2023-08-09 16.57.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2475427/f0bb528e-ad1d-5a77-b27d-4eca993f8be8.png)

セキュリティルールと取得する時のフィルタリングで、ユーザーIDで制限することになりますので、ユーザーIDの情報が必要です。

Flutter側からデータを追加するコードは下記になります。
Authenticationでログインしている前提のコードなので、ログイン後にこのコードを呼ぶように注意してください。
```dart
try {
  await FirebaseFirestore.instance.collection('todos').add({
    // TextFieldを使って名前を入力しているので_nameController.text使ってます。
    'name': _nameController.text,
    'userId': FirebaseAuth.instance.currentUser!.uid,
  });
  if (!mounted) return;
  Navigator.of(context).pop();
} catch (e) {
  print(e.toString());
}
```

## セキュリティルールでuserIdと合っているか確認
続いてはセキュリティルールです。
今回一番時間がかかった部分です。
初めてだったので、文法などがわからず困りました。

ログインしているユーザーのデータしか見れないように制限するセキュリテイルールは以下です。
解説はコメントに入れていて、上から順に読んでいって理解できると思います。
```
rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    // ↑この辺まではお作法

    // 関数が定義できる
    // 認証済みかを取得する
    function isAuthenticated() {
        // request.authには認証したユーザーの情報が入っている
        // 認証していない（非ログイン）だとnullになる
    	return request.auth != null;
    }
        
    // match /コレクション名/ドキュメントIDとかくと、対象のドキュメントにアクセスできる
    // {todoId}はワイルドカードで、すべてのドキュメントIDの参照となる
    // つまり、todosに追加したドキュメント全てにルールが適用される
    match /todos/{todoId} {
      // allow 操作: if 条件で、操作できるかどうかを制限できる
      // allow readの場合、読み取りができるかどうかを制限できる
      allow read: if
        // resource.dataでアクセスしようとするデータを参照できる
        // （アクセスしようとするデータについては別項で詳細説明を書こうと思います）
        // 今回の例のtodosの場合、nameとuserIdにアクセスできます
        // request.auth.uidで認証中のユーザーのIDが取得できるので、
        // アクセスデータのuserIdと等しければ操作を許可します
      	resource.data.userId == request.auth.uid;

      // allow createの場合、作成・追加ができるかどうかを制限できる
      // 追加は認証していれば可能とします
      allow create: if isAuthenticated();
    }
  }
}
```

セキュリテイルールについては、以下の記事（本）が良さそうでした。
全部読むには有料ですが、がっつり学べそう。
何かに詰まったら、買ってみようかな。

https://zenn.dev/sgr_ksmt/books/2f83a604d636b241cf3c/viewer/9dc840cb40394835afc6

この項としては書くこと以上なのですが、せっかくなので学んだことを下記に書いておこうかなと思います。
実装進めたい人は飛ばしてください。

### 操作について
`allow read`のように、対象のデータに対して操作を許可するかどうかでセキュリティルールを作っていきますが、
read ルールは get と list に分割でき、write ルールは create、update、delete に分割できます。
下記のように、各操作ごとにルールを決めれるということですね。

```
match /todos/{todoId} {
  allow get: if
    true;
  allow list: if
    false;
  allow create: if 
    true;
  allow update: if 
    false;
  allow delete: if 
    false;
}
```

逆に、writeでルールを書くと、そのルールが作成・更新・削除のすべての操作に適用されます。
作成はログインしたユーザーなら誰でもOK、更新・削除はTodoを作成したユーザーのみ行えるようにしたい場合は、writeでルールを書くのではなく、個別に定義しないといけないです。
条件が同じ場合は、まとめて書くこともできます。
`allow update, delete: if ...`


## 取得時whereでフィルタリングする
こちらハマったポイントだったのですが、Flutterだと下記のようなコードでTodo一覧が取れます。

```dart
FirebaseFirestore.instance
  .collection('todos')
  .snapshots()
```

ただこれで取得すると、すべてのTodoにアクセスするので、以下のルールで拒否されてエラーになってしまいます。
```
allow read: if
  resource.data.userId == request.auth.uid;
```

私が、セキュリティルールを勘違いしていたが悪いのですが、セキュリティルールはアクセスを許可するかどうかであって、フィルタリングするわけではないんですね。
なので、以下のように取得するときはフィルタリングする必要があります。

```dart
FirebaseFirestore.instance
  .collection('todos')
  .where('userId', isEqualTo: userId)
  .snapshots(),
```

下記のコードを参考に表示してみてください。

```dart
final userId = FirebaseAuth.instance.currentUser!.uid;
    print(userId);

    return StreamBuilder(
      stream: FirebaseFirestore.instance
          .collection('todos')
          .where('userId', isEqualTo: userId)
          .snapshots(),
      builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
        if (snapshot.hasError) {
          return Text(snapshot.error.toString());
        }

        if (snapshot.connectionState == ConnectionState.waiting) {
          return const CircularProgressIndicator();
        }

        final docs = snapshot.data!.docs;
        return ListView.builder(
          itemBuilder: (context, index) {
            final doc = docs[index];
            final data = doc.data()! as Map<String, dynamic>;
            return ListTile(
                onTap: () {},
                title: Text(data['name']),
              );
          },
          itemCount: docs.length,
        );
      },
    );
```

# さいごに
ピンポイントで悩みを解決できる記事がなかったので書いてみました。
参考になったら幸いです。
自分の認識違いの箇所もあると思うので、もしあればコメントしただけるとありがたいです。