Skip to content

第十二章 索引与约束 ​

本章目标 ​

  1. 会用 @Index 建单列索引、复合索引、唯一索引,会自定义索引名。
  2. 分清 @Column({ unique: true }) 与类级 @Unique 的适用场景。
  3. 理解 MySQL 复合索引的"最左前缀"原则,并能用 EXPLAIN 验证。
  4. 会用 onDelete 外键约束控制"删父行时子行怎么办"。
  5. 知道 @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)",
  ["索引"],
);