Rodzaje relacji w Doctrine
// ManyToOne — wiele artykułów należy do jednego użytkownika
#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\JoinColumn(nullable: false)]
private ?User $author = null;
// OneToMany — jeden artykuł ma wiele komentarzy
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'article', cascade: ['remove'])]
private Collection $comments;
// ManyToMany — artykuły i tagi
#[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'articles')]
#[ORM\JoinTable(name: 'article_tags')]
private Collection $tags;
Problem N+1
To najczęstszy błąd wydajnościowy w aplikacjach z ORM:
// ŹLE — 1 + N zapytań (1 dla listy + po jednym dla każdego autora)
$articles = $repo->findAll();
foreach ($articles as $article) {
echo $article->getAuthor()->getName(); // ← osobne SQL!
}
Przy 100 artykułach = 101 zapytań do bazy.
Rozwiązanie: JOIN FETCH
// DOBRZE — 1 zapytanie z JOIN
public function findWithAuthors(): array
{
return $this->createQueryBuilder('a')
->leftJoin('a.author', 'u')
->addSelect('u') // ← to jest kluczowe!
->leftJoin('a.tags', 't')
->addSelect('t')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.publishedAt', 'DESC')
->getQuery()
->getResult();
}
Paginacja z Pagerfanta
Nie ładuj wszystkiego — ogranicz wyniki:
use Pagerfanta\Doctrine\ORM\QueryAdapter;
use Pagerfanta\Pagerfanta;
public function findPaginated(int $page, int $perPage = 10): Pagerfanta
{
$qb = $this->createQueryBuilder('a')
->where('a.status = :status')
->setParameter('status', 'published')
->orderBy('a.publishedAt', 'DESC');
$pagerfanta = new Pagerfanta(new QueryAdapter($qb));
$pagerfanta->setMaxPerPage($perPage);
$pagerfanta->setCurrentPage($page);
return $pagerfanta;
}
Batch processing — import dużych zbiorów
public function importArticles(array $data): void
{
foreach ($data as $i => $row) {
$article = new BlogArticle();
$article->setTitle($row['title']);
// ...
$this->em->persist($article);
// co 100 rekordów — flush i czyszczenie Unit of Work
if ($i % 100 === 0) {
$this->em->flush();
$this->em->clear(); // ← bez tego pamięć eksploduje
}
}
$this->em->flush(); // reszta
}
Diagnostyka — SQL Logger
W środowisku deweloperskim włącz logowanie zapytań do profiler Symfony. Szukaj:
- zapytań > 100ms
- powtarzających się zapytań (znak N+1)
SELECT *zamiast konkretnych kolumn
# ile zapytań wykonało się dla danego request?
# Otwórz Symfony Profiler → zakładka "Doctrine"
Ściągawka — fetch mode
| Tryb | Kiedy | Zachowanie |
|---|---|---|
LAZY (domyślny) |
relacje rzadko potrzebne | ładuje przy dostępie |
EAGER |
relacje zawsze potrzebne | ładuje razem z encją |
EXTRA_LAZY |
duże kolekcje | count() bez ładowania |
JOIN FETCH |
ręczne w DQL | najlepsza kontrola |