CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
NestJS 实战入门/课时/数据库接入入门
RawBlame
课时目录12 节

NestJS 实战入门

  1. 01NestJS 是什么
  2. 02项目创建与结构
  3. 03Module 与依赖注入
  4. 04Controller 与路由
  5. 05Service 层
  6. 06Pipe 与校验
  7. 07Guard 与认证
  8. 08ConfigModule
  9. 09数据库接入入门
  10. 10异常与过滤器
  11. 11测试
  12. 12综合练习

数据库接入入门

TypeORM 或 Prisma 选一、实体与 Repository 直觉。

35 分钟

数据库接入入门

这一课你将能做到

  1. 画出 Controller → Service → Repository/ORM → 数据库的完整数据流
  2. 读懂 TypeORM 的 Entity + Repository 写法,以及 Prisma schema + Client 写法
  3. 在模块层正确登记连接与实体,并坚持「Controller 不写 SQL、synchronize 不开」两条纪律

为什么要学

内存数组只适合学框架:重启数据就没了,也无法支撑真实用户。真实的刷题平台必须把题库、作答记录持久化。

Nest 生态最常见两条持久化路线:TypeORM 用装饰器定义 Entity、注入 Repository,风格接近 Spring Data JPA;Prisma 用独立 schema 文件描述模型、生成类型安全的客户端,迁移工作流自成一体。

本课的边界先说清楚:本课程不要求你安装数据库,也不会执行任何同步或迁移操作。 下面的 ORM 代码以「读懂形状」为目标,你的 quiz-api 继续运行内存版本即可;将来环境就绪,再按注释的替换点切换。

先建立图像

QuestionsController
    → QuestionsService
        → Repository<Question>(TypeORM)或 prisma.question(Prisma)
            → MySQL / PostgreSQL / SQLite

AppModule:       TypeOrmModule.forRoot(...) 或 PrismaService(全局)
QuestionsModule: TypeOrmModule.forFeature([Question])

三端概念对照:

Spring Data JPANest + TypeORMNest + Prisma
@Entity 类@Entity() 装饰的类schema.prisma 中的 model
JpaRepository<T,ID>Repository<T>prisma.question
@TransactionalDataSource 事务 / QueryRunnerprisma.$transaction
Flyway / LiquibaseTypeORM migrationsprisma migrate

跟着做

步骤 1:先画数据流(不装任何东西)

在笔记里画出「提交作答」的四格流转:

  1. POST /api/submissions,JwtAuthGuard 解析出登录用户。
  2. ValidationPipe 校验 questionId、selectedIndex。
  3. SubmissionsService 调用 QuestionsService 查标准答案、判分。
  4. 持久化层执行 INSERT INTO submission ...。

重点结论:SQL 只允许出现在第四层。如果你曾在 Express 路由里直接 connection.query(...),这一课就是把这个习惯纠正过来。

步骤 2:读懂 TypeORM 实体

将来需要持久化时,安装命令是(本课先不执行):

npm install @nestjs/typeorm typeorm mysql2

新建 src/questions/question.entity.ts:

import {
  Column,
  CreateDateColumn,
  Entity,
  PrimaryGeneratedColumn,
} from "typeorm";

@Entity("question")
export class QuestionEntity {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ length: 500 })
  stem: string;

  @Column("simple-json") // 数组存为 JSON 字符串
  options: string[];

  @Column({ name: "answer_index" })
  answerIndex: number;

  @Column({ type: "varchar", length: 10, default: "easy" })
  difficulty: "easy" | "medium" | "hard";

  @Column("simple-json", { nullable: true })
  tags: string[];

  @CreateDateColumn({ name: "created_at" })
  createdAt: Date;
}

对比内存版 Question 接口:字段一一对应,只多了表列映射元数据。

步骤 3:读懂 Repository 注入版 Service

下面是将来的持久化版 QuestionsService(只读懂,不替换当前文件):

import { Injectable, NotFoundException } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { QuestionEntity } from "./question.entity";
import { CreateQuestionDto } from "./dto/create-question.dto";

@Injectable()
export class QuestionsService {
  constructor(
    @InjectRepository(QuestionEntity)
    private readonly repo: Repository<QuestionEntity>,
  ) {}

  findAll() {
    // 对应 SELECT * FROM question
    return this.repo.find();
  }

  async findOne(id: number): Promise<QuestionEntity> {
    const row = await this.repo.findOne({ where: { id } });
    if (!row) {
      throw new NotFoundException(`题目 ${id} 不存在`);
    }
    return row;
  }

  create(dto: CreateQuestionDto) {
    const row = this.repo.create(dto); // 先构造实体实例
    return this.repo.save(row); // 再 INSERT
  }
}

Service 依赖的是 Repository<QuestionEntity> 这个抽象,单元测试中只需 mock 它的 find/findOne/save,不需要真实数据库。

步骤 4:读懂模块登记(连接全局一次)

AppModule 中配置连接,全部值来自 ConfigService(不要照抄进项目运行):

TypeOrmModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    type: "mysql",
    host: config.get<string>("DB_HOST", "localhost"),
    port: config.get<number>("DB_PORT", 3306),
    username: config.get<string>("DB_USER", "root"),
    password: config.get<string>("DB_PASSWORD", ""),
    database: config.get<string>("DB_NAME", "quiz"),
    entities: [QuestionEntity],
    synchronize: false, // 红线:任何环境都不靠框架改表结构
    autoLoadEntities: true,
  }),
});

QuestionsModule 中登记本模块使用的实体:

@Module({
  imports: [TypeOrmModule.forFeature([QuestionEntity])],
  controllers: [QuestionsController],
  providers: [QuestionsService],
})
export class QuestionsModule {}

结构记忆:连接 forRoot 全局一次,实体 forFeature 按业务模块登记,类似 Spring 的数据源 Bean 加各模块的 Repository。

步骤 5:对照 Prisma 的写法

prisma/schema.prisma(概念示例):

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Question {
  id          Int      @id @default(autoincrement())
  stem        String   @db.VarChar(500)
  options     Json
  answerIndex Int      @map("answer_index")
  difficulty  String   @default("easy") @db.VarChar(10)
  tags        Json?
  createdAt   DateTime @default(now()) @map("created_at")
  submissions Submission[]

  @@map("question")
}

model Submission {
  id            Int      @id @default(autoincrement())
  questionId    Int      @map("question_id")
  userId        Int      @map("user_id")
  selectedIndex Int      @map("selected_index")
  correct       Boolean
  question      Question @relation(fields: [questionId], references: [id])

  @@map("submission")
}

配套的 PrismaService(将来才用):

import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}

查询风格对比:

// TypeORM
this.repo.find({ where: { difficulty: "hard" } });
// Prisma
this.prisma.question.findMany({ where: { difficulty: "hard" } });

步骤 6:记住迁移工作流与安全红线

  1. 表结构变更只走迁移文件:TypeORM 用 migration:generate,Prisma 用 migrate dev,并纳入代码评审。
  2. synchronize 永远关闭:它可能在生产直接 DROP 列、丢数据。
  3. 连接串与密码只走 ConfigService / 环境变量;数据库账号最小授权。
  4. 唯一约束建在数据库层(如登录名 unique),应用层查重只是体验优化,不能替代约束。

讲清楚要点

  1. ORM 藏在 Service 之下:将来切换持久化实现,Controller 和接口契约都不需要改动。
  2. 连接是基础设施:forRoot 放根模块、forFeature 放业务模块,职责清晰。
  3. synchronize 是开发期糖、生产期刀:本课程直接保持 false,从一开始养成迁移习惯。
  4. Repository 抽象带来可测性:mock 仓库比搭建测试数据库更轻量。
  5. 关联查询注意 N+1:入门先单表 CRUD;进阶再学 relations、include 的预加载策略。
  6. 两套方案二选一:TypeORM 与 Prisma 不要在同一项目双写同一批表,否则真相来源分裂。

你可能会懵的地方

  • 不接数据库,学这课有什么用? 初学者先建立分层和术语直觉,将来接入时你知道每样东西该放哪、配置长什么样;这比边连库边猜结构高效得多。

  • TypeORM 和 Prisma 到底选哪个? 团队熟悉 JPA、喜欢装饰器实体选 TypeORM;想要 schema 即文档、生成类型安全客户端选 Prisma。选定后把 CRUD 加迁移做完整,胜过两个都只点到为止。

  • Repository 层和 Service 层会不会重复? 小项目 Service 直接注入 Repository 很常见;只有查询逻辑复杂、需要复用数据装配时,再单独包一层 Repository 类。

  • JSON 字段和关联表怎么选? options 这类固定随题目读取、不参与查询的结构适合 JSON 列;Submission 与 Question 这种有独立生命周期、需要按关系检索的,必须建关联表。

  • 集成测试时数据库怎么办? 单测一律 mock 仓库;需要验证 SQL 与约束时,使用独立测试库并在测试后清理,绝不能连生产库。

错法对照

错法问题正解
Controller 里写 repo.query 或拼接 SQL难测试、注入风险Service 调用 ORM 参数化 API
生产开启 synchronize可能自动改表、丢数据永远 false + 迁移文件
数据库密码写进 forRoot 源码凭据泄露ConfigService + 环境变量
TypeORM 与 Prisma 同时操作同一表两套真相互相覆盖项目内二选一
只靠应用层查重不建唯一索引并发下仍产生脏数据数据库层加唯一约束

卡点急救

  1. 将来连接报 ECONNREFUSED:数据库未启动或主机端口不对;先用环境变量核对,再用本地客户端验证连通性。
  2. 报 EntityMetadataNotFound:实体没有放进 forFeature 或 forRoot 的 entities;使用自动加载时确认实体被对应模块引用。
  3. Prisma 客户端无类型提示:改完 schema 后执行 prisma generate 重新生成客户端,IDE 再重启 TS 服务。

面试常问

  • NestJS 接入数据库的标准分层是什么? Controller 处理 HTTP,Service 承载业务并通过 Repository(TypeORM)或 PrismaClient(Prisma)访问数据库;连接在根模块 forRoot 配置,实体按模块 forFeature 登记。

  • 为什么生产环境不能开 TypeORM 的 synchronize? 它根据实体内省直接执行 DDL,字段改名会被当作删除加新增,可能清掉整列数据;表结构必须通过可评审、可回滚的迁移文件演进。

  • TypeORM 的 Repository 怎么注入? 在模块中 TypeOrmModule.forFeature([Entity]),然后在 Service 构造器中用 @InjectRepository(Entity) 注入 Repository<Entity>,测试时可替换为 mock。

自测三问

  1. forRoot 与 forFeature 各自解决什么问题、分别放在哪一层?
  2. 为什么本课程坚持「不同步数据库」、synchronize 保持关闭?
  3. TypeORM 与 Prisma 如果只能选一个,你的依据是什么?

小结一句话

ORM 挂在 Service 之下,连接与实体按模块登记,表结构只靠迁移演进、绝不同步。

延伸

  • Database(官方数据库文档,TypeORM)
  • Prisma 官方文档
  • 下一课预告:学习异常与异常过滤器,把所有错误收口成统一 JSON,500 错误对客户端脱敏。
加载本节练习…
← 上一课ConfigModule下一课 →异常与过滤器