データベース
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);
}
}
}