TypeORM QueryBuilder: Performance, Aggregations e Prevenção do N+1
Como usar o QueryBuilder para consultas avançadas: leftJoinAndSelect vs innerJoin, funções de agregação (SUM, COUNT) com group by, raw results e como estruturar subqueries sem perder a tipagem do TypeScript.
O método repository.find() resolve 80% dos casos de uso de um CRUD. Mas, e quando precisamos de agregações ('Qual o faturamento mensal de cada vendedor ativo?') ou filtros dinâmicos complexos? Trazer todos os dados para a memória do Node.js e usar Array.reduce() é um suicídio de performance. O processamento pesado deve ocorrer onde os dados vivem: no banco de dados. É para isso que usamos o QueryBuilder do TypeORM.
Filtros Dinâmicos e Prevenção de SQL Injection
O QueryBuilder permite montar queries condicionalmente sem concatenar strings (o que causaria vulnerabilidade a SQL Injection). Sempre use os parâmetros com a sintaxe :parametro — o TypeORM passa os valores ao banco como prepared statements, tornando SQL Injection estruturalmente impossível. Filtros condicionais com andWhere são acumulados somente quando o valor existe, evitando a necessidade de if/else por toda a query.
import { DataSource, Repository } from 'typeorm';
import { Order } from '../entities/Order';
export class TypeORMOrdersRepository {
private repo: Repository<Order>;
constructor(dataSource: DataSource) {
this.repo = dataSource.getRepository(Order);
}
async searchOrders(status?: string, minAmount?: number) {
// 'order' é o alias principal (a raiz da query)
const query = this.repo.createQueryBuilder('order');
if (status) {
// andWhere: acumula as condições (WHERE ... AND ...)
query.andWhere('order.status = :status', { status });
}
if (minAmount) {
query.andWhere('order.totalAmount >= :minAmount', { minAmount });
}
// getMany() retorna instâncias da entidade Order perfeitamente tipadas
return query.orderBy('order.createdAt', 'DESC').getMany();
}
}O Problema do N+1 e Eager Loading
Fazer um find() em Usuários e, num for loop, fazer outro find() para buscar os Endereços de cada um, gera o temido problema do N+1 queries. Com 100 usuários, isso resulta em 101 queries — 1 para listar + 100 para buscar endereços. O QueryBuilder resolve isso com leftJoinAndSelect, gerando uma única query com LEFT JOIN que traz tudo de uma vez. A diferença de performance é geralmente de 10x a 100x em listas grandes.
// Executa apenas UMA query (LEFT JOIN) e mapeia os objetos aninhados
const users = await this.repo.createQueryBuilder('user')
// user.addresses aponta para a relação na Entidade @OneToMany
.leftJoinAndSelect('user.addresses', 'address')
.leftJoinAndSelect('user.company', 'company')
.where('user.active = :active', { active: true })
.getMany();
// users[0].addresses[0].street estará perfeitamente preenchidoAgregações e Raw Results (Relatórios)
Quando usamos SUM() ou COUNT(), o resultado não mapeia perfeitamente para a Entidade padrão. Nesses casos de relatórios, usamos getRawMany(), que devolve os dados brutos como vieram do banco (ideal para dashboards).
async getMonthlyRevenue(year: number) {
const results = await this.repo.createQueryBuilder('order')
.select("EXTRACT(MONTH FROM order.createdAt)", "month")
.addSelect("SUM(order.totalAmount)", "revenue")
.addSelect("COUNT(order.id)", "salesCount")
.where("EXTRACT(YEAR FROM order.createdAt) = :year", { year })
.andWhere("order.status = 'PAID'")
.groupBy("EXTRACT(MONTH FROM order.createdAt)")
.orderBy("month", "ASC")
.getRawMany(); // Retorna o JSON exato do SELECT
// results: [{ month: 1, revenue: "15000.00", salesCount: "42" }, ...]
return results.map(r => ({
month: Number(r.month),
revenue: Number(r.revenue),
salesCount: Number(r.salesCount)
}));
}`innerJoin` vs `leftJoin`: Use innerJoinAndSelect apenas se a relação for OBRIGATÓRIA (ex: Pedido e Cliente). Se a relação puder ser nula e você usar innerJoin, o registro principal nem será retornado. Em dúvida, o leftJoinAndSelect é o mais seguro para não perder registros primários.
Conclusão
O QueryBuilder do TypeORM é a ponte entre a tipagem rigorosa do TypeScript e o poder bruto do SQL. Ele permite que lógicas pesadas (como somas, agrupamentos e paginações com junções) ocorram no banco de dados, protegendo a memória da sua aplicação Node.js e evitando gargalos de N+1 queries nas listagens complexas.