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