在现有项目中使用 Drizzle 和 SQLite Durable Objects 快速开始

本指南假定您已熟悉:
  • dotenv - 管理环境变量的包 - 点击阅读
  • tsx - 运行 TypeScript 文件的包 - 点击阅读
  • Cloudflare SQLite Durable Objects - 嵌入在 Durable Object 内的 SQLite 数据库 - 点击阅读
  • wrangler - Cloudflare 开发者平台命令行工具 - 点击阅读

基本文件结构

这是项目的基本文件结构。在 src/db 目录中,我们在 schema.ts 中有表定义。在 drizzle 文件夹中,有 SQL 迁移文件和快照。

📦 <project root>
 ├ 📂 drizzle
 ├ 📂 src
 │   ├ 📂 db
 │   │  └ 📜 schema.ts
 │   └ 📜 index.ts
 ├ 📜 .env
 ├ 📜 drizzle.config.ts
 ├ 📜 package.json
 └ 📜 tsconfig.json

第1步 - 安装所需包

npm
yarn
pnpm
bun
npm i drizzle-orm  dotenv
npm i -D drizzle-kit tsx

第2步 - 配置 wrangler.toml

你需要一个配置 D1 数据库的 wrangler.toml 文件,内容大致如下:

#:schema node_modules/wrangler/config-schema.json
name = "sqlite-durable-objects"
main = "src/index.ts"
compatibility_date = "2024-11-12"
compatibility_flags = [ "nodejs_compat" ]

# 绑定 Durable Object。Durable Object 是基于演员模型的按需扩缩计算原语。
# Durable Objects 可以长期存在。用于需要长时间运行“服务器”的场景,例如实时应用。
# 文档:https://developers.cloudflare.com/workers/wrangler/configuration/#durable-objects
[[durable_objects.bindings]]
name = "MY_DURABLE_OBJECT"
class_name = "MyDurableObject"

# Durable Object 迁移配置
# 文档:https://developers.cloudflare.com/workers/wrangler/configuration/#migrations
[[migrations]]
tag = "v1"
new_sqlite_classes = ["MyDurableObject"]

# 规则配置,便于后续步骤中导入迁移文件
[[rules]] 
type = "Text"
globs = ["**/*.sql"]
fallthrough = true

第3步 - 配置 Drizzle 配置文件

Drizzle 配置 - 由 Drizzle Kit 使用的配置文件,包含数据库连接、迁移文件夹和模式文件等信息。

在项目根目录创建 drizzle.config.ts 文件,内容如下:

drizzle.config.ts
import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
  out: './drizzle',
  schema: './src/db/schema.ts',
  dialect: 'sqlite',
  driver: 'durable-sqlite',
});
提示

你可以查看我们的教程了解如何从 Cloudflare 获取环境变量

第4步 - 反向分析数据库模式

Drizzle Kit provides a CLI command to introspect your database and generate a schema file with migrations. The schema file contains all the information about your database tables, columns, relations, and indices.

For example, you have such table in your database:

CREATE TABLE `users_table` (
	`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
	`name` text NOT NULL,
	`age` integer NOT NULL,
	`email` text NOT NULL UNIQUE
);

Pull your database schema:

npx drizzle-kit pull --init

The result of introspection will be a schema.ts file, meta folder with snapshots of your database schema, sql file with the migration and relations.ts file for relational queries.

Here is an example of the generated schema.ts file:

drizzle/schema.ts
// table schema generated by introspection
import {
  sqliteTable,
  uniqueIndex,
  integer,
  text,
} from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable(
  "users_table",
  {
    id: integer().primaryKey({ autoIncrement: true }).notNull(),
    name: text().notNull(),
    age: integer().notNull(),
    email: text().notNull(),
  },
  (table) => [
    uniqueIndex("users_table_email_unique").on(table.email)
  ]
);

Learn more about introspection in the documentation.

第5步 - 把代码转移到你的实际 schema 文件

We recommend transferring the generated code from drizzle/schema.ts and drizzle/relations.ts to the actual schema file. In this guide we transferred code to src/db/schema.ts. Generated files for schema and relations can be deleted. This way you can manage your schema in a more structured way.

 ├ 📂 drizzle
 │ ├ 📂 20242409125510_premium_mister_fear
 │ │ ├ 📜 snapshot.json
 │ │ └ 📜 migration.sql
 │ ├ 📜 relations.ts ────────┐
 │ └ 📜 schema.ts ───────────┤
 ├ 📂 src                    │ 
 │ ├ 📂 db                   │
 │ │ ├ 📜 relations.ts <─────┤
 │ │ └ 📜 schema.ts <────────┘
 │ └ 📜 index.ts         
 └ …

第6步 - 连接 Drizzle ORM 到数据库

import { drizzle, type DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'

export class MyDurableObject extends DurableObject {
	storage: DurableObjectStorage;
	db: DrizzleSqliteDODatabase;

	constructor(ctx: DurableObjectState, env: Env) {
		super(ctx, env);
		this.storage = ctx.storage;
		this.db = drizzle(this.storage, { logger: false });
	}
}

第7步 - 查询数据库

import { drizzle, DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import migrations from '../drizzle/migrations';
import { usersTable } from './db/schema';

export class MyDurableObject extends DurableObject {
  storage: DurableObjectStorage;
  db: DrizzleSqliteDODatabase<any>;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.storage = ctx.storage;
    this.db = drizzle(this.storage, { logger: false });

    // 保证所有迁移完成后再接受查询请求。
    // 否则你需要在访问 Drizzle 数据库 this.db 的函数中调用 this.migrate()。
    ctx.blockConcurrencyWhile(async () => {
      await this._migrate();
    });
  }

  async insertAndList(user: typeof usersTable.$inferInsert) {
    await this.insert(user);
    return this.select();
  }

  async insert(user: typeof usersTable.$inferInsert) {
    await this.db.insert(usersTable).values(user);
  }

  async select() {
    return this.db.select().from(usersTable);
  }

  async _migrate() {
    migrate(this.db, migrations);
  }
}

export default {
  /**
   * 这是 Cloudflare Worker 的标准 fetch 处理函数
   *
   * @param request - 从客户端发送到 Worker 的请求
   * @param env - 引用 wrangler.toml 中声明的绑定接口
   * @param ctx - Worker 的执行上下文
   * @returns 返回给客户端的响应
   */
  async fetch(request: Request, env: Env): Promise<Response> {
    const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('durable-object');
    const stub = env.MY_DURABLE_OBJECT.get(id);

    // 方案 A - 性能最佳。
    // 推荐将所有数据库交互打包成单个 Durable Object 调用,
    // 因为在 DO 内数据库访问速度非常快。
    const usersAll = await stub.insertAndList({
      name: 'John',
      age: 30,
      email: 'john@example.com',
    });
    console.log('新增用户,获取数据库中的所有用户: ', users);
    /*
    const users: {
      id: number;
      name: string;
      age: number;
      email: string;
      phone: string | null;
    }[]
    */

    // 方案 B - 较慢,但有时适用于调试。
    // 也可以直接调用单个公开的 Drizzle 查询,
    // 但每个查询都是一次 Durable Object 实例的往返请求。
    await stub.insert({
      name: 'John',
      age: 30,
      email: 'john@example.com',
    });
    console.log('新增用户!');

    const users = await stub.select();
    console.log('获取数据库中的所有用户: ', users);
    /*
    const users: {
      id: number;
      name: string;
      age: number;
      email: string;
      phone: string | null;
    }[]
    */

    return Response.json(users);
  }
}

第8步 - 运行 index.ts 文件

要运行任何 TypeScript 文件,你有几种选择,但我们这里先专注于一种:使用 tsx

你已经安装了 tsx,所以现在可以运行我们的查询了

运行 index.ts 脚本

npm
yarn
pnpm
bun
npx tsx src/index.ts
提示

我们建议使用 bun 来运行 TypeScript 文件。使用 bun,无论你的项目是配置为 CommonJS(CJS)、ECMAScript 模块(ESM)还是其他模块格式,这类脚本都可以顺利运行,无需额外设置或调整。
使用 bun 运行脚本,执行以下命令:

bun src/index.ts

如果你还没有安装 bun,请查看 Bun 安装文档

第9步 - 更新你的表结构(可选)

If you want to update your table schema, you can do it in the schema.ts file. For example, let’s add a new column phone to the users_table:

src/db/schema.ts
// table schema generated by introspection
import {
  sqliteTable,
  uniqueIndex,
  integer,
  text,
} from "drizzle-orm/sqlite-core";

export const usersTable = sqliteTable(
  "users_table",
  {
    id: integer().primaryKey({ autoIncrement: true }).notNull(),
    name: text().notNull(),
    age: integer().notNull(),
    email: text().notNull(),
    phone: text(),
  },
  (table) => [
    uniqueIndex("users_table_email_unique").on(table.email)
  ]
);

第10步 - 将更改应用到数据库(可选)

您可以使用 drizzle-kit push 命令直接将更改应用到数据库中。这是一种方便的方法,适合在本地开发环境中快速测试新的架构设计或修改,允许快速迭代而无需管理迁移文件:

npx drizzle-kit push

文档 中了解更多关于 push 命令的信息。

小贴士

或者,您可以使用 drizzle-kit generate 命令生成迁移文件,然后使用 drizzle-kit migrate 命令应用这些迁移:

生成迁移:

npx drizzle-kit generate

应用迁移:

npx drizzle-kit migrate

文档 中了解更多关于迁移流程的信息。

第11步 - 使用新字段查询数据库(可选)

src/index.ts
import { drizzle, DrizzleSqliteDODatabase } from 'drizzle-orm/durable-sqlite';
import { DurableObject } from 'cloudflare:workers'
import { migrate } from 'drizzle-orm/durable-sqlite/migrator';
import migrations from '../drizzle/migrations';
import { usersTable } from './db/schema';

export class MyDurableObject extends DurableObject {
  storage: DurableObjectStorage;
  db: DrizzleSqliteDODatabase<any>;

  constructor(ctx: DurableObjectState, env: Env) {
    super(ctx, env);
    this.storage = ctx.storage;
    this.db = drizzle(this.storage, { logger: false });

    // 保证所有迁移完成后再接受查询请求。
    // 否则你需要在访问 Drizzle 数据库 this.db 的函数中调用 this.migrate()。
    ctx.blockConcurrencyWhile(async () => {
      await this._migrate();
    });
  }

  async insertAndList(user: typeof usersTable.$inferInsert) {
    await this.insert(user);
    return this.select();
  }

  async insert(user: typeof usersTable.$inferInsert) {
    await this.db.insert(usersTable).values(user);
  }

  async select() {
    return this.db.select().from(usersTable);
  }

  async _migrate() {
    migrate(this.db, migrations);
  }
}

export default {
  /**
   * 这是 Cloudflare Worker 的标准 fetch 处理函数
   *
   * @param request - 从客户端发送到 Worker 的请求
   * @param env - 引用 wrangler.toml 中声明的绑定接口
   * @param ctx - Worker 的执行上下文
   * @returns 返回给客户端的响应
   */
  async fetch(request: Request, env: Env): Promise<Response> {
    const id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('durable-object');
    const stub = env.MY_DURABLE_OBJECT.get(id);

    // 方案 A - 性能最佳。
    // 推荐将所有数据库交互打包成单个 Durable Object 调用,
    // 因为在 DO 内数据库访问速度非常快。
    const usersAll = await stub.insertAndList({
      name: 'John',
      age: 30,
      email: 'john@example.com',
      phone: '123-456-7890',
    });
    console.log('新增用户,获取数据库中的所有用户: ', users);
    /*
    const users: {
      id: number;
      name: string;
      age: number;
      email: string;
      phone: string | null;
    }[]
    */

    // 方案 B - 较慢,但有时适用于调试。
    // 也可以直接调用单个公开的 Drizzle 查询,
    // 但每个查询都是一次 Durable Object 实例的往返请求。
    await stub.insert({
      name: 'John',
      age: 30,
      email: 'john@example.com',
      phone: '123-456-7890',
    });
    console.log('新增用户!');

    const users = await stub.select();
    console.log('获取数据库中的所有用户: ', users);
    /*
    const users: {
      id: number;
      name: string;
      age: number;
      email: string;
      phone: string | null;
    }[]
    */

    return Response.json(users);
  }
}