Skip to content
第 13 章 后端 ⏱ 14 分钟阅读

第 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 生产) ​

开发生产
synchronizetruefalse
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

动手练习 ​

  1. 建 User 实体:字段 id、username(unique)、password、createdAt
  2. CRUD:写 UsersService 实现 findAll/findOne/create/update/remove
  3. 联调:用 curl 测试 5 个接口都通

下一章:第 14 章:实体与关联 →

本站基于 VitePress 构建 · 由 StackHub 团队维护