NestJS——使用TypeORM操作数据库、增删改查、关联查询、QueryBuilder

个人简介

👀个人主页: 前端杂货铺
🙋‍♂️学习方向: 主攻前端方向,正逐渐往全干发展
📃个人状态: 研发工程师,现效力于中国工业软件事业
🚀人生格言: 积跬步至千里,积小流成江海
🥇推荐学习:🍍前端面试宝典 🎨100个小功能 🍉Vue2 🍋Vue3 🍓Vue2/3项目实战 🥝Node.js实战 🍒Three.js

🌕个人推广:每篇文章最下方都有加入方式,旨在交流学习&资源分享,快加入进来吧

内容参考链接
NestJS(一)Docker入门
NestJS(二)NestJS——创建项目、编写User模块
NestJS(三)TypeScript入门
NestJS(四)编程思想——FP、OOP、FRP、AOP、IOC、DI、MVC、DTO、DAO
NestJS(五)NestJS——多环境配置方案(dotenv、config、@nestjs/config、joi配置校验)
NestJS(六)NestJS——使用TypeORM连接MySQL数据库(Docker拉取镜像、多环境适配)

使用 TypeORM 形成数据库

GitHub 提交记录

如下图所示为一个用户系统。users用户表、profile个人信息表、logs日志表、roles角色表。

一对一关系:一个用户对应一个个人信息
一对多关系:一个用户对应多个日志,多个日志对应一个用户
多对多关系:一个用户对应多个角色,一个角色对应多个用户

对于人和角色的关系,若使用一对多(One-to-Many)的话会存在缺陷:
若在 person 表中添加 role_id 字段,则每个人物只能绑定一个角色。
若在 role 表中添加 person_id 字段,则每个角色只能绑定一个人物。
无法满足双向的“多对多”需求。

在这里插入图片描述

参照目录如下:

在这里插入图片描述

创建 user.entity.ts 文件,存放用户的实体类。

import { Logs } from "src/logs/logs.entity"; // 导入日志实体
import { Roles } from "src/roles/roles.entity"; // 导入角色实体
import {
  Column,
  Entity,
  JoinTable,
  ManyToMany,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm"; // 从 TypeORM 导入装饰器

/**
 * 用户实体类,映射到数据库中的 `User` 表
 */
@Entity()
export class User {
  /**
   * 主键字段,自动生成唯一 ID
   */
  @PrimaryGeneratedColumn()
  id: number; // 主键

  /**
   * 用户名字段
   */
  @Column()
  username: string;

  /**
   * 密码字段
   */
  @Column()
  password: string;

  /**
   * 一对多关系:
   * - 一个用户可以有多个日志
   * - `Logs` 表中的 `user` 字段表示反向关系
   */
  @OneToMany(() => Logs, (logs) => logs.user)
  logs: Logs[]; // 一对多关系,一个用户对应多个日志

  /**
   * 多对多关系:
   * - 一个用户可以拥有多个角色
   * - 使用 `user_roles` 作为连接表
   * - `Roles` 表中的 `users` 字段表示反向关系
   */
  @ManyToMany(() => Roles, (roles) => roles.users)
  @JoinTable({ name: "user_roles" }) // 指定连接表的名称为 `user_roles`
  roles: Roles[]; // 多对多关系,一个用户可能拥有多个角色
}

创建 profile.entity.ts 文件,存放个人资料的实体类。

import {
  Column,
  Entity,
  JoinColumn,
  OneToOne,
  PrimaryGeneratedColumn,
} from "typeorm";
import { User } from "./user.entity"; // 导入用户实体

/**
 * 个人资料实体类,映射到数据库中的 `Profile` 表
 */
@Entity()
export class Profile {
  /**
   * 主键字段,自动生成唯一 ID
   */
  @PrimaryGeneratedColumn()
  id: number; // 主键

  /**
   * 性别字段
   * - 使用数字表示性别(如 0 表示未知,1 表示男性,2 表示女性)
   */
  @Column()
  gender: number;

  /**
   * 头像字段
   * - 存储用户头像的 URL 或路径
   */
  @Column()
  photo: string;

  /**
   * 地址字段
   * - 存储用户的居住地址
   */
  @Column()
  address: string;

  /**
   * 一对一关系:
   * - 一个用户对应一个个人资料
   * - `user` 字段表示与 `User` 实体的关联
   * - 使用 `@JoinColumn` 指定外键关系
   */
  @OneToOne(() => User) // 定义一对一关系
  @JoinColumn() // 指定外键关联,外键会存储在 `Profile` 表中
  user: User; // 关联的用户实体
}

创建 logs.entity.ts 文件,存放日志的实体类。

import { User } from "src/user/user.entity"; // 导入用户实体
import {
  Column,
  Entity,
  JoinColumn,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm"; // 从 TypeORM 导入装饰器

/**
 * 日志实体类,映射到数据库中的 `Logs` 表
 */
@Entity()
export class Logs {
  /**
   * 主键字段,自动生成唯一 ID
   */
  @PrimaryGeneratedColumn()
  id: number; // 主键

  /**
   * 请求路径字段
   * - 存储 API 请求的路径
   */
  @Column()
  path: string;

  /**
   * 请求方法字段
   * - 存储 HTTP 请求方法(如 GET、POST 等)
   */
  @Column()
  method: string;

  /**
   * 请求数据字段
   * - 存储请求的参数或数据
   */
  @Column()
  data: string;

  /**
   * 请求结果字段
   * - 存储请求的响应状态码(如 200、404 等)
   */
  @Column()
  result: number;

  /**
   * 多对一关系:
   * - 多个日志对应一个用户
   * - `user` 字段表示与 `User` 实体的关联
   * - 使用 `@JoinColumn` 指定外键关系
   */
  @ManyToOne(() => User, (user) => user.logs) // 定义多对一关系
  @JoinColumn() // 指定外键关联,外键会存储在 `Logs` 表中
  user: User; // 关联的用户实体
}

创建 roles.entity.ts 文件,存放角色的实体类。

import { User } from "src/user/user.entity"; // 导入用户实体
import {
  Column,
  Entity,
  ManyToMany,
  PrimaryGeneratedColumn,
} from "typeorm"; // 从 TypeORM 导入装饰器

/**
 * 角色实体类,映射到数据库中的 `Roles` 表
 */
@Entity()
export class Roles {
  /**
   * 主键字段,自动生成唯一 ID
   */
  @PrimaryGeneratedColumn()
  id: number; // 主键

  /**
   * 角色名称字段
   * - 存储角色的名称(如管理员、用户等)
   */
  @Column()
  name: string;

  /**
   * 多对多关系:
   * - 一个角色可以分配给多个用户
   * - `users` 字段表示与 `User` 实体的关联
   * - 反向关系由 `User` 实体中的 `roles` 字段定义
   */
  @ManyToMany(() => User, (user) => user.roles) // 定义多对多关系
  users: User[]; // 多对多关系,一个角色可能由多个用户拥有
}

app.module.ts 文件中添加相应实体。

在这里插入图片描述

至此,数据库的表格关系创建完成。


CRUD 操作

GitHub 操作记录

接下来,我们实现基本的 增删改查 操作。

user.controller.ts 文件中添加相应的路由。

import { Controller, Get, Post } from "@nestjs/common"; // 导入控制器和 HTTP 请求装饰器
import { UserService } from "./user.service"; // 导入用户服务
import { User } from "./user.entity"; // 导入用户实体

/**
 * 用户控制器类
 * - 定义与用户相关的 HTTP 路由和处理逻辑
 * - 使用 `UserService` 提供的业务逻辑操作用户数据
 */
@Controller("user") // 定义控制器的路由前缀为 "user"
export class UserController {
  /**
   * 构造函数
   * - 注入用户服务
   * @param userService 用户服务
   */
  constructor(private userService: UserService) {}

  /**
   * 获取所有用户
   * - 路由:GET /user/getAll
   * @returns 所有用户的列表
   */
  @Get("getAll")
  getUsers(): any {
    return this.userService.findAll(); // 调用服务层方法查询所有用户
  }

  /**
   * 获取单个用户
   * - 路由:GET /user/getOne
   * - 示例中固定查询用户名为 "test" 的用户
   * @returns 匹配的用户对象或 null
   */
  @Get("getOne")
  getOneUser(): any {
    const username = "test"; // 示例用户名
    return this.userService.find(username); // 调用服务层方法查询用户
  }

  /**
   * 添加新用户
   * - 路由:POST /user/add
   * - 示例中固定添加用户名为 "test" 的用户
   * @returns 保存后的用户对象
   */
  @Post("add")
  addUser(): any {
    const user = { username: "test", password: "123456" } as User; // 示例用户数据
    return this.userService.create(user); // 调用服务层方法创建用户
  }

  /**
   * 更新用户信息
   * - 路由:POST /user/update
   * - 示例中固定更新 ID 为 1 的用户
   * @returns 更新结果
   */
  @Post("update")
  updateUser(): any {
    const id = 1; // 示例用户 ID
    const user = { username: "zahuopu", password: "654321" } as User; // 示例更新数据
    return this.userService.update(id, user); // 调用服务层方法更新用户
  }

  /**
   * 删除用户
   * - 路由:GET /user/remove
   * - 示例中固定删除 ID 为 1 的用户
   * @returns 删除结果
   */
  @Get("remove")
  removeUser(): any {
    const id = 1; // 示例用户 ID
    return this.userService.remove(id); // 调用服务层方法删除用户
  }
}

user.service.ts 文件中进行相关业务操作,操作数据库。

import { Injectable } from "@nestjs/common"; // 导入 Injectable 装饰器,用于标记服务类
import { InjectRepository } from "@nestjs/typeorm"; // 用于注入 TypeORM 仓库
import { Repository } from "typeorm"; // 导入 TypeORM 的 Repository 类
import { User } from "./user.entity"; // 导入用户实体

/**
 * 用户服务类
 * - 提供用户相关的业务逻辑
 * - 使用 TypeORM 操作数据库中的 `User` 表
 */
@Injectable()
export class UserService {
  /**
   * 构造函数
   * - 注入用户实体的 TypeORM 仓库
   * @param userRepository 用户实体的仓库
   */
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>
  ) {}

  /**
   * 查询所有用户
   * @returns 所有用户的列表
   */
  findAll() {
    return this.userRepository.find(); // 使用 TypeORM 的 `find` 方法查询所有用户
  }

  /**
   * 根据用户名查询用户
   * @param username 用户名
   * @returns 匹配的用户对象或 null
   */
  find(username: string) {
    return this.userRepository.findOne({ where: { username } }); // 根据条件查询单个用户
  }
  
  /**
   * 根据 ID 查询用户
   * @param id 用户 ID
   * @returns 匹配的用户对象或 null
   */
  findOne(id: number) {
    return this.userRepository.findOne({ where: { id } }); // 根据 ID 查询单个用户
  }

  /**
   * 创建新用户
   * @param user 用户对象
   * @returns 保存后的用户对象
   */
  async create(user: User) {
    const newUser = await this.userRepository.create(user); // 创建用户实例
    return this.userRepository.save(newUser); // 保存用户到数据库
  }

  /**
   * 更新用户信息
   * @param id 用户 ID
   * @param user 部分用户信息
   * @returns 更新结果
   */
  update(id: number, user: Partial<User>) {
    return this.userRepository.update(id, user); // 根据 ID 更新用户信息
  }

  /**
   * 删除用户
   * @param id 用户 ID
   * @returns 删除结果
   */
  remove(id: number) {
    return this.userRepository.delete(id); // 根据 ID 删除用户
  }
}

一对一、一对多查询

我们打开数据库操作网址,对 userId 为 2 的用户添加一条 profile 个人信息及两条 logs 日志。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

修改 user.entity.ts 文件,完善对应关系。

  /**
   * 一对一关系:
   * - 一个用户对应一个个人资料
   * - `Profile` 表中的 `user` 字段表示反向关系
   */
  @OneToOne(() => Profile, (profile) => profile.user)
  profile: Profile; // 一对一关系,一个用户对应一个个人资料

修改 user.module.ts 文件,注册 Logs 实体(这是一种不规范的写法,方便测试先这么写)

imports: [TypeOrmModule.forFeature([User, Logs])], // 注册 User、Logs 实体

修改 user.service.ts 文件,添加查询用户资料和查询用户日志的逻辑。

 /**
   * 查询用户的个人资料
   * @param id 用户 ID
   * @param profile 个人资料对象
   * @returns 保存后的个人资料对象
   */
  findProfile(id: number) {
    return this.userRepository.findOne({
      where: { id },
      relations: {
        profile: true, // 关联查询个人资料
      },
    });
  }

  /**
   * 查询用户的日志
   * @param id 用户 ID
   * @returns 用户的日志列表
   */
  async findUserLogs(id: number) {
    const user = await this.findOne(id);
    return this.logsRepository.find({
      where: { user }, // 根据用户 ID 查询日志
      relations: {
        user: true, // 关联查询用户
      },
    });
  }

相应的,我们修改 user.controller.ts 文件,添加相应的路由。

  /**
   * 获取用户的个人资料
   * - 路由:GET /user/profile
   * @returns 用户的个人资料
   */
  @Get("profile")
  getUserProfile(): any {
    return this.userService.findProfile(2); // 调用服务层方法查询用户个人资料
  }

  /**
   * 获取用户的日志
   * @returns 用户的日志
   * - 路由:GET /user/logs
   * @description 获取用户的日志信息
   */
  @Get("logs")
  getUserLogs(): any {
    return this.userService.findUserLogs(2); // 调用服务层方法查询用户日志
  }

在这里插入图片描述

在这里插入图片描述


QueryBuilder

TypeORM 中文网 - QueryBuilder介绍

GitHub 提交记录

QueryBuilder 是 TypeORM 最强大的功能之一,它允许你使用优雅便捷的语法构建 SQL 查询,执行并获得自动转换的字体。

接下来,我们针对于 logs 表格进行新建数据,详情如下:

在这里插入图片描述

修改 user.service.ts 文件,添加查询用户日志的逻辑。

  /**
   * 查询用户的日志
   * @param id 用户 ID
   * @returns 用户的日志
   */
  async findLogsByGroup(id: number) {
    return this.logsRepository
      .createQueryBuilder("logs") // 创建查询构造器,指定查询的表别名为 "logs"
      .select("logs.result", "result") // 选择日志的结果字段,并将其别名为 "result"
      .addSelect('Count("logs.result")', "count") // 统计每种结果的数量,并将其别名为 "count"
      .leftJoinAndSelect("logs.user", "user") // 左连接 "logs" 表中的 "user" 字段,指定别名为 "user"
      .where("user.id = :id", { id }) // 添加条件,筛选出指定用户 ID 的日志
      .groupBy("logs.result") // 按 "logs.result" 字段分组
      .orderBy("result", "DESC") // 按 "result" 字段降序排序
      .offset(2) // 跳过前两个分组结果
      .addOrderBy("count", "DESC") // 按 "count" 字段降序排序
      .limit(3) // 限制返回的分组数量为 3
      .getRawMany(); // 执行查询并返回原始结果数组
  }

相应的,我们修改 user.controller.ts 文件,添加相应路由。

  /*
   * 获取用户的日志
   * @returns 用户的日志
   * - 路由:GET /user/logsByGroup
   * @description 获取用户的日志信息
   */
  @Get("logsByGroup")
  async getLogsByGroup(): Promise<any> {
    const res = await this.userService.findLogsByGroup(2); // 调用服务层方法查询用户日志按组
    // 处理查询结果
    return res.map((o) => ({
      result: o.result,
      count: o.count,
    }));
  }

在这里插入图片描述

修改 app.module.ts 文件,在开发环境下打印全部日志。

logging: process.env.NODE_ENV === "development", // 日志

这样,在发请求的时候我们就可以在终端看到相关 sql 语句。

在这里插入图片描述


总结

本篇文章,我们学习了如何使用TypeORM操作数据库,如何进行增删改查操作及关联查询,并使用 QueryBuilder 完成了一些 sql 查询。

好啦,本篇文章到这里就要和大家说再见啦,祝你这篇文章阅读愉快,你下篇文章的阅读愉快留着我下篇文章再祝!


参考资料:

  1. DeepSeek
  2. NestJS 从入门到实战

在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端杂货铺

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值