Skip to content

数据库管理

IAM 项目使用 Drizzle ORM 和 PostgreSQL(Neon)进行数据管理。Schema 定义在 packages/db/src/schema/,按职责分为三个模块,由 schema/index.ts 统一导出:

模块 内容
schema/auth.ts Better Auth 认证表、OAuth Provider 表、审计与限流表
schema/wecom.ts 企业微信通讯录同步表
schema/provisioning.ts 应用同步连接等 IAM 自有的业务表

因此导入时必须带模块子路径,例如 @IAM/db/schema/auth:

import { db } from '@IAM/db'
import { user } from '@IAM/db/schema/auth'

@IAM/db/schema 这种不带模块的裸路径无法解析,仓库中也没有这种写法。

模块 表
schema/auth.ts user、session、account、verification、jwks
oauth_client、oauth_resource、oauth_client_resource、oauth_client_role
oauth_client_user_access、oauth_client_department_access
oauth_access_token、oauth_refresh_token、oauth_consent、oauth_client_assertion
rate_limit、audit_log、user_profile_audit、app_provisioning_state
schema/wecom.ts wecom_department、wecom_user_profile、wecom_user_department、wecom_sync_task
schema/provisioning.ts provisioning_connection、provisioning_reconcile_job、provisioning_user_reconcile_job

使用 pgTable 定义表。下面是仓库中真实的 provisioning_connection 定义(packages/db/src/schema/provisioning.ts),演示了主键、外键、默认值与唯一索引:

import { boolean, integer, pgTable, text, timestamp, uniqueIndex } from 'drizzle-orm/pg-core'
import { oauthClient } from './auth'
export const provisioningConnection = pgTable(
'provisioning_connection',
{
clientId: text('client_id')
.primaryKey()
.references(() => oauthClient.clientId, { onDelete: 'cascade' }),
enabled: boolean('enabled').notNull().default(false),
endpoint: text('endpoint').notNull(),
keyId: text('key_id').notNull(),
encryptedSecret: text('encrypted_secret').notNull(),
version: integer('version').notNull().default(1),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow()
},
(table) => [uniqueIndex('provisioning_connection_key_id_uidx').on(table.keyId)]
)
  • text():文本字段
  • timestamp():时间戳字段
  • boolean():布尔字段
  • integer():整数字段
  • numeric():数值字段
  • .primaryKey():主键
  • .notNull():非空
  • .default():默认值
  • .unique():唯一约束
  • .references():外键引用

使用 relations 表达表之间的关系。真实示例(schema/auth.ts 中的 session 与 user):

import { relations } from 'drizzle-orm'
export const sessionRelations = relations(session, ({ one, many }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id]
}),
oauthRefreshTokens: many(oauthRefreshToken),
oauthAccessTokens: many(oauthAccessToken)
}))
  • one:一对一或多对一
  • many:一对多或多对多

以下示例使用真实的 user 表。

import { db } from '@IAM/db'
import { user } from '@IAM/db/schema/auth'
import { eq } from 'drizzle-orm'
// 查询所有记录
const allUsers = await db.select().from(user)
// 查询单条记录
const [found] = await db.select().from(user).where(eq(user.id, userId))

@IAM/db 还导出 db 与 sql,需要写原生 SQL 时使用 db.execute(sql\…`)`。

import { eq, and, or, like } from 'drizzle-orm'
// 等值查询
const activeUsers = await db.select().from(user).where(eq(user.status, 'active'))
// 多条件查询
const candidates = await db
.select()
.from(user)
.where(and(eq(user.status, 'active'), eq(user.banned, false)))
// 模糊查询
const matched = await db
.select()
.from(user)
.where(like(user.name, `%${keyword}%`))
await db.insert(provisioningConnection).values({
clientId: 'crm-client-id',
endpoint: 'https://crm.example.com/api/internal/iam/provisioning/users',
keyId: 'generated-key-id',
encryptedSecret: encrypted
})
// 插入并返回
const [created] = await db.insert(provisioningConnection).values(input).returning()
await db
.update(provisioningConnection)
.set({ enabled: true })
.where(eq(provisioningConnection.clientId, clientId))
// 更新并返回
const [updated] = await db
.update(provisioningConnection)
.set({ enabled: true })
.where(eq(provisioningConnection.clientId, clientId))
.returning()

注意 updated_at 上的 $onUpdate 由 Drizzle 在更新语句中写入,绕过 ORM 手写 SQL 时不会自动刷新。

await db.delete(provisioningConnection).where(eq(provisioningConnection.clientId, clientId))

外键带 onDelete: 'cascade' 时会随主记录级联删除(例如 provisioning_connection 随 oauth_client 删除)。

import { db } from '@IAM/db'
import { session, user } from '@IAM/db/schema/auth'
import { eq } from 'drizzle-orm'
const sessionsWithUser = await db
.select({
session,
user
})
.from(session)
.innerJoin(user, eq(session.userId, user.id))

开发、测试和生产环境都应优先使用可提交、可审查的 migration:

Terminal window
# 生成迁移文件
pnpm db:generate
# 应用迁移
pnpm db:migrate

如果调整 Better Auth 插件或认证字段,先固定使用 1.7.1 生成认证 schema:

Terminal window
pnpm dlx auth@1.7.1 generate --yes \
--config packages/auth/src/index.ts \
--output packages/db/src/schema/auth.ts
pnpm db:generate
pnpm db:migrate

DATABASE_URL 用于运行时访问;DATABASE_URL_DIRECT 用于 migration/DDL,未配置时 Drizzle 会回退到 DATABASE_URL。迁移文件输出到 packages/db/src/migrations/。

pnpm db:push 会直接修改数据库,可能造成 schema、migration 与真实数据库漂移,只允许在可丢弃的本地数据库临时验证。正式变更必须回到 migration 流程。

使用 Drizzle Studio 可视化管理数据库:

Terminal window
pnpm db:studio

这将打开一个 Web 界面,可以查看表结构、浏览和编辑数据、执行 SQL 查询。

  1. 使用迁移:所有正式变更都生成并提交 migration
  2. 索引优化:为常用查询字段添加索引
  3. 关系完整性:使用外键确保数据完整性
  4. 类型安全:利用 Drizzle 的类型推断
  5. 查询优化:避免 N+1 查询问题