使用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) | 主键 外键 |
+-------------+--------------+----------------------------+
你的多对多关系将如下所示:
现在让我们深入研究并展示这个设置,并使用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数据库建立连接。
你也可以选择使用SQLite ,Microsoft 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关系:
现在你的ManyToMany关系应该在你的连接表中被正确地组织,如下所示:
你现在可以使用Postman测试你的应用程序,如下所示:
要添加学生:向http://localhost:3000/students
发送一个POST请求:
添加课程:向http://localhost:3000/courses
发送一个POST请求,如下所示:
这些更改应该在你的数据库中反映出来。确保使用GET请求检查。
在两个实体之间建立ManyToMany关系使用TypeORM
到目前为止,您可以创建学生和课程。但是我们没有方法来创建学生课程关系。学生需要在可用课程中注册,我们需要使用NestJS和TypeORM来完成这个任务。
在这方面,您需要创建方法和端点,这些方法和端点在两个方向上指向ManyToMany关系。
您将创建一个PUT方法,当发送时,现有的学生将在可用的课程中注册。
转到你的src/student/student.service.ts
文件并添加:
- 在
CourseService
中添加一个constructor
和一个InjectRepository
到Course
实体。 - 一个
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]
}
检查你的Join表student_courses_course
,现在你的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数组
}
你可以看到添加Marceline Avila
学生被分配了ID7
,并注册了ID为1和2
的课程。我们来看一下链接表,验证一下:
实际上,现在我们已经完美地使用TypeORM和PostgreSQL实现了ManyToMany关系。
结论
本指南帮助您学习和实现NestJS中使用TypeORM的ManyToMany关系。您现在可以
- 创建实体并向它们添加Many-to-Many关系。
- 使用TypeORM创建联合Many-to-Many表。
- 更新和创建到您的连接表的关系。
在GitHub存储库 中查看整个代码。希望您找到本指南有用!