「一起学后端」Nest.js + MySQL 查询方法教学文档
Nest.js + MySQL 查询方法教学文档
文章目录
- Nest.js + MySQL 查询方法教学文档
- 1. 基础 CRUD 操作
- 1.1 创建记录
- 1.2 查询记录
- 1.3 更新记录
- 1.4 删除记录
- 2. 复杂查询
- 2.1 分页与排序
- 2.2 关联查询(JOIN)
- 2.3 聚合查询
- 2.4 子查询
- 3. 高级操作
- 3.1 事务处理
- 3.2 TypeORM 中实现事务的两种方式
- 3.3 事务中的错误处理
- 3.4 原生 SQL 查询
- 3.5 软删除与恢复
- 4. 装饰器(@)使用大全
- 4.1 TypeORM 实体装饰器
- 4.2 DTO 验证装饰器
- 4.3 装饰器组合用法
- 5. 实用技巧
- 5.1 错误处理增强
- 5.2 索引优化实战
- 5.3 索引的作用
- 5.4 索引的代价
- 5.5 何时创建索引?
- 5.6 TypeORM 中创建索引
- 5.7 索引命名与高级配置
- 6. 完整项目结构
- 7. 完整示例代码
- 实体类示例
- 服务类示例
- 总结
1. 基础 CRUD 操作
1.1 创建记录
// 创建一个用户对象(此时还未保存到数据库)
const user = this.userRepository.create({
name: 'John',
age: 25,
});
// 将创建的对象保存到数据库
await this.userRepository.save(user);
// 批量创建示例
const users = this.userRepository.create([
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 28 },
]);
await this.userRepository.save(users);
1.2 查询记录
// 查询所有用户
const allUsers = await this.userRepository.find();
// 条件查询:年龄大于20的用户,带分页(第1页,每页10条)
const filteredUsers = await this.userRepository.find({
where: { age: MoreThan(20) }, // MoreThan 是 TypeORM 的比较操作符
skip: 0, // 跳过0条 = 第一页
take: 10, // 取10条
});
// 查询单个用户(根据ID)
const user = await this.userRepository.findOneBy({ id: 1 });
// 查询并按创建时间倒序排列
const sortedUsers = await this.userRepository.find({
order: { createdAt: 'DESC' } // DESC 降序,ASC 升序
});
1.3 更新记录
// 直接更新(适合简单操作)
await this.userRepository.update(
{ id: 1 }, // 更新条件(找到id=1的用户)
{ age: 26 } // 更新内容
);
// 先查询后更新(推荐:可以处理复杂业务逻辑)
const user = await this.userRepository.findOneBy({ id: 1 });
user.age = 26; // 修改属性
await this.userRepository.save(user); // 保存修改
1.4 删除记录
// 物理删除(直接从数据库删除)
await this.userRepository.delete({ id: 1 });
// 软删除(需要实体类添加 @DeleteDateColumn)
await this.userRepository.softDelete({ id: 1 });
// 软删除后,数据不会被真正删除,而是标记删除时间
2. 复杂查询
2.1 分页与排序
// 分页查询用户列表(带总数统计)
const [users, total] = await this.userRepository.findAndCount({
where: { isActive: true }, // 查询条件:仅活跃用户
skip: (page - 1) * limit, // 跳过前 N 条(计算页码偏移量)
take: limit, // 每页数量
order: { createdAt: 'DESC' }, // 按创建时间倒序排列
});
// 返回结果示例:
// {
// data: users, // 当前页数据
// total: 100, // 总数据量
// page: 2, // 当前页码
// totalPages: 10 // 总页数
// }
2.2 关联查询(JOIN)
// 方式1:自动关联查询(需在实体类中预先定义关联关系)
const orders = await this.orderRepository.find({
relations: ['user'], // 加载关联的 user 数据
});
// 方式2:使用 QueryBuilder 手动关联(更灵活)
const orders = await this.orderRepository
.createQueryBuilder('order') // 主表别名
.leftJoinAndSelect('order.user', 'user') // 关联 user 表,别名为 user
.where('user.age > :age', { age: 18 }) // 条件:用户年龄大于18岁
.getMany(); // 获取最终结果
2.3 聚合查询
// 计算用户平均年龄
const result = await this.userRepository
.createQueryBuilder('user') // 使用别名 'user' 代表用户表
.select('AVG(user.age)', 'avgAge') // 计算年龄平均值,结果命名为 avgAge
.getRawOne(); // 获取原始数据(不经过实体转换)
console.log(result.avgAge); // 直接通过别名访问结果
// 统计活跃用户数量
const activeUserCount = await this.userRepository.count({
where: { isActive: true }
});
2.4 子查询
// 步骤1:构建子查询(获取年龄大于20的用户ID)
const subQuery = this.userRepository
.createQueryBuilder('user')
.select('user.id')
.where('user.age > :age', { age: 20 });
// 步骤2:在主查询中使用子查询
const users = await this.userRepository
.createQueryBuilder('user')
.where(`user.id IN (${subQuery.getQuery()})`) // 嵌入子查询
.setParameters(subQuery.getParameters()) // 传递子查询参数(防SQL注入)
.getMany();
3. 高级操作
3.1 事务处理
想象你要完成以下操作:
- 从A账户扣除100元
- 向B账户增加100元
如果步骤1成功但步骤2失败,A账户的钱会凭空消失!事务的作用就是确保这两个步骤绑定成一个原子操作:要么全部成功,要么全部回滚(撤销所有操作)。
当需要同时执行多个数据库操作,并确保它们全部成功或全部失败时(比如转账操作),使用事务。
// 使用 DataSource 对象管理事务
await this.dataSource.transaction(async (transactionManager) => {
// 在事务中创建用户
const user = transactionManager.create(User, { name: 'Alice' });
await transactionManager.save(user);
// 在事务中更新用户关联的 profile
await transactionManager.update(
Profile,
{ userId: user.id },
{ status: 'active' }
);
});
// 如果过程中任何一步出错,所有操作自动回滚
3.2 TypeORM 中实现事务的两种方式
方式1:transaction
方法(推荐)
直接使用 TypeORM 的 transaction
API,自动管理事务的生命周期。
import { DataSource } from 'typeorm';
// 在 Service 类中注入 DataSource
constructor(private dataSource: DataSource) {}
// 示例:转账事务
async transferMoney(fromUserId: number, toUserId: number, amount: number) {
await this.dataSource.transaction(async (transactionManager) => {
// 注意:事务内的所有操作必须使用 transactionManager,而不是普通 repository
// 1. 扣款
await transactionManager.decrement(
User,
{ id: fromUserId },
'balance',
amount
);
// 2. 收款
await transactionManager.increment(
User,
{ id: toUserId },
'balance',
amount
);
// 3. 记录交易日志(事务内操作)
await transactionManager.insert(TransactionLog, {
fromUser: fromUserId,
toUser: toUserId,
amount,
});
});
// 如果以上任何一步出错,所有操作自动回滚
}
关键点解释:
代码部分 | 说明 |
---|---|
transactionManager | 事务管理器对象,在事务内必须用它代替普通 repository 执行数据库操作 |
decrement / increment | TypeORM 内置的快捷方法,用于字段的增减操作 |
自动错误回滚 | 如果事务内抛出任何错误(如数据库错误、手动 throw),所有操作自动撤销 |
方式2:QueryRunner
手动控制(更灵活)
适合需要精细控制事务(如中途提交或回滚)的场景。
async manualTransactionDemo() {
// 1. 创建 QueryRunner
const queryRunner = this.dataSource.createQueryRunner();
try {
// 2. 建立数据库连接并启动事务
await queryRunner.connect();
await queryRunner.startTransaction();
// 3. 执行事务操作(通过 queryRunner.manager)
await queryRunner.manager.update(User, { id: 1 }, { balance: 100 });
await queryRunner.manager.delete(Order, { userId: 1 });
// 4. 手动提交事务(只有提交后操作才生效)
await queryRunner.commitTransaction();
} catch (error) {
// 5. 出错时回滚
await queryRunner.rollbackTransaction();
throw new Error('事务执行失败');
} finally {
// 6. 释放连接(重要!)
await queryRunner.release();
}
}
两种方式对比:
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
transaction | 自动管理连接和事务,代码简洁 | 灵活性较低 | 大多数常规场景 |
QueryRunner | 完全手动控制,灵活性高 | 代码量大,需手动释放连接 | 需要分段提交或复杂事务控制 |
3.3 事务中的错误处理
确保回滚的两种方法
- 自动回滚:事务内任何未捕获的异常(如数据库错误、手动
throw
)都会触发回滚。 - 手动回滚:在
catch
块中调用rollbackTransaction()
(仅限QueryRunner
方式)。
// 示例:在事务中检查业务条件
await this.dataSource.transaction(async (manager) => {
const user = await manager.findOneBy(User, { id: 1 });
// 业务逻辑错误也触发回滚
if (user.balance < 100) {
throw new Error('余额不足'); // 抛出错误 → 自动回滚
}
// ...其他操作
});
实际应用场景
- 金融交易:转账、支付、退款
- 订单系统:创建订单 → 扣减库存 → 生成物流单
- 用户注册:创建用户 → 初始化配置 → 发送欢迎消息(注意:外部API调用需额外处理)
注意事项
- 避免长事务:事务执行时间过长会导致数据库锁表,影响性能。
- 事务嵌套:TypeORM 支持嵌套事务,但不同数据库行为可能不同(需谨慎使用)。
- 隔离级别:默认使用数据库的隔离级别,可通过
SET TRANSACTION ISOLATION LEVEL
调整。
3.4 原生 SQL 查询
当 TypeORM 的 API 无法满足复杂查询需求时,可以直接写 SQL。
// 执行原生 SQL 查询(参数化查询防止 SQL 注入)
const rawData = await this.userRepository.query(
'SELECT * FROM users WHERE age > ? AND is_active = ?',
[18, true] // 参数按 ? 顺序传递
);
// 示例结果:返回原始数据库结果(字段名与数据库一致)
// [ { id: 1, name: 'John', age: 25, is_active: 1 }, ... ]
3.5 软删除与恢复
软删除不会真正删除数据,而是通过标记时间字段实现“伪删除”。
步骤 1:在实体类中添加 @DeleteDateColumn
import { DeleteDateColumn } from 'typeorm';
@Entity()
export class User {
// ...其他字段
@DeleteDateColumn({ name: 'deleted_at' }) // 字段名可自定义
deletedAt: Date; // 当调用 softDelete 时,此字段会自动记录删除时间
}
步骤 2:操作示例
// 软删除用户(id=1)
await this.userRepository.softDelete({ id: 1 });
// 恢复被软删除的用户
await this.userRepository.restore({ id: 1 });
// 查询时默认排除已软删除的数据
const activeUsers = await this.userRepository.find();
// 查询包含已删除的数据
const allData = await this.userRepository.find({
withDeleted: true, // 显式包含已删除数据
});
4. 装饰器(@)使用大全
以下整理 Nest.js 项目中常见的装饰器用法,涵盖 TypeORM 实体定义 和 DTO 验证 两大场景:
4.1 TypeORM 实体装饰器
用于定义数据库表结构的装饰器,需在实体类(Entity Class)中使用。
装饰器名称 | 用途 | 示例代码 | 备注 |
---|---|---|---|
@Entity() | 标记一个类为数据库实体 | @Entity('users') | 可指定表名,如不写则默认使用类名(小写) |
@Column() | 定义普通字段 | @Column({ type: 'varchar', length: 50 }) | 支持配置类型、长度、是否可为空等 |
@PrimaryGeneratedColumn | 定义自增主键 | @PrimaryGeneratedColumn() | 默认类型为 int ,可选 uuid 类型 |
@CreateDateColumn | 自动记录记录创建时间 | @CreateDateColumn({ name: 'create_time' }) | 由数据库自动填充时间 |
@UpdateDateColumn | 自动记录记录更新时间 | @UpdateDateColumn() | 每次更新时自动刷新 |
@DeleteDateColumn | 软删除标记字段(需配合 softDelete 使用) | @DeleteDateColumn() | 删除时自动填充时间,未删除时为 null |
@Index() | 为字段创建数据库索引 | @Index(['email'], { unique: true }) | 提升查询效率,unique 表示唯一约束 |
@OneToMany / @ManyToOne | 定义表关联关系(一对多/多对一) | @OneToMany(() => Order, (order) => order.user) @ManyToOne(() => User) | 需在关联的实体类中双向定义 |
4.2 DTO 验证装饰器
用于请求参数验证(搭配 class-validator
),需在 DTO 类中使用。
装饰器名称 | 用途 | 示例代码 | 可配置选项 |
---|---|---|---|
@IsString() | 验证字段是否为字符串 | @IsString({ message: '必须为字符串' }) | message : 自定义错误消息 |
@Length() | 验证字符串长度范围 | @Length(3, 20, { message: '长度3-20字符' }) | min : 最小长度max : 最大长度 |
@IsEmail() | 验证是否为合法邮箱格式 | @IsEmail({}, { message: '邮箱格式错误' }) | 可配置邮箱格式规则 |
@IsInt() | 验证是否为整数 | @IsInt() | - |
@Min() / @Max | 验证数字最小值/最大值 | @Min(18) @Max(100) | 配置数值范围 |
@IsOptional() | 允许字段为 null 或 undefined | @IsOptional() | 通常用于可选参数 |
@IsNotEmpty() | 验证字段不能为空(空字符串、null、undefined) | @IsNotEmpty() | 确保必填项 |
@IsBoolean() | 验证是否为布尔值 | @IsBoolean() | - |
@IsDate() | 验证是否为日期格式 | @IsDate() | - |
4.3 装饰器组合用法
// 示例:用户注册 DTO
export class CreateUserDto {
@IsString()
@Length(3, 20)
name: string;
@IsEmail()
email: string;
@IsInt()
@Min(18)
age: number;
@IsOptional()
@IsString()
address?: string;
}
5. 实用技巧
5.1 错误处理增强
在全局过滤器(Global Filter)中统一处理数据库错误:
// 示例:捕获 TypeORM 错误并返回友好提示
@Catch(QueryFailedError)
export class DatabaseExceptionFilter implements ExceptionFilter {
catch(exception: QueryFailedError, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
// 处理重复键错误(如唯一约束冲突)
if (exception.driverError?.code === 'ER_DUP_ENTRY') {
response.status(400).json({ message: '数据已存在' });
} else {
response.status(500).json({ message: '数据库操作失败' });
}
}
}
5.2 索引优化实战
合理使用索引可显著提升查询性能:
// 在用户实体中为常用查询字段添加索引
@Entity()
@Index(['email'], { unique: true }) // 唯一索引
@Index(['age', 'createdAt']) // 复合索引
export class User {
// ...
}
5.3 索引的作用
索引相当于数据库的“目录”,能快速定位数据,核心作用:
- 加速查询:特别是
WHERE
、JOIN
、ORDER BY
等操作 - 保证唯一性:唯一索引防止重复数据(如用户名、手机号)
- 减少全表扫描:避免逐行查找,降低数据库负载
5.4 索引的代价
- 占用存储空间:索引需要额外的磁盘空间
- 影响写入速度:每次增删改操作需同步更新索引
- 维护成本:不合理的索引会拖慢性能
5.5 何时创建索引?
场景 | 示例字段 | 索引类型推荐 |
---|---|---|
高频查询条件 | WHERE age > 18 | 普通索引 |
唯一性约束字段 | email 、手机号 | 唯一索引 |
排序或分组字段 | ORDER BY created_at DESC | 普通索引 |
多条件联合查询 | WHERE age > 18 AND city='北京' | 复合索引 |
外键关联字段 | user_id (关联查询) | 普通索引 |
5.6 TypeORM 中创建索引
单字段索引
直接在实体类字段上使用 @Index
:
@Entity()
export class User {
@Column()
@Index() // 为 email 字段创建普通索引
email: string;
@Column()
@Index({ unique: true }) // 创建唯一索引
username: string;
}
复合索引
为多个字段联合创建索引(注意字段顺序):
@Entity()
@Index(['lastName', 'firstName']) // 联合索引:lastName + firstName
export class User {
@Column()
firstName: string;
@Column()
lastName: string;
}
5.7 索引命名与高级配置
@Index('IDX_USER_AGE', ['age'], { unique: false, where: 'age > 18' })
@Entity()
export class User {
@Column()
age: number;
}
参数 | 说明 |
---|---|
'IDX_USER_AGE' | 自定义索引名称(推荐统一命名风格) |
['age'] | 索引字段(数组形式,复合索引填多个字段) |
where | 条件索引(仅对符合条件的数据创建索引,减少索引大小) |
6. 完整项目结构
src/
├── modules/
│ └── user/
│ ├── dto/
│ │ ├── create-user.dto.ts # 创建用户DTO(带验证装饰器)
│ │ └── update-user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts # 用户实体(TypeORM装饰器)
│ ├── user.controller.ts # API路由(@Get、@Post等)
│ └── user.service.ts # 业务逻辑(包含所有查询方法)
├── common/
│ ├── filters/ # 全局异常过滤器
│ └── decorators/ # 自定义装饰器
└── app.module.ts # 主模块(数据库配置、模块注册)
7. 完整示例代码
实体类示例
// user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, Index } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
@Index({ unique: true })
email: string;
@Column()
name: string;
@Column()
age: number;
@CreateDateColumn()
createdAt: Date;
}
服务类示例
// user.service.ts
@Injectable()
export class UserService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
) {}
// 查询用户列表(分页)
async getUsers(page: number, limit: number) {
const [data, total] = await this.userRepository.findAndCount({
skip: (page - 1) * limit,
take: limit,
});
return { data, total, page, totalPages: Math.ceil(total / limit) };
}
}
总结
通过 TypeORM 和 Nest.js 的结合,可以高效实现:
- 安全的数据操作(事务、软删除)
- 清晰的代码结构(实体、DTO、服务分层)
- 强大的参数验证(装饰器组合)
建议在实际开发中:
- 优先使用 TypeORM 的高级查询方法(如 QueryBuilder)
- 始终验证用户输入(DTO + 装饰器)
- 合理使用索引优化查询性能