FirebaseのデータベースからローカルPCにデータを保存する

アプリを使って、ユーザからのデータを取得し、ローカルPCにデータを保存する方法について、調べてみました。

FirebaseとGoogle CloudのBigQueryとを連携するとプログラムなしに簡単に処理できそうです。

なお、今回は実験用データの取得を念頭に、一連の処理を試してみましたが、当然のことながら個人情報の保護の観点から、データの暗号化、二段階認証、適切なアクセス権の設定など、扱うデータによって必要なセキュリティ対策をする必要があることをお伝えしておきます。

今回は、以下の流れとなります。

1)FlutterのWebアプリから入力されたテキストを、FirebaseのFirestoreに送信してデータを保存する。

2)FirestoreのデータをGoogle CloudのCloudStorageを経由して、BigQueryに送る。

3)BigQueryでクエリを実行して結果を保存する。

WebアプリからFirestoreへデータの保存

ユーザ情報を特定するために、今回はGoogle認証を使いました。Google認証の詳細については、以下の記事を参考にして下さい。

今回は、上記記事のchat.dartを修正してhome.dartとし、firestoreへの書き込みを実装してみました。

import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'home.dart';

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

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

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

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<LoginPage> createState() => LoginPageState();
}

class LoginPageState extends State<LoginPage> {
  // Googleアカウントの表示名
  String infoText = '';

  // 公式ページのコードをそのままコピー
  Future<UserCredential> signInWithGooglePopup() async {
    // Create a new provider
    GoogleAuthProvider googleProvider = GoogleAuthProvider();
    googleProvider
        .addScope('https://www.googleapis.com/auth/contacts.readonly');
    googleProvider.setCustomParameters({'login_hint': 'user@example.com'});
    return await FirebaseAuth.instance.signInWithPopup(googleProvider);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
        TextButton(
          onPressed: () async {
            try {
              await signInWithGooglePopup();
              final User? user = FirebaseAuth.instance.currentUser;
              if (user != null) {
                await Navigator.of(context).pushReplacement(
                  MaterialPageRoute(builder: (context) {
                    return HomePage(user);
                  }),
                );
              }
            } catch (e) {
              setState(() {
                infoText = 'Loginに失敗しました:${e.toString()}';
              });
            }
          },
          child: const Text(
            'login',
            style: TextStyle(fontSize: 50),
          ),
        ),
        Text(infoText), // loginされなかった時のメッセージを表示
      ]),
    ));
  }
}

import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter/material.dart';
import 'main.dart';

class HomePage extends StatelessWidget {
  HomePage(this.user);
  final User user;

  final TextEditingController _messageController = TextEditingController();

  void _sendMessage() {
    final String message = _messageController.text;
    final String userId = user.uid;
    final DateTime now = DateTime.now();

    final CollectionReference messages =
        FirebaseFirestore.instance.collection('messages');

    messages.add({
      'message': message,
      'userId': userId,
      'timestamp': now,
    });

    _messageController.clear();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('HOME'),
        actions: <Widget>[
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () async {
              await FirebaseAuth.instance.signOut();
              await Navigator.of(context).pushReplacement(
                MaterialPageRoute(builder: (context) {
                  return LoginPage(title: 'Login Page');
                }),
              );
            },
          ),
        ],
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            Expanded(
              child: StreamBuilder<QuerySnapshot>(
                stream: FirebaseFirestore.instance
                    .collection('messages')
                    .where('userId', isEqualTo: user.uid) // userIdが本人と同じもののみを取得
                    .snapshots(),
                builder: (context, snapshot) {
                  if (snapshot.hasData) {
                    final messages = snapshot.data!.docs;
                    return ListView.builder(
                      itemCount: messages.length,
                      itemBuilder: (context, index) {
                        final message = messages[index];
                        final String messageText = message['message'];
                        final String userId = message['userId'];
                        final Timestamp timestamp = message['timestamp'];
                        final DateTime dateTime = timestamp.toDate();

                        return ListTile(
                          title: Text(messageText),
                          subtitle:
                              Text('User ID: $userId\nTimestamp: $dateTime'),
                        );
                      },
                    );
                  } else {
                    return Center(child: CircularProgressIndicator());
                  }
                },
              ),
            ),
            SizedBox(height: 16.0),
            TextField(
              controller: _messageController,
              decoration: InputDecoration(
                hintText: 'メッセージを入力してください',
              ),
            ),
            SizedBox(height: 8.0),
            ElevatedButton(
              onPressed: _sendMessage,
              child: Text('送信'),
            ),
          ],
        ),
      ),
    );
  }
}

アプリからメッセージを送信すると、日付とuserIDとともにmessageがFirestoreに保存されます。

動作確認が終わったら、webアプリとしてFirebaseにホスティングしておきます。ホスティングの方法は以下の記事を参考にして下さい。

出来上がったアプリから登録したデータは以下のようになります。

FirestoreからCloud Storage

Firebaseのコンソールにもクエリビルダーがあります。ちょっとしたデータの確認などは、この画面から可能です。

ただ、大量のデータを分析する時には、このままではできませんので、エクスポートしたいと思います。

パネルビューの右肩の「Google cloudのその他の機能」から「インポート/エクスポート」を選択すると、GoogleCloudのfirestoreコンソールに遷移します。

対象となるコレクションを選択して、エクスポートします。

エクスポートはCloud Strageに対してのみ行われます。必要に応じてパケットを作成します。

最後にエクスポートをクリックすると、Cloud Storageにデータが転送されます。

Cloud Storageから BigQuery

まず、Cloud Storageで設定したパケットにデータが転送されていることを確認します。

次に、BigQueryのコンソールに移動します。

独自データの追加から「Google Cloud Storage」のデータを追加するをクリックします。

テーブルの作成については、以下の公式サイトを参考にしました。

パケットのファイルを選択する際には、間違えないように注意が必要です。

BigQueryでクエリを作成してローカルファイルとして保存

BigQueryのデータを直接エクスポートできるのは、Cloud Storageのみです。

ローカルファイルにダウンロードしたい場合には、クエリを作成して「結果を保存」します。

メニューから所望の形式を選択して保存を実行します。