Query Builder und DQL
Query Builder und DQL
~16 Min. Lesezeit Zuletzt aktualisiert am 8. August 2026
findBy() aus Kapitel 21 reicht für einfache Abfragen – für komplexere Fälle (Kombinationen aus Filtern, Sortierung, Aggregation) brauchen wir Doctrines Query Builder und DQL (Doctrine Query Language).
DQL vs. SQL: der entscheidende Unterschied
DQL sieht SQL syntaktisch ähnlich, arbeitet aber mit ENTITY-Klassen und deren PROPERTIES statt mit Tabellen und Spalten – Doctrine übersetzt DQL automatisch in die tatsächliche SQL-Dialekt Ihrer konfigurierten Datenbank (PostgreSQL, MySQL, ...), OHNE dass Sie sich um Dialekt-Unterschiede kümmern müssen.
Den Query Builder im Repository nutzen
Eigene, wiederverwendbare Abfrage-Methoden gehören INS Repository, NICHT in den Controller – der Controller soll NICHTS über die konkrete Abfrage-Implementierung wissen müssen:
// ... Constructor wie in Kapitel 21 ...
/**
* @return Task[]
*/
public function findUeberfaelligeAufgaben(): array
{
return $this->createQueryBuilder('t')
->andWhere('t.faelligAm < :heute')
->andWhere('t.status != :erledigt')
->setParameter('heute', new \DateTimeImmutable())
->setParameter('erledigt', 'erledigt')
->orderBy('t.faelligAm', 'ASC')
->getQuery()
->getResult()
;
}createQueryBuilder('t') erzeugt einen Query Builder mit t als Alias für Task – GENAU wie ein SQL-Tabellen-Alias. setParameter() bindet Werte SICHER (schützt automatisch vor SQL-Injection) statt sie direkt in den Query-String einzusetzen.
Achtung: NIEMALS Nutzereingaben direkt in einen DQL/Query-Builder-String einbauen (z. B. per String-Konkatenation) – IMMER setParameter() verwenden. Genau wie vorbereitete SQL-Statements schützt das zuverlässig vor Injection-Angriffen.
JOINs über Beziehungen
/**
* @return Task[]
*/
public function findAufgabenFuerNutzer(User $user): array
{
return $this->createQueryBuilder('t')
->innerJoin('t.project', 'p')
->innerJoin('p.mitglieder', 'm')
->andWhere('m = :nutzer')
->setParameter('nutzer', $user)
->getQuery()
->getResult()
;
}innerJoin('t.project', 'p') folgt der ManyToOne-Beziehung aus Kapitel 22 (KEIN manuelles ON-Statement nötig, Doctrine kennt die Verbindung bereits aus dem Entity-Mapping), innerJoin('p.mitglieder', 'm') folgt der ManyToMany-Beziehung aus Kapitel 23 – dieselbe Methode funktioniert für BEIDE Beziehungstypen identisch.
DQL direkt als String
Für manche Fälle ist rohes DQL lesbarer als der Query Builder – funktional GLEICHWERTIG, reine Geschmackssache/Team-Konvention:
$query = $this->getEntityManager()->createQuery(
'SELECT t FROM App\Entity\Task t WHERE t.status = :status ORDER BY t.faelligAm ASC'
);
$query->setParameter('status', 'offen');
$aufgaben = $query->getResult();Aggregation: COUNT, AVG und Co.
public function zaehleAufgabenNachStatus(Project $project): array
{
return $this->createQueryBuilder('t')
->select('t.status, COUNT(t.id) as anzahl')
->andWhere('t.project = :project')
->setParameter('project', $project)
->groupBy('t.status')
->getQuery()
->getResult()
;
}Ideal für Dashboard-Statistiken (Kapitel 45 nutzt genau diese Art von Abfrage, gecacht) – liefert ein Array mit status/anzahl-Paaren statt vollständiger Entity-Objekte.
getResult() vs. getOneOrNullResult() vs. getSingleResult()
| Methode | Verhalten |
|---|---|
| getResult() | Liefert ein Array – auch bei 0 oder 1 Treffern, NIEMALS einen Fehler. |
| getOneOrNullResult() | Erwartet 0 oder 1 Treffer – liefert das Objekt oder null; wirft bei MEHR als 1 Treffer eine Exception. |
| getSingleResult() | Erwartet GENAU 1 Treffer – wirft bei 0 ODER mehr als 1 Treffer eine Exception. |
Tipp: find() aus Kapitel 21 nutzt INTERN getOneOrNullResult()-artiges Verhalten (liefert null statt zu werfen) – für eigene Query-Builder-Methoden, die GENAU EIN Ergebnis erwarten (z. B. "das aktuellste Projekt"), ist getOneOrNullResult() meist die sicherere Wahl gegenüber getSingleResult().