6月19日 15:56

TypeORM 核心概念是什么?Entity 和 Repository 怎么用?

TypeORM 的核心概念可以用一句话理解:用 TypeScript 类描述数据库结构,用 Repository、EntityManager 或 QueryBuilder 操作数据,再由 DataSource 统一管理连接、实体、迁移和事务。

如果只会 save()find(),确实也能写业务;但一到关联查询、事务、迁移、多数据库配置,很多问题就会暴露出来。下面按实际项目里最常接触的顺序,把 TypeORM 的主要组件讲清楚。

Entity:数据库表在代码里的样子

Entity 是 TypeORM 的基础。一个 Entity 类通常对应数据库里的一张表,类的属性对应表字段。

typescript
import { 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() 已经不是新项目的推荐写法。

typescript
import { 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 负责管理数据库连接、实体注册、迁移、订阅器、缓存等配置。应用启动时通常先执行:

typescript
await AppDataSource.initialize();

需要特别注意 synchronize。开发环境里它可以帮你根据 Entity 自动同步表结构,但生产环境必须设为 false。生产库结构变更应该走 migrations,否则一次字段删除或类型变化就可能造成数据损坏。

Repository:最常用的数据访问入口

Repository 是操作某个 Entity 的仓储对象,适合大多数 CRUD 场景。

typescript
const 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 则可以统一操作多个实体。

typescript
const 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,否则可能出现事务不生效的问题。

typescript
await 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

typescript
const users = await userRepository.find({ relations: { posts: true, }, });

复杂查询更建议用 QueryBuilder,把 join 条件、筛选、排序写清楚,避免无意中加载过多数据。

QueryBuilder:复杂 SQL 的可控写法

当查询条件变多,或者需要关联查询、分页、聚合时,Repository 的 find 语法会显得吃力。这时可以用 QueryBuilder。

typescript
const 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 访问数据:

typescript
const userRepository = AppDataSource.getRepository(User); const users = await userRepository.find({ where: { name: 'Alice' }, });

小项目用 Active Record 会比较顺手;中大型项目更常用 Data Mapper,因为实体更干净,测试和分层也更容易。尤其是业务逻辑复杂时,把数据库访问放在 service 或 repository 层通常更稳。

Migrations:生产环境管理表结构的方式

迁移系统用于记录数据库结构变化,比如创建表、增加字段、添加索引、修改外键等。它的价值不是“自动建表”,而是让数据库结构变更可追踪、可回滚、可在不同环境重复执行。

常见命令类似:

bash
typeorm 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()

typescript
await 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 支持查询缓存,可以给不频繁变化的数据减少数据库压力。

typescript
const users = await userRepository.find({ cache: 60000, });

QueryBuilder 也可以启用缓存:

typescript
const 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 项目大概是这样运行的:

  1. 用 Entity 和 Column 装饰器描述表结构。
  2. 用 Relation 描述表之间的关系。
  3. 用 DataSource 初始化数据库连接和配置。
  4. 简单 CRUD 用 Repository。
  5. 跨实体操作或事务用 EntityManager。
  6. 复杂查询用 QueryBuilder。
  7. 生产环境结构变更用 migrations。
  8. 读多写少的数据按需开启 cache。

TypeORM 的核心不难,难的是边界:什么时候用 Repository,什么时候换 QueryBuilder;什么时候能用同步表结构,什么时候必须用迁移;什么时候关系可以自动加载,什么时候应该手写 join。把这些边界弄清楚,TypeORM 才不会从“省代码的工具”变成“线上排查 SQL 的麻烦”。

标签:TypeORM