第三章:主键装饰器
本章目标
- 掌握四种主键写法:自增、UUID、手动赋值、组合主键
- 理解
@PrimaryGeneratedColumn("uuid")在 MySQL 下的真实生成方式 - 会用
@Generated给非主键列生成值 - 能根据业务场景选择合适的主键策略
核心概念
主键就是每行数据的"身份证号":唯一、不可为空。TypeORM 提供三种思路:
@PrimaryColumn:自己发身份证(学号、手机号等天然唯一值)@PrimaryGeneratedColumn():数据库发号(MySQL 的 AUTO_INCREMENT)@PrimaryGeneratedColumn("uuid"):TypeORM 发号(生成一串全球唯一的随机字符串)
知识点详解
三种主键写法对照
ts
// ① 自增主键:插入时不传 id,数据库自动 1、2、3……
@Entity()
class Article {
@PrimaryGeneratedColumn()
id: number;
}
// ② UUID 主键:插入时不传 id,得到 "f16dee3d-b808-..." 这样的字符串
@Entity()
class Order {
@PrimaryGeneratedColumn("uuid")
id: string; // 注意类型是 string
}
// ③ 手动主键:插入时必须自己赋值
@Entity()
class Student {
@PrimaryColumn({ length: 20 })
studentNo: string;
}
"uuid"在 MySQL 下的真相(查MysqlDriver.js确认):MySQL 驱动isUUIDGenerationSupported()返回false——UUID 不是数据库函数生成的,而是 TypeORM 在 Node.js 端用crypto随机生成后随 INSERT 写入;列类型为varchar(36)。(MariaDB 10.7+ 有原生 uuid 列类型,MySQL 没有。)
@Generated:非主键的生成列
生成值不是主键的专利。@Generated 可以和 @Column 配合,给普通列生成值(注意:不是主键时要把生成列也写进表里):
ts
@Entity()
class Article {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 36 })
@Generated("uuid") // 对外暴露的业务编号,如 "b3a75a43-..."
code: string;
}@Generated 的策略为 "increment" | "uuid" | "rowid"(rowid 仅 CockroachDB)。
组合主键:多列共同唯一
一张表只能有一个主键,但主键可以由多列组合而成。典型场景:选课记录 = 学号 + 课程号,同一学生同一门课只能有一条记录:
ts
@Entity()
class Enrollment {
@PrimaryColumn()
studentId: number;
@PrimaryColumn()
courseId: number;
@Column({ nullable: true })
score: number | null;
}
// 查询时要给全所有主键列
await enRepo.findOneBy({ studentId: 1, courseId: 102 });自增起始值与步长(概念)
MySQL 自增行为由数据库控制,不是 TypeORM 管的事:
- 起始值:
ALTER TABLE article AUTO_INCREMENT = 1000;(下一个 id 从 1000 开始) - 步长/偏移:由服务器变量
auto_increment_increment(步长)和auto_increment_offset(偏移)控制,多用于多主复制避免主键冲突 - 注意:
repo.clear()(TRUNCATE)会把自增计数重置回 1;DELETE FROM不会
实验
演示(1)
ts
/**
* 第 03 章:主键策略
* - @PrimaryGeneratedColumn() 自增 int(AUTO_INCREMENT)
* - @PrimaryGeneratedColumn("uuid") UUID(MySQL 下由 TypeORM 在 Node 端生成,列为 varchar(36))
* - @PrimaryColumn 手动赋值
* - @Generated("uuid") + @Column 非主键的生成列
* - 组合主键:Enrollment(studentId + courseId)
*/
import "reflect-metadata";
import {
DataSource,
Entity,
Column,
PrimaryColumn,
PrimaryGeneratedColumn,
Generated,
} from "typeorm";
import {
createDatabaseIfNotExists,
BASE_DATASOURCE_OPTIONS,
} from "../utils/db";
/** 1. 自增主键 */
@Entity()
class Article {
@PrimaryGeneratedColumn() // int + AUTO_INCREMENT
id: number;
@Column()
title: string;
/** 非主键也可以让 TypeORM 生成值:如对外暴露的业务编号 */
@Column({ length: 36 })
@Generated("uuid")
code: string;
}
/** 2. UUID 主键 */
@Entity()
class Order {
@PrimaryGeneratedColumn("uuid") // varchar(36),值由 TypeORM 生成
id: string;
@Column()
amount: number;
}
/** 3. 手动主键(自己赋值,如学号、身份证号) */
@Entity()
class Student {
@PrimaryColumn({ length: 20 })
studentNo: string;
@Column()
name: string;
}
/** 4. 组合主键:选课记录 = 学号 + 课程号 */
@Entity()
class Enrollment {
@PrimaryColumn({ type: "int" })
studentId: number;
@PrimaryColumn({ type: "int" })
courseId: number;
@Column({ nullable: true })
score: number | null;
}
async function main() {
const dbName = "typeorm_ch03";
await createDatabaseIfNotExists(dbName);
const ds = new DataSource({
...BASE_DATASOURCE_OPTIONS,
database: dbName,
entities: [Article, Order, Student, Enrollment],
synchronize: true,
logging: false,
});
await ds.initialize();
console.log("✅ 1. 四张表已同步");
// 看看 uuid 主键在 MySQL 里到底是什么列类型
const cols: any[] = await ds.query(
`SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_KEY, EXTRA
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = ? AND COLUMN_KEY IN ('PRI') ORDER BY TABLE_NAME, ORDINAL_POSITION`,
[dbName],
);
console.log(" 各表主键列:");
for (const c of cols) {
console.log(
` ${c.TABLE_NAME}.${c.COLUMN_NAME} ${c.COLUMN_TYPE} EXTRA=${c.EXTRA || "(无)"}`,
);
}
// ---------- 1. 自增主键 ----------
const articleRepo = ds.getRepository(Article);
await articleRepo.clear();
const a1 = await articleRepo.save({ title: "第一篇" });
const a2 = await articleRepo.save({ title: "第二篇" });
console.log(
"✅ 2. 自增主键:a1.id =",
a1.id,
",a2.id =",
a2.id,
"(插入时不传,数据库自动生成)",
);
console.log(" @Generated('uuid') 非主键生成列 a1.code =", a1.code);
// ---------- 2. UUID 主键 ----------
const orderRepo = ds.getRepository(Order);
await orderRepo.clear();
const o1 = await orderRepo.save({ amount: 99.5 });
console.log("✅ 3. UUID 主键:o1.id =", o1.id);
console.log(
" 注意:MySQL 驱动不支持数据库侧生成 UUID,这个值是 TypeORM 在 Node.js 里生成后写进去的",
);
// ---------- 3. 手动主键 ----------
const studentRepo = ds.getRepository(Student);
await studentRepo.clear();
const s1 = await studentRepo.save({ studentNo: "S20260001", name: "小明" });
console.log(
"✅ 4. 手动主键:s1.studentNo =",
s1.studentNo,
"(必须由我们赋值,不传会报错)",
);
// ---------- 4. 组合主键 ----------
const enRepo = ds.getRepository(Enrollment);
await enRepo.clear();
await enRepo.save({ studentId: 1, courseId: 101, score: 90 });
await enRepo.save({ studentId: 1, courseId: 102, score: 85 });
await enRepo.save({ studentId: 2, courseId: 101, score: null });
const found = await enRepo.findOneBy({ studentId: 1, courseId: 102 });
console.log("✅ 5. 组合主键:查到 studentId=1 & courseId=102 →", found);
try {
await enRepo.insert({ studentId: 1, courseId: 101, score: 0 }); // 重复组合主键
} catch (e: any) {
console.log(
" 重复 (1,101) 插入被拒 →",
e.code,
"(组合主键同样保证唯一)",
);
}
await ds.destroy();
console.log("🎉 第 03 章演示完成");
}
main().catch((e) => {
console.error("❌ 运行失败:", e);
process.exit(1);
});小结
@PrimaryColumn手动;@PrimaryGeneratedColumn()自增;@PrimaryGeneratedColumn("uuid")生成 UUID(Node 端生成,varchar(36))@Generated("uuid")可给非主键列生成值- 组合主键 = 多个
@PrimaryColumn,查询/删除时要给全所有主键列 - 单体应用默认 int 自增;分布式或对外暴露标识考虑 uuid