Cloud Firestoreは、Google Cloud Platform (GCP) が提供するフルマネージドのNoSQLドキュメントデータベースサービスです。モバイル、ウェブ、サーバーアプリケーション向けに設計されており、リアルタイムデータ同期、オフラインサポート、強力なクエリ機能を備えています。Firestoreは、Firebase(Googleのモバイルアプリ開発プラットフォーム)とGoogle Cloudの両方から利用できるため、幅広いアプリケーション開発シナリオに対応できます。
Cloud Firestoreは以下の主要コンポーネントで構成されています:
コンポーネント | 説明 |
---|---|
コレクション | ドキュメントのコンテナ。SQLのテーブルに相当するが、スキーマは強制されない |
ドキュメント | データを格納する基本単位。キーと値のペアで構成され、様々なデータ型をサポート |
サブコレクション | ドキュメント内に含まれるコレクション。階層的なデータ構造を作成可能 |
フィールド | ドキュメント内の個々のデータ要素。名前と型付きの値で構成 |
リファレンス | コレクションやドキュメントへの参照。他のドキュメントへのリンクを作成可能 |
インデックス | クエリのパフォーマンスを向上させるためのデータ構造 |
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は、Google Cloud Console、Firebase Console、クライアントライブラリ、またはRESTful APIを使用して利用できます。
Google Cloud ConsoleまたはFirebase Consoleから新しいFirestoreデータベースを作成できます:
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();
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();
// 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);
}
}
// 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では、様々な条件でデータをクエリできます:
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では、複数の操作を原子的に実行するためのトランザクションとバッチ処理をサポートしています:
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();
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();
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;
}
}
}
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サービスコントロールを使用して、Firestoreへのアクセスをセキュリティ境界内に制限できます。
Firestoreのデータは、保存時と転送時に自動的に暗号化されます。さらに、カスタマー管理の暗号化キー(CMEK)を使用することもできます。
Firestoreを効率的に使用するためのパフォーマンス最適化のベストプラクティス:
Firestoreは自動的に単一フィールドのインデックスを作成しますが、複合クエリには複合インデックスが必要です:
{
"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": []
}
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++;
}
}
}
Firestoreは他のGoogle Cloudサービスと統合して、より強力なソリューションを構築できます:
サービス | 統合の利点 |
---|---|
Firebase Authentication | ユーザー認証とFirestoreセキュリティルールの連携によるアクセス制御 |
Cloud Functions | Firestoreのデータ変更をトリガーとしたサーバーレス関数の実行 |
Cloud Run | Firestoreと連携したマイクロサービスの実装 |
App Engine | Firestoreをバックエンドとしたウェブアプリケーションの構築 |
BigQuery | Firestoreデータのエクスポートと高度な分析 |
Cloud Storage | 大容量ファイルの保存とFirestoreでのメタデータ管理 |
Pub/Sub | Firestoreの変更に基づくイベント駆動型アーキテクチャの実装 |
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()
});
});
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
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
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 など)との適切な使い分けも検討すべきです。