个人简介
👀个人主页: 前端杂货铺
🙋♂️学习方向: 主攻前端方向,正逐渐往全干发展
📃个人状态: 研发工程师,现效力于中国工业软件事业
🚀人生格言: 积跬步至千里,积小流成江海
🥇推荐学习:🍍前端面试宝典 🎨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 形成数据库
如下图所示为一个用户系统。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 操作
接下来,我们实现基本的 增删改查 操作。
在 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
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 查询。
好啦,本篇文章到这里就要和大家说再见啦,祝你这篇文章阅读愉快,你下篇文章的阅读愉快留着我下篇文章再祝!
参考资料:
- DeepSeek
- NestJS 从入门到实战