TypeORM 集成
本教程共 47 篇 · 第 26 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:在 NestJS 中接入 TypeORM,学会定义实体、使用仓库模式做增删改查、写事务和迁移。
做后端绕不开数据库。NestJS 对数据库的态度是”不绑定任何一家”,你想用什么都行。不过官方提供了几个开箱即用的集成包,其中 @nestjs/typeorm 是最成熟的一个。
TypeORM 是 TypeScript 生态里资历最老的 ORM 之一。它能用装饰器把类映射成数据库表,写起来跟写普通 TypeScript 类差不多。NestJS 跟它配合得很好,因为两边都是 TypeScript 优先。
安装依赖
先装包。以 MySQL 为例:
npm install @nestjs/typeorm typeorm mysql2
如果你用 PostgreSQL,把 mysql2 换成 pg;用 SQLite 就装 better-sqlite3。TypeORM 支持的数据库很多,流程基本一样,只是驱动包不同。
配置数据库连接
在根模块里用 TypeOrmModule.forRoot() 建立连接:
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
entities: [], // 后面填实体类
synchronize: true, // 开发环境自动同步表结构
}),
],
})
export class AppModule {}
这段代码做的事情很简单:告诉 TypeORM 数据库在哪、怎么连、哪些类是实体。
Warning
synchronize: true在开发阶段很方便,但生产环境千万别开。它会根据实体定义自动改表结构,线上数据可能直接丢。生产环境请用迁移(migration)。
用环境变量配置
实际项目不会把数据库密码写死在代码里。配合 @nestjs/config 模块,从 .env 文件读取配置:
// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
ConfigModule.forRoot(),
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DB_HOST'),
port: configService.get('DB_PORT'),
username: configService.get('DB_USERNAME'),
password: configService.get('DB_PASSWORD'),
database: configService.get('DB_DATABASE'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get('NODE_ENV') !== 'production',
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}
forRootAsync() 和 forRoot() 的区别就是:前者支持异步,可以注入其他服务。这样数据库配置就能从环境变量、远程配置中心等地方动态获取。
Tip
entities用 glob 路径**/*.entity{.ts,.js}可以自动扫描所有实体文件,不用一个个手动导入。这个写法在实际项目中用得最多。
NestJS 额外配置项
除了 TypeORM 原生的配置项,@nestjs/typeorm 还多了几个实用的选项:
| 选项 | 说明 | 默认值 |
|---|---|---|
retryAttempts | 连接失败重试次数 | 10 |
retryDelay | 重试间隔(毫秒) | 3000 |
autoLoadEntities | 自动加载通过 forFeature 注册的实体 | false |
autoLoadEntities 是个好东西,设成 true 之后,凡是在各模块里用 forFeature() 注册过的实体,都会自动加到连接配置里。不用在根模块手动维护实体列表了。
定义实体
实体就是数据库表的 TypeScript 映射。一个类对应一张表,类的属性对应表的列。
基本实体
// user.entity.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@Column({ default: true })
isActive: boolean;
@Column({ type: 'text', nullable: true })
bio: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
@Entity('users') 里的 'users' 是表名。不传的话,TypeORM 会用类名的小写形式作为表名。建议显式指定,避免歧义。
@Column 的 select: false 选项很实用。查数据时默认不会带上这个字段,适合密码这种敏感信息。需要的时候再手动 select。
常用列装饰器
| 装饰器 | 作用 |
|---|---|
@PrimaryGeneratedColumn() | 自增主键 |
@PrimaryColumn() | 自定义主键(比如用 UUID) |
@Column() | 普通列 |
@CreateDateColumn() | 创建时间,插入时自动填 |
@UpdateDateColumn() | 更新时间,每次更新自动刷新 |
@DeleteDateColumn() | 软删除标记,调用 softRemove 时自动填 |
@VersionColumn() | 乐观锁版本号,每次更新自动 +1 |
Note
@CreateDateColumn和@UpdateDateColumn是自动维护的,你不需要手动赋值。很多人会忘记这一点,在创建数据时手动传了个时间,结果被覆盖了。
列选项速查
@Column({
type: 'varchar', // 数据库列类型
length: 100, // varchar 长度
nullable: false, // 是否允许 NULL
unique: true, // 是否唯一
default: 'active', // 默认值
select: false, // 查询时是否默认包含
comment: '用户邮箱', // 数据库注释
})
email: string;
实体关系
数据库里表跟表之间的关系,在 TypeORM 里用装饰器声明。三种关系类型:一对一、一对多、多对多。
一对一
一个人对应一个档案:
// user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, profile => profile.user)
@JoinColumn() // 外键在这张表上
profile: Profile;
}
// profile.entity.ts
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
avatar: string;
@OneToOne(() => User, user => user.profile)
user: User;
}
@JoinColumn() 只能放在关系拥有外键的那一侧。就像”谁持有外键,谁负责声明”。
一对多
一个用户有多篇文章:
// user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@OneToMany(() => Post, post => post.author)
posts: Post[];
}
// post.entity.ts
@Entity()
export class Post {
@PrimaryGeneratedColumn()
id: number;
@Column()
title: string;
@ManyToOne(() => User, user => user.posts)
author: User;
}
@OneToMany 和 @ManyToOne 是成对出现的。外键在”多”的那一侧,也就是 Post 表里会有一个 authorId 字段。
多对多
用户和角色:一个用户可以有多个角色,一个角色也可以分配给多个用户。
// user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@ManyToMany(() => Role, role => role.users)
@JoinTable() // 中间表在这侧
roles: Role[];
}
// role.entity.ts
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => User, user => user.roles)
users: User[];
}
@JoinTable() 告诉 TypeORM 在这张表这边建中间表。中间表的名字和字段名都可以自定义,不写的话 TypeORM 会自动生成。
仓库模式(Repository)
TypeORM 支持仓库模式——每个实体有自己的 Repository,封装了常用的数据库操作。NestJS 通过 @nestjs/typeorm 把它跟依赖注入无缝结合。
注册实体到模块
在功能模块里用 TypeOrmModule.forFeature() 注册实体:
// users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
forRoot() 管全局连接,forFeature() 管当前模块要用哪些实体。
注入 Repository
在 Service 里通过 @InjectRepository() 拿到 Repository 实例:
// users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
findAll(): Promise<User[]> {
return this.usersRepository.find();
}
findOne(id: number): Promise<User | null> {
return this.usersRepository.findOneBy({ id });
}
async create(userData: Partial<User>): Promise<User> {
const user = this.usersRepository.create(userData);
return this.usersRepository.save(user);
}
async update(id: number, userData: Partial<User>): Promise<User | null> {
await this.usersRepository.update(id, userData);
return this.findOne(id);
}
async remove(id: number): Promise<void> {
await this.usersRepository.delete(id);
}
}
Repository 自带了很多方法:find、findOneBy、create、save、update、delete,基本上 CRUD 不用自己写 SQL。
Tip
create()和save()是两步。create()只是把普通对象转成实体实例,不会操作数据库;save()才会真正写入。很多人直接用save(userData)也能跑,但先create再save是更规范的写法。
跨模块使用 Repository
如果另一个模块也需要操作用户表,你需要把 TypeOrmModule 导出:
// users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User])],
exports: [TypeOrmModule], // 导出,别的模块才能用
})
export class UsersModule {}
然后在其他模块导入 UsersModule 就行了。
查询构建器
Repository 的方法够应付简单场景。但实际开发中,复杂查询是家常便饭。TypeORM 提供了 QueryBuilder,用链式调用来构建 SQL。
基本查询
const users = await this.usersRepository
.createQueryBuilder('user')
.where('user.isActive = :isActive', { isActive: true })
.orderBy('user.createdAt', 'DESC')
.getMany();
createQueryBuilder('user') 里的 'user' 是别名,后面 where、orderBy 里都用这个别名来引用字段。
关联查询
const user = await this.usersRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.comments', 'comment')
.where('user.id = :id', { id: 1 })
.getOne();
leftJoinAndSelect 会做 LEFT JOIN 并且把关联数据也查出来。如果只需要关联数据做条件过滤、不需要返回,用 leftJoin 就行,性能更好。
分页查询
async findWithPagination(page: number, limit: number) {
const [data, total] = await this.usersRepository
.createQueryBuilder('user')
.skip((page - 1) * limit)
.take(limit)
.getManyAndCount();
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
skip 跳过多少条,take 取多少条,getManyAndCount 同时返回数据和总数。分页接口标配。
复杂条件
import { Brackets } from 'typeorm';
const users = await this.usersRepository
.createQueryBuilder('user')
.select(['user.id', 'user.name', 'user.email'])
.where('user.isActive = :isActive', { isActive: true })
.andWhere(
new Brackets(qb => {
qb.where('user.name LIKE :name', { name: '%John%' })
.orWhere('user.email LIKE :email', { email: '%@example.com%' });
}),
)
.orderBy('user.createdAt', 'DESC')
.getMany();
Brackets 用来分组条件。上面这段生成的 SQL 是 WHERE isActive = true AND (name LIKE '%John%' OR email LIKE '%@example.com%')。不用 Brackets 的话,OR 条件会跟外层条件混在一起,逻辑就乱了。
NoteQueryBuilder 里的参数用
:参数名的格式,通过第二个参数对象传值。不要直接拼字符串,那样会有 SQL 注入风险。
事务处理
数据库事务保证一组操作要么全部成功,要么全部回滚。比如转账:A 扣钱和 B 加钱必须同时成功,不能只做一半。
使用 QueryRunner(推荐)
import { Injectable } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { User } from './entities/user.entity';
@Injectable()
export class UsersService {
constructor(private dataSource: DataSource) {}
async createWithProfile(userData: any, profileData: any) {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const user = queryRunner.manager.create(User, userData);
await queryRunner.manager.save(user);
// 其他操作...
await queryRunner.commitTransaction();
return user;
} catch (error) {
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release(); // 一定要释放
}
}
}
DataSource 在 TypeOrmModule.forRoot() 之后就可以全局注入,不需要额外 import 模块。
这个模式很固定:创建 QueryRunner -> 连接 -> 开启事务 -> 操作 -> 提交/回滚 -> 释放。finally 里的 release() 不能忘,不然连接池会被耗尽。
Warning很多人忘记在
finally里释放 QueryRunner,导致数据库连接池被占满,服务直接挂掉。这不是理论问题,是真实踩坑。
使用 transaction 方法(简洁写法)
如果不需要精细控制,可以用回调式的写法:
async createWithProfile(userData: any, profileData: any) {
await this.dataSource.transaction(async manager => {
const user = manager.create(User, userData);
await manager.save(user);
const profile = manager.create(Profile, profileData);
await manager.save(profile);
});
}
回调函数里的 manager 就是事务内的 EntityManager。如果回调抛出异常,事务自动回滚;正常结束就自动提交。代码量更少,也不用操心释放。
数据库迁移
synchronize: true 在开发阶段自动同步表结构,但生产环境不能依赖它。迁移(migration)才是正确做法——每次表结构变更都生成一个迁移文件,像 git commit 一样记录变更历史。
创建迁移
npx typeorm migration:create migrations/CreateUserTable
这会生成一个空的迁移文件,你需要手动填写 up 和 down 方法:
// migrations/1700000000000-CreateUserTable.ts
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class CreateUserTable1700000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'users',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'name',
type: 'varchar',
length: '100',
},
{
name: 'email',
type: 'varchar',
length: '100',
isUnique: true,
},
{
name: 'createdAt',
type: 'datetime',
default: 'CURRENT_TIMESTAMP',
},
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('users');
}
}
up 是正向操作(建表),down 是逆向操作(删表)。这样任何时候都能回滚。
运行迁移
在 package.json 里配置脚本:
{
"scripts": {
"typeorm": "typeorm-ts-node-commonjs",
"migration:run": "npm run typeorm -- migration:run -d src/data-source.ts",
"migration:revert": "npm run typeorm -- migration:revert -d src/data-source.ts",
"migration:generate": "npm run typeorm -- migration:generate -d src/data-source.ts"
}
}
你需要单独创建一个 data-source.ts 文件给 TypeORM CLI 用:
// data-source.ts
import { DataSource } from 'typeorm';
export default new DataSource({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: 'password',
database: 'test',
entities: ['src/**/*.entity.ts'],
migrations: ['migrations/*.ts'],
});
Tip
migration:generate可以自动对比数据库现状和实体定义,生成差异迁移文件。比手写靠谱,不容易漏字段。
迁移的注意事项
迁移文件不归 NestJS 管,是 TypeORM CLI 维护的。所以迁移里不能用依赖注入,不能引用 NestJS 的模块系统。把它当成独立的数据库脚本就行。
事件订阅者(Subscriber)
TypeORM 支持监听实体事件,比如插入前、更新后。这在需要自动处理某些逻辑时很有用,比如密码加密。
import {
DataSource,
EntitySubscriberInterface,
EventSubscriber,
InsertEvent,
} from 'typeorm';
import { User } from './user.entity';
@EventSubscriber()
export class UserSubscriber implements EntitySubscriberInterface<User> {
constructor(dataSource: DataSource) {
dataSource.subscribers.push(this);
}
listenTo() {
return User;
}
beforeInsert(event: InsertEvent<User>) {
console.log('准备插入用户:', event.entity);
// 比如在这里做密码加密
}
}
把 UserSubscriber 加到模块的 providers 数组就能生效。
Note事件订阅者不能是请求作用域的(request-scoped),它跟 DataSource 的生命周期绑定。
测试时的 Mock
单元测试不想连真实数据库。NestJS 提供了 getRepositoryToken() 来生成 Repository 的注入令牌,方便用 mock 替换:
import { getRepositoryToken } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
const mockRepository = {
find: jest.fn().mockResolvedValue([]),
findOneBy: jest.fn().mockResolvedValue(null),
create: jest.fn().mockReturnValue({}),
save: jest.fn().mockResolvedValue({}),
delete: jest.fn().mockResolvedValue({}),
};
@Module({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: mockRepository,
},
],
})
export class UsersModule {}
这样 UsersService 注入的 Repository 就是 mock 对象,不会真正操作数据库。
多数据库连接
有些项目需要连多个数据库,比如用户数据在一个库,订单数据在另一个库。TypeORM 支持给连接命名:
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'user_db_host',
port: 5432,
username: 'user',
password: 'password',
database: 'user_db',
entities: [User],
synchronize: true,
}),
TypeOrmModule.forRoot({
type: 'postgres',
name: 'orderConnection', // 给连接起名
host: 'order_db_host',
port: 5432,
username: 'user',
password: 'password',
database: 'order_db',
entities: [Order],
synchronize: true,
}),
],
})
export class AppModule {}
默认连接的 name 是 'default'。有名字的连接在 forFeature 和 @InjectRepository 里也要指定:
TypeOrmModule.forFeature([Order], 'orderConnection')
constructor(
@InjectRepository(Order, 'orderConnection')
private orderRepository: Repository<Order>,
) {}
小结
TypeORM 跟 NestJS 的配合已经很成熟了。核心流程就是:forRoot 建连接、定义实体类、forFeature 注册到模块、@InjectRepository 注入使用。
几个容易踩的坑再强调一下:
synchronize: true只在开发环境用- QueryRunner 用完一定要
release() create()只是创建实例,save()才写数据库- 迁移文件独立于 NestJS,不能用依赖注入
- 多数据库连接时,name 要在
forRoot和forFeature两侧都指定
掌握了这些,TypeORM 的日常使用就够用了。下一章咱们看看另一个热门选择——Prisma。