<? phpukraine СПІВБЕСІДИ
Пошук по платформі
SYMFONY · MIDDLE

Що таке Unit of Work у Doctrine?

Doctrine відслідковує всі керовані обʼєкти й накопичує зміни в памʼяті; на flush() вона обчислює change set і виконує запити в правильному порядку.

Чому Doctrine не пише в базу після persist?
Чому flush у циклі це погано?
Що таке identity map і чим вона небезпечна при масовій обробці?
Doctrine Unit of Work flush

Unit of Work — це реєстр усього, що Doctrine завантажила або отримала через persist у межах одного EntityManager. Для кожної managed-сутності він зберігає копію початкових значень, а на flush() порівнює її з поточним станом, обчислює change set і генерує INSERT, UPDATE, DELETE у порядку, який враховує звʼязки між сутностями. Усе це виконується в одній транзакції.

Звідси два наслідки, які часто дивують. По-перше, persist не робить запиту, а update не існує: зміна властивості через сеттер достатня. По-друге, identity map гарантує один PHP-обʼєкт на один рядок бази, тому повторний find того самого id не йде в базу, а звʼязки завжди вказують на той самий екземпляр.

Ціна цієї моделі — памʼять і вартість flush. Кожен flush обходить усі managed-обʼєкти, тому виклик у циклі дає квадратичну складність і по транзакції на ітерацію. Identity map тримає обʼєкти до clear(), тому імпорт на мільйон рядків без очищення закінчується OOM. Стандартний рецепт — батчі: flush і clear через кожні кількасот записів, toIterable() замість getResult(), а для простих масових оновлень без доменної логіки — DQL або SQL UPDATE одним запитом.

// persist лише реєструє обʼєкт; SQL немає до flush()
$user = new User('[email protected]');
$em->persist($user);        // стан: managed, INSERT ще не виконано

$existing = $em->find(User::class, 42);
$existing->rename('Олена'); // change set порахується на flush, update() не потрібен

$em->flush();               // одна транзакція: INSERT + UPDATE у правильному порядку

// Погано: N обходів identity map і N транзакцій
foreach ($users as $u) {
    $u->activate();
    $em->flush();
}

// Добре для масової обробки: батчі з flush + clear
$batch = 500;
foreach ($query->toIterable() as $i => $u) {
    $u->activate();
    if (($i + 1) % $batch === 0) {
        $em->flush();
        $em->clear();       // identity map звільняється, памʼять не росте
    }
}
$em->flush();

// Для сотень тисяч рядків без логіки в сутностях краще один DQL UPDATE
$em->createQuery('UPDATE App\Entity\User u SET u.active = true WHERE u.invitedAt < :d')
    ->setParameter('d', $threshold)->execute();
Що persist не робить INSERT, а лише переводить обʼєкт у стан managed: реальні запити відбуваються на flush.
Що для managed-сутностей Doctrine зберігає копію оригінальних даних і на flush порівнює її з поточним станом, тому явний update не потрібен.
Що identity map гарантує один PHP-обʼєкт на один рядок у межах EntityManager, звідси й економія запитів, і ріст памʼяті.
Що flush упорядковує запити за залежностями сутностей і загортає їх в одну транзакцію.
Практику масової обробки: батчі з flush і clear через кожні N записів, або взагалі DQL/SQL для UPDATE великих обсягів.
Казати, що Unit of Work це транзакція БД або кеш запитів: це патерн відстеження змін обʼєктів у памʼяті.
Викликати flush після кожного persist у циклі: кожен flush проходить по всій identity map і відкриває транзакцію.
Забувати clear() при обробці сотень тисяч записів: identity map тримає всі обʼєкти й процес падає з OOM.
Викликати clear() і далі використовувати старі обʼєкти: вони стали detached, і зміни в них Doctrine не побачить.
Не знати, що після виключення в flush EntityManager закривається й потребує нового екземпляра.
ПОРАДА

Згадайте clear() під час обробки великих наборів — інакше identity map зʼїдає памʼять. І поясніть, чому один flush у кінці транзакції ефективніший за flush у циклі.

оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Doctrine відстежує керовані обʼєкти, рахує change set і виконує запити у правильному порядку на flush.