TypeORM 查询该用 find 还是 QueryBuilder?三种方式适用场景对比
TypeORM 查询数据有三条路:find 系列方法、QueryBuilder、原生 SQL。很多人上来就用 QueryBuilder,其实 80% 的查询 find 就够了——更简洁、类型安全、不容易出错。这篇把三种方式的适用边界和常见坑讲清楚。
find:日常查询的首选
基础查询
typescript// 查所有 const users = await userRepository.find(); // 按 ID 查一条 const user = await userRepository.findOne({ where: { id: 1 } }); // 按条件查 const activeUsers = await userRepository.find({ where: { active: true }, });
条件组合
typescriptimport { And, Or, LessThan, MoreThan, Like, Between, In, IsNull } from 'typeorm'; // AND 条件——多个字段自动 AND const users = await userRepository.find({ where: { active: true, age: MoreThan(18), }, }); // OR 条件——数组内自动 OR const users = await userRepository.find({ where: [ { role: 'admin' }, { age: MoreThan(30) }, ], }); // 常用 FindOperator const users = await userRepository.find({ where: { name: Like('%john%'), // 模糊搜索 id: In([1, 2, 3]), // IN 查询 createdAt: Between(start, end), // 范围查询 deletedAt: IsNull(), // IS NULL }, });
find 的 OR 有个坑:数组里每个元素是一个独立的 OR 分支,不是字段级别的 OR。如果你需要 role = 'admin' AND (age > 30 OR name LIKE '%john%') 这种混合逻辑,find 写不出来——得上 QueryBuilder。
选择字段
typescriptconst users = await userRepository.find({ select: { id: true, name: true, email: true, // 不选 password 等敏感字段 }, where: { active: true }, });
排序和分页
typescriptconst users = await userRepository.find({ where: { active: true }, order: { createdAt: 'DESC' }, skip: 0, take: 20, }); // 带总数的分页——一次查询拿到数据 + 总数 const [users, total] = await userRepository.findAndCount({ where: { active: true }, order: { createdAt: 'DESC' }, skip: (page - 1) * pageSize, take: pageSize, });
加载关联
typescript// 简单关联 const users = await userRepository.find({ relations: { posts: true, profile: true }, }); // 嵌套关联 const users = await userRepository.find({ relations: { posts: { comments: true } }, });
find 的 N+1 陷阱
find 加载关联时会发多条 SQL——每个关联单独查一次。如果查 100 个用户且关联了 posts,可能产生几十条查询:
typescript// 可能触发 N+1 const users = await userRepository.find({ relations: { posts: true }, }); // SQL 1: SELECT * FROM user // SQL 2: SELECT * FROM post WHERE userId IN (1, 2, 3, ... 100)
TypeORM 0.3+ 已经优化了这个问题——会用 IN 批量查而不是逐个查。但如果关联层级很深(用户 → 文章 → 评论 → 作者),查询数仍然会膨胀。深层关联建议用 QueryBuilder 的 leftJoinAndSelect 一次性 JOIN。
QueryBuilder:find 搞不定的场景
关联查询带条件过滤
find 的 relations 只能全量加载,不能过滤。QueryBuilder 可以:
typescript// 只查有已发布文章的用户 const users = await userRepository .createQueryBuilder('user') .innerJoinAndSelect( 'user.posts', 'post', 'post.isPublished = :published', { published: true }, ) .getMany();
第三个参数是 JOIN 条件——find 做不到这个。
条件组合:AND + OR + 括号
typescriptconst users = await userRepository .createQueryBuilder('user') .where('user.active = :active', { active: true }) .andWhere( new Brackets((qb) => { qb.where('user.role = :admin', { admin: 'admin' }).orWhere( 'user.createdAt > :date', { date: '2024-01-01' }, ); }), ) .getMany(); // WHERE active = true AND (role = 'admin' OR createdAt > '2024-01-01')
Brackets 生成括号——保证 OR 的优先级正确。没有它,AND 和 OR 的优先级会让你拿到错误的结果。
聚合查询
typescriptconst stats = await userRepository .createQueryBuilder('user') .select('user.role', 'role') .addSelect('COUNT(*)', 'count') .addSelect('AVG(user.age)', 'avgAge') .groupBy('user.role') .getRawMany(); // 返回: [{ role: 'admin', count: '5', avgAge: '32.5' }, ...]
聚合查询必须用 getRawMany()——返回原始数据库行,字段类型都是字符串。getMany() 返回实体对象,但聚合结果不是实体,强用会出错。
子查询
typescriptconst users = await userRepository .createQueryBuilder('user') .where((qb) => { const subQuery = qb .subQuery() .select('post.authorId') .from(Post, 'post') .groupBy('post.authorId') .having('COUNT(post.id) > :count') .getQuery(); return `user.id IN ${subQuery}`; }) .setParameter('count', 5) .getMany();
QueryBuilder 的类型安全坑
find 是完全类型安全的——where 里的字段名写错了 TypeScript 直接报错。QueryBuilder 的 SQL 片段是字符串,写错了只有运行时才知道:
typescript// 字段名拼错了,TypeScript 不会报错,运行时才炸 .where('user.actve = :active', { active: true }) // ↑ typo: active vs actve
建议:QueryBuilder 的 SQL 片段尽量短,把条件值用参数传递(:param),减少字符串拼写出错的可能。
原生 SQL:最后的手段
QueryBuilder 也搞不定时,直接写 SQL:
typescript// 查询 const result = await dataSource.query( 'SELECT * FROM user WHERE created_at > $1', [startDate], ); // 事务中执行 const result = await queryRunner.query( 'UPDATE user SET last_login = NOW() WHERE id = $1', [userId], );
什么时候用原生 SQL:
- 数据库特有的函数或语法(如 PostgreSQL 的
JSONB操作、UPSERT) - 复杂的窗口函数(
ROW_NUMBER() OVER (...)) - 需要极致优化的性能关键查询
- 迁移老项目的 SQL 不想重写
注意:$1 是 PostgreSQL 占位符,MySQL 用 ?。原生 SQL 没有方言抽象——换数据库要手动改。
性能对比
同样的查询,三种方式的性能差异:
typescript// 1. find —— 最慢(多条 SQL + 实体转换开销) const users = await userRepository.find({ where: { active: true }, relations: { posts: true } }); // 2. QueryBuilder —— 中等(一条 JOIN SQL,但仍有实体转换) const users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.posts', 'post') .where('user.active = :active', { active: true }) .getMany(); // 3. 原生 SQL —— 最快(一条 SQL,跳过实体转换) const rows = await dataSource.query( 'SELECT u.*, p.id as post_id, p.title FROM user u LEFT JOIN post p ON p.userId = u.id WHERE u.active = $1', [true], );
差距不大时(毫秒级),优先用 find 或 QueryBuilder——可维护性和类型安全比那几毫秒更有价值。只有查询确实是瓶颈时才用原生 SQL 优化。
选择决策
| 场景 | 用什么 | 原因 |
|---|---|---|
| 单表简单查询 | find | 简洁、类型安全 |
| 单表条件组合 | find + FindOperator | In、Like、Between 够用 |
| 多表关联 + 过滤 | QueryBuilder | find 的 relations 不能加条件 |
| 聚合/分组 | QueryBuilder | find 不支持 GROUP BY |
| 子查询 | QueryBuilder | find 不支持 |
| 复杂 OR + AND 混合 | QueryBuilder | find 的 OR 只支持数组级 |
| 数据库特有语法 | 原生 SQL | TypeORM 不覆盖所有特性 |
| 性能关键查询 | 原生 SQL | 跳过实体转换 |
经验法则:先试 find,搞不定再上 QueryBuilder,最后才用原生 SQL。层级越低灵活性越高,但可维护性和类型安全越差。