Skip to content

第七章 利用QueryBuilder 查询构造器 执行聚合查询

本章目标

  • 知道什么时候 find 选项不够用,需要请出 QueryBuilder
  • 熟练使用 createQueryBuilder("别名") 拼装查询:select / where / orderBy / take / skip
  • 分清 getMany / getOne / getManyAndCount / getRawMany / getRawOne / getRawAndEntities 的使用场景
  • 会用 QueryBuilder 做聚合统计、join 基础查询、子查询和 insert/update/delete

核心概念

repo.find({ where, order, take, skip }) 就像超市的标准套餐——常见需求都能满足,但菜单上没有的组合(聚合统计、多表 join、子查询)就点不了。

QueryBuilder 是"拼装 SQL 的积木":每个方法(.where().orderBy().groupBy()……)就是一块积木,链式拼下去,最后 .getMany() 一下执行。它的表达能力约等于 SQL 本身。

入口与别名规则

  • 只能利用仓库的方式查询,不能直接利用实体类的方式查询
ts
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";

AppDataSource.initialize()
  .then(async () => {
    const userRepo = AppDataSource.getRepository(User);
    const query = userRepo.createQueryBuilder("user"); // 这里query就是别名
    const result = await query.getMany(); // 获取结果必须调用
    console.log(JSON.stringify(result));
  })
  .catch((error) => console.log(error));

where / andWhere / orWhere 与参数绑定

ts
const userRepo = AppDataSource.getRepository(User);
await userRepo
  .createQueryBuilder("user")
  .where("p.price >= :min", { min: 500 }) // :min 是参数占位符
  .andWhere("p.stock > :stock", { stock: 10 })
  .getMany();

await userRepo
  .createQueryBuilder("user")
  .where("p.category = :c1", { c1: "手机" })
  .orWhere("p.price > :high", { high: 1000 })
  .getMany();

⚠️ 永远不要这样写.where("p.price > " + userInput)。字符串拼接 SQL 等于给 SQL 注入敞开大门。一律用 :参数名 占位 + 第二个参数传值。

注意:where覆盖之前的条件,andWhere/orWhere追加

排序与分页

ts
const userRepo = AppDataSource.getRepository(User);
await userRepo
  .createQueryBuilder("user")
  .orderBy("user.firstName", "ASC") // 第一排序键
  .addOrderBy("user.lastName", "DESC") // 第二排序键
  .skip(5) // 跳过 5 条
  .take(5) // 取 5 条 → 第 2 页
  .getMany();

六种"执行"方法怎么选

方法返回值使用场景
getMany()Product[]查多条实体(最常用)
getOne()Product | null查一条,取不到返回 null,不报错
getManyAndCount()[Product[], number]分页:count 不受 take/skip 影响
getRawMany()any[]聚合/只选部分字段,只要原始行
getRawOne()any | undefined只取一行原始数据(如总数统计)
getRawAndEntities(){ entities, raw }既要实体、又要附加计算列时
ts
// getManyAndCount:分页标配
const [items, total] = await repo
  .createQueryBuilder("p")
  .where("p.category = :c", { c: "配件" })
  .take(2)
  .getManyAndCount(); // items 2 条,total 是该分类全部条数

// getRawAndEntities:实体 + 计算列一起拿
const { entities, raw } = await repo
  .createQueryBuilder("p")
  .select("p")
  .addSelect("p.price * p.stock", "totalValue") // 计算列只出现在 raw 里
  .where("p.id <= :id", { id: 3 })
  .getRawAndEntities();

5. 聚合统计:groupBy / having

聚合结果不是完整实体(实体里没有 avgPrice 这个字段),所以配 getRawMany()

ts
const stats = await repo
  .createQueryBuilder("p")
  .select("p.category", "category")
  .addSelect("COUNT(p.id)", "cnt")
  .addSelect("AVG(p.price)", "avgPrice")
  .addSelect("SUM(p.stock)", "totalStock")
  .groupBy("p.category")
  .having("COUNT(p.id) >= :minCnt", { minCnt: 3 })
  .orderBy("avgPrice", "DESC")
  .getRawMany();
// stats = [{ category: "家电", cnt: "5", avgPrice: "650.00", ... }, ...]

原生SQL

ts
import { MoreThan } from "typeorm";
import { AppDataSource } from "./data-source";
import { User } from "./entities/User";

AppDataSource.initialize()
  .then(async () => {
    const result = await AppDataSource.manager.query(
      "SELECT age from user group by age",
    );
    console.log(JSON.stringify(result));
  })
  .catch((error) => console.log(error));