第 13 章:数据库与 TypeORM
学习目标
- 集成 TypeORM + PostgreSQL/MySQL
- 定义第一个 Entity
- 注入 Repository 做增删改查
- 避开 4 个 TypeORM 集成坑
一、为什么选 TypeORM
NestJS 一等公民,装饰器风格和 NestJS 配套,支持 Active Record 和 Data Mapper 两种模式。本教程用 Data Mapper。
二、安装
bash
pnpm add @nestjs/typeorm typeorm pg # PostgreSQL
# 或
pnpm add @nestjs/typeorm typeorm mysql2 # MySQL三、连接数据库
typescript
// app.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'admin',
password: 'secret',
database: 'mydb',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true, // ⚠️ 仅开发用,生产必须改成 false,自动建表很危险
logging: ['error', 'warn'], // 生产推荐保留(只看错误和警告)
}),
],
})
export class AppModule {}四、第一个实体(Entity)
typescript
// users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn } from 'typeorm';
@Entity('users') // 表名 users
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true, length: 50 })
username: string;
@Column()
password: string;
@Column({ default: true })
active: boolean;
@CreateDateColumn()
createdAt: Date;
}启动后会自动建表(因为 synchronize: true)。
五、Repository 注入
typescript
// users/users.module.ts
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './user.entity';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}typescript
// users/users.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User) // 注入 Repository
private readonly repo: Repository<User>,
) {}
findAll() { return this.repo.find(); }
findOne(id: number) { return this.repo.findOne({ where: { id } }); }
save(data: Partial<User>) { return this.repo.save(data); }
remove(id: number) { return this.repo.delete(id); }
}⚠️ 坑 1:
User必须先在forFeature([User])注册,才能@InjectRepository(User)。
为什么不能省 forFeature? —— 两套系统,两套注册:@Entity() 给 TypeORM 看,forFeature([X]) 给 NestJS DI 看。
| 装饰器 / API | 谁来读 | 干啥 |
|---|---|---|
@Entity('users') | TypeORM | 告诉 TypeORM:"这是实体类,加载到数据库连接" |
forFeature([User]) | NestJS | 告诉 NestJS DI:"为 User 创建 Repository<User> token" |
@InjectRepository(User) | NestJS | 从 DI 容器拿 Repository<User> |
两者独立:
User 类 ─┬─ @Entity('users') → TypeORM 知道
└─ forFeature([User]) → NestJS 知道(没这一步就报 DI 错)记忆口诀:
@Entity()= 给 TypeORM 看("这是数据库表")forFeature([X])= 给 NestJS 看("为 X 准备 Repository")@InjectRepository(X)= 从 NestJS DI 容器拿 Repository- 两套注册都要做,不能省任何一步
六、CRUD 完整示例
typescript
// users/users.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common';
@Controller('users')
export class UsersController {
constructor(private readonly svc: UsersService) {}
@Get()
list() { return this.svc.findAll(); }
@Get(':id')
detail(@Param('id', ParseIntPipe) id: number) {
return this.svc.findOne(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.svc.create(dto);
}
@Put(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateUserDto) {
return this.svc.update(id, dto);
}
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: number) {
return this.svc.remove(id);
}
}typescript
// users/users.service.ts
async create(dto: CreateUserDto) {
const exists = await this.repo.findOne({ where: { username: dto.username } });
if (exists) throw new ConflictException('username exists');
const user = this.repo.create(dto); // ⚠️ 用 create 不要直接 new
return this.repo.save(user);
}
async update(id: number, dto: UpdateUserDto) {
await this.repo.update(id, dto); // 返回 { affected }
return this.repo.findOne({ where: { id } });
}七、常用查询选项
typescript
// where
this.repo.find({ where: { active: true } });
this.repo.find({ where: { name: Like('%tom%') } });
// select
this.repo.find({ select: ['id', 'username'] });
// order + skip + take
this.repo.find({
where: { active: true },
order: { createdAt: 'DESC' },
skip: 0,
take: 20,
});八、数据库连接配置(开发 vs 生产)
| 开发 | 生产 | |
|---|---|---|
| synchronize | true | false |
| logging | 全开 | error |
| 迁移 | 不用 | 必用 |
| 实体加载 | 自动 | 精确列表 |
⚠️ 坑 2:
synchronize: true上生产 → 一次字段改名可能丢数据。 ⚠️ 坑 3:环境变量忘记.env不加载 →connect ECONNREFUSED 127.0.0.1:5432。
九、MySQL 注意点
typescript
TypeOrmModule.forRoot({
type: 'mysql',
charset: 'utf8mb4',
timezone: '+08:00',
// ...
});中文/emoji 必须 utf8mb4,时区设东八区避免 Date 偏差。
十、实战:Logger 打印 SQL
typescript
TypeOrmModule.forRoot({
// ...
logging: ['query', 'error'],
});生产建议关掉,或者自定义 Logger:
typescript
import { Logger } from '@nestjs/common';
new TypeOrmLogger(Logger) // 自定义十一、本章小结
| 要点 | 关键 |
|---|---|
| 模块 | TypeOrmModule.forRoot({...}) 全局 |
| 实体 | @Entity + @Column + @PrimaryGeneratedColumn |
| 注入 | TypeOrmModule.forFeature([User]) |
| Repository | @InjectRepository(User) |
| 生产 | synchronize: false,用 Migration |
| 时区/编码 | MySQL 设 utf8mb4 + +08:00 |
动手练习
- 建 User 实体:字段 id、username(unique)、password、createdAt
- CRUD:写 UsersService 实现 findAll/findOne/create/update/remove
- 联调:用 curl 测试 5 个接口都通
下一章:第 14 章:实体与关联 →