数据库模式意识技能
加载方式:base.md + [你的数据库技能]
问题: Claude在会话中忘记了模式细节 - 错误的列名,缺失的字段,错误的类型。TDD在运行时捕获这个问题,但我们可以在更早的阶段预防。
核心规则:在编写数据库代码之前先阅读模式
强制性:在编写任何涉及数据库的代码之前:
┌─────────────────────────────────────────────────────────────┐
│ 1. 阅读模式文件(见下方位置) │
│ 2. 验证你即将使用的列/类型是否存在 │
│ 3. 在编写查询时参考模式在你的回应中 │
│ 4. 使用生成的类型进行类型检查(Drizzle/Prisma等) │
└─────────────────────────────────────────────────────────────┘
如果模式文件不存在 → 在继续之前创建它。
模式文件位置(按技术栈)
| 技术栈 | 模式位置 | 类型生成 |
|---|---|---|
| Drizzle | src/db/schema.ts 或 drizzle/schema.ts |
内置TypeScript |
| Prisma | prisma/schema.prisma |
npx prisma generate |
| Supabase | supabase/migrations/*.sql + 类型 |
supabase gen types typescript |
| SQLAlchemy | app/models/*.py 或 src/models.py |
Pydantic模型 |
| TypeORM | src/entities/*.ts |
装饰器=类型 |
| Raw SQL | schema.sql 或 migrations/ |
需要手动类型 |
模式参考文件(推荐)
创建_project_specs/schema-reference.md用于快速查找:
# 数据库模式参考
*自动生成或手动维护。Claude:在数据库工作前阅读此文件。*
## 表
### users
| 列 | 类型 | 可空 | 默认 | 注释 |
|--------|------|----------|---------|-------|
| id | uuid | 否 | gen_random_uuid() | PK |
| email | text | 否 | - | 唯一 |
| name | text | 是 | - | 显示名称 |
| created_at | timestamptz | 否 | now() | - |
| updated_at | timestamptz | 否 | now() | - |
### orders
| 列 | 类型 | 可空 | 默认 | 注释 |
|--------|------|----------|---------|-------|
| id | uuid | 否 | gen_random_uuid() | PK |
| user_id | uuid | 否 | - | FK → users.id |
| status | text | 否 | 'pending' | 枚举:pending/paid/shipped/delivered |
| total_cents | integer | 否 | - | 分数额 |
| created_at | timestamptz | 否 | now() | - |
## 关系
- users 1:N orders (user_id)
## 枚举
- order_status: pending, paid, shipped, delivered
代码前检查表(数据库工作)
在编写任何数据库代码之前,Claude必须:
### 模式验证检查表
- [ ] 阅读模式文件:`[path to schema]`
- [ ] 我使用的列存在:[list columns]
- [ ] 类型与我的代码匹配:[list type mappings]
- [ ] 关系正确:[list FKs]
- [ ] 处理可空字段:[list nullable columns]
实际操作示例:
### TODO-042(添加订单历史端点)的模式验证
- [x] 阅读模式:`src/db/schema.ts`
- [x] 列存在:orders.id, orders.user_id, orders.status, orders.total_cents, orders.created_at
- [x] 类型:id=uuid→string, total_cents=integer→number, status=text→OrderStatus枚举
- [x] 关系:orders.user_id → users.id(多对一)
- [x] 可空:这些列都不是可空的
类型生成命令
Drizzle(TypeScript)
// 模式自动定义类型
// src/db/schema.ts
import { pgTable, uuid, text, integer, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
export const orders = pgTable('orders', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id),
status: text('status').notNull().default('pending'),
totalCents: integer('total_cents').notNull(),
createdAt: timestamp('created_at').notNull().defaultNow(),
});
// 推断类型 - 使用这些
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Order = typeof orders.$inferSelect;
export type NewOrder = typeof orders.$inferInsert;
Prisma
// prisma/schema.prisma
model User {
id String @id @default(uuid())
email String @unique
name String?
orders Order[]
createdAt DateTime @default(now()) @map("created_at")
@@map("users")
}
model Order {
id String @id @default(uuid())
userId String @map("user_id")
user User @relation(fields: [userId], references: [id])
status String @default("pending")
totalCents Int @map("total_cents")
createdAt DateTime @default(now()) @map("created_at")
@@map("orders")
}
# 生成类型后模式更改
npx prisma generate
Supabase
# 从实时数据库生成TypeScript类型
supabase gen types typescript --local > src/types/database.ts
# 或从远程
supabase gen types typescript --project-id your-project-id > src/types/database.ts
// 使用生成的类型
import { Database } from '@/types/database';
type User = Database['public']['Tables']['users']['Row'];
type NewUser = Database['public']['Tables']['users']['Insert'];
type Order = Database['public']['Tables']['orders']['Row'];
SQLAlchemy(Python)
# app/models/user.py
from sqlalchemy import Column, String, DateTime
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.sql import func
from app.db import Base
import uuid
class User(Base):
__tablename__ = "users"
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
email = Column(String, nullable=False, unique=True)
name = Column(String, nullable=True)
created_at = Column(DateTime(timezone=True), server_default=func.now())
# 关系
orders = relationship("Order", back_populates="user")
# app/schemas/user.py - Pydantic用于API验证
from pydantic import BaseModel, EmailStr
from uuid import UUID
from datetime import datetime
class UserBase(BaseModel):
email: EmailStr
name: str | None = None
class UserCreate(UserBase):
pass
class User(UserBase):
id: UUID
created_at: datetime
class Config:
from_attributes = True
模式感知TDD工作流程
扩展数据库工作的标凈TDD工作流程:
┌─────────────────────────────────────────────────────────────┐
│ 0. 模式:在其他任何事之前阅读和验证模式 │
│ └─ 阅读模式文件 │
│ └─ 完成模式验证检查表 │
│ └─ 注意任何缺失的列/表所需的 │
├─────────────────────────────────────────────────────────────┤
│ 1. RED:使用正确的列名编写测试 │
│ └─ 导入生成的类型 │
│ └─ 在测试中使用类型安全的查询 │
│ └─ 测试应在逻辑上失败,而不是模式错误 │
├─────────────────────────────────────────────────────────────┤
│ 2. GREEN:用类型安全的查询实现 │
│ └─ 使用ORM类型,而不是原始字符串 │
│ └─ TypeScript/mypy捕获列不匹配 │
├─────────────────────────────────────────────────────────────┤
│ 3. VALIDATE:类型检查捕获模式漂移 │
│ └─ tsc --noEmit / mypy捕获错误的列 │
│ └─ 测试验证运行时行为 │
└─────────────────────────────────────────────────────────────┘
常见模式错误(及如何预防)
| 错误 | 示例 | 预防 |
|---|---|---|
| 错误的列名 | user.userName vs user.name |
阅读模式,使用生成的类型 |
| 错误的类型 | totalCents作为字符串 |
类型生成捕获这个 |
| 缺少可空检查 | user.name!当可空时 |
模式显示可空字段 |
| 错误的FK关系 | order.userId vs order.user_id |
检查模式列名 |
| 缺少列 | 使用不存在的user.avatar |
编写代码前阅读模式 |
| 错误的枚举值 | status: 'complete' vs 'completed' |
在模式参考中记录枚举 |
类型安全查询示例
Drizzle(在编译时捕获错误):
// ✅ 正确 - 使用模式定义的列
const user = await db.select().from(users).where(eq(users.email, email));
// ❌ 错误 - TypeScript错误:'userName'不存在
const user = await db.select().from(users).where(eq(users.userName, email));
Prisma(在编译时捕获错误):
// ✅ 正确
const user = await prisma.user.findUnique({ where: { email } });
// ❌ 错误 - TypeScript错误
const user = await prisma.user.findUnique({ where: { userName: email } });
原始SQL(无保护 - 避免):
// ❌ 危险 - 没有类型检查,容易出错
const result = await db.query('SELECT * FROM users WHERE user_name = $1', [email]);
// 应该是'email'而不是'user_name' - 直到运行时才会捕获
迁移工作流程
当需要模式更改时:
┌─────────────────────────────────────────────────────────────┐
│ 1. 更新模式文件(Drizzle/Prisma/SQLAlchemy) │
├─────────────────────────────────────────────────────────────┤
│ 2. 生成迁移 │
│ └─ Drizzle: npx drizzle-kit generate │
│ └─ Prisma: npx prisma migrate dev --name add_column │
│ └─ Supabase: supabase migration new add_column │
├─────────────────────────────────────────────────────────────┤
│ 3. 重新生成类型 │
│ └─ Prisma: npx prisma generate │
│ └─ Supabase: supabase gen types typescript │
├─────────────────────────────────────────────────────────────┤
│ 4. 更新schema-reference.md │
├─────────────────────────────────────────────────────────────┤
│ 5. 运行类型检查 - 找到所有破损的代码 │
│ └─ npm run typecheck │
├─────────────────────────────────────────────────────────────┤
│ 6. 修复类型错误,更新测试,运行完整验证 │
└─────────────────────────────────────────────────────────────┘
会话开始协议
当开始涉及数据库工作的会话时:
- 立即阅读模式文件
- 如果存在,阅读
_project_specs/schema-reference.md - 在会话状态中注明相关的表/列
- 在编写代码时明确参考模式
会话状态示例:
## 当前会话 - 数据库上下文
**模式已读:**✓ src/db/schema.ts
**范围内的表:** users, orders, order_items
**关键列:**
- users: id, email, name, created_at
- orders: id, user_id, status, total_cents
- order_items: id, order_id, product_id, quantity, price_cents
反模式
- ❌ 猜测列名 - 总是先阅读模式
- ❌ 使用原始SQL字符串 - 使用带类型生成的ORM
- ❌ 不验证就硬编码 - 使用任何列之前检查模式
- ❌ 忽略类型错误 - 模式漂移显示为类型错误
- ❌ 不重新生成类型 - 迁移后总是重新生成
- ❌ 假设可空 - 检查模式中的可空列
清单
设置
- [ ] 模式文件存在于标准位置
- [ ] 类型生成配置
- [ ] 创建
_project_specs/schema-reference.md - [ ] 模式更改时重新生成类型
每项任务
- [ ] 在编写数据库代码之前阅读模式
- [ ] 完成模式验证检查表
- [ ] 使用生成的类型(不是原始字符串)
- [ ] 类型检查通过(捕获列错误)
- [ ] 测试使用正确的模式