D1 の読み取りレプリケーション は、D1 データベースを複数リージョンに複製する機能です。E コマースサイトでは、読み取りレイテンシを下げ、読み取りスループットを上げられます。このチュートリアルでは、E コマースサイトで D1 の読み取りレプリケーションを使う方法を学びます。
架空の E コマースサイトを使いますが、原則は、低い読み取りレイテンシと読み取りのスケールが必要な用途に当てはめられます。ニュースサイト、ソーシャルメディア、マーケティングサイトなどです。
手順を飛ばしてすぐ始める場合は、次のボタンをクリックします。
GitHub アカウントにリポジトリが作成され、アプリケーションが Cloudflare Workers にデプロイされます。D1 データベースの作成とバインド、必要なテーブルの作成、サンプルデータの追加も行われます。デプロイ時に Enable read replication にチェックを入れ、読み取りレプリケーションを有効にします。
デプロイしたアプリケーションにアクセスできます。
- Cloudflare アカウント ↗ に登録します。
Node.js↗ をインストールします。
Node.js のバージョンマネージャー
権限の問題を避け、Node.js のバージョンを切り替えられるよう、Volta ↗ や nvm ↗ などの Node バージョンマネージャーを使います。このガイドの後半で説明する Wrangler には、Node バージョン 16.17.0 以降が必要です。
次のコマンドで、新しい Workers プロジェクトを作成します。
npm create cloudflare@latest -- fast-commerceyarn create cloudflare fast-commercepnpm create cloudflare@latest fast-commerceセットアップでは、次のオプションを選びます。
- What would you like to start with? では、
Hello World exampleを選びます。 - Which template would you like to use? では、
SSR / full-stack appを選びます。 - Which language do you want to use? では、
TypeScriptを選びます。 - Do you want to use git for version control? では、
Yesを選びます。 - Do you want to deploy your application? では、
Noを選びます(デプロイ前にいくつか変更します)。
API ルートには Hono ↗ を使います。次のコマンドで Hono をインストールします。
npm i honoyarn add honopnpm add honobun add hono前のステップで、デフォルトのフロントエンドを持つ Workers プロジェクトが作成され、Hono がインストールされます。フロントエンドを更新し、商品一覧を表示します。単一商品を表示するページも追加します。
作成した Worker プロジェクトのフォルダーへ移動します。
cd fast-commercepublic/index.html を更新し、商品一覧を表示します。次のコードを参考にしてください。
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>E-commerce Store</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
body {
background-color: #f9fafb;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background-color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e5e7eb;
}
.store-title {
font-weight: bold;
font-size: 1.25rem;
}
.cart-button {
padding: 0.5rem 1rem;
cursor: pointer;
background: none;
border: none;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
padding: 2rem;
}
.product-card {
background-color: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.product-info {
padding: 1rem;
}
.product-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.product-description {
color: #4b5563;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.product-price {
font-size: 1.25rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.product-stock {
color: #4b5563;
font-size: 0.875rem;
margin-bottom: 1rem;
}
.view-details-btn {
display: block;
width: 100%;
padding: 0.5rem 0;
background-color: #2563eb;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
text-align: center;
text-decoration: none;
font-size: 0.875rem;
}
.view-details-btn:hover {
background-color: #1d4ed8;
}
footer {
background-color: white;
padding: 1rem 2rem;
text-align: center;
border-top: 1px solid #e5e7eb;
color: #4b5563;
font-size: 0.875rem;
}
/* Basic Responsiveness */
@media (max-width: 768px) {
.products-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (max-width: 480px) {
.products-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<header>
<h1 class="store-title">E-commerce Store</h1>
<button class="cart-button">Cart</button>
</header>
<main class="products-grid" id="products-container">
<!-- Products will be loaded here by JavaScript -->
</main>
<footer>
<p>© 2025 E-commerce Store. All rights reserved.</p>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
let products = [];
let d1Duration,
queryDuration = 0;
let dbLocation;
let isPrimary = true;
// Function to create product HTML
function createProductCard(product) {
return `
<div class="product-card" data-category="${product.category}">
<div class="product-info">
<h3 class="product-title">${product.name}</h3>
<p class="product-description">${product.description}</p>
<p class="product-price">$${product.price.toFixed(2)}</p>
<p class="product-stock">${product.inventory} in stock</p>
<a href="product-details.html?id=${product.id}" class="view-details-btn">View Details</a>
</div>
</div>
`;
}
// Function to render content
function renderContent() {
try {
const productsContainer = document.getElementById('products-container');
if (!productsContainer) return;
productsContainer.innerHTML = '';
products.forEach((product) => {
productsContainer.innerHTML += createProductCard(product);
});
} catch (error) {
console.error('Error rendering content:', error);
}
}
// Fetch products
fetch('/api/products')
.then((response) => response.json())
.then((data) => {
products = data;
renderContent();
})
.catch((error) => console.error('Error fetching products:', error));
});
</script>
</body>
</html>単一商品を表示する public/product-details.html を新規作成します。
public/product-details.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Product Details - E-commerce Store</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, sans-serif;
}
body {
background-color: #f9fafb;
min-height: 100vh;
display: flex;
flex-direction: column;
}
header {
background-color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e5e7eb;
}
.store-title {
font-weight: bold;
font-size: 1.25rem;
text-decoration: none;
color: black;
}
.cart-button {
padding: 0.5rem 1rem;
cursor: pointer;
background: none;
border: none;
}
.product-container {
max-width: 800px;
margin: 2rem auto;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 2rem;
}
.product-title {
font-size: 1.875rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.product-description {
color: #4b5563;
margin-bottom: 1.5rem;
}
.product-price {
font-size: 1.875rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.product-stock {
font-size: 0.875rem;
color: #4b5563;
text-align: right;
}
.add-to-cart-btn {
display: block;
width: 100%;
padding: 0.75rem;
background-color: #2563eb;
color: white;
border: none;
border-radius: 0.375rem;
cursor: pointer;
text-align: center;
font-size: 1rem;
margin-top: 1.5rem;
}
.add-to-cart-btn:hover {
background-color: #1d4ed8;
}
.price-stock-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
footer {
background-color: white;
padding: 1rem 2rem;
text-align: center;
border-top: 1px solid #e5e7eb;
color: #4b5563;
font-size: 0.875rem;
margin-top: auto;
}
/* Back button */
.back-button {
display: inline-block;
margin-bottom: 1.5rem;
color: #2563eb;
text-decoration: none;
font-size: 0.875rem;
}
.back-button:hover {
text-decoration: underline;
}
/* Notification */
.notification {
position: fixed;
top: 1rem;
right: 1rem;
background-color: #10b981;
color: white;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
transform: translateX(150%);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
</style>
</head>
<body>
<header>
<a href="index.html" class="store-title">E-commerce Store</a>
<button class="cart-button">Cart</button>
</header>
<main class="product-container">
<a href="index.html" class="back-button">← Back to products</a>
<h1 class="product-title" id="product-title">Product Name</h1>
<p class="product-description" id="product-description">
Product description goes here.
</p>
<div class="price-stock-container">
<p class="product-price" id="product-price">$0.00</p>
<p class="product-stock" id="product-stock">0 in stock</p>
</div>
<button class="add-to-cart-btn" id="add-to-cart">Add to Cart</button>
</main>
<div class="notification" id="notification">Added to cart!</div>
<footer>
<p>© 2025 E-commerce Store. All rights reserved.</p>
</footer>
<script>
// Get query parameter from URL
const url = new URL(window.location.href);
const searchParams = new URLSearchParams(url.search);
const productId = searchParams.get("id");
// Fetch product details
fetch(`/api/products/${productId}`)
.then((response) => response.json())
.then((product) => displayContent(product))
.catch((error) =>
console.error("Error fetching product details:", error),
);
// Function to display product details
function displayContent(product) {
document.title = `${product[0].name} - E-commerce Store`;
document.getElementById("product-title").textContent = product[0].name;
document.getElementById("product-description").textContent =
product[0].description;
document.getElementById("product-price").textContent =
`$${product[0].price.toFixed(2)}`;
document.getElementById("product-stock").textContent =
`${product[0].inventory} in stock`;
}
</script>
</body>
</html>商品一覧と単一商品を表示するフロントエンドができました。ただし、まだ D1 データベースにはつながっていません。いま開発サーバーを起動しても、商品は表示されません。次のステップで D1 データベースを作成し、商品を取得してフロントエンドに表示する API を作ります。
次のコマンドで、新しい D1 データベースを作成します。
npx wrangler d1 create fast-commerceターミナルに返った D1 バインディングを wrangler ファイルに追加します。
{
"d1_databases": [
{
"binding": "DB",
"database_name": "fast-commerce",
"database_id": "YOUR_DATABASE_ID"
}
]
}[[d1_databases]]
binding = "DB"
database_name = "fast-commerce"
database_id = "YOUR_DATABASE_ID"次のコマンドを実行し、worker-configuration.d.ts の Env インターフェイスを更新します。
npm run cf-typegen次に、D1 データベースで読み取りレプリケーションを有効にします。Workers & Pages > D1 ↗ を開き、既存のデータベースを選択して Settings > Enable Read Replication を選びます。
src/index.ts を更新し、Hono ライブラリをインポートして API ルートを作成します。
import { Hono } from "hono";
// Set db session bookmark in the cookie
import { getCookie, setCookie } from "hono/cookie";
const app = new Hono<{ Bindings: Env }>();
// Get all products
app.get("/api/products", async (c) => {
return c.json({ message: "get list of products" });
});
// Get a single product
app.get("/api/products/:id", async (c) => {
return c.json({ message: "get a single product" });
});
// Upsert a product
app.post("/api/product", async (c) => {
return c.json({ message: "create or update a product" });
});
export default app;上記のコードは、次の 3 つの API ルートを作成します。
GET /api/products: 商品一覧を返します。GET /api/products/:id: 単一商品を返します。POST /api/product: 商品を作成または更新します。
ただし、API ルートはまだ D1 データベースにつながっていません。次のステップで D1 に products テーブルを作成し、API ルートを D1 に接続します。
次のコマンドで、D1 データベースに products テーブルを作成します。
npx wrangler d1 execute fast-commerce --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"次に、次のコマンドで products テーブルにインデックスを作成します。
npx wrangler d1 execute fast-commerce --command "CREATE INDEX IF NOT EXISTS idx_products_id ON products (id)"開発用に、次のコマンドでローカル D1 データベースへ INSERT 文を実行することもできます。
npx wrangler d1 execute fast-commerce --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')"アプリケーションを堅牢にするため、API ルートにリトライロジックを追加できます。src ディレクトリに retry.ts を新規作成します。
export interface RetryConfig {
maxRetries: number;
initialDelay: number;
maxDelay: number;
backoffFactor: number;
}
const shouldRetry = (error: unknown): boolean => {
const errMsg = error instanceof Error ? error.message : String(error);
return (
errMsg.includes("Network connection lost") ||
errMsg.includes("storage caused object to be reset") ||
errMsg.includes("reset because its code was updated")
);
};
// Helper function for sleeping
const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
export const defaultRetryConfig: RetryConfig = {
maxRetries: 3,
initialDelay: 100,
maxDelay: 1000,
backoffFactor: 2,
};
export async function withRetry<T>(
operation: () => Promise<T>,
config: Partial<RetryConfig> = defaultRetryConfig,
): Promise<T> {
const maxRetries = config.maxRetries ?? defaultRetryConfig.maxRetries;
const initialDelay = config.initialDelay ?? defaultRetryConfig.initialDelay;
const maxDelay = config.maxDelay ?? defaultRetryConfig.maxDelay;
const backoffFactor =
config.backoffFactor ?? defaultRetryConfig.backoffFactor;
let lastError: Error | unknown;
let delay = initialDelay;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const result = await operation();
return result;
} catch (error) {
lastError = error;
if (!shouldRetry(error) || attempt === maxRetries) {
throw error;
}
// Add randomness to avoid synchronizing retries
// Wait for a random delay between delay and delay*2
await sleep(delay * (1 + Math.random()));
// Calculate next delay with exponential backoff
delay = Math.min(delay * backoffFactor, maxDelay);
}
}
throw lastError;
}withRetry は、指数バックオフで指定した操作を再試行するユーティリティ関数です。設定オブジェクトを受け取り、リトライ回数、初期遅延、最大遅延、バックオフ係数をカスタマイズできます。再試行するのは、ネットワーク切断、ストレージリセット、コード更新が原因のエラーのときだけです。
次に、src/index.ts を更新し、withRetry 関数をインポートして API ルートで使います。
import { withRetry } from "./retry";API ルートを更新し、D1 データベースに接続します。
app.post("/api/product", async (c) => {
const product = await c.req.json();
if (!product) {
return c.json({ message: "No data passed" }, 400);
}
const db = c.env.DB;
const session = db.withSession("first-primary");
const { id } = product;
try {
return await withRetry(async () => {
// Check if the product exists
const { results } = await session
.prepare("SELECT * FROM products where id = ?")
.bind(id)
.run();
if (results.length === 0) {
const fields = [...Object.keys(product)];
const values = [...Object.values(product)];
// Insert the product
await session
.prepare(
`INSERT INTO products (${fields.join(", ")}) VALUES (${fields.map(() => "?").join(", ")})`,
)
.bind(...values)
.run();
const latestBookmark = session.getBookmark();
latestBookmark &&
setCookie(c, "product_bookmark", latestBookmark, {
maxAge: 60 * 60, // 1 hour
});
return c.json({ message: "Product inserted" });
}
// Update the product
const updates = Object.entries(product)
.filter(([_, value]) => value !== undefined)
.map(([key, _]) => `${key} = ?`)
.join(", ");
if (!updates) {
throw new Error("No valid fields to update");
}
const values = Object.entries(product)
.filter(([_, value]) => value !== undefined)
.map(([_, value]) => value);
await session
.prepare(`UPDATE products SET ${updates} WHERE id = ?`)
.bind(...[...values, id])
.run();
const latestBookmark = session.getBookmark();
latestBookmark &&
setCookie(c, "product_bookmark", latestBookmark, {
maxAge: 60 * 60, // 1 hour
});
return c.json({ message: "Product updated" });
});
} catch (e) {
console.error(e);
return c.json({ message: "Error upserting product" }, 500);
}
});上記のコードでは、次を行います。
- リクエストボディから商品データを取得します。
- データベースにその商品があるかを確認します。
- ある場合は、商品を更新します。
- ない場合は、商品を挿入します。
- ブックマークを Cookie に設定します。
- 最後に応答を返します。
最新データでセッションを始めたいので、first-primary 制約を使います。first-unconstrained を使う場合やブックマークを渡す場合でも、書き込みリクエストは常にプライマリデータベースへルーティングされます。
Cookie に設定したブックマークは、新しいセッションが、指定したブックマークと同等以上に新しいデータベースバージョンを読むことを保証するために使えます。
商品管理に外部プラットフォームを使っている場合は、この API をそのプラットフォームに接続できます。外部で商品が作成または更新されると、D1 データベースの商品詳細も自動で更新されます。
app.get("/api/products", async (c) => {
const db = c.env.DB;
// Get bookmark from the cookie
const bookmark = getCookie(c, "product_bookmark") || "first-unconstrained";
const session = db.withSession(bookmark);
try {
return await withRetry(async () => {
const { results } = await session.prepare("SELECT * FROM products").run();
const latestBookmark = session.getBookmark();
// Set the bookmark in the cookie
latestBookmark &&
setCookie(c, "product_bookmark", latestBookmark, {
maxAge: 60 * 60, // 1 hour
});
return c.json(results);
});
} catch (e) {
console.error(e);
return c.json([]);
}
});上記のコードでは、次を行います。
- Cookie からデータベースセッションのブックマークを取得します。
- ブックマークが未設定の場合は、
first-unconstrained制約を使います。
- ブックマークが未設定の場合は、
- そのブックマークでデータベースセッションを作成します。
- データベースからすべての商品を取得し、最新のブックマークを得ます。
- このブックマークを Cookie に設定します。
- 最後に結果を返します。
app.get("/api/products/:id", async (c) => {
const id = c.req.param("id");
if (!id) {
return c.json({ message: "Invalid id" }, 400);
}
const db = c.env.DB;
// Get bookmark from the cookie
const bookmark = getCookie(c, "product_bookmark") || "first-unconstrained";
const session = db.withSession(bookmark);
try {
return await withRetry(async () => {
const { results } = await session
.prepare("SELECT * FROM products where id = ?")
.bind(id)
.run();
const latestBookmark = session.getBookmark();
// Set the bookmark in the cookie
latestBookmark &&
setCookie(c, "product_bookmark", latestBookmark, {
maxAge: 60 * 60, // 1 hour
});
console.log(results);
return c.json(results);
});
} catch (e) {
console.error(e);
return c.json([]);
}
});上記のコードでは、次を行います。
- リクエストパラメーターから商品 ID を取得します。
- ブックマークでデータベースセッションを作成します。
- データベースから商品を取得し、最新のブックマークを得ます。
- このブックマークを Cookie に設定します。
- 最後に結果を返します。
API ルートを D1 データベースに接続しました。開発サーバーを起動し、フロントエンドを開いてアプリケーションをテストできます。
npm run devhttp://localhost:8787 を開きます。商品一覧が表示されます。商品をクリックすると、商品詳細を確認できます。
開発サーバーの起動中に、次のコマンドで新しい商品を挿入します。
curl -X POST http://localhost:8787/api/product \
-H "Content-Type: application/json" \
-d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1000.00, "inventory": 10, "category": "Electronics"}'http://localhost:8787/product-details?id=6 を開きます。新しい商品が表示されます。
次のコマンドで商品を更新し、もう一度 http://localhost:8787/product-details?id=6 を開きます。更新後の商品が表示されます。
curl -X POST http://localhost:8787/api/product \
-H "Content-Type: application/json" \
-d '{"id": 6, "name": "Fast Computer", "description": "A computer for your home or office", "price": 1050.00, "inventory": 10, "category": "Electronics"}'前のステップで使ったデータベースはローカルです。リモートデータベースにも products テーブルを作成する必要があります。次の D1 コマンドを実行し、リモートデータベースに products テーブルを作成します。
npx wrangler d1 execute fast-commerce --remote --command "CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, inventory INTEGER NOT NULL DEFAULT 0, category TEXT NOT NULL, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP)"次に、次のコマンドで products テーブルにインデックスを作成します。
npx wrangler d1 execute fast-commerce --remote --command "CREATE INDEX IF NOT EXISTS idx_products_id ON products (id)"任意で、次のコマンドを実行し、商品をリモートデータベースに挿入できます。
npx wrangler d1 execute fast-commerce --remote --command "INSERT INTO products (id, name, description, price, inventory, category) VALUES (1, 'Fast Ergonomic Chair', 'A comfortable chair for your home or office', 100.00, 10, 'Furniture'), (2, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing'), (3, 'Fast Wooden Desk', 'A wooden desk for your home or office', 150.00, 5, 'Furniture'), (4, 'Fast Leather Sofa', 'A leather sofa for your home or office', 300.00, 3, 'Furniture'), (5, 'Fast Organic Cotton T-shirt', 'A comfortable t-shirt for your home or office', 20.00, 100, 'Clothing')"次のコマンドでアプリケーションをデプロイできます。
npm run deployアプリケーションが Workers にデプロイされ、D1 データベースはリモートリージョンへ複製されます。ユーザーがどのリージョンからリクエストしても、データベースが複製されている最寄りのリージョンへリダイレクトされます。
このチュートリアルでは、E コマースサイトで D1 の読み取りレプリケーションを使う方法を学びました。D1 データベースを作成し、読み取りレプリケーションを有効にしました。データベース上で商品を作成・更新する API も作成しました。ブックマークを使って、データベースから最新データを取得する方法も学びました。
リモートデータベースに products テーブルを作成し、アプリケーションをデプロイしました。
読み取りが多い既存アプリケーションにも、同じ方法で読み取りレイテンシを下げ、読み取りスループットを上げられます。コンテンツ管理に外部プラットフォームを使っている場合は、そのプラットフォームを D1 データベースに接続し、コンテンツをデータベースへ自動更新できます。
このチュートリアルの完全なコードは GitHub リポジトリ ↗ にあります。