Astro + drizzle ORM + Cloudflare worker + D1 使用指南

在Astro + drizzle ORM + Cloudflare worker + D1 技术栈使用时,我被开发环境和 cloudflare worker 环境使用 D1 数据库 搞的晕头转向,这里记录一下。

drizzle.config.ts 配置

import { defineConfig } from 'drizzle-kit';
import fs from 'node:fs';
import path from 'node:path';

export default defineConfig({
  out: './src/db/migrations',
  schema: './src/db/schema.ts',
  dialect: 'sqlite',
  driver: 'd1-http',
  dbCredentials: {
    accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
    databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
    token: process.env.CLOUDFLARE_D1_TOKEN!,
  },
});

如果你想使用drizzle-kit generate 、migrate、studio等命令,就必须使用 d1-http 库链接

注意:需要在环境变量中配置 CLOUDFLARE_ACCOUNT_ID、CLOUDFLARE_DATABASE_ID 和 CLOUDFLARE_D1_TOKEN。

src/db/index.ts代码

这里是最麻烦的,你要考虑到 Cloudflare Worker 的运行环境和本地开发环境的差异。

Cloudflare Worker是需要直接使用 DBindings 来访问 D1 数据库的,而本地开发环境则需要使用 HTTP 接口来访问 D1 数据库。

import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
import { getEnv } from '@/lib/env';

export type DbType = ReturnType<typeof drizzle<typeof schema>>;

let cachedDb: DbType | null = null;
let cachedBinding: any = null;

class D1RemoteBinding {
  constructor(
    private accountId: string,
    private databaseId: string,
    private apiToken: string
  ) {}

  prepare(sql: string) {
    const execute = async (params: any[] = []) => {
      const url = `https://api.cloudflare.com/client/v4/accounts/${this.accountId}/d1/database/${this.databaseId}/query`;
      const response = await fetch(url, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${this.apiToken}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ sql, params }),
      });

      const data = await response.json();
      if (!data.success) {
        throw new Error(`D1 远程查询失败: ${JSON.stringify(data.errors)}`);
      }
      // console.log('D1 API Response:', JSON.stringify(data, null, 2));
      return data.result[0];
    };

    const statement = {
      bind: (...params: any[]) => ({
        // all() 需要返回 { results: [...] } 格式
        all: () => execute(params),
        get: () => execute(params).then((res) => res.results[0]),
        run: () => execute(params),
        // values() 应返回二维数组,每行是值数组(按列顺序)
        values: () => execute(params).then((res) => res.results.map((row: Record<string, any>) => Object.values(row))),
        first: () => execute(params).then((res) => res.results[0]),
        // raw() 应返回二维数组格式,这是 Drizzle ORM 用于字段映射的核心方法
        raw: () => execute(params).then((res) => res.results.map((row: Record<string, any>) => Object.values(row))),
      }),
      // 无参数版本
      all: () => execute([]),
      get: () => execute([]).then((res) => res.results[0]),
      run: () => execute([]),
      values: () => execute([]).then((res) => res.results.map((row: Record<string, any>) => Object.values(row))),
      first: () => execute([]).then((res) => res.results[0]),
      raw: () => execute([]).then((res) => res.results.map((row: Record<string, any>) => Object.values(row))),
    };

    return statement;
  }

  async batch(statements: any[]) {
    // 简单实现 batch
    return Promise.all(statements.map((s) => s.run()));
  }

  async exec(sql: string) {
    return this.prepare(sql).run();
  }
}

export async function getDb(): Promise<DbType> {
  if (cachedDb && cachedBinding) {
    return cachedDb;
  }

  const globalBinding = (globalThis as any).__D1_BINDING__;
  if (globalBinding) {
    cachedBinding = globalBinding;
    cachedDb = drizzle(globalBinding, { schema });
    return cachedDb;
  }

  if (typeof process !== 'undefined') {
    const { config } = await import('dotenv');
    config();
  }

  const accountId = getEnv('CLOUDFLARE_ACCOUNT_ID');
  const databaseId = getEnv('CLOUDFLARE_DATABASE_ID');
  const apiToken = getEnv('CLOUDFLARE_D1_TOKEN');

  if (accountId && databaseId && apiToken) {
    try {
      const remoteBinding = new D1RemoteBinding(accountId, databaseId, apiToken);
      cachedBinding = remoteBinding;
      cachedDb = drizzle(remoteBinding as any, { schema });
      console.log('🔗 已通过 Cloudflare API 连接到生产环境 D1 数据库');
      return cachedDb;
    } catch (error) {
      console.error('无法初始化远程 D1 连接:', error);
    }
  }

  throw new Error(
    'D1 数据库连接失败。\n' +
    '请确保在 .env 中正确配置了生产环境的 D1 凭证:\n' +
    '- CLOUDFLARE_ACCOUNT_ID\n' +
    '- CLOUDFLARE_DATABASE_ID\n' +
    '- CLOUDFLARE_D1_TOKEN'
  );
}

export function setGlobalD1Binding(binding: any) {
  (globalThis as any).__D1_BINDING__ = binding;
  cachedBinding = binding;
  cachedDb = drizzle(binding, { schema });
}

本地开发环境可以自己实现一个D1RemoteBinding 或者使用第三方 proxy:例如 @nerdfolio/drizzle-d1-proxy 这类库提供运行时 HTTP 代理访问 D1。

那么这里能不能直接使用 d1-http 库替换 D1RemoteBinding 呢?

答案是不能。这里不能“直接用 d1-http 库替换”而不改代码。d1-http 是 Drizzle Kit 的 CLI 连接驱动(用于 migrate/push/studio 等),不是 drizzle-orm 运行时公开的 DB 适配器,所以它不适合在你的应用运行时直接替代 D1RemoteBinding。(orm.drizzle.team)

更具体一点:

d1-http 的官方定位是 Drizzle Kit(迁移/管理工具)。 driver: ‘d1-http’ 是给 drizzle-kit 用的,不是给应用运行时 drizzle() 用的。

应用运行时在 Cloudflare 上的标准做法:

  • 正式做法是直接用 D1 binding:drizzle(env.DB),即 Worker 运行时的 D1 绑定。(orm.drizzle.team)
  • D1RemoteBinding 是为本地/非 Worker 环境用 API Token 访问 D1 的“替代方案”。
Share :

Related Posts

Astro + Cloudflare worker 环境变量问题

locals?.runtime?.env , import.meta.env ,process.env 等环境变量有什么区别?

阅读全文 →

10 天上线一个网站,一次不完美的上站经历

10 天上线一个网站,3 次技术架构重构。从 Vercel + Neon,到 Cloudflare Container,再到 Cloudflare Worker + Astro + D1,一路踩坑、调优、推翻重来。这是一篇关于性能瓶颈、冷启动误判、数据库选型与边缘计算实践的完整复盘。

阅读全文 →