6月5日 22:02

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 }, });

条件组合

typescript
import { 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。

选择字段

typescript
const users = await userRepository.find({ select: { id: true, name: true, email: true, // 不选 password 等敏感字段 }, where: { active: true }, });

排序和分页

typescript
const 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 搞不定的场景

关联查询带条件过滤

findrelations 只能全量加载,不能过滤。QueryBuilder 可以:

typescript
// 只查有已发布文章的用户 const users = await userRepository .createQueryBuilder('user') .innerJoinAndSelect( 'user.posts', 'post', 'post.isPublished = :published', { published: true }, ) .getMany();

第三个参数是 JOIN 条件——find 做不到这个。

条件组合:AND + OR + 括号

typescript
const 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 的优先级正确。没有它,ANDOR 的优先级会让你拿到错误的结果。

聚合查询

typescript
const 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() 返回实体对象,但聚合结果不是实体,强用会出错。

子查询

typescript
const 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 + FindOperatorInLikeBetween 够用
多表关联 + 过滤QueryBuilderfind 的 relations 不能加条件
聚合/分组QueryBuilderfind 不支持 GROUP BY
子查询QueryBuilderfind 不支持
复杂 OR + AND 混合QueryBuilderfind 的 OR 只支持数组级
数据库特有语法原生 SQLTypeORM 不覆盖所有特性
性能关键查询原生 SQL跳过实体转换

经验法则:先试 find,搞不定再上 QueryBuilder,最后才用原生 SQL。层级越低灵活性越高,但可维护性和类型安全越差。

标签:TypeORM