第十二章 索引与约束
本章目标
- 会用
@Index建单列索引、复合索引、唯一索引,会自定义索引名。 - 分清
@Column({ unique: true })与类级@Unique的适用场景。 - 理解 MySQL 复合索引的"最左前缀"原则,并能用
EXPLAIN验证。 - 会用
onDelete外键约束控制"删父行时子行怎么办"。 - 知道
@Check与全文索引(FULLTEXT + ngram)在 1.1.1 + MySQL 下的真实表现。
核心概念
索引像书的目录:没有索引,数据库找"张三"要把整本书(全表)一页页翻完(全表扫描);有了索引,直接按目录跳到对应页。代价是:目录本身占空间,每写一行数据还要同时更新目录(写入变慢)。
约束像安检门:唯一约束(UNIQUE)保证"一个人只能占一个坑";外键约束(FOREIGN KEY)保证"你引用的人必须真实存在";检查约束(CHECK)保证"数值不能离谱"。数据在进门时就被拦住,脏数据进不了库。
@Index 的四种用法
- 第一种复合索引+自定义名
ts
@Entity()
@Index("idx_firstname_lastname", ["firstName", "lastName"]) // ① 类级:复合索引 + 自定义名
class IdxUser {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true }) // ② 列级唯一约束(本质是唯一索引)
email: string;
@Index() // ③ 属性级:单列索引(自动命名)
@Column()
city: string;
}
// ④ 唯一索引:@Index(["a","b"], { unique: true })| 写法 | 位置 | 效果 |
|---|---|---|
@Index() | 属性上 | 该列单列索引 |
@Index("name", ["a","b"]) | 类上 | 复合索引,自定义名 |
@Index(["a","b"], { unique: true }) | 类上 | 复合唯一索引 |
@Column({ unique: true }) | 属性上 | 该列唯一约束(= 唯一索引) |
@Unique("uq_x", ["a","b"]) | 类上 | 复合唯一约束 |
@Column({unique:true}) 与 @Unique 的取舍:单列唯一两者都行(生成的 DDL 都是 UNIQUE KEY);多列联合唯一只能用类级 @Unique 或 @Index({unique:true})。
用 EXPLAIN 验证索引是否生效
ts
const r1 = await ds.query(
"EXPLAIN SELECT * FROM idx_user WHERE firstName = '三'",
);
console.log(r1[0].key); // "idx_firstname_lastname" —— 命中复合索引
const r2 = await ds.query(
"EXPLAIN SELECT * FROM idx_user WHERE lastName = '张'",
);
console.log(r2[0].key); // null —— 非最左列,用不上EXPLAIN 结果里的 key 列是实际使用的索引,null 表示全表扫描。
外键约束 onDelete / onUpdate
写在关系选项里(RelationOptions.d.ts),取值:"RESTRICT" | "CASCADE" | "SET NULL" | "DEFAULT" | "NO ACTION":
ts
// 删作者时,文章跟着被数据库删掉
@ManyToOne(() => Author, { onDelete: "CASCADE" })
author: Author;
// 删作者时,文章保留,authorId 置为 NULL(外键列必须 nullable: true)
@ManyToOne(() => Author, { onDelete: "SET NULL", nullable: true })
author: Author | null;| 取值 | 删父行时的行为 |
|---|---|
CASCADE | 子行一起删除 |
SET NULL | 子行保留,外键列置 NULL(该列必须允许 NULL) |
RESTRICT / NO ACTION | 有子行时拒绝删除父行(MySQL 默认值) |
这是数据库层的级联,delete 父行时数据库自动处理,无需 ORM 加载子实体;而 cascade: ["remove"] 是 TypeORM 应用层的行为。
@Check 检查约束
- 有
@Check装饰器(decorator/Check.d.ts),但实测 MySQL 驱动建表时不会生成 CHECK 子句——装饰器被静默忽略,负数价格照样插入成功。MySQL 8.0.16+ 本身支持 CHECK,补救办法是原生 SQL(或第 14 章的迁移):
ts
await ds.query(
"ALTER TABLE `product` ADD CONSTRAINT `chk_product_price` CHECK (price >= 0)",
);
// 之后插入 price = -5 会报错:Check constraint 'chk_product_price' is violated.6. 全文索引 FULLTEXT(中文要配 ngram)
IndexOptions 里的 fulltext: true 仅 MySQL 可用,parser 可指定解析器。MySQL 默认解析器按空格/标点分词,不支持中文;MySQL 内置的 ngram 解析器按每 2 字切分,可检索中文:
ts
@Entity()
@Index("ft_title_content", ["title", "content"], {
fulltext: true,
parser: "ngram",
})
class Article {
@Column()
title: string;
@Column("text")
content: string;
}
// 查询用原生 SQL 的 MATCH ... AGAINST
const rows = await ds.query(
"SELECT id, title FROM `article` WHERE MATCH(title, content) AGAINST (? IN NATURAL LANGUAGE MODE)",
["索引"],
);