第 178 章:Repository 与查询构建器
学习目标
- 掌握 Repository 高级用法
- 深入 QueryBuilder
- 学会事务处理
- 理解原始 SQL
一、Repository 方法速查
1.1 查询类
typescript
// 查全部
const all = await repo.find();
// 按主键查
const one = await repo.findOneBy({ id: 1 });
const byIds = await repo.findByIds([1, 2, 3]);
// 条件
const users = await repo.findBy({ role: 'admin', status: 'active' });
// 复杂条件
const users = await repo.find({
where: { role: 'admin' },
order: { id: 'DESC' },
skip: 0,
take: 10,
});
// 分页
const [items, total] = await repo.findAndCount({
where: { role: 'admin' },
skip: 0,
take: 10,
});
// 计数
const count = await repo.count({ where: { role: 'admin' } });
// 存在?
const exists = await repo.exists({ where: { email: 'a@b.com' } });
// 单字段求和 / 平均
const sum = await repo.sum('age', { role: 'admin' });
const avg = await repo.average('age', { role: 'admin' });
const min = await repo.min('age', { role: 'admin' });
const max = await repo.max('age', { role: 'admin' });1.2 写入类
typescript
// 创建 + 保存
const user = repo.create({ name: 'Tom' });
const saved = await repo.save(user);
// 批量保存
await repo.save([user1, user2]);
// 更新(不查)
await repo.update({ id: 1 }, { name: 'Tom' });
await repo.update({ role: 'user' }, { status: 'banned' });
// 新增或更新
await repo.upsert([{ id: 1, name: 'Tom' }], ['id']);
// 软删除
await repo.softDelete({ id: 1 });
// 硬删除
await repo.delete({ id: 1 });
await repo.remove(user);
// 恢复软删
await repo.restore({ id: 1 });1.3 Where 高级语法
typescript
import { Like, Between, In, IsNull, MoreThan, LessThan, Not } from 'typeorm';
// 模糊
await repo.find({ where: { name: Like('%Tom%') } });
// 区间
await repo.find({ where: { age: Between(18, 30) } });
// IN
await repo.find({ where: { role: In(['admin', 'user']) } });
// NULL
await repo.find({ where: { avatar: IsNull() } });
// 大于 / 小于
await repo.find({ where: { age: MoreThan(18) } });
await repo.find({ where: { age: LessThan(60) } });
// NOT
await repo.find({ where: { role: Not('admin') } });
// AND / OR(数组里逗号是 AND,嵌套数组是 OR)
await repo.find({
where: [
{ name: 'Tom', age: MoreThan(18) },
{ role: 'admin' },
],
});二、QueryBuilder 入门
2.1 创建
typescript
const qb = repo.createQueryBuilder('alias');2.2 WHERE
typescript
// 等值
qb.where('user.name = :name', { name: 'Tom' })
// 比较
qb.andWhere('user.age > :age', { age: 18 })
qb.orWhere('user.role = :role', { role: 'admin' })
// IN
qb.andWhere('user.id IN (:...ids)', { ids: [1, 2, 3] })
// BETWEEN
qb.andWhere('user.createdAt BETWEEN :start AND :end', {
start: '2024-01-01',
end: '2024-12-31',
})
// LIKE
qb.andWhere('user.name LIKE :name', { name: '%Tom%' })2.3 JOIN
typescript
// LEFT JOIN
qb.leftJoin('user.posts', 'post')
// 关联查询(SELECT)
qb.leftJoinAndSelect('user.posts', 'post')
// INNER JOIN
qb.innerJoin('post.tags', 'tag')
// 带条件 JOIN
qb.leftJoin('user.posts', 'post', 'post.published = :pub', { pub: true })2.4 ORDER / GROUP / LIMIT
typescript
qb.orderBy('user.createdAt', 'DESC')
qb.addOrderBy('user.id', 'ASC')
qb.groupBy('user.role')
qb.having('COUNT(user.id) > :count', { count: 5 })
qb.limit(10)
qb.offset(20)
// 取一条
qb.limit(1).getOne()2.5 结果获取
typescript
qb.getMany(); // 数组
qb.getOne(); // 单个
qb.getManyAndCount(); // [items, total]
qb.getCount(); // 数量
qb.getExists(); // boolean
qb.getRawMany(); // 原始行
qb.getRawOne(); // 原始行单条
qb.execute(); // 用于 INSERT/UPDATE/DELETE三、QueryBuilder 实战
3.1 动态条件查询
typescript
async search(filter: UserFilterDto) {
const qb = this.repo.createQueryBuilder('user');
if (filter.name) {
qb.andWhere('user.name LIKE :name', { name: `%${filter.name}%` });
}
if (filter.role) {
qb.andWhere('user.role = :role', { role: filter.role });
}
if (filter.startDate) {
qb.andWhere('user.createdAt >= :start', { start: filter.startDate });
}
if (filter.endDate) {
qb.andWhere('user.createdAt <= :end', { end: filter.endDate });
}
qb.orderBy('user.id', 'DESC');
const [items, total] = await qb
.skip((filter.page - 1) * filter.size)
.take(filter.size)
.getManyAndCount();
return { items, total };
}3.2 关联查询 + 过滤
typescript
async findUserWithPosts(userId: number) {
return this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post', 'post.published = :pub', { pub: true })
.leftJoinAndSelect('post.tags', 'tag')
.where('user.id = :id', { id: userId })
.getOne();
}3.3 子查询
typescript
async findActiveUsersWithPostCount() {
return this.repo
.createQueryBuilder('user')
.loadRelationCountAndMap('user.postCount', 'user.posts')
.where('user.status = :status', { status: 'active' })
.getMany();
}四、聚合查询
4.1 SELECT 子句
typescript
const stats = await userRepo
.createQueryBuilder('user')
.select('user.role', 'role')
.addSelect('COUNT(*)', 'count')
.addSelect('AVG(user.age)', 'avgAge')
.groupBy('user.role')
.getRawMany();
// [{ role: 'admin', count: '5', avgAge: '28' }, ...]4.2 子查询
typescript
const users = await userRepo
.createQueryBuilder('user')
.where(qb => {
const subQuery = qb
.subQuery()
.select('post.authorId')
.from(Post, 'post')
.where('post.published = :pub', { pub: true });
return 'user.id IN ' + subQuery.getQuery();
})
.getMany();五、事务
5.1 自动事务
typescript
await userRepo.manager.transaction(async manager => {
const user = await manager.save(User, { name: 'Tom' });
await manager.save(Post, { authorId: user.id, title: 'Hi' });
});5.2 手动事务
typescript
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
await queryRunner.manager.save(User, { name: 'Tom' });
await queryRunner.manager.save(Post, { title: 'Hello' });
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}5.3 隔离级别
typescript
await dataSource.transaction('SERIALIZABLE', async manager => {
// ...
});六、原始 SQL
typescript
// 查询
const users = await dataSource.query(
'SELECT * FROM users WHERE role = ?',
['admin'],
);
// 写操作
await dataSource.query(
'DELETE FROM users WHERE createdAt < ?',
['2024-01-01'],
);安全
直接拼接字符串会有 SQL 注入风险,务必使用参数化(? 或 :name)!
七、软删除
7.1 配置
typescript
import { DeleteDateColumn } from 'typeorm';
@Entity()
export class User {
@DeleteDateColumn()
deletedAt?: Date;
}7.2 查询
typescript
// 默认不查已删除
await repo.find(); // 不返回软删数据
// 查全部(含软删)
await repo.find({ withDeleted: true });
// 只查已删除
await repo.find({ withDeleted: true, where: { deletedAt: Not(IsNull()) } });7.3 软删 / 恢复
typescript
await repo.softDelete({ id: 1 }); // 设置 deletedAt
await repo.restore({ id: 1 }); // 清空 deletedAt八、Repository API 速查表
| 方法 | 用途 |
|---|---|
find / findBy / findOneBy | 查询 |
findAndCount | 分页查询 |
count / exists | 统计 |
save | 保存(新增/更新) |
create | 创建实例(不保存) |
update | 更新(不查) |
delete / remove | 删除 |
softDelete | 软删 |
restore | 恢复 |
upsert | 新增或更新 |
increment / decrement | 增减 |
createQueryBuilder | 构建器 |
九、QueryBuilder vs Repository 对比
typescript
// Repository 简单场景
const users = await repo.find({ where: { role: 'admin' } });
// QueryBuilder 复杂场景
const users = await repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.where('user.role = :role', { role: 'admin' })
.andWhere('post.published = :pub', { pub: true })
.orderBy('user.id', 'DESC')
.getMany();十、本章小结
| 方法 | 用途 |
|---|---|
find | 简单查询 |
findOneBy | 查一条 |
findAndCount | 分页 |
createQueryBuilder | 复杂查询 |
transaction | 事务 |
query | 原始 SQL |
softDelete | 软删除 |
manager.save | 事务中保存 |
动手练习
- 写一个 findUsers API,支持多条件 + 分页
- 用 QueryBuilder 实现联表查询(用户 + 文章 + 标签)
- 用事务实现转账功能(A 减钱,B 加钱)
- 实现软删除 + 恢复
推荐阅读
下一章:第 179 章:Migration 数据迁移 →