Skip to content

数据库集成

好的,我们来攻克任何后端应用都无法回避的核心主题:数据库集成 (Database Integration)。一个应用如果不能持久化地存储和检索数据,那它就只是一个“金鱼”,记忆只有七秒。NestJS 作为一门全能型框架,提供了与各种数据库和 ORM(对象关系映射)工具集成的优雅方案。

为你的应用装上“记忆硬盘”:NestJS 数据库集成完全指南

想象你的 NestJS 应用是一家繁忙的图书馆。

  • 访客(HTTP 请求) 来到前台,想要借阅或归还书籍。
  • 图书管理员(你的 Service) 负责处理这些请求。
  • 巨大的书库(数据库) 存放着所有的书籍(数据)。

现在的问题是,图书管理员如何与这个巨大的书库进行高效、有序的交互?

  1. 直接用“梯子”一本一本找(原生驱动):图书管理员可以推着一个梯子,在浩如烟海的书架中,根据书的编码(SQL 查询语句)一本一本地寻找。这种方式最底层、最灵活,但对于复杂的“借阅清单”(复杂的业务逻辑),操作起来非常繁琐和容易出错。
  2. 使用“智能图书管理系统”(ORM/ODM):图书馆引入了一套智能系统。管理员只需要在电脑上输入书名(book.name = '...),系统就会自动告诉他这本书在哪个书架的第几层(自动生成 SQL 查询)。这套系统就是 ORM (对象关系映射)ODM (对象文档映射)

ORM/ODM 的核心思想是:让我们用操作对象 (Objects) 的方式,来间接地操作数据库中的数据。这使得开发者可以更多地关注业务逻辑,而不是费力地编写和拼接 SQL 语句。

NestJS 并不绑定任何特定的数据库技术,但它与流行的 ORM/ODM 库(如 TypeORM, Sequelize, Mongoose 等)都提供了官方的集成模块,让整合过程如丝般顺滑。

本文将以 TypeORM (用于 SQL 数据库,如 PostgreSQL, MySQL) 为例,因为它与 NestJS 的 TypeScript 特性结合得最好。

1. 第一步:搭建“智能图书管理系统” (@nestjs/typeorm)

第一步:安装必要的依赖

bash
# 安装 TypeORM 集成模块
npm install @nestjs/typeorm typeorm

# 安装你选择的数据库驱动
# 以 PostgreSQL 为例
npm install pg

第二步:在 AppModule 中配置数据库连接

这是最关键的一步。我们需要在根模块中使用 TypeOrmModule.forRoot() 来告诉 NestJS 如何连接到我们的数据库。这就像是为“智能图书管理系统”进行首次的安装和配置。

前置知识:ConfigModule

我们绝不会把数据库密码等敏感信息硬编码。所以,配置数据库连接的最佳实践是结合我们之前学过的 @nestjs/config

.env 文件

env
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=myuser
DB_PASSWORD=mypassword
DB_DATABASE=mydatabase```

**`src/app.module.ts`**
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }), // 确保 ConfigModule 是全局的

    // 使用 forRootAsync 来异步配置,因为它需要等待 ConfigService 就绪
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule], // 导入 ConfigModule
      inject: [ConfigService], // 注入 ConfigService
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get<string>('DB_HOST'),
        port: configService.get<number>('DB_PORT'),
        username: configService.get<string>('DB_USERNAME'),
        password: configService.get<string>('DB_PASSWORD'),
        database: configService.get<string>('DB_DATABASE'),
        entities: [__dirname + '/../**/*.entity{.ts,.js}'], // 关键!自动加载实体
        synchronize: true, // 开发环境使用,自动根据实体创建数据库表(生产环境慎用!)
      }),
    }),
  ],
})
export class AppModule {}
  • forRootAsync: 因为数据库的配置依赖于 ConfigService,而 ConfigService 的加载也需要时间,所以我们必须使用异步工厂的方式来配置。
  • entities: 这是 TypeORM 的核心概念之一。它告诉 TypeORM 去哪里寻找你定义的“实体类”。
  • synchronize: true: 这是一个非常方便的开发工具。它会在每次应用启动时,自动比较你的实体定义和数据库结构,并自动创建或修改数据表。警告: 在生产环境中,这可能会导致数据丢失,生产环境应该使用数据库迁移 (Migrations)

2. 第二步:定义“书籍卡片” (Entity)

实体 (Entity) 是一个映射到数据库表的类。你可以把它想象成图书馆里每本书的“索引卡片”,上面定义了这本书有哪些属性(书名、作者、ISBN 等)。

src/cats/entities/cat.entity.ts

typescript
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

// @Entity() 装饰器将这个类标记为一个 TypeORM 实体
@Entity()
export class Cat {
  // @PrimaryGeneratedColumn() 标记一个自增的主键列
  @PrimaryGeneratedColumn()
  id: number;

  // @Column() 标记一个普通的数据列
  @Column()
  name: string;

  @Column()
  age: number;

  @Column({ length: 100 }) // 可以传递选项,比如长度
  breed: string;
}
```现在,当你启动应用时,`TypeORM` 会自动在你的数据库中创建一张名为 `cat` 的表,它包含 `id`, `name`, `age`, `breed` 这几个列。

### 3. 第三步:为特定“书架”创建管理员 (Repository)

有了数据库连接,有了实体定义,现在我们需要一个专门的工具来操作 `cat` 这张表。这个工具就是 **Repository (仓库)**。

你可以把 Repository 想象成某个特定书架(比如“科幻小说区”)的专属管理员。他只负责这个书架上的书,并提供一套标准的操作方法,如 `find()` (查找所有书), `findOne()` (找一本书), `save()` (放一本新书或更新信息), `remove()` (拿走一本书)。

**如何在模块中使用 Repository?**

我们需要在一个特性模块(比如 `CatsModule`)中,使用 `TypeOrmModule.forFeature()` 来注册我们想要使用的实体。

**`src/cats/cats.module.ts`**
```typescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatsService } from './cats.service';
import { CatsController } from './cats.controller';
import { Cat } from './entities/cat.entity'; // 导入实体

@Module({
  imports: [
    // 使用 forFeature 注册 Cat 实体
    // 这会让 CatRepository 在这个模块的作用域内可用
    TypeOrmModule.forFeature([Cat]),
  ],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

4. 第四步:在服务中注入并使用 Repository

现在,我们终于可以在 CatsService 中注入 CatRepository,并用它来操作数据库了!

src/cats/cats.service.ts

typescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Cat } from './entities/cat.entity';
import { CreateCatDto } from './dto/create-cat.dto';

@Injectable()
export class CatsService {
  // 使用 @InjectRepository() 装饰器来注入 Cat 的 Repository
  constructor(
    @InjectRepository(Cat)
    private readonly catsRepository: Repository<Cat>
  ) {}

  // 创建一只猫 (Create)
  async create(createCatDto: CreateCatDto): Promise<Cat> {
    const cat = this.catsRepository.create(createCatDto); // 创建实体实例
    return this.catsRepository.save(cat); // 保存到数据库
  }

  // 查找所有猫 (Read All)
  async findAll(): Promise<Cat[]> {
    return this.catsRepository.find();
  }

  // 查找一只猫 (Read One)
  async findOne(id: number): Promise<Cat> {
    const cat = await this.catsRepository.findOneBy({ id });
    if (!cat) {
      throw new NotFoundException(`Cat with ID ${id} not found`);
    }
    return cat;
  }

  // 删除一只猫 (Delete)
  async remove(id: number): Promise<void> {
    await this.catsRepository.delete(id);
  }
}

看到了吗?我们的服务代码变得非常清晰和具有表现力。我们完全没有写一行 SQL 语句,而是通过调用 this.catsRepository 上语义明确的方法 (find, create, save) 来完成所有数据库操作。

总结

将 NestJS 与数据库集成,遵循一个清晰的四步流程:

  1. 全局配置 (forRootAsync):在根模块中,使用 @nestjs/typeorm 结合 @nestjs/config 来配置好全局的数据库连接。
  2. 定义实体 (@Entity):创建 TypeScript 类,并使用 TypeORM 的装饰器将其映射到数据库表。
  3. 特性模块注册 (forFeature):在需要操作某个实体的特性模块中,使用 TypeOrmModule.forFeature([MyEntity]) 来注册它。
  4. 注入并使用仓库 (@InjectRepository):在服务中,通过 @InjectRepository(MyEntity) 注入对应的 Repository,然后调用其方法来执行 CRUD 操作。

这个模式将数据库的底层细节与你的业务逻辑完美地分离开来,让你的代码更易于编写、阅读和测试。掌握了这个流程,就等于为你的 NestJS 应用装上了一颗强大、可靠的“记忆硬盘”。