Firestoreで10万件超のECサイト顧客データを設計する
本稿では、Google Cloud Firestoreを使用したECサイトにおける大規模顧客データ(10万件以上)の実践的な設計方法について解説する。
Firestoreの概要
Firestoreの特徴
Firestoreは、Google Cloudが提供するNoSQLドキュメント指向データベースである。
主な利点:
- 自動スケーリング: データ量が増えても自動的にスケール
- リアルタイム同期: データ変更をリアルタイムで反映
- 構造化されたクエリ: 単純なクエリに最適化
- オフライン対応: モバイルアプリとの相性が良い
注意すべき制限:
- JOIN非対応: 複数のコレクションを結合できない
- 柔軟な検索の制限: 全文検索や複雑なOR条件に制限
- 集計関数の非サポート: SUM、COUNT、AVGなどの集計が不可
ECサイトでの利用シーン:
- 顧客管理(会員情報、購入履歴)
- 商品カタログ
- 注文処理
- 在庫管理
RDBとの比較
| 項目 | RDB(MySQL/PostgreSQL) | Firestore(NoSQL) |
|---|---|---|
| データ構造 | テーブル・行・列 | コレクション・ドキュメント |
| スキーマ | 固定(厳密) | 柔軟(スキーマレス) |
| JOIN | サポート | 非サポート(非正規化で対応) |
| スケーリング | 垂直スケール | 水平スケール |
| トランザクション | ACID保証 | 限定的なトランザクション |
| 全文検索 | LIKE、正規表現 | 非サポート(外部サービス必要) |
| OR条件 | 自由に使用可能 | 制限あり(in演算子で一部対応) |
| 複数フィールドの不等号 | 可能 | 1フィールドのみ |
| 集計関数 | SUM、COUNT、AVG等 | 非サポート(クライアント側で計算) |
Firestoreのクエリ制限と回避策
主要な制限事項
Firestoreは高速な読み書きに最適化されているが、RDBと比較して以下の制限がある。
1. JOIN非対応
制限:
// ❌ RDBのようなJOINは不可能
SELECT customers.*, orders.*
FROM customers
INNER JOIN orders ON customers.id = orders.customer_id回避策: 非正規化(データの重複を許容)
// ✅ 注文データに顧客情報を埋め込む
{
"orderId": "order001",
"customerId": "user123",
"customerName": "山田太郎", // 重複データ
"customerEmail": "yamada@example.com", // 重複データ
"totalAmount": 5000
}2. 全文検索非対応
制限:
// ❌ 部分一致検索は不可能
db.collection('customers')
.where('name', 'contains', '山田') // このような検索はできない回避策: 外部検索サービスの利用
// ✅ Algoliaを使用した全文検索
const algoliaIndex = algolia.initIndex('customers');
// Algoliaで検索
const { hits } = await algoliaIndex.search('山田');
// 検索結果のIDでFirestoreから詳細を取得
const customerIds = hits.map(hit => hit.objectID);
const customers = await Promise.all(
customerIds.map(id => db.collection('customers').doc(id).get())
);3. 複雑なOR条件の制限
制限:
// ❌ 複数フィールドのOR条件は不可能
db.collection('customers')
.where('membershipTier', '==', 'gold')
.or() // ORメソッドは存在しない
.where('totalSpent', '>', 100000)回避策: in演算子または複数クエリの統合
// ✅ in演算子(最大10個まで)
db.collection('customers')
.where('membershipTier', 'in', ['gold', 'platinum'])
// ✅ 複数クエリを個別に実行して統合
const goldMembers = await db.collection('customers')
.where('membershipTier', '==', 'gold')
.get();
const highSpenders = await db.collection('customers')
.where('totalSpent', '>', 100000)
.get();
// クライアント側で統合
const allCustomers = [...goldMembers.docs, ...highSpenders.docs];4. 複数フィールドへの不等号演算子
制限:
// ❌ 複数フィールドで不等号演算子は使用不可
db.collection('orders')
.where('totalAmount', '>', 1000)
.where('createdAt', '>', new Date('2026-01-01')) // エラー回避策: 1つのフィールドのみ不等号を使用
// ✅ 1つのフィールドのみ不等号、他は等号
db.collection('orders')
.where('status', '==', 'completed')
.where('totalAmount', '>', 1000)
// または、クライアント側でフィルタリング
const snapshot = await db.collection('orders')
.where('totalAmount', '>', 1000)
.get();
const filtered = snapshot.docs.filter(doc => {
const data = doc.data();
return data.createdAt.toDate() > new Date('2026-01-01');
});5. 集計関数非対応
制限:
// ❌ データベース側での集計は不可能
SELECT SUM(totalAmount) FROM orders WHERE customerId = 'user123'回避策: 非正規化(集計値を事前計算)
// ✅ 顧客ドキュメントに集計値を保存
{
"customerId": "user123",
"name": "山田太郎",
"totalOrders": 12, // 集計値
"totalSpent": 58000, // 集計値
"lastOrderDate": "2026-02-05T09:20:00Z"
}
// 注文作成時に顧客の集計値を更新
async function createOrderAndUpdateStats(customerId, orderAmount) {
const customerRef = db.collection('customers').doc(customerId);
await db.runTransaction(async (transaction) => {
const doc = await transaction.get(customerRef);
const currentTotal = doc.data().totalSpent;
const currentOrders = doc.data().totalOrders;
transaction.update(customerRef, {
totalSpent: currentTotal + orderAmount,
totalOrders: currentOrders + 1,
lastOrderDate: admin.firestore.FieldValue.serverTimestamp()
});
});
}全文検索の実装(Algolia連携)
ECサイトでは商品名や顧客名の部分一致検索が必要となる。Firestoreでは全文検索が不可能なため、Algoliaなどの外部サービスとの連携が必須となる。
Algoliaのセットアップ
npm install algoliasearchFirestoreとの同期
const algoliasearch = require('algoliasearch');
const admin = require('firebase-admin');
const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_API_KEY');
const index = algolia.initIndex('customers');
// Cloud Functionsでリアルタイム同期
exports.syncCustomerToAlgolia = functions.firestore
.document('customers/{customerId}')
.onWrite(async (change, context) => {
const customerId = context.params.customerId;
// ドキュメント削除時
if (!change.after.exists) {
await index.deleteObject(customerId);
return;
}
// ドキュメント作成・更新時
const customer = change.after.data();
const algoliaObject = {
objectID: customerId,
name: customer.name,
email: customer.email,
phoneNumber: customer.phoneNumber,
membershipTier: customer.membershipTier,
totalSpent: customer.totalSpent,
createdAt: customer.createdAt.toDate().getTime()
};
await index.saveObject(algoliaObject);
});Algoliaでの検索
const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY');
const index = algolia.initIndex('customers');
async function searchCustomers(query) {
const { hits } = await index.search(query, {
attributesToRetrieve: ['objectID', 'name', 'email'],
hitsPerPage: 20,
filters: 'membershipTier:gold' // フィルタも可能
});
// Firestoreから詳細データを取得(必要に応じて)
const customers = await Promise.all(
hits.map(hit =>
db.collection('customers').doc(hit.objectID).get()
)
);
return customers.map(doc => doc.data());
}
// 使用例
const results = await searchCustomers('山田');Firestoreの基本概念
ドキュメント指向データベース
Firestoreはドキュメント指向のデータベースである。
データベース
└── コレクション(Collection)
└── ドキュメント(Document)
├── フィールド(Field)
└── サブコレクション(Subcollection)コレクションとドキュメント
// コレクション: customers
// ドキュメント: user123
{
"id": "user123",
"name": "山田太郎",
"email": "yamada@example.com",
"createdAt": "2026-01-01T00:00:00Z"
}要点:
- コレクション: ドキュメントの集合(RDBのテーブルに相当)
- ドキュメント: JSON形式のデータ(RDBの行に相当)
- フィールド: ドキュメント内のキー・バリュー(RDBの列に相当)
ECサイトの要件定義
対象データ
顧客データ:
- 会員情報: 10万件以上
- 注文履歴: 1人あたり平均10件
- 配送先住所: 1人あたり1〜3件
必要なクエリパターン:
| クエリ | Firestoreでの実現性 | 実装方法 |
|---|---|---|
| 顧客IDで検索 | ✅ 可能 | ドキュメントID指定 |
| メールアドレスで完全一致検索 | ✅ 可能 | where('email', '==', '...') |
| 顧客名の部分一致検索 | ❌ 不可能 | Algolia等の外部サービス |
| 特定期間の顧客取得 | ✅ 可能 | where('createdAt', '>=', '...') |
| 顧客の注文履歴取得 | ✅ 可能 | サブコレクション |
| 注文の合計金額でソート | ✅ 可能 | orderBy('totalAmount', 'desc') |
| ゴールド会員かつ高額購入者 | ⚠️ 制限あり | 複数クエリを統合 |
パフォーマンス要件
- 読み取り速度: 単一ドキュメント取得 < 100ms
- 書き込み速度: 新規登録 < 500ms
- スケーラビリティ: 100万件まで対応可能
- 可用性: 99.9%以上
スキーマ設計の原則
1. 非正規化(Denormalization)
RDBでは正規化が基本であるが、Firestoreでは非正規化が推奨される。
理由:
- JOINが使えない
- 読み取り速度を最適化
- データの重複を許容
例: 注文データに顧客名を含める
// ❌ RDBスタイル(正規化)
// orders テーブル
{
"orderId": "order001",
"customerId": "user123", // 外部キー
"totalAmount": 5000
}
// → 顧客名を取得するにはJOINが必要
// ✅ Firestoreスタイル(非正規化)
{
"orderId": "order001",
"customerId": "user123",
"customerName": "山田太郎", // 重複を許容
"customerEmail": "yamada@example.com",
"totalAmount": 5000
}
// → 1回のクエリで完結2. コレクション分割の戦略
ルートコレクション vs サブコレクション
ルートコレクション:
/customers/{customerId}
/orders/{orderId}
サブコレクション:
/customers/{customerId}/orders/{orderId}判断基準:
| パターン | 用途 | 例 |
|---|---|---|
| ルートコレクション | グローバルな検索が必要 | 全注文を検索 |
| サブコレクション | 特定の親に紐づくデータ | 特定顧客の注文 |
3. インデックス設計
Firestoreは自動インデックスと複合インデックスをサポートします。
// 自動インデックス(単一フィールド)
db.collection('customers').where('email', '==', 'yamada@example.com')
// 複合インデックス(複数フィールド)
db.collection('orders')
.where('customerId', '==', 'user123')
.where('status', '==', 'completed')
.orderBy('createdAt', 'desc')複合インデックスは手動での作成が必要となる(後述)。
実践: 顧客データの設計
コレクション構造
/customers/{customerId}
- name: string
- email: string
- phoneNumber: string
- createdAt: timestamp
- totalOrders: number (非正規化)
- totalSpent: number (非正規化)
/orders/{orderId} (サブコレクション)
- orderId: string
- items: array
- totalAmount: number
- status: string
- createdAt: timestamp
/addresses/{addressId} (サブコレクション)
- name: string
- postalCode: string
- prefecture: string
- city: string
- street: string
- isDefault: booleanドキュメント設計
顧客ドキュメント:
{
"customerId": "user123",
"name": "山田太郎",
"email": "yamada@example.com",
"phoneNumber": "090-1234-5678",
"createdAt": "2026-01-15T10:30:00Z",
"updatedAt": "2026-02-10T15:45:00Z",
// 非正規化データ(集計値)
"totalOrders": 12,
"totalSpent": 58000,
"lastOrderDate": "2026-02-05T09:20:00Z",
// メタデータ
"isActive": true,
"membershipTier": "gold" // bronze, silver, gold
}注文ドキュメント:
{
"orderId": "order001",
"customerId": "user123",
// 顧客情報の非正規化
"customerName": "山田太郎",
"customerEmail": "yamada@example.com",
// 注文内容
"items": [
{
"productId": "prod001",
"productName": "ワイヤレスイヤホン",
"price": 5000,
"quantity": 2
}
],
"subtotal": 10000,
"tax": 1000,
"shippingFee": 500,
"totalAmount": 11500,
// ステータス
"status": "completed", // pending, processing, shipped, completed, cancelled
"paymentStatus": "paid",
// タイムスタンプ
"createdAt": "2026-02-05T09:20:00Z",
"shippedAt": "2026-02-06T14:00:00Z",
"completedAt": "2026-02-08T10:00:00Z"
}配送先住所ドキュメント:
{
"addressId": "addr001",
"name": "山田太郎",
"postalCode": "100-0001",
"prefecture": "東京都",
"city": "千代田区",
"street": "千代田1-1-1",
"building": "千代田ビル101",
"phoneNumber": "090-1234-5678",
"isDefault": true,
"createdAt": "2026-01-15T10:30:00Z"
}サンプルコード(再現可能)
セットアップ
# Firebase Admin SDKをインストール
npm install firebase-admin
# サービスアカウントキーを取得
# Firebase Console > プロジェクト設定 > サービスアカウント > 秘密鍵を生成初期化コード
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
const db = admin.firestore();
module.exports = db;顧客データの作成
const db = require('./firebase-setup');
async function createCustomer(customerData) {
const customersRef = db.collection('customers');
const customerRef = customersRef.doc(customerData.customerId);
await customerRef.set({
customerId: customerData.customerId,
name: customerData.name,
email: customerData.email,
phoneNumber: customerData.phoneNumber,
createdAt: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
totalOrders: 0,
totalSpent: 0,
isActive: true,
membershipTier: 'bronze'
});
console.log(`顧客 ${customerData.customerId} を作成`);
return customerRef;
}
// 使用例
createCustomer({
customerId: 'user123',
name: '山田太郎',
email: 'yamada@example.com',
phoneNumber: '090-1234-5678'
});大量データの投入
const db = require('./firebase-setup');
async function seedCustomers(count) {
const batch = db.batch();
const customersRef = db.collection('customers');
for (let i = 0; i < count; i++) {
const customerId = `user${String(i).padStart(6, '0')}`;
const customerRef = customersRef.doc(customerId);
batch.set(customerRef, {
customerId: customerId,
name: `顧客 ${i}`,
email: `customer${i}@example.com`,
phoneNumber: `090-${String(i).padStart(8, '0')}`,
createdAt: admin.firestore.Timestamp.now(),
updatedAt: admin.firestore.Timestamp.now(),
totalOrders: Math.floor(Math.random() * 20),
totalSpent: Math.floor(Math.random() * 100000),
isActive: Math.random() > 0.1,
membershipTier: ['bronze', 'silver', 'gold'][Math.floor(Math.random() * 3)]
});
// Firestoreのバッチは500件まで
if ((i + 1) % 500 === 0) {
await batch.commit();
console.log(`${i + 1}件のデータを投入`);
batch = db.batch();
}
}
// 残りをコミット
await batch.commit();
console.log(`合計 ${count}件のデータを投入`);
}
// 10万件のデータを投入
seedCustomers(100000);クエリ例
const db = require('./firebase-setup');
// 1. 顧客IDで検索
async function getCustomerById(customerId) {
const customerRef = db.collection('customers').doc(customerId);
const doc = await customerRef.get();
if (!doc.exists) {
console.log('顧客が見つかりません');
return null;
}
return doc.data();
}
// 2. メールアドレスで検索
async function getCustomerByEmail(email) {
const snapshot = await db.collection('customers')
.where('email', '==', email)
.limit(1)
.get();
if (snapshot.empty) {
console.log('顧客が見つかりません');
return null;
}
return snapshot.docs[0].data();
}
// 3. アクティブなゴールド会員を取得
async function getActiveGoldMembers() {
const snapshot = await db.collection('customers')
.where('isActive', '==', true)
.where('membershipTier', '==', 'gold')
.orderBy('totalSpent', 'desc')
.limit(100)
.get();
return snapshot.docs.map(doc => doc.data());
}
// 4. 特定期間に登録された顧客を取得
async function getCustomersByDateRange(startDate, endDate) {
const snapshot = await db.collection('customers')
.where('createdAt', '>=', startDate)
.where('createdAt', '<=', endDate)
.orderBy('createdAt', 'desc')
.get();
return snapshot.docs.map(doc => doc.data());
}
// 5. 顧客の注文履歴を取得
async function getCustomerOrders(customerId) {
const snapshot = await db.collection('customers')
.doc(customerId)
.collection('orders')
.orderBy('createdAt', 'desc')
.get();
return snapshot.docs.map(doc => doc.data());
}
// 6. 顧客名の部分一致検索(Firestore単体では不可能)
// ❌ このようなクエリは実行できない
async function searchCustomersByNameNaive(query) {
// Firestoreには LIKE や CONTAINS がない
const snapshot = await db.collection('customers')
.where('name', 'contains', query) // エラー: 存在しない演算子
.get();
}
// ✅ 回避策1: 前方一致のみ可能(制限的)
async function searchCustomersByPrefix(prefix) {
const snapshot = await db.collection('customers')
.where('name', '>=', prefix)
.where('name', '<=', prefix + '\uf8ff') // Unicodeの最大文字
.get();
return snapshot.docs.map(doc => doc.data());
}
// ✅ 回避策2: Algoliaで全文検索(推奨)
async function searchCustomersWithAlgolia(query) {
const algolia = algoliasearch('YOUR_APP_ID', 'YOUR_SEARCH_KEY');
const index = algolia.initIndex('customers');
const { hits } = await index.search(query);
return hits;
}
// 7. 複雑なOR条件(制限あり)
// ❌ 複数フィールドのOR条件は不可能
async function getVIPCustomersNaive() {
// このようなクエリは実行できない
const snapshot = await db.collection('customers')
.where('membershipTier', '==', 'gold')
.or() // ORメソッドは存在しない
.where('totalSpent', '>', 100000)
.get();
}
// ✅ 回避策: 複数クエリを統合
async function getVIPCustomers() {
const goldMembers = await db.collection('customers')
.where('membershipTier', '==', 'gold')
.get();
const highSpenders = await db.collection('customers')
.where('totalSpent', '>', 100000)
.get();
// クライアント側で重複を削除しつつ統合
const customerMap = new Map();
goldMembers.docs.forEach(doc => {
customerMap.set(doc.id, doc.data());
});
highSpenders.docs.forEach(doc => {
customerMap.set(doc.id, doc.data());
});
return Array.from(customerMap.values());
}ページネーション(カーソルベース)
const db = require('./firebase-setup');
async function getCustomersWithPagination(pageSize = 50, lastDoc = null) {
let query = db.collection('customers')
.orderBy('createdAt', 'desc')
.limit(pageSize);
if (lastDoc) {
query = query.startAfter(lastDoc);
}
const snapshot = await query.get();
return {
customers: snapshot.docs.map(doc => doc.data()),
lastDoc: snapshot.docs[snapshot.docs.length - 1],
hasMore: snapshot.docs.length === pageSize
};
}
// 使用例
async function paginateAllCustomers() {
let lastDoc = null;
let page = 1;
do {
const result = await getCustomersWithPagination(50, lastDoc);
console.log(`ページ ${page}: ${result.customers.length}件`);
lastDoc = result.lastDoc;
page++;
if (!result.hasMore) break;
} while (true);
}パフォーマンス最適化
1. 複合インデックスの作成
複合クエリには複合インデックスが必要となる。
// このクエリは複合インデックスが必要
db.collection('customers')
.where('isActive', '==', true)
.where('membershipTier', '==', 'gold')
.orderBy('totalSpent', 'desc')インデックスの作成方法:
Firebase Consoleで作成:
- Firestore > インデックス > 複合インデックスを追加
firestore.indexes.jsonで定義:
{
"indexes": [
{
"collectionGroup": "customers",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "isActive", "order": "ASCENDING" },
{ "fieldPath": "membershipTier", "order": "ASCENDING" },
{ "fieldPath": "totalSpent", "order": "DESCENDING" }
]
},
{
"collectionGroup": "orders",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "customerId", "order": "ASCENDING" },
{ "fieldPath": "status", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
]
}デプロイ:
firebase deploy --only firestore:indexes2. キャッシング戦略
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5分間キャッシュ
async function getCustomerWithCache(customerId) {
// キャッシュを確認
const cached = cache.get(customerId);
if (cached) {
console.log('Cache hit');
return cached;
}
// Firestoreから取得
const doc = await db.collection('customers').doc(customerId).get();
const data = doc.data();
// キャッシュに保存
cache.set(customerId, data);
return data;
}3. バッチ処理
// 複数の顧客を一括取得
async function getCustomersByIds(customerIds) {
const promises = customerIds.map(id =>
db.collection('customers').doc(id).get()
);
const docs = await Promise.all(promises);
return docs.map(doc => doc.data());
}
// トランザクションで一括更新
async function updateCustomerStats(customerId, orderAmount) {
const customerRef = db.collection('customers').doc(customerId);
await db.runTransaction(async (transaction) => {
const doc = await transaction.get(customerRef);
if (!doc.exists) {
throw new Error('顧客が存在しません');
}
const newTotalOrders = doc.data().totalOrders + 1;
const newTotalSpent = doc.data().totalSpent + orderAmount;
transaction.update(customerRef, {
totalOrders: newTotalOrders,
totalSpent: newTotalSpent,
lastOrderDate: admin.firestore.FieldValue.serverTimestamp(),
updatedAt: admin.firestore.FieldValue.serverTimestamp()
});
});
}セキュリティルール
Firestoreのセキュリティルールで、アクセス制御とバリデーションを実装します。
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 認証済みユーザーのみアクセス可能
match /customers/{customerId} {
// 読み取り: 本人または管理者のみ
allow read: if request.auth != null &&
(request.auth.uid == customerId ||
request.auth.token.admin == true);
// 作成: 管理者のみ
allow create: if request.auth != null &&
request.auth.token.admin == true;
// 更新: 本人または管理者のみ
allow update: if request.auth != null &&
(request.auth.uid == customerId ||
request.auth.token.admin == true);
// 削除: 管理者のみ
allow delete: if request.auth != null &&
request.auth.token.admin == true;
// サブコレクション: 注文
match /orders/{orderId} {
allow read: if request.auth != null &&
(request.auth.uid == customerId ||
request.auth.token.admin == true);
allow write: if request.auth != null &&
request.auth.token.admin == true;
}
// サブコレクション: 配送先住所
match /addresses/{addressId} {
allow read, write: if request.auth != null &&
request.auth.uid == customerId;
}
}
// バリデーション
function isValidCustomer() {
return request.resource.data.keys().hasAll([
'customerId', 'name', 'email', 'createdAt'
]) &&
request.resource.data.email.matches('.*@.*\\..*') &&
request.resource.data.name.size() > 0;
}
}
}まとめ
Firestoreでの大規模データ設計のポイント
- 非正規化を活用: JOIN非対応のため、データの重複を許容
- 適切なコレクション分割: ルート vs サブコレクション
- 複合インデックスの設計: 複雑なクエリに対応
- ページネーション: カーソルベースで効率的に取得
- キャッシング: アプリケーション層でキャッシュ
- セキュリティルール: アクセス制御とバリデーション
- 全文検索にはAlgolia: 部分一致検索が必要な場合
- 集計値の事前計算: SUM/COUNTなどは非正規化で対応
- 複雑なOR条件は統合: 複数クエリをクライアント側で結合
スケーリングの考慮事項
- ドキュメントサイズ: 1MBまで(通常は数KBに抑える)
- コレクションサイズ: 無制限(10億件以上も可能)
- 書き込み速度: 1ドキュメントあたり1秒に1回
- 読み取り速度: 制限なし(ただしコスト考慮)
次のステップ
- Algolia連携: 全文検索機能の実装
- Cloud Functions: バッチ処理と定期実行
- BigQuery エクスポート: 複雑な集計やデータ分析
- Elasticsearch: Algoliaの代替として自前で全文検索
- Firebase Extensions: バックアップや削除機能の自動化
Firestoreが適さないケース
以下の要件がある場合、PostgreSQLなどのRDBを検討すべきである:
- 複雑なJOINが頻繁に必要
- リアルタイムな集計(SUM、AVG等)が必須
- 複雑なトランザクション処理(複数のコレクションを跨ぐACID保証)
- 高度な全文検索を自前で実装(Algoliaなどの外部サービスを使いたくない)
Firestoreを使用することで、10万件を超える大規模データの効率的な管理が可能である。本稿で示した設計パターンを参考に、スケーラブルなECサイトの構築を行われたい。