Cloudflare Workers + D1:零成本搭建无服务器数据库

Cloudflare Workers + D1:零成本搭建无服务器数据库

还在为个人项目的数据库费用发愁?

今天带你用 Cloudflare 的 D1 数据库,搭配 Workers 构建一个完全免费的无服务器后端!D1 是基于 SQLite 的关系型数据库,零配置、自动扩展,而且有慷慨的免费额度。


D1 是什么?

D1 是 Cloudflare 推出的无服务器关系型数据库,基于 SQLite 构建。它和 Workers、Pages、R2 等产品深度集成,让你可以用纯 JavaScript/TypeScript 操作数据库。

核心特性

| 特性 | 说明 |
|------|------|
| 引擎 | SQLite(轻量、可靠) |
| 读写 | Workers 内直接调用,延迟极低 |
| 备份 | 自动备份与时间点恢复 |
| 免费额度 | 5 GB 存储 + 500 万行读取/月 |
| 计费方式 | 按实际使用量付费,超出后也很便宜 |

适合场景


  • 个人博客 / CMS

  • 小型 SaaS 应用

  • API 服务的数据层

  • 原型与 MVP 开发

  • 学习与实验项目

  • 环境准备

    前置条件


  • Node.js 18+

  • Cloudflare 账号(免费注册)

  • wrangler CLI
  • 安装 wrangler

    bash
    npm install -g wrangler

    登录 Cloudflare

    bash
    wrangler login

    浏览器会打开授权页面,登录你的 Cloudflare 账号即可。


    创建项目

    初始化 Workers 项目

    bash
    npx create-cloudflare my-app --type javascript
    cd my-app

    创建 D1 数据库

    bash
    # 创建数据库
    wrangler d1 create my-database

    执行后会输出数据库 ID,类似:

    text
    ✅ Successfully created DB 'my-database'
    [[d1_databases]]
    binding = "DB"
    database_name = "my-database"
    database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

    配置 wrangler.toml

    将输出的配置添加到 wrangler.toml

    toml
    name = "my-app"
    main = "src/index.js"
    compatibility_date = "2025-12-01"
    
    [[d1_databases]]
    binding = "DB"
    database_name = "my-database"
    database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"


    建表与数据操作

    创建表结构

    创建一个 schema.sql 文件:

    sql
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      email TEXT UNIQUE NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );
    
    CREATE TABLE IF NOT EXISTS posts (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      title TEXT NOT NULL,
      content TEXT,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY (user_id) REFERENCES users(id)
    );

    初始化数据库

    bash
    wrangler d1 execute my-database --file=schema.sql

    编写 Worker

    src/index.js 中:

    javascript
    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
        
        if (url.pathname === '/api/users' && request.method === 'GET') {
          const { results } = await env.DB.prepare(
            'SELECT * FROM users ORDER BY created_at DESC'
          ).all();
          return Response.json(results);
        }
        
        if (url.pathname === '/api/users' && request.method === 'POST') {
          const { name, email } = await request.json();
          const { success } = await env.DB.prepare(
            'INSERT INTO users (name, email) VALUES (?, ?)'
          ).bind(name, email).run();
          return Response.json({ success });
        }
        
        if (url.pathname.startsWith('/api/users/') && request.method === 'GET') {
          const id = url.pathname.split('/')[3];
          const user = await env.DB.prepare(
            'SELECT * FROM users WHERE id = ?'
          ).bind(id).first();
          return Response.json(user || { error: 'not found' });
        }
        
        return new Response('D1 API is running!');
      }
    };

    部署

    bash
    wrangler deploy


    进阶技巧

    1. 参数化查询(防 SQL 注入)

    始终使用 ? 占位符和 .bind() 来传递参数:

    javascript
    // 正确做法
    const { results } = await env.DB.prepare(
      'SELECT * FROM posts WHERE user_id = ? AND status = ?'
    ).bind(userId, 'published').all();
    
    // 错误做法(不要拼接字符串)
    const { results } = await env.DB.prepare(
      `SELECT * FROM posts WHERE user_id = ${userId}`  // ❌ SQL 注入风险
    ).all();

    2. 批量插入

    javascript
    const stmt = env.DB.prepare(
      'INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)'
    );
    
    const batch = posts.map(post => 
      stmt.bind(post.user_id, post.title, post.content)
    );
    
    const result = await env.DB.batch(batch);

    3. 事务处理

    javascript
    const result = await env.DB.batch([
      env.DB.prepare('UPDATE users SET posts_count = posts_count + 1 WHERE id = ?').bind(userId),
      env.DB.prepare('INSERT INTO posts (user_id, title) VALUES (?, ?)').bind(userId, title)
    ]);

    4. 读取复制(Read Replication)

    D1 自动将数据复制到全球多个节点,读取操作会从最近的副本返回数据,延迟极低:

    javascript
    // 默认就是读取最近的副本
    const { results } = await env.DB.prepare('SELECT * FROM posts').all();


    本地开发

    使用 wrangler 的本地开发模式,不需要部署就能调试:

    bash
    wrangler dev --local --persist

    --persist 参数会让数据持久化到本地磁盘,重启后数据还在。


    与 Workers + Hono 组合

    使用 Hono 框架可以让路由更优雅:

    javascript
    import { Hono } from 'hono';
    
    const app = new Hono();
    
    app.get('/api/posts', async (c) => {
      const { results } = await c.env.DB.prepare(
        'SELECT * FROM posts ORDER BY created_at DESC LIMIT 20'
      ).all();
      return c.json(results);
    });
    
    app.post('/api/posts', async (c) => {
      const body = await c.req.json();
      const { success } = await c.env.DB.prepare(
        'INSERT INTO posts (title, content) VALUES (?, ?)'
      ).bind(body.title, body.content).run();
      return c.json({ success });
    });
    
    export default app;


    免费额度计算

    D1 的免费额度对个人项目来说非常充足:

    | 资源 | 免费额度 | 个人博客月消耗(估算) |
    |------|---------|----------------------|
    | 存储 | 5 GB | 50-100 MB |
    | 读取行数 | 500 万/月 | 5-20 万 |
    | 写入行数 | 10 万/月 | 1-5 万 |

    即使超出,价格也很低:每额外 100 万行读取仅 $0.001。


    常见问题

    Q: D1 和 KV 有什么区别?

    KV 是键值存储,适合简单读写、缓存场景。D1 是关系型数据库,支持 SQL 查询、联表、事务。需要复杂查询就用 D1,只存简单配置就用 KV。

    Q: D1 支持外键吗?

    支持。在创建表时定义 FOREIGN KEY 约束即可。

    Q: 数据会丢失吗?

    不会。D1 每天自动备份,支持时间点恢复(Point-in-Time Recovery)。

    Q: 可以用 Prisma 吗?

    可以。D1 支持 Prisma ORM,官方有集成方案。通过 wrangler d1 exportprisma migrate 配合使用。


    总结

    Cloudflare D1 让无服务器数据库变得前所未有的简单:

  • ✅ 零配置,一行命令创建数据库

  • ✅ Workers 内直接调用,延迟极低

  • ✅ 5GB 免费存储,个人项目完全够用

  • ✅ 自动备份,数据安全可靠

  • ✅ 全球分布读取,用户体验好
  • 如果你正在做一个新项目,不妨试试 D1 —— 它可能就是你一直在找的数据库方案。


    推荐指数:⭐⭐⭐⭐⭐

    官方文档:https://developers.cloudflare.com/d1/

    💬 评论区 (0)

    暂无评论,快来抢沙发吧!