データベース

Firebase Firestore

概要

Firebase Firestoreは、Googleが提供するクラウド型のNoSQLドキュメントデータベースです。リアルタイム同期、オフライン対応、自動スケーリングを特徴とし、モバイル、Web、サーバーアプリケーション開発に最適化されています。Firebase Realtime Databaseの後継として設計され、より柔軟なクエリ機能とサーバーレスアーキテクチャを提供します。

詳細

Firebase Firestoreは2017年にGoogleによって発表され、Firebase Realtime Databaseの進化版として開発されました。以下の特徴を持ちます。

主要特徴

  • リアルタイム同期: クライアント間でのデータ自動同期
  • オフライン対応: ネットワーク接続がなくても動作継続
  • 自動スケーリング: サーバーレスアーキテクチャによる無制限スケーリング
  • 豊富なクエリ: 複合インデックス、配列クエリ、地理位置クエリをサポート
  • ACID トランザクション: 複数ドキュメントにわたる原子性保証
  • マルチリージョン: 世界各地でのデータ複製と低レイテンシアクセス
  • セキュリティルール: 細かなアクセス制御とデータ検証
  • バッチ操作: 複数の読み書き操作を効率的に実行

2024年の新機能

  • ベクター検索: AI・機械学習アプリケーション向けのセマンティック検索
  • MongoDB互換性: 既存のMongoDBコードとドライバーをそのまま利用可能
  • 集約クエリ: SUM、AVERAGE、COUNTなどの集約関数をサポート
  • LangChain統合: RAG(Retrieval-Augmented Generation)アーキテクチャ対応
  • パフォーマンス向上: 読み取り・書き込み性能の最適化

データモデル

  • 階層構造: コレクション > ドキュメント > サブコレクション
  • スキーマレス: 柔軟なJSONライクドキュメント構造
  • ネストした配列・オブジェクト: 複雑なデータ構造をサポート
  • 参照型: ドキュメント間の関連付け
  • 地理位置データ: GeoPointとGeoHashによる位置情報処理

セキュリティ

  • Firebase Authentication統合: ユーザー認証との連携
  • セキュリティルール: 宣言的なアクセス制御
  • 暗号化: 保存時・転送時の自動暗号化
  • 監査ログ: Cloud Loggingとの統合

メリット・デメリット

メリット

  • 開発速度: サーバーレスでバックエンド構築不要
  • リアルタイム性: 即座にデータ変更が全クライアントに反映
  • オフライン対応: ネットワーク断絶時も継続動作
  • 自動スケーリング: トラフィック増加に自動対応
  • 豊富なSDK: iOS、Android、Web、サーバー向けネイティブSDK
  • Firebaseエコシステム: Authentication、Analytics、Hostingとの統合
  • 99.999% SLA: 業界最高水準の可用性保証
  • 無料枠: 開発・小規模利用に十分な無料制限

デメリット

  • ベンダーロックイン: Firebase/Google Cloud依存
  • コスト: 大規模利用時の料金が高額
  • 複雑なクエリ制限: JOINやサブクエリ未対応
  • データサイズ制限: ドキュメントあたり1MBまで
  • リレーショナル制約: 正規化されたデータ構造に不向き
  • Cold Start: サーバーレス特有の初回実行遅延
  • デバッグ困難: クライアントサイドでのエラー特定が複雑

主要リンク

書き方の例

インストール・セットアップ

# Firebase CLI インストール
npm install -g firebase-tools

# Firebase プロジェクト初期化
firebase login
firebase init firestore

# Web SDK インストール
npm install firebase

# React Native インストール
npm install @react-native-firebase/app
npm install @react-native-firebase/firestore

# Flutter プラグイン追加
flutter pub add cloud_firestore
// Firebase初期化(Web)
import { initializeApp } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: "your-api-key",
  authDomain: "your-project.firebaseapp.com",
  projectId: "your-project-id",
  storageBucket: "your-project.appspot.com",
  messagingSenderId: "1234567890",
  appId: "your-app-id"
};

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

// エミュレーター接続(開発時)
if (process.env.NODE_ENV === 'development') {
  connectFirestoreEmulator(db, 'localhost', 8080);
}

export { db };

基本操作(CRUD)

// 基本的なCRUD操作
import { 
  collection, 
  doc, 
  addDoc, 
  getDoc, 
  getDocs, 
  updateDoc, 
  deleteDoc,
  query,
  where,
  orderBy,
  limit
} from 'firebase/firestore';

// ドキュメント作成
async function createProduct() {
  try {
    const docRef = await addDoc(collection(db, 'products'), {
      name: 'ワイヤレスイヤホン',
      brand: 'Sample Brand',
      price: 15000,
      category: 'electronics',
      features: ['ノイズキャンセリング', 'Bluetooth 5.0', '防水'],
      specifications: {
        battery: '24時間',
        weight: '50g',
        color: ['黒', '白', '青']
      },
      availability: {
        inStock: true,
        quantity: 100,
        releaseDate: new Date('2024-01-15')
      },
      reviews: [],
      metadata: {
        createdAt: new Date(),
        updatedAt: new Date(),
        version: 1
      }
    });
    
    console.log('ドキュメント作成完了:', docRef.id);
    return docRef.id;
  } catch (error) {
    console.error('作成エラー:', error);
  }
}

// ドキュメント読み取り
async function getProduct(productId) {
  try {
    const docRef = doc(db, 'products', productId);
    const docSnap = await getDoc(docRef);
    
    if (docSnap.exists()) {
      console.log('商品データ:', docSnap.data());
      return docSnap.data();
    } else {
      console.log('商品が見つかりません');
      return null;
    }
  } catch (error) {
    console.error('読み取りエラー:', error);
  }
}

// 複数ドキュメント取得とクエリ
async function getProducts() {
  try {
    // 基本的なクエリ
    const q = query(
      collection(db, 'products'),
      where('category', '==', 'electronics'),
      where('price', '<=', 20000),
      orderBy('price', 'desc'),
      limit(10)
    );
    
    const querySnapshot = await getDocs(q);
    const products = [];
    
    querySnapshot.forEach((doc) => {
      products.push({
        id: doc.id,
        ...doc.data()
      });
    });
    
    console.log('商品一覧:', products);
    return products;
  } catch (error) {
    console.error('クエリエラー:', error);
  }
}

// ドキュメント更新
async function updateProduct(productId, updates) {
  try {
    const docRef = doc(db, 'products', productId);
    await updateDoc(docRef, {
      ...updates,
      'metadata.updatedAt': new Date(),
      'metadata.version': increment(1)
    });
    
    console.log('商品更新完了');
  } catch (error) {
    console.error('更新エラー:', error);
  }
}

// ドキュメント削除
async function deleteProduct(productId) {
  try {
    await deleteDoc(doc(db, 'products', productId));
    console.log('商品削除完了');
  } catch (error) {
    console.error('削除エラー:', error);
  }
}

リアルタイム同期

// リアルタイムリスナー
import { onSnapshot, serverTimestamp } from 'firebase/firestore';

// 単一ドキュメントの監視
function watchProduct(productId, callback) {
  const docRef = doc(db, 'products', productId);
  
  const unsubscribe = onSnapshot(docRef, (doc) => {
    if (doc.exists()) {
      callback({
        id: doc.id,
        ...doc.data()
      });
    } else {
      callback(null);
    }
  }, (error) => {
    console.error('リスナーエラー:', error);
  });
  
  return unsubscribe; // クリーンアップ用
}

// コレクションの監視
function watchProducts(callback) {
  const q = query(
    collection(db, 'products'),
    where('availability.inStock', '==', true),
    orderBy('metadata.updatedAt', 'desc')
  );
  
  const unsubscribe = onSnapshot(q, (querySnapshot) => {
    const products = [];
    
    querySnapshot.docChanges().forEach((change) => {
      const product = {
        id: change.doc.id,
        ...change.doc.data()
      };
      
      if (change.type === 'added') {
        console.log('新商品:', product.name);
        products.push(product);
      }
      if (change.type === 'modified') {
        console.log('商品更新:', product.name);
      }
      if (change.type === 'removed') {
        console.log('商品削除:', product.name);
      }
    });
    
    callback(products);
  }, (error) => {
    console.error('クエリリスナーエラー:', error);
  });
  
  return unsubscribe;
}

// チャットアプリケーション例
function setupChatListener(roomId, callback) {
  const messagesRef = collection(db, 'chatRooms', roomId, 'messages');
  const q = query(messagesRef, orderBy('timestamp', 'asc'));
  
  return onSnapshot(q, (snapshot) => {
    const messages = snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data()
    }));
    callback(messages);
  });
}

// 使用例
const unsubscribe = watchProducts((products) => {
  console.log('在庫商品一覧更新:', products.length, '件');
});

// コンポーネントのクリーンアップ
// useEffect(() => {
//   return () => unsubscribe();
// }, []);

高度なクエリとインデックス

// 複雑なクエリとフィルタリング
import { 
  query, 
  where, 
  orderBy, 
  limit, 
  startAfter, 
  endBefore,
  arrayContains,
  arrayContainsAny,
  increment,
  arrayUnion,
  arrayRemove
} from 'firebase/firestore';

// 複合クエリ
async function advancedQueries() {
  // 配列フィールドでのクエリ
  const featuresQuery = query(
    collection(db, 'products'),
    where('features', 'array-contains', 'Bluetooth 5.0')
  );
  
  // 複数値での配列クエリ
  const colorsQuery = query(
    collection(db, 'products'),
    where('specifications.color', 'array-contains-any', ['黒', '白'])
  );
  
  // 範囲クエリ
  const priceRangeQuery = query(
    collection(db, 'products'),
    where('price', '>=', 10000),
    where('price', '<=', 50000),
    orderBy('price', 'asc')
  );
  
  // ページネーション
  let lastVisible = null;
  
  async function getNextPage() {
    let q = query(
      collection(db, 'products'),
      orderBy('metadata.createdAt', 'desc'),
      limit(20)
    );
    
    if (lastVisible) {
      q = query(q, startAfter(lastVisible));
    }
    
    const snapshot = await getDocs(q);
    lastVisible = snapshot.docs[snapshot.docs.length - 1];
    
    return snapshot.docs.map(doc => ({
      id: doc.id,
      ...doc.data()
    }));
  }
  
  return { getNextPage };
}

// 地理位置クエリ
import { GeoPoint } from 'firebase/firestore';

async function locationBasedQuery() {
  // 地理位置ポイント作成
  const tokyoLocation = new GeoPoint(35.6762, 139.6503);
  
  // 店舗ドキュメント作成
  await addDoc(collection(db, 'stores'), {
    name: '渋谷店',
    location: tokyoLocation,
    address: '東京都渋谷区',
    phoneNumber: '03-1234-5678',
    businessHours: {
      open: '09:00',
      close: '21:00'
    }
  });
  
  // 近隣店舗検索(半径計算は別途ライブラリが必要)
  const nearbyStores = query(
    collection(db, 'stores'),
    where('location', '!=', null)
  );
  
  return nearbyStores;
}

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

// トランザクション処理
import { runTransaction, writeBatch } from 'firebase/firestore';

// 在庫管理トランザクション
async function purchaseProduct(productId, quantity) {
  try {
    const result = await runTransaction(db, async (transaction) => {
      const productRef = doc(db, 'products', productId);
      const productDoc = await transaction.get(productRef);
      
      if (!productDoc.exists()) {
        throw new Error('商品が存在しません');
      }
      
      const productData = productDoc.data();
      const currentStock = productData.availability.quantity;
      
      if (currentStock < quantity) {
        throw new Error('在庫不足です');
      }
      
      // 在庫を減らす
      transaction.update(productRef, {
        'availability.quantity': currentStock - quantity,
        'metadata.updatedAt': serverTimestamp()
      });
      
      // 注文履歴を追加
      const orderRef = doc(collection(db, 'orders'));
      transaction.set(orderRef, {
        productId: productId,
        quantity: quantity,
        status: 'pending',
        createdAt: serverTimestamp()
      });
      
      return { orderId: orderRef.id, remainingStock: currentStock - quantity };
    });
    
    console.log('購入完了:', result);
    return result;
  } catch (error) {
    console.error('購入エラー:', error);
    throw error;
  }
}

// バッチ操作
async function batchOperations() {
  const batch = writeBatch(db);
  
  // 複数商品の価格一括更新
  const products = ['product1', 'product2', 'product3'];
  
  products.forEach(productId => {
    const docRef = doc(db, 'products', productId);
    batch.update(docRef, {
      price: increment(1000), // 1000円値上げ
      'metadata.updatedAt': serverTimestamp()
    });
  });
  
  // セール情報追加
  const saleRef = doc(collection(db, 'sales'));
  batch.set(saleRef, {
    title: 'Summer Sale',
    discount: 0.2,
    startDate: new Date('2024-07-01'),
    endDate: new Date('2024-07-31'),
    products: products
  });
  
  try {
    await batch.commit();
    console.log('バッチ操作完了');
  } catch (error) {
    console.error('バッチエラー:', error);
  }
}

セキュリティルールとベストプラクティス

// セキュリティルール例(firestore.rules)
/*
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // ユーザー自身のドキュメントのみアクセス可能
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // 商品は誰でも読み取り可能、管理者のみ書き込み可能
    match /products/{productId} {
      allow read: if true;
      allow write: if request.auth != null && 
        exists(/databases/$(database)/documents/admins/$(request.auth.uid));
    }
    
    // 注文は本人と管理者のみアクセス可能
    match /orders/{orderId} {
      allow read, write: if request.auth != null && 
        (resource.data.userId == request.auth.uid ||
         exists(/databases/$(database)/documents/admins/$(request.auth.uid)));
    }
  }
}
*/

// クライアントサイドでの認証統合
import { getAuth, onAuthStateChanged } from 'firebase/auth';

const auth = getAuth();

// 認証状態の監視
onAuthStateChanged(auth, (user) => {
  if (user) {
    console.log('ログイン中:', user.uid);
    // ユーザー固有のデータにアクセス
    setupUserData(user.uid);
  } else {
    console.log('ログアウト状態');
  }
});

// パフォーマンス最適化
class FirestoreOptimizer {
  constructor(db) {
    this.db = db;
    this.cache = new Map();
  }
  
  // キャッシュ付きドキュメント取得
  async getCachedDoc(collection, id, maxAge = 300000) { // 5分キャッシュ
    const cacheKey = `${collection}/${id}`;
    const cached = this.cache.get(cacheKey);
    
    if (cached && Date.now() - cached.timestamp < maxAge) {
      return cached.data;
    }
    
    const docSnap = await getDoc(doc(this.db, collection, id));
    const data = docSnap.exists() ? docSnap.data() : null;
    
    this.cache.set(cacheKey, {
      data,
      timestamp: Date.now()
    });
    
    return data;
  }
  
  // オフライン対応
  async getWithFallback(collection, id) {
    try {
      // オンライン時は最新データを取得
      const docSnap = await getDoc(doc(this.db, collection, id));
      return docSnap.exists() ? docSnap.data() : null;
    } catch (error) {
      console.warn('オンライン取得失敗、キャッシュから取得:', error);
      return this.getCachedDoc(collection, id, Infinity);
    }
  }
}