使用TypeORM和PostgreSQL数据库在NestJS中完美地实现ManytoMany(M2M)关系

2025-03-25T14:41:46+08:00 | 12分钟阅读 | 更新于 2025-03-25T14:41:46+08:00

Macro Zhao

使用TypeORM和PostgreSQL数据库在NestJS中完美地实现ManytoMany(M2M)关系

@TOC

推荐超级课程:

这个指南教你如何使用TypeORM 和PostgreSQL数据库在NestJS 中完美地实现ManytoMany(M2M)关系。你将使用TypeORM的@ManyToMany装饰器和查询构建器来建模两个实体之间的多对多关系。

你将学会:

  • 如何创建两个实体,并使用TypeORM查询构建器添加多对多关系。
  • 使用TypeORM @JoinTable()装饰器创建联接表。
  • 使用PostgreSQL 或MySQL添加你的多对多实体关系。
  • 更新和保存ManytoMany关系
  • 如何创建一个单一的POST方法来加载和应用TypeORM的ManytoMany关系。
  • 使用级联删除多对多关系。

仔细看一看TypeORM的ManyToMany关系

多对多表示两个或更多实体。每个实体包含另一个实体的一个元素。所提到的元素与另一个实体中的多个元素相关联,反之亦然,以创建多对多关系。然后,这些多个元素使用一个连接/联接表来表示。

连接表现在将包含两个实体的主键的外键引用,这两个实体参与你的ManyToMany关系。

以这个例子说明:学生和课程。在这里,你将有如下形式的学生和课程实体:

  • 学生
id(主键)
学生姓名
+-------------+--------------+----------------------------+
| 学生 |
+-------------+--------------+----------------------------+
| id | int(11) | 主键 自动增量 |
| 学生姓名 | varchar(255) | |
+-------------+--------------+----------------------------+
  • 课程
id(主键)
课程名
+-------------+--------------+----------------------------+
| 课程 |
+-------------+--------------+----------------------------+
| id | int(11) | 主键 自动增量 |
| 课程名 | varchar(255) | |
+-------------+--------------+----------------------------+

到目前为止,这两个表是独立的,没有关联。这就是你现在要创建一个联接表,将这两者用它们各自的主键作为外键引用连接起来的地方。

让我们将连接表命名为StudentCourses,用它来创建ManyToMany关系,以便Student可以有许多Courses,而Course可以属于许多Students

基于这种关系,连接表将被表示如下:

学生ID(引用学生的外键)
课程ID(引用课程的外键)
+-------------+--------------+----------------------------+
| StudentCourse |
+-------------+--------------+----------------------------+
| 学生ID | int(11) | 主键 外键 |
| 课程ID | int(11) | 主键 外键 |
+-------------+--------------+----------------------------+

你的多对多关系将如下所示:

Master TypeORM ManyToMany Relation with NestJS and Postgres

现在让我们深入研究并展示这个设置,并使用TypeORM、NestJS和PostgreSQL创建一个ManytoMany关系。

使用TypeORM实体创建多对多关系

在继续之前,请确保使用以下命令准备好您的NestJS:

npm i -g @nestjs/cli
nest new typeorm-manytomany

进入新创建的typeorm-manytomany目录并安装必要的依赖项:

cd typeorm-manytomany
npm install @nestjs/typeorm typeorm pg

因为我们使用了两个实体,我们将分别使用模块来表示每个实体:

  • 学生
nest g module student
nest g service student --no-spec
nest g controller student --no-spec
  • 课程
nest g module course
nest g service course --no-spec
nest g controller course --no-spec

使用TypeORM实体创建多对多关系

TypeORM使用实体来表示实际数据库,而不是SQL语法。让我们深入研究并表示两个实体(学生和课程)并在它们之间添加多对多关系。

前往src/student目录并创建一个student.entity.ts文件。这将表示数据库中的学生如下:

// student.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from '../course/course.entity';

@Entity()
export class Student {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}

同样,前往src/course目录,并创建一个course.entity.ts文件。这将表示数据库中的课程如下:

// course.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Student } from '../student/student.entity';

@Entity()
export class Course {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;
}

请注意,TypeORM使用装饰器来表示你的实体和字段在数据库中应如何安排。现在将ManyToMany关系添加到这两个实体中。

我们将从学生实体开始,更新代码如下所示:

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Course } from '../course/course.entity';

@Entity()
export class Student {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Course, course => course.students)
  @JoinTable()
  courses: Course[];
}

在这里:

  • @ManyToMany装饰器定义了学生和课程之间的多对多关系。
  • @JoinTable()创建一个连接表。TypeORM将自动创建你的连接表,并添加相关的外键关系,无需手动操作。

这种关系还不完整。学生必须与课程相关联,course => course.students必须指向学生所关联的课程,如下所示:

import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Student } from '../student/student.entity';

@Entity()
export class Course {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @ManyToMany(() => Student, student => student.courses)
  students: Student[];
}

在第二个设置中,你只需要添加你的@ManyToMany装饰器,TypeORM将指向正确的关系。不需要@JoinTable(),因为它已经处理了。

在大多数情况下,@JoinTable() 应该被添加到"所有者"上。这里是学生是课程的所有者,但课程没有拥有学生。基本上,是学生报名课程,而不是反过来。重点是 @JoinTable 必须仅出现在关系的一个方向上。

请注意,关系可以是单向的或双向的。单向关系仅在关系的一侧使用@ManyToMany装饰器。但是双向关系在关系的双方都使用@ManyToMany装饰器,就像我们在上述两个实体中所做的那样。

如何使用级联删除删除ManyToMany关系

当使用TypeORM时,你会发现装饰器非常有用。它们处理你的应用程序的软行为,无需创建复杂的函数。

正如前面所述,@JoinTable会在我们之间创建一个连接表。但问题是,如果删除一个学生或一个课程,联接表会发生什么变化呢?

有可能,表不会改变,尽管你已经删除了相应的外键。

为了解决这个问题,TypeORM使用级联来自动传播此类操作(软删除)到你的实体中。在这种情况下,级联必须被添加以在移除所属实体时自动移除相关实体。

在这里,你可以使用:

{ cascade: true }

这样,你指示TypeORM当实体被删除时自动移除连接表中的相关记录。将你的@ManyToMany装饰器更新如下:

  • 学生实体:
// src/student/student.entity.ts

// .. 其他导入
export class Student {
  // ... 其他代码
  // 添加 `cascade: true` 如下
  @ManyToMany(() => Course, course => course.students, { cascade: true })
  @JoinTable()
  courses: Course[];
}
  • 课程实体:
// src/course/course.entity.ts

// .. 其他导入
export class Student {
  // ... 其他代码
  // 添加 `cascade: true` 如下
  @ManyToMany(() => Student, student => student.courses, { cascade: true })
  @JoinTable()
  students: Student[];
}

使用ManyToMany和TypeORM添加数据

现在你的实体已经准备就绪。你现在需要创建所需的模块和控制器,以创建HTTP方法和路由来执行并将TypeORM的Many-to-Many数据添加到数据库中。

首先,你需要创建学生和课程。这样,你将需要加入这两个并建立你的关系。

src/student/student.service.ts文件中,添加以下方法:

// student.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Student } from './student.entity';

@Injectable()

export class StudentService {
  constructor(
    @InjectRepository(Student)
    private studentRepository: Repository<Student>,
  ) {}

  async createStudent(studentData: Partial<Student>): Promise<Student> {
    const student = this.studentRepository.create(studentData);
    return this.studentRepository.save(student);
  }

    async getAllStudents(): Promise<Student[]> {
    return this.studentRepository.find();
  }
}

在这里,我们要能够创建和获取学生。你的src/student/student.controller.ts文件将使用HTTP方法执行它们,并创建如下形式的学生终点:

// student.controller.ts
import { Controller, Get, Param, Post, Body, Put, Delete, HttpException, HttpStatus } from '@nestjs/common';
import { StudentService } from './student.service';
import { Student } from './student.entity';

@Controller('students')
export class StudentController {
  constructor(private readonly studentService: StudentService) {}

  @Post()
  async createStudent(@Body() studentData: Partial<Student>): Promise<Student> {
    return this.studentService.createStudent(studentData);
  }

  @Get()
  async getAllStudents(): Promise<Student[]> {
    return this.studentService.getAllStudents();
  }
}

请现在将你的src/student/student.module.ts更新如下:

// ... other imports
import { CourseModule } from './course/course.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
// 设置数据库 here
  }),
  StudentModule,
  CourseModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

重复相同的过程并更新课程代码库。为了简单起见,我们将仅使用添加课程的方法(你可以在GitHub 上找到完整的代码)如下:

  • src/course/course.service.ts
// course.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Course } from './course.entity';

@Injectable()
export class CourseService {
  constructor(
    @InjectRepository(Course)
    private courseRepository: Repository<Course>,
  ) {}

  async createCourse(courseData: Partial<Course>): Promise<Course> {
    const course = this.courseRepository.create(courseData);
    return this.courseRepository.save(course);
  }
}
  • src/course/course.controller.ts
// course.controller.ts
import { Controller, Get, Param, Post, Body, Put, Delete } from '@nestjs/common';
import { CourseService } from './course.service';
import { Course } from './course.entity';

@Controller('courses')
export class CourseController {
  constructor(private readonly courseService: CourseService) {}
  
  @Post()
  async createCourse(@Body() courseData: Partial<Course>): Promise<Course> {
    return this.courseService.createCourse(courseData);
  }
}
  • src/course/course.module.ts
import { Module } from '@nestjs/common';
import { CourseService } from './course.service';
import { CourseController } from './course.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Course } from './course.entity';

@Module({
  imports: [
     // Import the Course entity into the module `TypeOrmModule`
    TypeOrmModule.forFeature([Course]),
  ],

  providers: [CourseService],
  controllers: [CourseController]
})
export class CourseModule {}

使用TypeORM和PostgreSQL加载ManyToMany关系

为了使这种关系工作,TypeORM必须与你的PostgreSQL数据库建立连接。

你也可以选择使用SQLiteMicrosoft SQL Server (MSSQL)MySQL 作为TypeORM的数据库选择。

你只需要获取数据库连接值,确保你有一个已创建的数据库并更新你的src/app.module.ts文件如下:

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { StudentModule } from './student/student.module';
import { CourseModule } from './course/course.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  // 使用TypeOrmModule定义以下数据库设置
  imports: [TypeOrmModule.forRoot({
    // TypeORM将要与之通信的数据库
    "type":"postgres",
    // 数据库所在的位置,本地环境在此
    "host":"localhost",
    // DB端口
    "port":5432,
    // 如果你有不同的数据库用户名,请相应地更新
    "username":"postgres",
    // 添加你的数据库密码
    "password":"pass",
    // 确保你已经创建数据库并在此处添加
    "database":"school",
    // 加载实体。这应该保持不变,TypeORM将加载实体并在你的数据库中表示它们
    "entities":[__dirname + "/**/**/**.entity{.ts,.js}"],
    "synchronize":true
  }),
  StudentModule,
  CourseModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

现在确保你的PostgreSQL正在运行,并执行以下命令,启动你的NestJS TypeORM服务器:

npm run start:dev

TypeORM将立即创建你的表并使用Join建立ManyToMany关系:

Master TypeORM ManyToMany Relation with NestJS and Postgres

现在你的ManyToMany关系应该在你的连接表中被正确地组织,如下所示:

Master TypeORM ManyToMany Relation with NestJS and Postgres

你现在可以使用Postman测试你的应用程序,如下所示:

要添加学生:向http://localhost:3000/students发送一个POST请求:

Master TypeORM ManyToMany Relation with NestJS and Postgres

添加课程:向http://localhost:3000/courses发送一个POST请求,如下所示:

这些更改应该在你的数据库中反映出来。确保使用GET请求检查。

在两个实体之间建立ManyToMany关系使用TypeORM

到目前为止,您可以创建学生和课程。但是我们没有方法来创建学生课程关系。学生需要在可用课程中注册,我们需要使用NestJS和TypeORM来完成这个任务。

在这方面,您需要创建方法和端点,这些方法和端点在两个方向上指向ManyToMany关系。

您将创建一个PUT方法,当发送时,现有的学生将在可用的课程中注册。

转到你的src/student/student.service.ts文件并添加:

  • CourseService中添加一个constructor和一个InjectRepositoryCourse实体。
  • 一个enrollStudentInCourses方法将加载学生的课程关系。
  • 将获取的课程分配给学生
  • 使用新分配的课程保存更新后的学生。

以下是您需要执行上述操作的完整代码示例:

// student.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Student } from './student.entity';
import { CourseService } from '../course/course.service'; // 导入CourseService
import { Course } from '../course/course.entity'; // 导入Course实体

@Injectable()

export class StudentService {
  constructor(
    @InjectRepository(Student)
    private studentRepository: Repository<Student>,
    private courseService: CourseService,
    @InjectRepository(Course)
    private courseRepository: Repository<Course>, // 注入Course存储库
  ) {}

  async enrollStudentInCourses(studentId: number, courseIds: number[]): Promise<Student> {
    const student = await this.studentRepository.findOne({
      where: { id: studentId },
      relations: ['courses'], // 为学生加载课程关系
    });

    if (!student) {
      throw new Error('学生未找到');
    }

    const courses = await this.courseRepository.findByIds(courseIds); // 通过ID获取课程

    if (!courses.length) {
      throw new Error('未找到提供的ID的课程');
    }

    student.courses = courses; // 将获取的课程分配给学生

    return this.studentRepository.save(student); // 保存更新后的学生
  }
}

转到你的src/student/student.controller.ts并创建一个PUT方法来执行enrollStudentInCourses。在这种情况下,端点必须获得要更新的学生的ID,并发送一个PUT请求,如下所示:

  @Put(':id/courses')
  async enrollStudentInCourses(
    @Param('id') id: string,
    @Body() body: { courses: number[] },
  ): Promise<any> {
    const studentId = +id;
    const courseIds = body.courses;

    try {
      const updatedStudent = await this.studentService.enrollStudentInCourses(studentId, courseIds);
      return { message: '学生成功注册课程', data: updatedStudent };
    } catch (error) {
      if (error.status === HttpStatus.NOT_FOUND) {
        throw new HttpException('未找到学生', HttpStatus.NOT_FOUND);
      }
      throw new HttpException('在课程中注册学生时出错', HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

因为您在StudentService中使用了CourseService,所以需要更新src/student/student.module.ts如下:

// ... other imports
import { CourseModule } from './course/course.module';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
// db setting here
  }),
  StudentModule,
  CourseModule],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

现在让我们测试一下这些是否按预期工作。向http://localhost:3000/students/1/courses发送一个PUT请求。在这种情况下,1是要将课程关系添加到的学生的ID。

你的JSON负载将包含您希望学生1拥有的课程ID数组,如下所示:

{
  "courses": [1,2,3]
}

NestJS和Postgres中的Master TypeORM ManyToMany关系

检查你的Join表student_courses_course,现在你的ManyToMany关系应该在链接表中得到很好的建立,如下所示:

NestJS和Postgres中的Master TypeORM ManyToMany关系

您可以看到,ID为1的学生拥有课程1和7。ID为4的学生拥有课程1, 2和3,验证了你的ManyToMany关系正常工作。

通过POST请求建立ManyToMany关系

您已经有了准备好的课程和学生。这意味着您不必像我们上面所做的那样发送一个PUT请求。如果您的数据库表中已经有可用课程,您可以创建一个学生POST请求。

这样,您只需要创建新的学生,并同时将它们注册到可用课程中,同时保持您的ManyToMany关系有效。

您需要在src/student/student.service.ts文件中创建一个名为createStudentWithCourses的新函数:

  async createStudentWithCourses(name: string, courseIds: number[]): Promise<Student> {
    const student = this.studentRepository.create({ name });

    try {
      const courses = await this.courseRepository.findByIds(courseIds);

      if (courses.length !== courseIds.length) {
        throw new Error('找不到一个或多个课程');
      }

      student.courses = courses;
      return this.studentRepository.save(student);
    } catch (error) {
      throw new Error(`创建学生时出错: ${error.message}`);
    }
  }

在您的src/student/student.controller.ts中,更新POST请求如下:

  @Post()
  async createStudentWithCourses(@Body() body: { name: string, courses: number[] }): Promise<any> {
    const { name, courses } = body;
    return this.studentService.createStudentWithCourses(name, courses);
  }

去Postman发送一个POST请求到http://localhost:3000/students和以下JSON负载:

{
  "name": "Marceline Avila",
  "courses": [1,2] // 学生注册的课程ID数组
}

NestJS和Postgres中的Master TypeORM ManyToMany关系

你可以看到添加Marceline Avila学生被分配了ID7,并注册了ID为1和2的课程。我们来看一下链接表,验证一下:

NestJS和Postgres中的Master TypeORM ManyToMany关系

实际上,现在我们已经完美地使用TypeORM和PostgreSQL实现了ManyToMany关系。

结论

本指南帮助您学习和实现NestJS中使用TypeORM的ManyToMany关系。您现在可以

  • 创建实体并向它们添加Many-to-Many关系。
  • 使用TypeORM创建联合Many-to-Many表。
  • 更新和创建到您的连接表的关系。

GitHub存储库 中查看整个代码。希望您找到本指南有用!

© 2011 - 2025 Macro Zhao的分享站

关于我

如遇到加载502错误,请尝试刷新😄

Hi,欢迎访问 Macro Zhao 的博客。Macro Zhao(或 Macro)是我在互联网上经常使用的名字。

我是一个热衷于技术探索和分享的IT工程师,在这里我会记录分享一些关于技术、工作和生活上的事情。

我的CSDN博客:
https://macro-zhao.blog.csdn.net/

欢迎你通过评论或者邮件与我交流。
Mail Me

推荐好玩(You'll Like)
  • AI 动·画
    • 这是一款有趣·免费的能让您画的画中的角色动起来的AI工具。
    • 支持几十种动作生成。
我的项目(My Projects)
  • 爱学习网

  • 小乙日语App

    • 这是一个帮助日语学习者学习日语的App。
      (当然初衷也是为了自用😄)
    • 界面干净,简洁,漂亮!
    • 其中包含 N1 + N2 的全部单词和语法。
    • 不需注册,更不需要订阅!完全免费!
  • 小乙日文阅读器

    • 词汇不够?照样能读日语名著!
    • 越读积累越多,积跬步致千里!
    • 哪里不会点哪里!妈妈再也不担心我读不了原版读物了!
赞助我(Sponsor Me)

如果你喜欢我的作品或者发现它们对你有所帮助,可以考虑给我买一杯咖啡 ☕️。这将激励我在未来创作和分享更多的项目和技术。🦾

👉 请我喝一杯咖啡

If you like my works or find them helpful, please consider buying me a cup of coffee ☕️. It inspires me to create and share more projects in the future. 🦾

👉 Buy me a coffee