2. 数据建模基础
在进入 Prisma 或 TypeORM 的具体 API 之前,必须先建立一套稳定的关系型数据建模心智模型。ORM 只是把数据库结构映射成代码,如果底层概念不清楚,写出来的 Schema 或 Entity 往往只是“能跑”,却难以维护。本节围绕实体、关系、主键、外键、索引与约束展开,并对比 Prisma Schema 与 TypeORM Entity 在建模思想上的差异。
一、实体、属性与关系:从业务对象到表结构
关系型数据库用二维表表达业务对象。一个“实体”通常对应一张表,实体的“属性”对应列,实体的一个具体实例对应一行记录。例如电商场景中,User(用户)、Product(商品)、Order(订单)都是实体。实体之间通过关系连接,常见有三类:
- 一对一(1:1):如用户与其实名认证信息,一个用户只有一条认证记录。
- 一对多(1:N):如用户与订单,一个用户可下多笔订单,每笔订单只属于一个用户。
- 多对多(M:N):如订单与商品,一笔订单含多个商品,一个商品可出现在多笔订单中。关系型数据库无法直接表达多对多,必须引入中间表(如
OrderItem),把 M:N 拆成两个 1:N。
建模的第一步,是判断业务对象之间的基数(cardinality)与可选性(optionality)。基数回答“一个对多个还是多个对多个”,可选性回答“关系是否必须存在”。例如订单必须属于某个用户(必选),而用户可以不填手机号(可选)。这两个判断直接决定外键是否允许为 NULL、是否加唯一约束。
二、主键、外键与约束:保证数据完整性
主键(Primary Key)唯一标识一行记录,不可重复、不可为 NULL。常见选择有两类:自增整数(如 BIGINT AUTO_INCREMENT)和全局唯一标识(如 UUID、ULID)。自增主键写入快、索引小,但暴露业务量且分库分表时易冲突;UUID 便于分布式生成,但随机写入会带来索引页分裂,通常用有序 UUID(如 UUIDv7)缓解。
外键(Foreign Key)是子表中指向父表主键的列,用于表达 1:N 关系。外键约束保证“子表引用的父记录必须存在”,并可定义级联行为:ON DELETE CASCADE(父删子删)、ON DELETE RESTRICT(有子记录时禁止删父)、ON DELETE SET NULL(父删后子外键置空)。选择哪种级联,取决于业务语义:订单明细通常随订单级联删除,而用户被删除时订单往往应保留(改为软删除或 RESTRICT)。
约束(Constraint)还包括:非空(NOT NULL)、唯一(UNIQUE)、检查(CHECK,如 price >= 0)、默认值(DEFAULT)。约束是数据库层的最后一道防线,即使应用层校验被绕过,数据仍能保持合法。
三、索引:用空间换查询速度
索引是加速查询的数据结构,最常见是 B+ 树。没有索引时,查询需要全表扫描;有索引后,可按值快速定位。但索引并非越多越好:每个索引都会增加写入成本(INSERT/UPDATE/DELETE 需维护索引)并占用存储。
常见索引类型与适用场景:
- 主键索引:唯一且非空,通常聚簇存储。
- 唯一索引:保证列值唯一,如
email。 - 普通索引:加速等值与范围查询,如
created_at。 - 复合索引:多列组合,遵循最左前缀原则。例如
(user_id, created_at)可加速“某用户按时间排序的订单”,但单独查created_at用不上。 - 外键索引:多数数据库不会自动为外键建索引,需手动添加,否则父表删除时子表检查会全表扫描。
EXPLAIN 查看执行计划:若 type 为 ALL 表示全表扫描,ref 或 range 表示命中索引。示例中的执行计划为教学假设,实际结果依赖数据分布与优化器。四、Prisma Schema 与 TypeORM Entity 的建模思想对比
两者都做“代码到表”的映射,但哲学不同。Prisma 使用独立的 schema.prisma 声明式 DSL,模型、字段、关系、索引集中描述,再通过 prisma migrate 生成 SQL 迁移;TypeORM 则用 TypeScript 类与装饰器,把实体定义直接写在代码里,通过 @Entity、@Column、@ManyToOne 等装饰器表达结构。
下面用同一业务模型(用户与订单)展示两种写法:
// Prisma Schema
model User {
id String @id @default(uuid())
email String @unique
orders Order[]
}
model Order {
id String @id @default(uuid())
userId String
total Decimal @db.Decimal(10, 2)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
@@index([userId, createdAt])
}
// TypeORM Entity
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ unique: true })
email: string;
@OneToMany(() => Order, (order) => order.user)
orders: Order[];
}
@Entity()
@Index(['userId', 'createdAt'])
export class Order {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column('decimal', { precision: 10, scale: 2 })
total: string;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => User, (user) => user.orders, { onDelete: 'RESTRICT' })
@JoinColumn({ name: 'userId' })
user: User;
@Column()
userId: string;
}
对比可见:Prisma 的关系由两侧共同声明(orders 与 user),外键与索引写在模型内,迁移由工具生成;TypeORM 的关系由装饰器双向声明,外键列需显式 @Column 或由 @JoinColumn 生成,索引可用 @Index 类级声明。Prisma 更接近“先设计 Schema,再生成客户端”,TypeORM 更接近“用类描述表,运行时同步或迁移”。
五、建模时的关键判断与常见陷阱
第一,主键选择要尽早确定。若业务可能分库分表或需要对外暴露 ID,优先 UUID/ULID;若单库自增且不暴露,自增整数更高效。第二,外键约束与级联行为必须与业务语义一致,不能默认全部 CASCADE,否则误删父记录会连带清空子数据。第三,索引要围绕真实查询建立:先看 WHERE、JOIN、ORDER BY 用到的列,再决定复合索引顺序,避免“每个列都建单列索引”的浪费。第四,唯一约束与业务校验要区分:邮箱唯一应由数据库 UNIQUE 保证,应用层只做友好提示。
@ManyToOne 而不写 @JoinColumn,可能导致外键列名不符合预期;在 Prisma 中忘记在关系另一侧声明数组字段,会导致 prisma validate 报错。两者都应在修改模型后运行迁移或校验命令,确认数据库结构与代码一致。理解这些底层概念后,无论使用 Prisma 还是 TypeORM,都能把业务关系准确翻译成表结构、外键与索引,并为后续的查询优化、迁移管理和数据一致性打下基础。