Skip to content

集成 Drizzle

以 PostgreSQL 为例

安装依赖

bash
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit @types/pg
详细说明
  • drizzle-kit:Drizzle 迁移工具。
  • drizzle-orm:Drizzle ORM。
  • pg:PostgreSQL 驱动。
  • @types/pg:提供 PostgreSQL 驱动的类型定义。

环境变量

.env

md
DATABASE_URL=postgres://app_user:app_password@localhost:5432/app_db

生产环境不要提交真实连接串。应用连接数据库应使用独立业务用户,不要使用 PostgreSQL 超级用户。

推荐目录

bash
src
├── database
   ├── database.module.ts
   ├── database.provider.ts
   └── schema
       ├── index.ts
       └── users.ts
└── users
    ├── users.module.ts
    └── users.service.ts
drizzle
drizzle.config.ts

database 模块只负责数据库连接、Drizzle 实例和 schema 出口。业务模块通过注入数据库实例访问表。

定义 Schema

src/database/schema/users.ts

ts
import { pgTable, timestamp, uuid, varchar } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: varchar('name', { length: 64 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
})

src/database/schema/index.ts

ts
export * from './users'

常见字段习惯:

  • 主键优先使用 uuid 或 PostgreSQL identity。
  • 时间点优先使用 timestamp(..., { withTimezone: true })
  • 金额使用 numeric,不要使用浮点数。
  • 需要 JSON 查询和索引时使用 jsonb

配置 Drizzle Kit

drizzle.config.ts

ts
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  dialect: 'postgresql',
  schema: './src/database/schema/index.ts',
  out: './drizzle',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
})

如果 CLI 不能自动加载 .env,可以安装 dotenv 并在配置文件顶部引入:

ts
import 'dotenv/config'

生成和执行迁移

生成迁移:

bash
pnpm drizzle-kit generate

执行迁移:

bash
pnpm drizzle-kit migrate

开发阶段可以使用 push 快速同步 schema:

bash
pnpm drizzle-kit push

push 适合本地快速验证,不适合多人协作和生产环境。正式流程应提交 drizzle/ 目录下生成的迁移文件。

封装数据库 Provider

src/database/database.provider.ts

ts
import { Provider } from '@nestjs/common'
import { drizzle } from 'drizzle-orm/node-postgres'
import type { NodePgDatabase } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'

import * as schema from './schema'

export const DRIZZLE = Symbol('DRIZZLE')

export type Database = NodePgDatabase<typeof schema>

export const databaseProvider: Provider = {
  provide: DRIZZLE,
  useFactory: () => {
    const pool = new Pool({
      connectionString: process.env.DATABASE_URL,
    })

    return drizzle({
      client: pool,
      schema,
    })
  },
}

src/database/database.module.ts

ts
import { Global, Module } from '@nestjs/common'

import { databaseProvider } from './database.provider'

@Global()
@Module({
  providers: [databaseProvider],
  exports: [databaseProvider],
})
export class DatabaseModule {}

@Global() 可以减少重复导入,但会降低依赖显式性。大型项目也可以不使用全局模块,在需要数据库的业务模块中显式导入 DatabaseModule

在业务服务中使用

src/users/users.service.ts

ts
import { Inject, Injectable } from '@nestjs/common'
import { eq } from 'drizzle-orm'

import { Database, DRIZZLE } from '../database/database.provider'
import { users } from '../database/schema'

@Injectable()
export class UsersService {
  constructor(@Inject(DRIZZLE) private readonly db: Database) {}

  findAll() {
    return this.db.select().from(users)
  }

  findById(id: string) {
    return this.db.query.users.findFirst({
      where: eq(users.id, id),
    })
  }

  create(input: { name: string; email: string }) {
    return this.db.insert(users).values(input).returning()
  }
}

返回值由 Drizzle 根据 schema 推导。需要 DTO 校验时,仍然使用 NestJS 的 class-validatorzod 或其他校验方案处理请求输入。

事务

ts
await this.db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values({ name: 'vfan', email: 'vfan@example.com' }).returning()

  return user
})

事务内部只使用 tx,不要混用外层 this.db,避免部分语句不在同一个事务中执行。

生命周期

上面的 Provider 没有显式关闭连接池。更完整的做法是把 Pool 封装成可关闭的服务,在 onModuleDestroy 中调用 pool.end()

简单项目可以先使用 Provider 写法;长期运行服务建议改为 DatabaseService

ts
import { Injectable, OnModuleDestroy } from '@nestjs/common'
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'

import * as schema from './schema'

@Injectable()
export class DatabaseService implements OnModuleDestroy {
  private readonly pool = new Pool({
    connectionString: process.env.DATABASE_URL,
  })

  readonly db = drizzle({
    client: this.pool,
    schema,
  })

  onModuleDestroy() {
    return this.pool.end()
  }
}

业务服务注入 DatabaseService 后使用 databaseService.db

常见约定

  • schema 文件按业务域拆分,再从 schema/index.ts 统一导出。
  • 每次修改 schema 后生成迁移,并检查生成 SQL。
  • 生产环境只执行已提交的迁移,不使用 push
  • 数据库连接串放在环境变量或配置中心,不写入代码。
  • 查询条件、分页、排序由业务层显式控制,不直接透传客户端字段。
  • 复杂查询可以使用 Drizzle 的 sql 模板,但要保持参数化,避免 SQL 注入。

参考

基于 MIT 许可发布