Google Cloud Platformに戻る

Cloud Firestore

Cloud Firestoreとは

Cloud Firestoreは、Google Cloud Platform (GCP) が提供するフルマネージドのNoSQLドキュメントデータベースサービスです。モバイル、ウェブ、サーバーアプリケーション向けに設計されており、リアルタイムデータ同期、オフラインサポート、強力なクエリ機能を備えています。Firestoreは、Firebase(Googleのモバイルアプリ開発プラットフォーム)とGoogle Cloudの両方から利用できるため、幅広いアプリケーション開発シナリオに対応できます。

Cloud Firestoreの主な特徴

Cloud Firestoreのアーキテクチャ

Cloud Firestoreは以下の主要コンポーネントで構成されています:

コンポーネント 説明
コレクション ドキュメントのコンテナ。SQLのテーブルに相当するが、スキーマは強制されない
ドキュメント データを格納する基本単位。キーと値のペアで構成され、様々なデータ型をサポート
サブコレクション ドキュメント内に含まれるコレクション。階層的なデータ構造を作成可能
フィールド ドキュメント内の個々のデータ要素。名前と型付きの値で構成
リファレンス コレクションやドキュメントへの参照。他のドキュメントへのリンクを作成可能
インデックス クエリのパフォーマンスを向上させるためのデータ構造

Cloud Firestore と他のデータベースの違い

Cloud Firestoreと他のデータベースサービスには以下のような違いがあります:

Cloud Firestoreのデータモデル

Firestoreのデータモデルは、コレクションとドキュメントの階層構造に基づいています:

ドキュメント構造

ドキュメントは、以下のデータ型をサポートするフィールドの集合です:

ドキュメント構造の例

{
  "id": "user123",
  "name": "山田太郎",
  "email": "yamada@example.com",
  "age": 30,
  "isActive": true,
  "createdAt": Timestamp(2023, 1, 15, 10, 30, 0),
  "location": GeoPoint(35.6812, 139.7671),
  "tags": ["premium", "verified"],
  "profile": {
    "bio": "エンジニア",
    "company": "テック株式会社",
    "website": "https://example.com"
  },
  "friendRef": DocumentReference("users/friend456")
}

コレクションとサブコレクション

Firestoreのデータは以下のような階層構造で編成されます:

データ階層の例

データベース
└── users(コレクション)
    └── user123(ドキュメント)
        ├── name: "山田太郎"
        ├── email: "yamada@example.com"
        └── orders(サブコレクション)
            └── order456(ドキュメント)
                ├── date: 2023-05-10
                ├── amount: 5000
                └── items(サブコレクション)
                    └── item789(ドキュメント)
                        ├── name: "商品A"
                        └── price: 2500

Cloud Firestoreの使用方法

Cloud Firestoreは、Google Cloud Console、Firebase Console、クライアントライブラリ、またはRESTful APIを使用して利用できます。

データベースの作成

Google Cloud ConsoleまたはFirebase Consoleから新しいFirestoreデータベースを作成できます:

  1. Google Cloud ConsoleまたはFirebase Consoleにログイン
  2. プロジェクトを選択または作成
  3. Firestoreを選択し、「データベースの作成」をクリック
  4. ロケーションモード(単一リージョンまたはマルチリージョン)を選択
  5. リージョンを選択(例:asia-northeast1)
  6. 「作成」をクリックしてデータベースを作成

クライアントライブラリを使用したデータの操作

Node.jsでのドキュメント作成例

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function addDocument() {
  try {
    const docRef = firestore.collection('users').doc('user123');
    await docRef.set({
      name: '山田太郎',
      email: 'yamada@example.com',
      age: 30,
      createdAt: Firestore.Timestamp.now()
    });
    console.log('ドキュメントが正常に作成されました');
  } catch (error) {
    console.error('エラー:', error);
  }
}

addDocument();

Node.jsでのドキュメント取得例

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function getDocument() {
  try {
    const docRef = firestore.collection('users').doc('user123');
    const doc = await docRef.get();
    
    if (doc.exists) {
      console.log('ドキュメントデータ:', doc.data());
    } else {
      console.log('ドキュメントが存在しません');
    }
  } catch (error) {
    console.error('エラー:', error);
  }
}

getDocument();

Webアプリケーションでの使用例(JavaScript)

// Firebaseの初期化
import { initializeApp } from "firebase/app";
import { getFirestore, collection, doc, setDoc, getDoc, query, where, getDocs } from "firebase/firestore";

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT_ID.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT_ID.appspot.com",
  messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
  appId: "YOUR_APP_ID"
};

// Firebaseアプリの初期化
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

// ドキュメントの作成
async function addUser() {
  try {
    await setDoc(doc(db, "users", "user123"), {
      name: "山田太郎",
      email: "yamada@example.com",
      age: 30,
      createdAt: new Date()
    });
    console.log("ユーザーが追加されました");
  } catch (error) {
    console.error("エラー:", error);
  }
}

// ドキュメントの取得
async function getUser() {
  try {
    const docRef = doc(db, "users", "user123");
    const docSnap = await getDoc(docRef);
    
    if (docSnap.exists()) {
      console.log("ドキュメントデータ:", docSnap.data());
    } else {
      console.log("ドキュメントが存在しません");
    }
  } catch (error) {
    console.error("エラー:", error);
  }
}

// クエリの実行
async function queryUsers() {
  try {
    const usersRef = collection(db, "users");
    const q = query(usersRef, where("age", ">=", 20));
    const querySnapshot = await getDocs(q);
    
    querySnapshot.forEach((doc) => {
      console.log(doc.id, " => ", doc.data());
    });
  } catch (error) {
    console.error("エラー:", error);
  }
}

Androidでの使用例(Kotlin)

// Firebaseの初期化
val db = FirebaseFirestore.getInstance()

// ドキュメントの作成
fun addUser() {
    val user = hashMapOf(
        "name" to "山田太郎",
        "email" to "yamada@example.com",
        "age" to 30,
        "createdAt" to FieldValue.serverTimestamp()
    )
    
    db.collection("users").document("user123")
        .set(user)
        .addOnSuccessListener { 
            Log.d(TAG, "ドキュメントが正常に作成されました") 
        }
        .addOnFailureListener { e -> 
            Log.w(TAG, "エラー", e) 
        }
}

// ドキュメントの取得
fun getUser() {
    val docRef = db.collection("users").document("user123")
    docRef.get()
        .addOnSuccessListener { document ->
            if (document != null && document.exists()) {
                Log.d(TAG, "ドキュメントデータ: ${document.data}")
            } else {
                Log.d(TAG, "ドキュメントが存在しません")
            }
        }
        .addOnFailureListener { e ->
            Log.w(TAG, "エラー", e)
        }
}

// リアルタイムリスナーの設定
fun addRealtimeListener() {
    db.collection("users")
        .whereGreaterThanOrEqualTo("age", 20)
        .addSnapshotListener { snapshot, e ->
            if (e != null) {
                Log.w(TAG, "リスナーエラー", e)
                return@addSnapshotListener
            }
            
            if (snapshot != null) {
                for (document in snapshot.documents) {
                    Log.d(TAG, "${document.id} => ${document.data}")
                }
            }
        }
}

クエリの実行

Firestoreでは、様々な条件でデータをクエリできます:

基本的なクエリ例(Node.js)

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function queryData() {
  try {
    // 単純なクエリ
    const usersRef = firestore.collection('users');
    const snapshot = await usersRef.where('age', '>=', 20)
                                  .where('isActive', '==', true)
                                  .orderBy('age', 'desc')
                                  .limit(10)
                                  .get();
    
    if (snapshot.empty) {
      console.log('一致するドキュメントがありません');
      return;
    }
    
    snapshot.forEach(doc => {
      console.log(doc.id, '=>', doc.data());
    });
    
    // 複合クエリ(複合インデックスが必要)
    const complexSnapshot = await usersRef.where('tags', 'array-contains', 'premium')
                                         .where('age', '>=', 20)
                                         .orderBy('age')
                                         .orderBy('name')
                                         .get();
    
    complexSnapshot.forEach(doc => {
      console.log('複合クエリ結果:', doc.id, '=>', doc.data());
    });
  } catch (error) {
    console.error('エラー:', error);
    // インデックスエラーの場合、コンソールに表示されるURLからインデックスを作成できます
  }
}

queryData();

トランザクションとバッチ処理

Firestoreでは、複数の操作を原子的に実行するためのトランザクションとバッチ処理をサポートしています:

トランザクションの例(Node.js)

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function transferPoints() {
  try {
    // ユーザー間でポイントを移動するトランザクション
    await firestore.runTransaction(async (transaction) => {
      const fromUserRef = firestore.collection('users').doc('user1');
      const toUserRef = firestore.collection('users').doc('user2');
      
      // ドキュメントを読み取り
      const fromUserDoc = await transaction.get(fromUserRef);
      const toUserDoc = await transaction.get(toUserRef);
      
      if (!fromUserDoc.exists || !toUserDoc.exists) {
        throw new Error('ユーザーが存在しません');
      }
      
      const fromUserPoints = fromUserDoc.data().points || 0;
      const toUserPoints = toUserDoc.data().points || 0;
      const transferAmount = 100;
      
      if (fromUserPoints < transferAmount) {
        throw new Error('ポイントが不足しています');
      }
      
      // ドキュメントを更新
      transaction.update(fromUserRef, { 
        points: fromUserPoints - transferAmount 
      });
      transaction.update(toUserRef, { 
        points: toUserPoints + transferAmount 
      });
      
      return { success: true };
    });
    
    console.log('トランザクションが成功しました');
  } catch (error) {
    console.error('トランザクションエラー:', error);
  }
}

transferPoints();

バッチ処理の例(Node.js)

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function batchUpdate() {
  try {
    // バッチ処理で複数のドキュメントを更新
    const batch = firestore.batch();
    
    // 複数のユーザーのステータスを更新
    const users = ['user1', 'user2', 'user3'];
    users.forEach(userId => {
      const userRef = firestore.collection('users').doc(userId);
      batch.update(userRef, { status: 'active', lastUpdated: Firestore.Timestamp.now() });
    });
    
    // 新しいお知らせを作成
    const noticeRef = firestore.collection('notices').doc();
    batch.set(noticeRef, {
      title: 'システムメンテナンスのお知らせ',
      content: '明日の午前2時からメンテナンスを実施します。',
      createdAt: Firestore.Timestamp.now()
    });
    
    // バッチをコミット
    await batch.commit();
    console.log('バッチ処理が成功しました');
  } catch (error) {
    console.error('バッチ処理エラー:', error);
  }
}

batchUpdate();

Cloud Firestoreのセキュリティ

Firestoreでは、データへのアクセスを制御するための複数のセキュリティ機能が提供されています:

セキュリティルール

Firebaseセキュリティルールを使用して、データへのアクセスを詳細に制御できます:

基本的なセキュリティルールの例

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 認証済みユーザーのみがデータを読み取り可能
    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow write: if request.auth != null && request.auth.uid == userId;
    }
    
    // 公開データは誰でも読み取り可能だが、書き込みは管理者のみ
    match /public/{document=**} {
      allow read: if true;
      allow write: if request.auth != null && request.auth.token.admin == true;
    }
    
    // データ検証を含むルール
    match /posts/{postId} {
      allow read: if true;
      allow create: if request.auth != null 
                    && request.resource.data.authorId == request.auth.uid
                    && request.resource.data.title.size() <= 100
                    && request.resource.data.content.size() <= 20000;
      allow update: if request.auth != null 
                    && resource.data.authorId == request.auth.uid;
      allow delete: if request.auth != null 
                    && resource.data.authorId == request.auth.uid;
    }
  }
}

Identity and Access Management (IAM)

Google Cloud IAMを使用して、プロジェクトレベルでのアクセス制御を設定できます:

gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:my-service-account@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/datastore.user

VPCサービスコントロール

VPCサービスコントロールを使用して、Firestoreへのアクセスをセキュリティ境界内に制限できます。

暗号化

Firestoreのデータは、保存時と転送時に自動的に暗号化されます。さらに、カスタマー管理の暗号化キー(CMEK)を使用することもできます。

Cloud Firestoreのパフォーマンス最適化

Firestoreを効率的に使用するためのパフォーマンス最適化のベストプラクティス:

インデックス管理

Firestoreは自動的に単一フィールドのインデックスを作成しますが、複合クエリには複合インデックスが必要です:

firestore.indexes.jsonの例

{
  "indexes": [
    {
      "collectionGroup": "users",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "status", "order": "ASCENDING" },
        { "fieldPath": "age", "order": "DESCENDING" }
      ]
    },
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "tags", "arrayConfig": "CONTAINS" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ],
  "fieldOverrides": []
}

データ構造の最適化

クエリの最適化

ページネーションの実装例(Node.js)

const { Firestore } = require('@google-cloud/firestore');
const firestore = new Firestore();

async function paginatedQuery(lastDoc = null, pageSize = 10) {
  try {
    let query = firestore.collection('posts')
                        .orderBy('createdAt', 'desc')
                        .limit(pageSize);
    
    // 前のページの最後のドキュメントがある場合、そこから開始
    if (lastDoc) {
      query = query.startAfter(lastDoc);
    }
    
    const snapshot = await query.get();
    const posts = [];
    
    snapshot.forEach(doc => {
      posts.push({
        id: doc.id,
        ...doc.data()
      });
    });
    
    // 次のページのために最後のドキュメントを返す
    const lastVisible = snapshot.docs[snapshot.docs.length - 1];
    
    return {
      posts,
      lastDoc: lastVisible
    };
  } catch (error) {
    console.error('ページネーションエラー:', error);
    return { posts: [], lastDoc: null };
  }
}

// 使用例
async function fetchAllPages() {
  let lastDoc = null;
  let hasMoreData = true;
  let pageNumber = 1;
  
  while (hasMoreData) {
    console.log(`ページ ${pageNumber} を取得中...`);
    const result = await paginatedQuery(lastDoc);
    
    if (result.posts.length === 0) {
      hasMoreData = false;
      console.log('すべてのデータを取得しました');
    } else {
      console.log(`${result.posts.length} 件のデータを取得しました`);
      lastDoc = result.lastDoc;
      pageNumber++;
    }
  }
}

Cloud Firestoreのベストプラクティス

コスト最適化のヒント

Cloud Firestoreとの統合サービス

Firestoreは他のGoogle Cloudサービスと統合して、より強力なソリューションを構築できます:

サービス 統合の利点
Firebase Authentication ユーザー認証とFirestoreセキュリティルールの連携によるアクセス制御
Cloud Functions Firestoreのデータ変更をトリガーとしたサーバーレス関数の実行
Cloud Run Firestoreと連携したマイクロサービスの実装
App Engine Firestoreをバックエンドとしたウェブアプリケーションの構築
BigQuery Firestoreデータのエクスポートと高度な分析
Cloud Storage 大容量ファイルの保存とFirestoreでのメタデータ管理
Pub/Sub Firestoreの変更に基づくイベント駆動型アーキテクチャの実装

Cloud Functionsとの統合例

Firestoreトリガー関数の例(Node.js)

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

// ドキュメントが作成されたときに実行される関数
exports.onUserCreate = functions.firestore
  .document('users/{userId}')
  .onCreate((snapshot, context) => {
    const userData = snapshot.data();
    const userId = context.params.userId;
    
    console.log(`新しいユーザーが作成されました: ${userId}`);
    
    // ユーザー作成時に歓迎メッセージを送信
    return admin.firestore().collection('notifications').add({
      userId: userId,
      message: `${userData.name}さん、ようこそ!`,
      createdAt: admin.firestore.FieldValue.serverTimestamp(),
      read: false
    });
  });

// ドキュメントが更新されたときに実行される関数
exports.onOrderStatusUpdate = functions.firestore
  .document('orders/{orderId}')
  .onUpdate((change, context) => {
    const newValue = change.after.data();
    const previousValue = change.before.data();
    const orderId = context.params.orderId;
    
    // 注文ステータスが変更された場合のみ処理
    if (newValue.status === previousValue.status) {
      return null;
    }
    
    console.log(`注文 ${orderId} のステータスが ${previousValue.status} から ${newValue.status} に変更されました`);
    
    // ステータス変更を履歴に記録
    return admin.firestore().collection('orders').doc(orderId).collection('statusHistory').add({
      from: previousValue.status,
      to: newValue.status,
      changedAt: admin.firestore.FieldValue.serverTimestamp()
    });
  });

BigQueryとの統合

FirestoreのデータをBigQueryにエクスポートして分析できます:

gcloud firestore export gs://my-bucket/my-export

# BigQueryにインポート
bq load \
  --source_format=DATASTORE_BACKUP \
  my_dataset.users \
  gs://my-bucket/my-export/all_namespaces/kind_users/all_namespaces_kind_users.export_metadata

Cloud Firestoreのユースケース

Firestoreは以下のようなユースケースに適しています:

ユースケース例:チャットアプリケーション

チャットアプリのデータモデル

// コレクション構造
chats/
  chatId1/
    messages/
      messageId1: {
        text: "こんにちは",
        userId: "user123",
        createdAt: Timestamp,
        read: false
      }
      messageId2: {...}
    members/
      user123: {
        displayName: "山田太郎",
        joinedAt: Timestamp,
        role: "admin"
      }
      user456: {...}

users/
  user123/
    displayName: "山田太郎"
    email: "yamada@example.com"
    photoURL: "https://example.com/photo.jpg"
    status: "online"
    lastActive: Timestamp
    chats/
      chatId1: true
      chatId2: true

リアルタイムチャットの実装例(JavaScript)

import { initializeApp } from "firebase/app";
import { getFirestore, collection, doc, addDoc, query, orderBy, limit, onSnapshot } from "firebase/firestore";

// Firebaseの初期化
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

// メッセージの送信
async function sendMessage(chatId, userId, text) {
  try {
    await addDoc(collection(db, "chats", chatId, "messages"), {
      text: text,
      userId: userId,
      createdAt: new Date(),
      read: false
    });
    console.log("メッセージが送信されました");
  } catch (error) {
    console.error("メッセージ送信エラー:", error);
  }
}

// リアルタイムでメッセージを受信
function subscribeToMessages(chatId, callback) {
  const messagesRef = collection(db, "chats", chatId, "messages");
  const q = query(messagesRef, orderBy("createdAt"), limit(50));
  
  return onSnapshot(q, (snapshot) => {
    const messages = [];
    snapshot.forEach((doc) => {
      messages.push({
        id: doc.id,
        ...doc.data()
      });
    });
    callback(messages);
  }, (error) => {
    console.error("メッセージ受信エラー:", error);
  });
}

// 使用例
const unsubscribe = subscribeToMessages("chatId1", (messages) => {
  console.log("新しいメッセージを受信:", messages);
  // UIを更新
  updateChatUI(messages);
});

// コンポーネントのアンマウント時にリスナーをデタッチ
function cleanup() {
  unsubscribe();
}

まとめ

Cloud Firestore は、Google Cloud Platform上でのNoSQLドキュメントデータベースサービスとして、モバイル、ウェブ、サーバーアプリケーション向けに設計されています。リアルタイムデータ同期、オフラインサポート、強力なクエリ機能を備え、スケーラブルで信頼性の高いデータベースソリューションを提供します。

Firestoreの主な利点は以下の通りです:

Firestoreを効果的に活用するには、適切なデータモデルの設計、セキュリティルールの設定、パフォーマンス最適化のベストプラクティスの適用が重要です。また、アプリケーションの要件に基づいて、他のデータベースサービス(Cloud SQL、Bigtable、Spanner など)との適切な使い分けも検討すべきです。