DQL e QueryBuilder do Doctrine
1. Introdução à DQL (Doctrine Query Language)
A Doctrine Query Language (DQL) é uma linguagem de consulta orientada a objetos que abstrai o SQL tradicional para trabalhar diretamente com entidades e seus relacionamentos. Diferente do SQL nativo, que opera sobre tabelas e colunas, a DQL consulta objetos e suas propriedades, respeitando o mapeamento objeto-relacional (ORM) definido nas entidades.
Principais vantagens da DQL:
- Abstração de banco: o mesmo código funciona em MySQL, PostgreSQL, SQLite, etc.
- Reutilização de entidades: consultas retornam objetos totalmente hidratados e gerenciados pelo Doctrine
- Segurança: parâmetros nomeados previnem injeção SQL automaticamente
- Produtividade: navegação intuitiva por relacionamentos entre entidades
2. Sintaxe Básica da DQL
A estrutura fundamental de uma consulta DQL segue o padrão SELECT ... FROM ... WHERE ..., mas utilizando aliases para referenciar entidades e suas propriedades.
use Doctrine\ORM\EntityManagerInterface;
/** @var EntityManagerInterface $entityManager */
$query = $entityManager->createQuery(
'SELECT e FROM App\Entity\Usuario e WHERE e.ativo = :ativo'
);
$query->setParameter('ativo', true);
$usuarios = $query->getResult();
Parâmetros nomeados (:nome) e posicionais (?1) são suportados:
// Parâmetros nomeados
$query = $entityManager->createQuery(
'SELECT p FROM App\Entity\Produto p WHERE p.preco >= :precoMinimo AND p.categoria = :categoria'
);
$query->setParameter('precoMinimo', 100.00);
$query->setParameter('categoria', 'Eletrônicos');
// Parâmetros posicionais
$query = $entityManager->createQuery(
'SELECT p FROM App\Entity\Produto p WHERE p.preco >= ?1 AND p.categoria = ?2'
);
$query->setParameter(1, 100.00);
$query->setParameter(2, 'Eletrônicos');
3. Consultas com Relacionamentos e Joins
A DQL permite navegar por associações de forma intuitiva. Joins implícitos ocorrem quando acessamos propriedades de relacionamentos, enquanto joins explícitos são declarados com JOIN ou LEFT JOIN.
// Join implícito (acessando propriedade de relacionamento)
$query = $entityManager->createQuery(
'SELECT p FROM App\Entity\Pedido p WHERE p.usuario.nome = :nome'
);
// Join explícito com LEFT JOIN
$query = $entityManager->createQuery(
'SELECT u, p FROM App\Entity\Usuario u LEFT JOIN u.pedidos p WHERE u.ativo = :ativo'
);
// FETCH JOIN para eager loading
$query = $entityManager->createQuery(
'SELECT u, p FROM App\Entity\Usuario u JOIN u.pedidos p WHERE u.id = :id'
);
$usuario = $query->setParameter('id', 42)->getSingleResult();
// Agora $usuario->getPedidos() já está carregado, evitando N+1
4. Funções, Agregações e Condições Avançadas
A DQL oferece funções de agregação e condicionais similares ao SQL:
// Funções de agregação
$query = $entityManager->createQuery(
'SELECT c.nome, COUNT(p.id) as total_produtos, AVG(p.preco) as preco_medio
FROM App\Entity\Categoria c
JOIN c.produtos p
GROUP BY c.id
HAVING COUNT(p.id) > 5'
);
// Funções de string e data
$query = $entityManager->createQuery(
'SELECT u FROM App\Entity\Usuario u
WHERE LOWER(u.nome) LIKE :termo
AND YEAR(u.dataCadastro) = :ano'
);
$query->setParameter('termo', '%joão%');
$query->setParameter('ano', 2024);
// Expressões condicionais
$query = $entityManager->createQuery(
'SELECT p FROM App\Entity\Produto p
WHERE p.preco BETWEEN :min AND :max
AND p.categoria IN (:categorias)
AND p.descricao IS NOT NULL
AND (p.nome LIKE :busca OR p.descricao LIKE :busca)'
);
5. QueryBuilder: Construção Programática de Consultas
O QueryBuilder permite construir consultas dinamicamente através de métodos encadeáveis, ideal para filtros condicionais e consultas complexas.
use Doctrine\ORM\EntityManagerInterface;
/** @var EntityManagerInterface $entityManager */
$qb = $entityManager->createQueryBuilder();
$qb->select('p')
->from('App\Entity\Produto', 'p')
->innerJoin('p.categoria', 'c')
->where('p.ativo = :ativo')
->andWhere('p.preco >= :precoMinimo')
->orderBy('p.nome', 'ASC')
->setParameter('ativo', true)
->setParameter('precoMinimo', 50.00);
// Adicionando condições dinâmicas
if ($categoriaId) {
$qb->andWhere('c.id = :categoriaId')
->setParameter('categoriaId', $categoriaId);
}
if ($busca) {
$qb->andWhere($qb->expr()->orX(
$qb->expr()->like('p.nome', ':busca'),
$qb->expr()->like('p.descricao', ':busca')
))->setParameter('busca', '%' . $busca . '%');
}
$resultados = $qb->getQuery()->getResult();
6. Paginação e Limitação de Resultados
Para paginação eficiente, utilize setFirstResult() e setMaxResults():
$pagina = 2;
$itensPorPagina = 20;
$qb = $entityManager->createQueryBuilder();
$qb->select('u')
->from('App\Entity\Usuario', 'u')
->orderBy('u.nome', 'ASC')
->setFirstResult(($pagina - 1) * $itensPorPagina)
->setMaxResults($itensPorPagina);
$usuarios = $qb->getQuery()->getResult();
// Para consultas complexas, use o Paginator
use Doctrine\ORM\Tools\Pagination\Paginator;
$query = $entityManager->createQuery(
'SELECT u, p FROM App\Entity\Usuario u JOIN u.pedidos p'
)->setFirstResult(0)->setMaxResults(10);
$paginator = new Paginator($query);
$totalResultados = count($paginator);
$totalPaginas = ceil($totalResultados / $itensPorPagina);
7. Hidratação e Resultados Customizados
O Doctrine oferece diferentes modos de hidratação para controlar como os resultados são retornados:
// HYDRATE_OBJECT (padrão) - retorna objetos da entidade
$usuarios = $query->getResult(Query::HYDRATE_OBJECT);
// HYDRATE_ARRAY - retorna arrays aninhados
$dados = $query->getResult(Query::HYDRATE_ARRAY);
// HYDRATE_SCALAR - retorna arrays planos
$dados = $query->getResult(Query::HYDRATE_SCALAR);
// SELECT NEW para DTOs
$query = $entityManager->createQuery(
'SELECT NEW App\DTO\UsuarioResumo(
u.id,
u.nome,
u.email,
COUNT(p.id) as totalPedidos
)
FROM App\Entity\Usuario u
LEFT JOIN u.pedidos p
GROUP BY u.id'
);
$resumos = $query->getResult();
8. Boas Práticas e Performance
Evitando N+1 Queries
// RUIM: causa N+1 queries
$pedidos = $entityManager->getRepository(Pedido::class)->findAll();
foreach ($pedidos as $pedido) {
echo $pedido->getUsuario()->getNome(); // Cada acesso gera uma query
}
// BOM: FETCH JOIN carrega tudo em uma query
$query = $entityManager->createQuery(
'SELECT p, u FROM App\Entity\Pedido p JOIN p.usuario u'
);
Selecionando Apenas Colunas Necessárias
// Se precisar apenas de alguns campos, use SELECT parcial
$query = $entityManager->createQuery(
'SELECT p.id, p.nome, p.preco FROM App\Entity\Produto p WHERE p.ativo = :ativo'
);
Cache de Consultas
use Doctrine\ORM\Query;
$query = $entityManager->createQuery('SELECT u FROM App\Entity\Usuario u WHERE u.ativo = :ativo');
$query->setParameter('ativo', true);
$query->setCacheable(true);
$query->setCacheRegion('my_cache_region');
$query->setLifetime(3600); // 1 hora de cache
Utilizando Índices
Sempre adicione índices no banco de dados para colunas frequentemente usadas em cláusulas WHERE, JOIN e ORDER BY:
// Na entidade (via annotations)
/**
* @ORM\Entity
* @ORM\Table(name="produtos", indexes={
* @ORM\Index(name="idx_preco", columns={"preco"}),
* @ORM\Index(name="idx_categoria", columns={"categoria_id"})
* })
*/
class Produto { ... }
Referências
- Documentação Oficial da DQL do Doctrine — Guia completo sobre a sintaxe e funcionalidades da DQL
- QueryBuilder do Doctrine ORM — Documentação oficial com exemplos de construção programática de consultas
- Paginator do Doctrine ORM — Tutorial oficial sobre paginação eficiente com o Doctrine Paginator
- Doctrine ORM Performance Tips — Guia de boas práticas para performance com Doctrine, incluindo cache e otimização de queries
- Symfony Doctrine DQL and QueryBuilder Tutorial — Tutorial prático da documentação do Symfony sobre DQL e QueryBuilder com exemplos reais