Symfony findAll() verknüpfte Child-Entities mitladen

Technik

Einer der schönsten Nebeneffekte von Symphonie ist, dass es SQL-Befehle unsichtbar macht. Doch sobald man etwas kompliziertere Anfragen hat muss man sich mit DQL herumschlagen. Hat man nicht verstanden ist dies eigentlich ziemlich einfach, jedoch entfällt mir der richtige Weg immer wieder gerne. Z.b. beim Abrufen von child entities/relations - also verknüpften Tabellen.

Stellt euch vor ihr habt die Entity Product und die Entity ProductsSizes als OneToMany Verknüpfung. Üblicherweise würdet ihr zuerst im Controller die Produkte abrufen und dann in einer Schleife alle Produktgrößen laden.

src/Controller/ProductController.php:

// get products
$products = $em->getRepository(Product::class)->findAll();

/* @var $product Product */
/* @var $size Size */
foreach ($products as $product) {
	foreach ($product->getSizes() as $size) {
		// do something
	}
}

Abhängig davon wie viele Produkte ihr jedoch habt würde dies genauso viele SQL Anfragen an den Server stellen. Das ist nicht besonders ressourcenschonend und kann Systeme auch schnell überlasten. Das kommt daher, dass Symfony eigentlich lazy ist. Mit lazy loading verhindert Symfony eigentlich unnötige Datenbank anfragen, da es die Abfragen erst dann durchführt, wenn die Daten auch tatsächlich benötigt werden. Wird z.B. ein einzelnes Produkt geladen, heißt das ja nicht unbedingt, dass auch die Produktgrößen geladen werden müssen.

Wenn wir allerdings mit der findAll() Methode alle Produkte laden möchten und dabei auch alle Produktgrößen benötigen, müssen wir einen besseren Weg finden, um die Datenbankabfragen zu reduzieren. Hier kommen Repositories ins Spiel. In der zur Produkt-Entity gehörenden Repository müsst ihr einfach einen neuen Befehl anlegen. Mit der Methode leftjoin() könnt ihr dann sofort alle Produktgrößen abrufen. Wichtig ist auch dass ihr die Produktgrößen mit einem select() Befehl im Querybuilder hinzufügt. Da im Hintergrund richtigerweise die OneToMany Verknüpfung hinterlegt ist, er kennt Symfony bzw. Doctrine aufgrund des leftjoin() dass dieser Join sozusagen sofort aufgelöst werden soll und die Produktgrößen gleich mitgeladen werden sollen. 

src/Repository/ProductRepository.php:

<?php

namespace App\Repository;

use App\Entity\Product;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class ProductRepository extends ServiceEntityRepository
{
    public function findAllWithSizes()
    {
        return $this->createQueryBuilder('p')
            ->select('p')
            ->addSelect('ps')
            ->leftJoin('p.productsize', 'ps')
            ->getQuery()
            ->getResult()
        ;
    }
}

src/Controller/ProductController.php:

// get products
$products = $em->getRepository(Product::class)->findAllWithSizes();

/* @var $product Product */
/* @var $size Size */
foreach ($products as $product) {
	foreach ($product->getSizes() as $size) {
		// do something
	}
}

Das Ergebnis ist eine einzige Datenbankabfrage, die definitiv Zeit und Ressourcen schonender ist als die erste Variante. Die Methode getSizes() führt jetzt nicht mehr zu einer Datenbankabfrage, vielmehr sind die Produktgrößen bereits in $product hinterlegt.

Permalink: https://to.ptmr.io/3bsuC6j