Cloudflare Workers Cloudflare Workers Cloudflare Workers · Lab 4/7 Lab 4/7 Lab 4/7 · 20 phút · 20 min · 20 នាទី

04

D1 kèm cache KV D1 Database with KV Caching D1 ជាមួយ cache KV

Chuyển bookmark sang D1 để query quan hệ và tag, giữ KV làm cache đọc. Migrate bookmarks to a D1 SQL database for relational queries and tags, while keeping KV as a read cache for fast lookups. ផ្លាស់ bookmark ទៅ D1 សម្រាប់ query ទំនាក់ទំនង និង tag រក្សា KV ជា cache អាន។

Nội dung bước (lệnh, code) giữ nguyên tiếng Anh từ nguồn chính thức. Step body (commands, code) stays in English from the official source. ខ្លឹមសារជំហាន (ពាក្យបញ្ជា និង code) រក្សាភាសាអង់គ្លេសពីប្រភពផ្លូវការ។ labs.cloudflare.dev ↗

Cần trước Prerequisites តម្រូវការជាមុន

  • Đã xong bước 3 Completed Step 3 បានបញ្ចប់ជំហាន 3
  • bookmark-api đã dùng KV bookmark-api with KV storage bookmark-api បានប្រើ KV

Bạn sẽ làm được Learning objectives គោលបំណងសិក្សា

  • Định nghĩa schema SQL và chạy migration trên D1 Define a SQL schema and apply migrations with D1 កំណត់ schema SQL និងរត់ migration លើ D1
  • Làm cache-aside: D1 là nguồn sự thật, KV là cache đọc Implement the cache-aside pattern with D1 as the source of truth and KV as a read cache ធ្វើ cache-aside៖ D1 ជាប្រភពពិត KV ជា cache អាន
  • Quyết định khi nào dùng storage quan hệ hay key-value trong cùng một app Decide when to use relational storage vs key-value storage in the same application សម្រេចពេលណាប្រើ storage ទំនាក់ទំនង ឬ key-value ក្នុងកម្មវិធីតែមួយ

Bước 1: Tạo database D1 Step 1: Create the D1 Database ជំហាន 1: បង្កើត database D1

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A D1 database bound to your Worker alongside the existing KV namespace.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
D1 gives you SQL queries, joins, and filtering that KV cannot provide.
bash
npx wrangler d1 create bookmark-db

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.

When prompted “Would you like Wrangler to add it on your behalf?”, type Y. Wrangler will then ask for a binding name. Enter DB so it matches the env.DB calls used throughout this workshop.

Regenerate types:

Regenerate types:

Regenerate types:

bash
npx wrangler types

Bước 2: Định nghĩa schema Step 2: Define the Schema ជំហាន 2: កំណត់ schema

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
A bookmarks table with columns for URL, title, tags, and timestamps.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
A well-designed schema is the foundation of your relational data layer.

Create schema.sql in your project root:

Create schema.sql in your project root:

Create schema.sql in your project root:

sql
CREATE TABLE IF NOT EXISTS bookmarks (
  id TEXT PRIMARY KEY,
  url TEXT NOT NULL,
  title TEXT NOT NULL,
  tags TEXT DEFAULT '',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Apply it to the local database. Make sure the dev server is stopped before running this:

Apply it to the local database. Make sure the dev server is stopped before running this:

Apply it to the local database. Make sure the dev server is stopped before running this:

bash
npx wrangler d1 execute bookmark-db --local --file=./schema.sql

Bước 3: Cập nhật Worker Step 3: Update the Worker ជំហាន 3: ធ្វើបច្ចុប្បន្នភាព Worker

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Handlers that use D1 as the source of truth and KV as a read cache.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
This demonstrates a real-world caching pattern: write to D1, cache in KV, read from KV first.

Replace src/index.ts with the following. Key changes from Step 3 are marked:

Replace src/index.ts with the following. Key changes from Step 3 are marked:

Replace src/index.ts with the following. Key changes from Step 3 are marked:

typescript

interface Bookmark {
  id: string;
  url: string;
  title: string;
  tags: string;       // NEW: comma-separated tags
  created_at: string; // NEW: from D1 DATETIME
}

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const path = url.pathname;
    const method = request.method;

    if (path === '/bookmarks' && method === 'GET') {
      return listBookmarks(env, url);
    }

    if (path === '/bookmarks' && method === 'POST') {
      return createBookmark(request, env);
    }

    const match = path.match(/^\/bookmarks\/([a-zA-Z0-9_-]+)$/);
    if (match && method === 'GET') {
      return getBookmark(match[1], env);
    }

    if (match && method === 'DELETE') {
      return deleteBookmark(match[1], env);
    }

    if (path === '/') {
      return Response.json({
        name: 'Bookmark API',
        version: '3.0.0',
        storage: 'D1 + KV cache',
        endpoints: [
          'GET /bookmarks',
          'GET /bookmarks?tag=docs',
          'POST /bookmarks',
          'GET /bookmarks/:id',
          'DELETE /bookmarks/:id'
        ]
      });
    }

    return Response.json({ error: 'Not Found' }, { status: 404 });
  },
};

// CHANGED: query D1, support filtering by tag
async function listBookmarks(env: Env, url: URL): Promise<Response> {
  const tag = url.searchParams.get('tag');

  let results: Bookmark[];

  if (tag) {
    // Filter by tag using LIKE (tags is comma-separated)
    const { results: rows } = await env.DB.prepare(
      `SELECT * FROM bookmarks WHERE tags LIKE ? ORDER BY created_at DESC`
    ).bind(`%${tag}%`).all<Bookmark>();
    results = rows;
  } else {
    const { results: rows } = await env.DB.prepare(
      `SELECT * FROM bookmarks ORDER BY created_at DESC`
    ).all<Bookmark>();
    results = rows;
  }

  return Response.json({ bookmarks: results, count: results.length });
}

// CHANGED: write to D1, then cache in KV
// NOTE on SQL safety: every user-provided value is passed through .bind(),
// which uses parameterized queries. This prevents SQL injection, even when
// using LIKE with wildcards like %${tag}%. Never concatenate user input
// directly into SQL strings.
async function createBookmark(request: Request, env: Env): Promise<Response> {
  let body: { url?: string; title?: string; tags?: string };

  try {
    body = await request.json() as { url?: string; title?: string; tags?: string };
  } catch {
    return Response.json(
      { error: 'Invalid JSON in request body' },
      { status: 400 }
    );
  }

  if (!body.url || !body.title) {
    return Response.json(
      { error: 'Missing required fields: url, title' },
      { status: 400 }
    );
  }

  const id = crypto.randomUUID().slice(0, 8);
  const tags = body.tags || '';

  // Write to D1 (source of truth)
  const result = await env.DB.prepare(
    `INSERT INTO bookmarks (id, url, title, tags)
     VALUES (?, ?, ?, ?)
     RETURNING *`
  ).bind(id, body.url, body.title, tags).first<Bookmark>();

  if (!result) {
    return Response.json({ error: 'Failed to create bookmark' }, { status: 500 });
  }

  // Cache in KV for fast reads
  await env.BOOKMARKS.put(id, JSON.stringify(result), { expirationTtl: 3600 });

  return Response.json(result, { status: 201 });
}

// CHANGED: check KV cache first, fall back to D1
async function getBookmark(id: string, env: Env): Promise<Response> {
  // Try KV cache first
  const cached = await env.BOOKMARKS.get<Bookmark>(id, 'json');
  if (cached) {
    return Response.json({ ...cached, _cached: true });
  }

  // Fall back to D1
  const bookmark = await env.DB.prepare(
    'SELECT * FROM bookmarks WHERE id = ?'
  ).bind(id).first<Bookmark>();

  if (!bookmark) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }

  // Populate cache for next time
  await env.BOOKMARKS.put(id, JSON.stringify(bookmark), { expirationTtl: 3600 });

  return Response.json(bookmark);
}

// CHANGED: delete from D1 and invalidate KV cache
async function deleteBookmark(id: string, env: Env): Promise<Response> {
  const existing = await env.DB.prepare(
    'SELECT id FROM bookmarks WHERE id = ?'
  ).bind(id).first();

  if (!existing) {
    return Response.json({ error: 'Bookmark not found' }, { status: 404 });
  }

  // Delete from D1
  await env.DB.prepare('DELETE FROM bookmarks WHERE id = ?').bind(id).run();

  // Invalidate KV cache
  await env.BOOKMARKS.delete(id);

  return Response.json({ message: 'Bookmark deleted' });
}

Bước 4: Test D1 + cache KV Step 4: Test D1 + KV Caching ជំហាន 4: Test D1 + cache KV

Chúng ta đang xây What we're building អ្វីដែលយើងកំពុងសង់
Bookmarks stored in D1 with KV caching verified.
Vì sao quan trọng Why this matters ហេតុអ្វីសំខាន់
Testing confirms the migration is complete and both storage layers work together.

Tạo bookmark có tag Create bookmarks with tags បង្កើត bookmark មាន tag

bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://developers.cloudflare.com/workers/","title":"Workers Docs","tags":"docs,cloudflare,workers"}'
bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://developers.cloudflare.com/d1/","title":"D1 Docs","tags":"docs,cloudflare,database"}'
bash
curl -X POST http://localhost:8787/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/cloudflare/workers-sdk","title":"Workers SDK","tags":"github,cloudflare,tools"}'

Lọc theo tag Filter by tag ច្រោះតាម tag

All bookmarks tagged “docs”:

All bookmarks tagged “docs”:

All bookmarks tagged “docs”:

bash
curl -s "http://localhost:8787/bookmarks?tag=docs" | jq

All bookmarks tagged “cloudflare”:

All bookmarks tagged “cloudflare”:

All bookmarks tagged “cloudflare”:

bash
curl -s "http://localhost:8787/bookmarks?tag=cloudflare" | jq

Kiểm tra cache Verify caching ពិនិត្យ cache

Get a bookmark by ID (first request comes from D1, no _cached field):

Get a bookmark by ID (first request comes from D1, no _cached field):

Get a bookmark by ID (first request comes from D1, no _cached field):

bash
curl -s http://localhost:8787/bookmarks/REPLACE_ID | jq

Get the same bookmark again (second request comes from KV cache, should include "_cached": true):

Get the same bookmark again (second request comes from KV cache, should include "_cached": true):

Get the same bookmark again (second request comes from KV cache, should include "_cached": true):

bash
curl -s http://localhost:8787/bookmarks/REPLACE_ID | jq

Kiểm tra persistence Verify persistence ពិនិត្យ persistence

Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).

Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).

Stop and restart the dev server. List bookmarks. They should still be there (D1 is persistent).