Skip to content

第三章:主键装饰器

本章目标

  • 掌握四种主键写法:自增、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