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 查询,生产环境建议显式指定,避免隐式联表带来的性能问题。
四、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 的数据访问层就有了可靠的基础。