第 15 章:查询构造器
学习目标
- 掌握 QueryBuilder 复杂查询
- 用 where/andWhere/orWhere 组合条件
- 写分页、聚合、join
- 避开 4 个 QB 坑
一、为什么需要 QueryBuilder
Repository.find() 适合简单查询;复杂场景(动态 where、聚合、原始 SQL)用 QueryBuilder。
typescript
const users = await this.repo
.createQueryBuilder('user')
.where('user.active = :active', { active: true })
.andWhere('user.age > :age', { age: 18 })
.orderBy('user.createdAt', 'DESC')
.getMany();二、基本查询
typescript
// 查一条
this.repo.createQueryBuilder('u')
.where('u.id = :id', { id: 1 })
.getOne();
// 查多条
this.repo.createQueryBuilder('u')
.where('u.active = true')
.getMany();
// 计数
this.repo.createQueryBuilder('u')
.where('u.active = true')
.getCount();⚠️ 坑 1:
getOne()找不到返回null,不抛异常,需要手动 null 判断。
三、参数化(必须用占位符)
typescript
.where('u.username = :name', { name: input })
// ✅ 防 SQL 注入typescript
.where(`u.username = '${input}'`)
// ❌ 千万别字符串拼接!四、动态 where
typescript
async search(q: SearchDto) {
const qb = this.repo.createQueryBuilder('u');
if (q.name) qb.andWhere('u.name LIKE :name', { name: `%${q.name}%` });
if (q.minAge) qb.andWhere('u.age >= :age', { age: q.minAge });
if (q.active !== undefined) qb.andWhere('u.active = :a', { a: q.active });
return qb.orderBy('u.id', 'DESC').getMany();
}⚠️ 坑 2:
Like操作符要手动拼%→LIKE :name然后传{ name: '%tom%' }。
五、join 关联查询
typescript
const list = await this.repo
.createQueryBuilder('user')
.leftJoinAndSelect('user.posts', 'post')
.leftJoinAndSelect('post.tags', 'tag')
.where('user.id = :id', { id: 1 })
.getOne();leftJoinAndSelect = LEFT JOIN + SELECT。结果带 user.posts[].tags[]。
六、子查询
typescript
const qb = this.repo
.createQueryBuilder('u')
.select(['u.id', 'u.username'])
.addSelect(q =>
q.select('COUNT(p.id)', 'count')
.from(Post, 'p')
.where('p.authorId = u.id'),
'postCount');给每个 user 加一个 postCount 字段(统计这个 user 的文章数)。
生成的 SQL:
sql
SELECT u.id, u.username,
(SELECT COUNT(p.id) FROM post p WHERE p.authorId = u.id) AS postCount
FROM user u返回结果:
typescript
[
{ id: 1, username: 'Tom', postCount: '5' }, // Tom 有 5 篇
{ id: 2, username: 'Mike', postCount: '0' }, // Mike 没文章
]addSelect 两个参数:
typescript
.addSelect(子查询函数, '字段别名')
// ↑ 子查询返回的结果,叫什么名字实战更常用 leftJoin + groupBy(比子查询更直观):
typescript
const users = await userRepo
.createQueryBuilder('u')
.leftJoin('u.posts', 'p')
.select(['u.id', 'u.username'])
.addSelect('COUNT(p.id)', 'postCount')
.groupBy('u.id') // 按 user 分组
.orderBy('postCount', 'DESC') // 按文章数排序
.getMany();什么时候用子查询? 数据量大、join 性能差,或需要嵌套复杂子查询。
记忆口诀:
addSelect(q => 子查询, '别名')= 加一个子查询字段- 等价 SQL =
SELECT (子查询) AS 别名 - 常见场景:每条记录附带一个聚合数字(文章数、评论数)
- 实战常用:
leftJoin + groupBy + addSelect COUNT
七、分页
typescript
async paginate(page: number, limit: number) {
const [items, total] = await this.repo
.createQueryBuilder('u')
.skip((page - 1) * limit)
.take(limit)
.getManyAndCount(); // 同时取总数
return { items, total, page, limit };
}八、聚合
typescript
const stats = await this.repo
.createQueryBuilder('u')
.select('COUNT(*)', 'count')
.addSelect('AVG(u.age)', 'avgAge')
.where('u.active = true')
.getRawOne();
// { count: '100', avgAge: '28.5' }⚠️ 坑 3:
getRawOne()返回的是数据库原始值,字符串需要parseInt或Number。
九、原生 SQL(慎用)
typescript
const rows = await this.repo.query(
`SELECT * FROM users WHERE created_at > $1`,
['2026-01-01'],
);适合报表、复杂统计。
十、事务
typescript
await this.dataSource.transaction(async manager => {
await manager.update(User, { id: 1 }, { active: false });
await manager.insert(Profile, { userId: 1, bio: '...' });
// 任意一步失败自动回滚
});⚠️ 坑 4:不开事务 → 一半成功一半失败,数据不一致。
十一、缓存查询
typescript
this.repo
.createQueryBuilder('u')
.where('u.id = :id', { id: 1 })
.cache(true) // 内存缓存
.getOne();或自定义 cache id:
typescript
.cache('user_' + id, 60_000) // 60 秒.cache() 是 TypeORM 提供的能力,不是 SQL 自带的。TypeORM 拦截查询:先查缓存 → 没命中再查 DB → 写回缓存。
前提:要在 forRoot({ cache: {...} }) 配存储(redis / database / memory),不配就静默不生效。
forRoot({ cache: {...} }) 配存储:
typescript
TypeOrmModule.forRoot({
cache: {
type: 'redis', // 存哪
options: { host: '...', port: 6379 }, // 怎么连
duration: 5000, // 默认 TTL(毫秒)
},
});| 字段 | 作用 |
|---|---|
type | 存哪(redis / database / memory) |
options | 连接配置 |
duration | 默认 TTL |
不配 → .cache(true) 静默不生效(不报错)。
十二、实战:动态搜索
typescript
async searchUsers(filters: UserFilterDto) {
const qb = this.repo.createQueryBuilder('u');
if (filters.keyword) {
qb.andWhere('(u.username ILIKE :kw OR u.email ILIKE :kw)', {
kw: `%${filters.keyword}%`,
});
}
if (filters.minAge) qb.andWhere('u.age >= :a', { a: filters.minAge });
if (filters.maxAge) qb.andWhere('u.age <= :a', { a: filters.maxAge });
if (filters.sortBy) {
qb.orderBy(`u.${filters.sortBy}`, filters.order ?? 'ASC');
}
const [items, total] = await qb.skip(filters.offset).take(filters.limit).getManyAndCount();
return { items, total };
}十三、本章小结
| 方法 | 用途 |
|---|---|
getOne / getMany | 取实体 |
getRawOne / getRawMany | 取原始值 |
getCount | 计数 |
getManyAndCount | 分页 |
cache | 缓存 |
transaction | 事务 |
query() | 原生 SQL |
动手练习
- 分页:写
paginate(page, limit),返回{ items, total } - 搜索:用动态 where 拼 keyword + age 范围
- 统计:用聚合查每个用户的文章数
下一章:第 16 章:迁移 →