Doctrine
02 / 02

Doctrine: Associations, N+1 & Migrations

Doctrine: Associations, N+1 & Migrations

Associations & Cascading

class Post
{
    #[ORM\ManyToOne(targetEntity: User::class)]
    private User $author;
}

class User
{
    // cascade: ['remove'] -- deleting a User also deletes their Posts
    // automatically, without manually iterating and deleting each one
    #[ORM\OneToMany(mappedBy: 'author', targetEntity: Post::class, cascade: ['remove'])]
    private Collection $posts;
}

Lazy Loading & the N+1 Problem

// N+1: fetches N posts with one query, then triggers a SEPARATE
// query for EACH post's author when getAuthor() is first accessed
foreach ($posts as $post) {
    echo $post->getAuthor()->getName();
}

// FIXED: eager-load author with a JOIN -- one single query total
$query = $entityManager->createQuery(
    'SELECT p, a FROM App\Entity\Post p JOIN p.author a'
);
$posts = $query->getResult();

QueryBuilder for Conditional Queries

$qb = $repository->createQueryBuilder('u');

// Conditionally add a WHERE clause -- more readable than manually
// concatenating raw DQL strings
if ($emailFilter) {
    $qb->andWhere('u.email = :email')->setParameter('email', $emailFilter);
}

$users = $qb->getQuery()->getResult();

Migrations

# Generates a migration reflecting the diff between entity
# definitions and the current database schema
php bin/console doctrine:migrations:diff

# Applies pending migrations in order -- consistent across
# dev/staging/production rather than ad-hoc manual SQL changes
php bin/console doctrine:migrations:migrate

Doctrine vs. Eloquent

Doctrine follows the Data Mapper pattern -- entities are plain objects with no built-in save/query methods; a separate EntityManager handles persistence. Laravel's Eloquent follows Active Record instead ($user->save()), where the model itself knows how to persist. Both are legitimate ORM philosophies with different coupling tradeoffs.

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free