手把手带你入门 TypeORM ------ 面向新手的实战指南
本文面向第一次接触 ORM / TypeORM 的同学,结合真实项目(NestJS + PostgreSQL),从"为什么需要 ORM"讲起,一步步构建、迁移、查询、测试,帮你真正用起来。
1. 背景:为什么需要 ORM?
| 方式 | 特点 | 存在的问题 |
|---|---|---|
| 直接写 SQL | 灵活、性能好 | 每次都写 SQL,易错、难维护,不利于重构 |
| DAO 自封装 | 把 SQL 包在函数里 | 仍需自己管理连接、事务、缓存等细节 |
| ORM | "对象 ↔ 数据库"自动映射,类型安全 | 需学习曲线,但维护成本低 |
ORM(Object Relational Mapping) 是一套让你用"面向对象"的方式操作关系型数据库的工具。开发者只需定义实体类、调用方法,ORM 就会帮你生成 SQL、执行、转换结果。
TypeORM 是 Node.js 世界最常用的 ORM 之一,具备:
- TypeScript 支持、类型安全;
- 装饰器定义实体,代码直观;
- 多数据库支持(PostgreSQL/MySQL/SQLite 等);
- 自动迁移、事务、关系映射等高级特性;
- NestJS 官方推荐(
@nestjs/typeorm集成)。
2. 快速上手:搭建 TypeORM + NestJS 环境
2.1 安装依赖
bash
npm install @nestjs/typeorm typeorm pg
如果数据库是 MySQL,则安装 mysql2;SQLite 则安装 sqlite3。
2.2 创建数据源配置(支持 CLI / 迁移)
typeorm.config.ts:
ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
import { join } from 'path';
config(); // 读取 .env
export const AppDataSource = new DataSource({
type: 'postgres',
url: process.env.DATABASE_URL,
synchronize: false, // 推荐迁移管理
logging: false,
entities: [join(__dirname, 'src/**/*.entity.{ts,js}')],
migrations: [join(__dirname, 'src/migrations/*.{ts,js}')],
});
synchronize: false,避免上线环境自动改表。我们用"迁移"来管理结构变化。
3. 定义实体(Entity)------ 模型驱动数据库
3.1 基础实体
src/modules/users/entities/user.entity.ts
ts
import {
Entity, PrimaryGeneratedColumn, Column,
CreateDateColumn, UpdateDateColumn, Index,
} from 'typeorm';
@Entity({ name: 'users' })
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 128, unique: true })
@Index('idx_users_email')
email!: string;
@Column({ length: 255 })
password!: string;
@Column({ length: 64 })
name!: string;
@Column({ length: 16, default: 'active' })
status!: 'active' | 'disabled';
@CreateDateColumn({ name: 'created_at' })
createdAt!: Date;
@UpdateDateColumn({ name: 'updated_at' })
updatedAt!: Date;
}
常用装饰器:
| 装饰器 | 作用 |
|---|---|
@Entity() |
声明实体类 |
@PrimaryGeneratedColumn |
主键,自增/UUID |
@Column() |
普通字段 |
@CreateDateColumn |
自动维护创建时间 |
@UpdateDateColumn |
自动维护更新时间 |
@ManyToOne, @OneToMany, @ManyToMany |
关系映射 |
3.2 关系映射示例
ts
// Category 与 Item 是 一对多/多对一
@Entity({ name: 'categories' })
export class Category {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 64 })
name!: string;
@OneToMany(() => Item, (item) => item.category)
items!: Item[];
}
@Entity({ name: 'items' })
export class Item {
@PrimaryGeneratedColumn('uuid')
id!: string;
@ManyToOne(() => Category, (category) => category.items, {
nullable: false,
onDelete: 'RESTRICT',
})
category!: Category;
@Column({ length: 128 })
name!: string;
}
3.3 多对多标签
ts
@Entity({ name: 'tags' })
export class Tag {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ length: 64, unique: true })
name!: string;
@ManyToMany(() => Item, (item) => item.tags)
items!: Item[];
}
@Entity({ name: 'items' })
export class Item {
// ...
@ManyToMany(() => Tag, (tag) => tag.items)
@JoinTable({
name: 'item_tags',
joinColumn: { name: 'item_id', referencedColumnName: 'id' },
inverseJoinColumn: { name: 'tag_id', referencedColumnName: 'id' },
})
tags!: Tag[];
}
4. 增删改查:Repository & QueryBuilder
TypeORM 提供两种操作方式:Repository API 和 QueryBuilder。
4.1 Repository API
ts
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly usersRepo: Repository<User>,
) {}
findByEmail(email: string) {
return this.usersRepo.findOne({ where: { email } });
}
async create(dto: CreateUserDto) {
const user = this.usersRepo.create(dto);
return this.usersRepo.save(user);
}
async updateStatus(id: string, status: User['status']) {
await this.usersRepo.update(id, { status });
return this.findById(id);
}
async remove(id: string) {
await this.usersRepo.delete(id);
}
}
常用方法:
| 方法 | 说明 |
|---|---|
find() |
查询列表 |
findOne() |
单个查询 |
create() + save() |
新建并保存 |
update() / delete() |
批量更新/删除 |
count() |
统计 |
4.2 QueryBuilder 示例
适合复杂筛选或多表关联:
ts
const qb = this.itemsRepo
.createQueryBuilder('item')
.leftJoinAndSelect('item.category', 'category')
.leftJoinAndSelect('item.tags', 'tag')
.where('item.name ILIKE :keyword', { keyword: `%${keyword}%` });
if (categoryId) {
qb.andWhere('category.id = :categoryId', { categoryId });
}
if (tagIds?.length) {
qb.andWhere('tag.id IN (:...tagIds)', { tagIds }).distinct(true);
}
const items = await qb.orderBy('item.created_at', 'DESC').getMany();
5. 事务与并发控制
5.1 事务(Transaction)
ts
await this.dataSource.transaction(async (manager) => {
const user = await manager.findOne(User, { where: { id: userId } });
const stock = manager.create(Stock, { ... });
await manager.save(stock);
// 事务内调用其他服务也可以传 EntityManager 进来
});
5.2 悲观锁(Pessimistic Lock)
ts
const stock = await manager.findOne(Stock, {
where: { id },
lock: { mode: 'pessimistic_write' },
});
多用户同时调整库存时,锁住记录,保证数据一致性。
6. 管理数据库迁移
6.1 生成迁移
bash
npm run typeorm migration:generate -- src/migrations/InitSchema -d typeorm.config.ts
package.json 示例:
json
{
"scripts": {
"typeorm": "node --loader ts-node/esm ./node_modules/typeorm/cli.js",
"migration:generate": "npm run typeorm -- migration:generate",
"migration:run": "npm run typeorm -- migration:run",
"migration:revert": "npm run typeorm -- migration:revert"
}
}
6.2 迁移文件示例(片段)
ts
export class InitSchema1234567890123 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "users" (...)
`);
await queryRunner.query(`
CREATE TABLE "categories" (...)
`);
// ...
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "users"`);
await queryRunner.query(`DROP TABLE "categories"`);
// ...
}
}
6.3 执行迁移
bash
npm run migration:run -- -d typeorm.config.ts
用于本地开发 / CI / 上线部署。
7. 与 NestJS 的深度整合
7.1 @nestjs/typeorm Module
ts
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService<{ app: AppConfigType }, true>) => {
const { database } = config.getOrThrow<AppConfigType>('app');
return {
type: 'postgres',
url: database.url,
autoLoadEntities: true,
synchronize: false,
logging: database.logging,
};
},
});
7.2 测试用内存数据库(e2e)
ts
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User, Category, Item],
synchronize: true,
});
让 e2e 测试可以独立执行,不依赖外部数据库。
8. 单元 / e2e 测试实践
8.1 单元测试 ------ Mock Repository
ts
const moduleRef = await Test.createTestingModule({
providers: [
UsersService,
{
provide: getRepositoryToken(User),
useValue: {
findOne: jest.fn(),
create: jest.fn(),
save: jest.fn(),
update: jest.fn(),
},
},
],
}).compile();
createQueryBuilder也可以通过 jest 模拟。- 配合类验证器 DTO 可测试参数校验逻辑。
8.2 e2e 测试 ------ Supertest + SQLite
ts
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User, Category, Item],
synchronize: true,
}),
ItemsModule,
],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalFilters(new HttpExceptionFilter());
app.useGlobalInterceptors(new TransformInterceptor());
await app.init();
const server = app.getHttpServer();
await request(server)
.post('/items')
.send({ ... })
.expect(201);
9. 项目中遇到的常见问题 & 解决方案
| 问题描述 | 解决办法 |
|---|---|
| 实体改变但忘记迁移,导致线上/本地结构不一致 | 统一使用迁移管理,严禁 synchronize: true 上线 |
TypeError: decorator metadata not found |
emitDecoratorMetadata + reflect-metadata |
类型错误:Type 'T' does not satisfy ObjectLiteral |
mock QueryBuilder 时把泛型约束成 <T extends ObjectLiteral> |
多对多关系 JoinTable 属性不支持 onDelete |
在迁移里设置外键行为,而不是在装饰器里 |
| NestJS 热重载时端口占用(EADDRINUSE) | 关闭旧进程或启用 --watch 时配置 webpack 热更新 |
10. 实战经验与最佳实践
- 配置集中化 :统一在
configuration.ts管理数据库、Redis、MQ、邮件等配置,方便多环境部署。 - DTO + 验证器 :结合
class-validator、class-transformer,确保输入数据合法,再交给 ORM。 - 仓储模式(Repository Pattern):业务逻辑封装在 Service,Repository 只做数据访问。
- 幂等 / 事务控制:库存、订单类操作要用事务,利用悲观锁或乐观锁确保一致性。
- 测试优先:单测保证逻辑正确,e2e 保证接口联通,避免改动带来回归问题。
- 迁移文档化 :每次迁移都记录在
docs/progress-log.md、docs/mq-explanation.md等文档里,方便回顾。 - 多模块协作:Auth、Users、Items、Stock、Tags、Notifications 等模块各司其职,通过 TypeORM 实体串联。
11. 写在最后
TypeORM 不只是"简化 CRUD",它能帮助你:
- 用类型安全的方式管理复杂的数据库;
- 快速构建实体关系模型,降低维护成本;
- 统一管理数据库变更(迁移),支持 CI/CD;
- 与 NestJS 深度融合,搭建企业级服务。
掌握 TypeORM,你就能把更多精力放在业务逻辑,而不是重复的 SQL 和手动数据转换上。希望这篇文章对你有所帮助,也欢迎把你的实践经验补充到评论里,一起进步!💡