Pular para o conteúdo

Query Builder

O Repository.find() cobre a maioria dos casos. Quando você precisa de GROUP BY, HAVING, JOINs manuais, agregações ou expressões SQL arbitrárias, use o QueryBuilder.

const queryBuilder = conn.getRepository(Post).createQueryBuilder();

MétodoRetornoDescrição
select(keys)thisColunas a selecionar
where(condition)thisCondição WHERE (sobrescreve chamadas anteriores)
andWhere(sql, params?)thisAcrescenta condição WHERE com SQL raw
leftJoin(relation, alias)thisLEFT JOIN por chave de relação
groupBy(columns)thisGROUP BY
having(condition)thisHAVING
orderBy(options)thisORDER BY
limit(n)thisLIMIT
offset(n)thisOFFSET
getMany()Promise<T[]>Executa e retorna entidades hidratadas
getRaw()Promise<Record[]>Executa e retorna objetos simples
getCount()Promise<number>Executa COUNT(*)
build(){ sql, params }Gera o SQL sem executar
explain()Promise<string>Retorna o EXPLAIN ANALYZE (PostgreSQL)

const posts = await conn.getRepository(Post)
.createQueryBuilder()
.where({ status: 'published' })
.orderBy({ createdAt: 'DESC' })
.limit(10)
.getMany();

As chaves em where e orderBy são propriedades da entidade — o QueryBuilder mapeia automaticamente para os nomes de coluna no banco:

.where({ viewCount: MoreThan(100) })
// WHERE "view_count" > $1

  • getMany() — retorna instâncias da entidade, com type casting e todos os mapeamentos aplicados
  • getRaw() — retorna objetos JavaScript simples com os nomes das colunas do banco, sem hidratação
// getMany — entidades tipadas
const posts = await queryBuilder.getMany();
// posts[0] instanceof Post === true
// posts[0].viewCount === 42 (number)
// getRaw — objetos simples
const rows = await queryBuilder.getRaw();
// rows[0] = { id: 1, title: 'Olá', view_count: '42' }
// ^^^^ string crua do banco

Use getRaw() quando você seleciona colunas calculadas ou agregações que não existem na entidade.


Para COUNT, SUM, AVG e similares, passe as expressões SQL diretamente no select() e use getRaw():

const stats = await conn.getRepository(Post)
.createQueryBuilder()
.select(['authorId', 'COUNT(*) AS total', 'SUM("view_count") AS views'])
.groupBy('"author_id"')
.having('COUNT(*) > 5')
.orderBy({ 'COUNT(*)': 'DESC' })
.getRaw();
// stats = [{ author_id: 1, total: '12', views: '3400' }, ...]

Para apenas contar registros com um filtro, use getCount():

const total = await conn.getRepository(Post)
.createQueryBuilder()
.where({ status: 'published' })
.getCount();
// SELECT COUNT(*) FROM "posts" WHERE "status" = $1

Todos os operadores do find() funcionam no QueryBuilder:

import { MoreThan, Like, In, IsNull } from 'mirror-orm';
await conn.getRepository(Post)
.createQueryBuilder()
.where({
viewCount: MoreThan(100),
title: Like('%mirror%'),
status: In(['published', 'featured']),
deletedAt: IsNull(),
})
.getMany();
await conn.getRepository(Post)
.createQueryBuilder()
.where([
{ status: 'published' },
{ status: 'featured', viewCount: MoreThan(1000) },
])
.getMany();
// WHERE (status = $1) OR (status = $2 AND view_count > $3)

Para condições que os operadores não cobrem, acrescente SQL arbitrário. Os parâmetros são automaticamente reposicionados:

await conn.getRepository(Post)
.createQueryBuilder()
.where({ status: 'published' })
.andWhere('"view_count" > (SELECT AVG("view_count") FROM "posts")')
.getMany();
// WHERE "status" = $1
// AND "view_count" > (SELECT AVG("view_count") FROM "posts")

Com parâmetros:

await conn.getRepository(Post)
.createQueryBuilder()
.where({ authorId: 1 })
.andWhere('"score" BETWEEN $1 AND $2', [50, 100])
.getMany();
// WHERE "author_id" = $1 AND "score" BETWEEN $2 AND $3
// ^^^ reposicionado automaticamente

Chamar where() mais de uma vez sobrescreve a condição anterior. Para acumular condições, use andWhere() ou passe tudo em um único objeto.


O leftJoin() recebe a chave da relação na entidade (não o nome da tabela) e um alias:

const books = await conn.getRepository(Book)
.createQueryBuilder()
.leftJoin('author', 'author')
.where({ 'author.name': Like('%Knuth%') })
.getMany();
// LEFT JOIN "authors" "author" ON "books"."author_id" = "author"."id"
// WHERE "author"."name" LIKE $1

Use o alias com dot notation no where para filtrar pela tabela joinada:

.where({ 'author.country': 'BR', status: 'published' })
// WHERE "author"."country" = $1 AND "status" = $2

Apenas leftJoin() está disponível. Para INNER JOIN, use andWhere() com SQL raw.


const page = 2;
const perPage = 10;
const posts = await conn.getRepository(Post)
.createQueryBuilder()
.where({ status: 'published' })
.orderBy({ createdAt: 'DESC' })
.limit(perPage)
.offset((page - 1) * perPage)
.getMany();

Se a entidade tiver @DeletedAt, o QueryBuilder sempre acrescenta WHERE deleted_at IS NULL automaticamente — incluindo no getCount(). Esse filtro não pode ser desativado no QueryBuilder; para incluir deletados use repo.find({ withDeleted: true }).


Útil para debug, logging ou testes:

const { sql, params } = conn.getRepository(Post)
.createQueryBuilder()
.select(['id', 'title'])
.where({ status: 'published' })
.orderBy({ createdAt: 'DESC' })
.limit(5)
.build();
console.log(sql);
// SELECT "id", "title" FROM "posts"
// WHERE "status" = $1 AND "deleted_at" IS NULL
// ORDER BY "created_at" DESC
// LIMIT 5
console.log(params);
// ['published']
const plan = await conn.getRepository(Post)
.createQueryBuilder()
.where({ authorId: 1 })
.explain();
console.log(plan);
// Index Scan using posts_author_id_idx on posts
// (cost=0.28..8.30 rows=1 width=32)
// (actual time=0.021..0.022 rows=1 loops=1)

O QueryBuilder herda o contexto de transação via AsyncLocalStorage automaticamente — o mesmo mecanismo do Repository:

await conn.transaction(async (transaction) => {
const drafts = await transaction.getRepository(Post)
.createQueryBuilder()
.where({ status: 'draft', authorId: userId })
.getMany();
for (const draft of drafts) {
draft.status = 'published';
await transaction.getRepository(Post).save(draft);
}
});

SituaçãoComportamento
Chamar where() duas vezesSegunda chamada sobrescreve a primeira
Chave desconhecida em where()Silenciosamente ignorada
Chave desconhecida em select()Passada como raw SQL (permite expressões)
Chave desconhecida em orderBy()Passada como raw SQL (permite expressões)
Entidade com @DeletedAtWHERE deleted_at IS NULL acrescentado automaticamente
getCount() com soft deleteCOUNT respeita o filtro de soft delete
Parâmetros em andWhere()Reposicionados automaticamente após os do where()