🐶

Cloud Firestoreの自動ドキュメントIDを取得してデータを削除する

2024/03/22に公開2

Cloud Firestore登録データの削除機能を作るため公式ドキュメントを見たが

いや、「DC」なんてドキュメントIDのドキュメントを登録していないのだが・・・

db.collection("cities").doc("DC").delete().then(
      (doc) => print("Document deleted"),
      onError: (e) => print("Error updating document $e"),
    );

https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja

自動ドキュメントIDを取得して削除はできないのか

Cloud Firestoreにデータを登録するときはドキュメントIDが自動で割り当てられる。ランダムな英字20桁くらいのものだ。
試しにRecipeクラスのオブジェクトrecipeに.idをつけてみたが、「そんなgetterありません」と言われてしまった。自動で取得できるんじゃないの?
ちなみにrecipe.nameだとちゃんとレシピ名が取得できる状態なのでオブジェクトの問題ではなさそう・・・

try {
    final db = FirebaseFirestore.instance;
    await db.collection('recipe').doc(recipe.id).delete();
    Navigator.of(context).pop();
    Navigator.of(context).pop();
} catch (e) {
    print(e);
}
flutter: NoSuchMethodError: Class 'Recipe' has no instance getter 'id'.

答え

Recipeクラスの方にidフィールドを追加したらrecipe.idでドキュメントIDが取得できるようになりました。
わかってしまえば「なんだ、そんなことか〜」という感じですが、意外と参考情報がなかったので共有しておきます。

Recipe.dart
import 'package:cloud_firestore/cloud_firestore.dart';

class Recipe {
  // ↓id要定義
  final String id;
  final String uid;
  final String name;
  final Timestamp date;
  final String picture;
  final int category;
  final int style;

  Recipe({
    // ↓id要定義
    required this.id,
    required this.uid,
    required this.name,
    required this.date,
    required this.picture,
    required this.category,
    required this.style,
  });

  factory Recipe.fromFirestore(DocumentSnapshot doc) {
    final data = doc.data() as Map<String, dynamic>;
    return Recipe(
      // ↓id要定義
      id: doc.id,
      uid: data['uid'],
      name: data['name'],
      date: data['date'],
      picture: data['picture'],
      category: data['category'],
      style: data['style'],
    );
  }
}

Discussion

JboyHashimotoJboyHashimoto

DocumentIDを取得する方法それは、DocumentSnapshotを使うこと。withConverter使ったら楽ですけどね。やり方は色々。
snapshotの中に、idが存在する。

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:todo_app/domain/todo.dart';
// todoコレクションを操作するwithConverter

final todoWithConverter = FirebaseFirestore.instance
    .collection('todo')
    .withConverter<Todo>(
      fromFirestore: (snapshot, _) {
        final data = snapshot.data()!;
        data['id'] = snapshot.id;
        return Todo.fromJson(data);
      },
      toFirestore: (todo, _) => todo.toJson(),
    );

私なら、こんな感じで削除のロジックを書きますね。

import 'package:todo_app/domain/todo.dart';
import 'package:todo_app/domain/todo_converter.dart';

abstract interface class TodoAPI {
  Stream<List<Todo>> getTodo();
  Future<void> addTodo(Todo todo);
  Future<void> editTodo(Todo todo);
  Future<void> deleteTodo(Todo todo);
}

class TodoAPIImpl implements TodoAPI {

  
  Stream<List<Todo>> getTodo() {
    return todoWithConverter.snapshots().map((snapshot) {
      return snapshot.docs.map((doc) => doc.data()).toList();
    });
  }

  
  Future<void> addTodo(Todo todo) {
    return todoWithConverter.add(todo);
  }

  
  Future<void> editTodo(Todo todo) {
    return todoWithConverter.doc(todo.id).update(todo.toJson());
  }

  
  Future<void> deleteTodo(Todo todo) {
    return todoWithConverter.doc(todo.id).delete();
  }
}

View側で、DocumentIDを取得すれば、削除はできますね。モデルクラス作って、メンバー変数のidを渡せば、指定できます。withConverter使えば編集のfunctionにidを渡して、別のページで更新もできるかな。

コンストラクターで編集ページへDocumentIDを渡す。

import 'package:flutter/material.dart';
import 'package:todo_app/application/todo_api.dart';
import 'package:todo_app/domain/todo.dart';

class EditTodoView extends StatefulWidget {
  const EditTodoView({super.key, required this.todo});

  final Todo todo;

  
  State<EditTodoView> createState() => _EditTodoViewState();
}

class _EditTodoViewState extends State<EditTodoView> {
  final _titleController = TextEditingController();

  // nullだったらエラーを出すgetter
  String? get _title =>
      _titleController.text.isNotEmpty ? _titleController.text : null;

  
  void dispose() {
    _titleController.dispose();
    super.dispose();
  }

  
  Widget build(BuildContext context) {
    final todoAPI = TodoAPIImpl();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Todo App'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          children: [
            TextField(
              controller: _titleController,
              decoration: const InputDecoration(
                labelText: 'Title',
              ),
            ),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () async {
                if (_title != null) {
                  final todoId = widget.todo.id;
                  final todo = Todo(
                    id: todoId,
                    title: _title,
                  );
                  await todoAPI.editTodo(todo);
                  if (context.mounted) {
                    Navigator.of(context).pop();
                  }
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Formに値を入力してください'),
                    ),
                  );
                }
              },
              child: const Text('編集'),
            ),
          ],
        ),
      ),
    );
  }
}

全体のコード

ご興味あれば試してみてください。