第六部分:第五节 - 数据持久化 (基础):管理厨房的原材料库存
一个真正的后端应用通常需要与数据库交互,存储和读取数据。数据库就像中央厨房的原材料仓库,所有的食材都整齐地存放在这里,并有专门的管理系统。
在 Node.js 生态中,有多种数据库可供选择(如 PostgreSQL, MySQL, MongoDB 等),也有多种库可以帮助我们与数据库交互。常用的方式是使用 ORM (Object-Relational Mapper) 处理关系型数据库(如 TypeORM, Sequelize)或 ODM (Object-Document Mapper) 处理 NoSQL 数据库(如 Mongoose 处理 MongoDB)。ORM/ODM 就像仓库管理员提供的标准化接口,让我们可以用面向对象的方式来操作数据库中的数据,而无需直接写复杂的 SQL 语句或处理底层数据库驱动。
NestJS 并没有内置强制使用某个特定的 ORM/ODM,但它提供了良好的集成能力。通常的做法是:
- 安装数据库驱动和 ORM/ODM 库。
- 配置数据库连接。 这通常在一个专门的数据库模块中完成。
- 定义数据模型(Entities/Schemas)。 描述数据在数据库中的结构。
- 在 Service 中使用 ORM/ODM 提供的 Repository 或 Model 来进行数据操作(CRUD)。 Service 作为业务逻辑层,负责协调数据访问。
Service 与数据交互的角色:
在 NestJS 中,Service (Provider) 是进行数据访问的理想位置。控制器负责处理 HTTP 请求和调用 Service,而 Service 则负责执行具体的业务逻辑,包括与数据库进行交互。这符合关注点分离的原则:控制器只关心如何接收请求和返回响应,Service 则关心如何实现某个功能(包括数据读写)。
想象一下,控制器(服务员)把订单交给服务(厨师长),厨师长根据食谱(业务逻辑)决定需要哪些原材料,然后通过仓库管理员(ORM/ODM)从仓库(数据库)获取原材料,最后完成菜品。
模拟数据存储(使用内存数组):
在这个基础教程阶段,为了避免引入复杂的数据库安装和配置,我们可以先在 Service 中使用一个简单的内存数组来模拟数据存储。这让我们能够专注于 NestJS 的核心架构,理解 Service 如何作为数据交互层。
小例子:修改 UsersService,使用内存数组存储用户数据
修改 src/users/users.service.ts
:
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';// 定义一个简单的用户接口,用于类型安全
interface User {id: number;name: string;age: number;
}@Injectable()
export class UsersService {// 使用内存数组模拟用户数据存储private users: User[] = [{ id: 1, name: 'Alice', age: 28 },{ id: 2, name: 'Bob', age: 32 },];// 获取所有用户 (Read)findAll(): User[] {return this.users;}// 获取单个用户 (Read)findOne(id: number): User | undefined {return this.users.find(user => user.id === id);}// 创建用户 (Create) - 简单实现create(userData: { name: string; age: number }): User {const newUser = {id: this.users.length > 0 ? this.users[this.users.length - 1].id + 1 : 1, // 简单生成 ID...userData,};this.users.push(newUser);return newUser;}// 更新用户 (Update) - 简单实现update(id: number, updateData: Partial<User>): User | undefined {const userIndex = this.users.findIndex(user => user.id === id);if (userIndex === -1) {return undefined; // 找不到用户}this.users[userIndex] = { ...this.users[userIndex], ...updateData };return this.users[userIndex];}// 删除用户 (Delete) - 简单实现remove(id: number): boolean {const initialLength = this.users.length;this.users = this.users.filter(user => user.id !== id);return this.users.length < initialLength; // 判断是否删除了元素}
}
修改 src/users/users.controller.ts
以使用这些方法:
// src/users/users.controller.ts (修改)
import { Controller, Get, Post, Put, Delete, Param, Body, NotFoundException, HttpCode, HttpStatus } from '@nestjs/common';
import { UsersService } from './users.service';
import { ParseIntPipe } from '@nestjs/common'; // 导入 ParseIntPipe// 定义一个简单的 DTO 用于创建用户 (后面会详细讲 DTO)
class CreateUserDto {name: string;age: number;
}@Controller('api/users')
export class UsersController {constructor(private readonly usersService: UsersService) {}@Get() // GET /api/usersfindAll() {return this.usersService.findAll();}@Get(':id') // GET /api/users/:idfindOne(@Param('id', ParseIntPipe) id: number) {const user = this.usersService.findOne(id);if (!user) {throw new NotFoundException(`找不到 ID 为 ${id} 的用户`); // 抛出 HttpException}return user;}@Post() // POST /api/users@HttpCode(HttpStatus.CREATED) // 返回 201 Created 状态码create(@Body() createUserDto: CreateUserDto) {const newUser = this.usersService.create(createUserDto);return newUser;}@Put(':id') // PUT /api/users/:idupdate(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: Partial<CreateUserDto>) {const updatedUser = this.usersService.update(id, updateUserDto);if (!updatedUser) {throw new NotFoundException(`找不到 ID 为 ${id} 的用户`);}return updatedUser;}@Delete(':id') // DELETE /api/users/:id@HttpCode(HttpStatus.NO_CONTENT) // 返回 204 No Content 状态码remove(@Param('id', ParseIntPipe) id: number) {const removed = this.usersService.remove(id);if (!removed) {// 如果删除不成功(比如 ID 不存在),可以选择抛出 404throw new NotFoundException(`找不到 ID 为 ${id} 的用户,无法删除`);}// NestJS 会自动处理返回 204,因为方法没有返回值,且我们设置了 @HttpCode(HttpStatus.NO_CONTENT)// 如果不设置 @HttpCode,默认会返回 200}
}
小结: 数据库是后端应用存放数据的“原材料仓库”。ORM/ODM 提供了与数据库交互的便捷方式。在 NestJS 中,Service 负责与数据源(可以是内存数组、数据库、其他服务)进行交互,而控制器则通过调用 Service 来获取或修改数据。使用内存数组是学习阶段模拟数据持久化的简单方法。
练习:
- 在你的
my-backend
项目中,确认你已经创建了products
模块和服务。 - 修改
products.service.ts
,使用一个内存数组来存储产品数据(包含 ID、名称、价格等属性)。 - 在
ProductsService
中实现以下方法,操作这个内存数组:findAll()
: 返回所有产品。findOne(id: number)
: 根据 ID 查找单个产品。create(productData: { name: string; price: number })
: 添加新产品,并简单生成一个唯一 ID。update(id: number, updateData: Partial<{ name: string; price: number }>)
: 根据 ID 更新产品。remove(id: number)
: 根据 ID 删除产品。
- 修改
products.controller.ts
,更新你的路由处理函数 (findAll
,findOne
,create
,update
,remove
),使其调用ProductsService
中对应的方法来处理数据。 - 在
findOne
,update
,remove
方法中,如果通过 ID 找不到产品,则抛出NotFoundException
。 - 运行应用,使用 Postman 等工具测试你的产品 API 的增删改查功能。