4. TypeORM实体定义与同步

TypeORM 的实体(Entity)是数据库表在应用层的映射对象。它既描述表结构,也承载查询与关系导航能力。理解实体定义,关键要抓住三条线:装饰器如何声明元数据、列类型如何映射到数据库、关系装饰器如何表达表间连接。在此基础上,再理解 synchronize 与迁移生成,才能把“写实体”与“改数据库”安全地衔接起来。

一、Entity 装饰器与实体注册

用 @Entity() 标记一个类为实体,括号内可指定表名。若不指定,TypeORM 默认使用类名(部分驱动会转为小写或蛇形)。实体必须被数据源(DataSource)的 entities 配置收集到,否则元数据不会被加载。常见做法是传入实体类数组,或使用 glob 路径匹配编译后的 .js 文件。

import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('increment')
  id: number;

  @Column({ type: 'varchar', length: 64, unique: true })
  email: string;

  @Column({ name: 'display_name', type: 'varchar', length: 32, nullable: true })
  displayName: string | null;

  @Column({ type: 'boolean', default: true })
  isActive: boolean;
}

这里 @PrimaryGeneratedColumn('increment') 声明自增主键;@Column 的 name 用于把属性名 displayName 映射为数据库列 display_name,这是 TypeORM 中处理命名差异的标准手段。属性类型(如 string、number)在 TypeScript 中只是编译期约束,真正决定数据库列类型的是 type 选项或驱动推断结果。

二、列类型与常用选项

列类型应显式声明,避免依赖隐式推断。常见类型包括:int、bigint、varchar、text、boolean、timestamp、date、decimal、json、uuid、enum。不同数据库对类型支持不同,例如 PostgreSQL 有 jsonb,MySQL 有 tinyint。跨库项目应优先使用 TypeORM 的通用类型,或在配置中按驱动分支处理。

常用列选项包括:nullable(是否允许 NULL)、default(默认值)、unique(唯一约束)、length(长度)、precision/scale(小数精度)、select: false(默认查询不返回该列,适合密码哈希)、transformer(读写时转换值)。例如金额字段:

@Column({ type: 'decimal', precision: 12, scale: 2, default: 0 })
balance: string; // decimal 在 TypeORM 中通常以字符串返回,避免浮点误差

注意 decimal 返回字符串而非 number,这是很多初学者踩坑的地方。若业务需要数值计算,应在应用层显式转换,或使用 transformer 统一处理。

三、关系装饰器:一对一、一对多、多对多

关系装饰器用于表达表间连接,常见有 @OneToOne、@OneToMany、@ManyToOne、@ManyToMany。其中 @OneToMany 与 @ManyToOne 通常成对出现,且“多”的一方持有外键。以用户与文章为例:

@Entity('posts')
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @ManyToOne(() => User, (user) => user.posts, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  user: User;

  @Column({ name: 'user_id' })
  userId: number;
}

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @OneToMany(() => Post, (post) => post.user)
  posts: Post[];
}

这里 @JoinColumn 指定外键列名;显式声明 userId 列可以避免每次查询都联表,也能在插入时直接赋值。多对多关系需要中间表,使用 @JoinTable 声明,并可自定义中间表名与列名。关系加载策略有 eager、lazy 与显式 relations 查询,生产环境建议显式指定,避免隐式联表带来的性能问题。

TypeORM 实体定义与同步机制 实体定义 @Entity('users') @PrimaryGeneratedColumn @Column({ type, name, ... }) @ManyToOne / @OneToMany @JoinColumn / @JoinTable → 生成 EntityMetadata DataSource entities: [User, Post] synchronize: false migrations: [...] migrationsTableName → 加载元数据 → 建立连接池 synchronize: true 对比实体与数据库结构 自动 ALTER TABLE ⚠ 仅适合开发环境 迁移生成 migration:generate 生成 SQL 迁移文件 migration:run 执行 EntitySchema 与装饰器对比 装饰器方式(推荐) • 类即实体,类型安全好 • 需开启 experimentalDecorators • 关系导航自然 EntitySchema 方式 • 纯对象描述,无需装饰器 • 适合动态/多租户场景 • 类型推断较弱

四、EntitySchema:不依赖装饰器的定义方式

当项目无法使用装饰器(如纯 JavaScript、或需要动态生成实体)时,可用 EntitySchema 以纯对象描述实体。它与装饰器方式在元数据层面等价,只是写法不同:

import { EntitySchema } from 'typeorm';

export const UserSchema = new EntitySchema({
  name: 'User',
  tableName: 'users',
  columns: {
    id: { type: Number, primary: true, generated: true },
    email: { type: String, length: 64, unique: true },
    displayName: { type: String, name: 'display_name', nullable: true },
  },
  relations: {
    posts: { type: 'one-to-many', target: 'Post', inverseSide: 'user' },
  },
});

EntitySchema 的优点是定义集中、便于程序化生成;缺点是失去类上的方法扩展能力,类型推断也不如装饰器直观。选择哪种方式,取决于团队技术栈与是否需要运行时动态建模。

五、synchronize 机制与迁移生成

synchronize: true 会在应用启动时对比实体元数据与数据库实际结构,自动执行建表、加列、改类型等操作。它开发期很方便,但生产环境必须关闭,原因有三:一是自动变更可能锁表或丢数据;二是无法审计变更历史;三是多实例启动时可能并发执行 DDL。正确做法是使用迁移(Migration)。

迁移生成流程:先保证 synchronize: false,再运行 typeorm migration:generate,TypeORM 会对比实体与当前数据库,生成包含 up 与 down 的迁移文件。检查 SQL 后,用 migration:run 执行,migration:revert 回滚。迁移表 migrations 记录已执行版本,保证幂等。

npx typeorm migration:generate src/migrations/AddUserTable -d src/data-source.ts
npx typeorm migration:run -d src/data-source.ts
npx typeorm migration:revert -d src/data-source.ts
迁移生成依赖数据库当前状态。若数据库已被手工改动,生成的迁移可能包含意外差异。建议在生成前先确认数据库结构与实体一致,或使用空库生成基线迁移。
开发环境可短暂开启 synchronize 快速迭代,但一旦进入联调或预发,就应切换为迁移流程,并把迁移文件纳入版本控制。

六、实践建议与常见判断

实体定义阶段,建议统一命名策略:数据库列用蛇形,TypeScript 属性用驼峰,通过 name 或命名策略统一映射。关系字段尽量显式声明外键列,避免隐式生成难以控制的列名。对于 select: false 的敏感列,查询时需用 addSelect 显式取出。迁移文件生成后,务必人工审查 up 中的 DDL,确认没有意外的 DROP 或类型收窄。判断同步是否成功,可查看迁移表记录、数据库实际表结构,以及应用启动日志中是否报元数据错误。

总结来说,实体定义解决“表长什么样”,关系装饰器解决“表怎么连”,EntitySchema 提供另一种声明路径,而 synchronize 与迁移解决“结构怎么安全地变”。把这几件事分清,TypeORM 的数据访问层就有了可靠的基础。