TypeORM 核心概念是什么?Entity 和 Repository 怎么用?
TypeORM 的核心概念可以用一句话理解:用 TypeScript 类描述数据库结构,用 Repository、EntityManager 或 QueryBuilder 操作数据,再由 DataSource 统一管理连接、实体、迁移和事务。
如果只会 save() 和 find(),确实也能写业务;但一到关联查询、事务、迁移、多数据库配置,很多问题就会暴露出来。下面按实际项目里最常接触的顺序,把 TypeORM 的主要组件讲清楚。
Entity:数据库表在代码里的样子
Entity 是 TypeORM 的基础。一个 Entity 类通常对应数据库里的一张表,类的属性对应表字段。
typescriptimport { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, } from 'typeorm'; @Entity('users') export class User { @PrimaryGeneratedColumn() id: number; @Column({ length: 50 }) name: string; @Column({ unique: true }) email: string; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }
这里的 User 不是普通 DTO,而是 TypeORM 能识别的数据库模型。@Entity('users') 表示它映射到 users 表;如果不传表名,TypeORM 会按类名推导。
Column 装饰器:字段如何落到数据库里
Column 装饰器决定类属性和数据库列之间如何映射。常见装饰器有这些:
@Column():普通字段,可配置类型、长度、默认值、是否可空等。@PrimaryColumn():手动指定主键。@PrimaryGeneratedColumn():自增主键或 UUID 主键。@CreateDateColumn():插入时自动写入创建时间。@UpdateDateColumn():更新时自动刷新修改时间。@DeleteDateColumn():软删除时间字段。@Generated():生成额外值,比如 UUID。
例如:
typescript@Column({ type: 'varchar', length: 120, nullable: false }) title: string; @Column({ type: 'int', default: 0 }) viewCount: number;
这些配置最好和真实数据库约束保持一致。不要只在业务代码里判断“必填”,数据库层也要有对应约束,否则线上数据很容易脏。
DataSource:TypeORM 0.3 之后的连接入口
从 TypeORM 0.3 开始,官方用 DataSource 取代了旧版的 Connection。也就是说,以前很多文章里的 createConnection()、getConnection() 已经不是新项目的推荐写法。
typescriptimport { DataSource } from 'typeorm'; import { User } from './entity/User'; export const AppDataSource = new DataSource({ type: 'mysql', host: 'localhost', port: 3306, username: 'root', password: 'password', database: 'app', entities: [User], migrations: ['dist/migrations/*.js'], synchronize: false, logging: true, });
DataSource 负责管理数据库连接、实体注册、迁移、订阅器、缓存等配置。应用启动时通常先执行:
typescriptawait AppDataSource.initialize();
需要特别注意 synchronize。开发环境里它可以帮你根据 Entity 自动同步表结构,但生产环境必须设为 false。生产库结构变更应该走 migrations,否则一次字段删除或类型变化就可能造成数据损坏。
Repository:最常用的数据访问入口
Repository 是操作某个 Entity 的仓储对象,适合大多数 CRUD 场景。
typescriptconst userRepository = AppDataSource.getRepository(User); const user = userRepository.create({ name: 'Alice', email: 'alice@example.com', }); await userRepository.save(user); const users = await userRepository.find(); const oneUser = await userRepository.findOne({ where: { id: 1 }, }); await userRepository.update(1, { name: 'Alice Updated' }); await userRepository.delete(1);
Repository 的优点是直观、类型友好,适合写简单查询和常规业务逻辑。项目里如果每张表都有独立的服务层,Repository 通常就是服务层访问数据库的第一选择。
EntityManager:跨多个实体时更方便
Repository 更像“只管一张表”,EntityManager 则可以统一操作多个实体。
typescriptconst manager = AppDataSource.manager; const user = await manager.findOne(User, { where: { id: 1 }, }); const post = manager.create(Post, { title: 'Hello TypeORM', author: user, }); await manager.save(post);
EntityManager 在事务里尤其常见。事务回调里拿到的 transactionalEntityManager,必须用它来执行本次事务内的所有数据库操作,不要混用外面的 Repository,否则可能出现事务不生效的问题。
typescriptawait AppDataSource.transaction(async manager => { const user = await manager.save(User, { name: 'Bob', email: 'bob@example.com', }); await manager.save(Profile, { bio: 'TypeORM user', user, }); });
Relation:实体之间的关系怎么表达
TypeORM 支持常见的关系映射:
@OneToOne():一对一,比如用户和用户资料。@OneToMany():一对多,比如一个用户有多篇文章。@ManyToOne():多对一,比如多篇文章属于一个作者。@ManyToMany():多对多,比如文章和标签。
typescript@Entity() export class Post { @PrimaryGeneratedColumn() id: number; @Column() title: string; @ManyToOne(() => User, user => user.posts) author: User; } @Entity() export class User { @PrimaryGeneratedColumn() id: number; @Column() name: string; @OneToMany(() => Post, post => post.author) posts: Post[]; }
关系映射看起来简单,真正要小心的是加载方式。简单场景可以用 relations:
typescriptconst users = await userRepository.find({ relations: { posts: true, }, });
复杂查询更建议用 QueryBuilder,把 join 条件、筛选、排序写清楚,避免无意中加载过多数据。
QueryBuilder:复杂 SQL 的可控写法
当查询条件变多,或者需要关联查询、分页、聚合时,Repository 的 find 语法会显得吃力。这时可以用 QueryBuilder。
typescriptconst users = await AppDataSource .getRepository(User) .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.email LIKE :keyword', { keyword: '%@example.com' }) .andWhere('post.published = :published', { published: true }) .orderBy('user.createdAt', 'DESC') .skip(0) .take(20) .getMany();
QueryBuilder 的优势是接近 SQL,同时保留参数绑定,能减少 SQL 注入风险。调试时还可以用 getSql() 或 getQuery() 看最终生成的 SQL。
Active Record 和 Data Mapper 有什么区别
TypeORM 同时支持 Active Record 和 Data Mapper 两种模式。
Active Record 是把数据访问方法放到实体类上。实体需要继承 BaseEntity:
typescript@Entity() export class User extends BaseEntity { @PrimaryGeneratedColumn() id: number; @Column() name: string; static findByName(name: string) { return this.find({ where: { name } }); } } const users = await User.findByName('Alice');
Data Mapper 则把实体和数据库操作分开,通过 Repository 或 EntityManager 访问数据:
typescriptconst userRepository = AppDataSource.getRepository(User); const users = await userRepository.find({ where: { name: 'Alice' }, });
小项目用 Active Record 会比较顺手;中大型项目更常用 Data Mapper,因为实体更干净,测试和分层也更容易。尤其是业务逻辑复杂时,把数据库访问放在 service 或 repository 层通常更稳。
Migrations:生产环境管理表结构的方式
迁移系统用于记录数据库结构变化,比如创建表、增加字段、添加索引、修改外键等。它的价值不是“自动建表”,而是让数据库结构变更可追踪、可回滚、可在不同环境重复执行。
常见命令类似:
bashtypeorm migration:generate src/migrations/AddUserTable -d src/data-source.ts typeorm migration:run -d src/data-source.ts typeorm migration:revert -d src/data-source.ts
实际命令会受项目构建方式影响,比如 ts-node、NestJS、ESM/CJS 配置不同,写法也会不同。关键是:生产环境不要依赖 synchronize: true 改表,应该生成 migration,审核 SQL,再执行。
Transactions:多步写入必须一起成功
涉及扣库存、创建订单、写流水这类操作时,事务是必需的。TypeORM 可以直接用 DataSource.transaction():
typescriptawait AppDataSource.transaction(async manager => { await manager.update(User, { id: 1 }, { name: 'New Name' }); await manager.save(AuditLog, { action: 'update_user', targetId: 1, }); });
事务里最容易犯的错,是一部分操作用了 manager,另一部分又用了外部的 AppDataSource.getRepository()。这样代码看着在事务里,实际可能不在同一个事务上下文里。
Cache:查询缓存适合读多写少的数据
TypeORM 支持查询缓存,可以给不频繁变化的数据减少数据库压力。
typescriptconst users = await userRepository.find({ cache: 60000, });
QueryBuilder 也可以启用缓存:
typescriptconst users = await userRepository .createQueryBuilder('user') .where('user.active = :active', { active: true }) .cache(60000) .getMany();
缓存不是越多越好。用户权限、库存、余额这类强一致数据不适合随便缓存;配置项、分类、字典表这类读多写少的数据更合适。
多数据库支持:统一 API,不等于没有差异
TypeORM 支持 MySQL、MariaDB、PostgreSQL、SQLite、SQL Server、Oracle、CockroachDB、MongoDB 等多种数据库。它提供了统一的 Entity、Repository、QueryBuilder API,能降低切换数据库时的学习成本。
但不要误以为所有数据库能力都完全一样。比如 JSON 字段、全文索引、分页语法、锁、事务隔离级别,不同数据库仍然有差异。写通用业务可以依赖 TypeORM 抽象;写到数据库特性时,最好明确当前数据库类型,并在测试环境验证生成的 SQL。
怎么把这些概念串起来
一个典型 TypeORM 项目大概是这样运行的:
- 用 Entity 和 Column 装饰器描述表结构。
- 用 Relation 描述表之间的关系。
- 用 DataSource 初始化数据库连接和配置。
- 简单 CRUD 用 Repository。
- 跨实体操作或事务用 EntityManager。
- 复杂查询用 QueryBuilder。
- 生产环境结构变更用 migrations。
- 读多写少的数据按需开启 cache。
TypeORM 的核心不难,难的是边界:什么时候用 Repository,什么时候换 QueryBuilder;什么时候能用同步表结构,什么时候必须用迁移;什么时候关系可以自动加载,什么时候应该手写 join。把这些边界弄清楚,TypeORM 才不会从“省代码的工具”变成“线上排查 SQL 的麻烦”。