Skip to content
第 15 章 后端 ⏱ 13 分钟阅读

第 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

动手练习 ​

  1. 分页:写 paginate(page, limit),返回 { items, total }
  2. 搜索:用动态 where 拼 keyword + age 范围
  3. 统计:用聚合查每个用户的文章数

下一章:第 16 章:迁移 →

本站基于 VitePress 构建 · 由 StackHub 团队维护