Doctrine відслідковує всі керовані обʼєкти й накопичує зміни в памʼяті; на flush() вона обчислює change set і виконує запити в правильному порядку.
Як питають
Чому Doctrine не пише в базу після persist?
Чому flush у циклі це погано?
Що таке identity map і чим вона небезпечна при масовій обробці?
DoctrineUnit of Workflush
Пояснення
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 одним запитом.
КодPHP
// 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 у циклі.
Додаткові питанняЗ ВІДПОВІДЯМИ
Кожен flush обходить усі managed-обʼєкти, рахує change set, відкриває й комітить транзакцію. У циклі на 10 000 ітерацій це 10 000 обходів identity map, яка з кожною ітерацією росте, тобто квадратична складність, плюс 10 000 транзакцій замість однієї. Правильно: один flush наприкінці або батчами по 500–1000 з clear.
Коли обʼєкти після збереження більше не потрібні в памʼяті. Після flush кожного батчу clear відʼєднує всі сутності, і GC їх звільняє. Треба памʼятати, що clear detach-ить усе, зокрема сутності, отримані раніше й потрібні далі: їх доведеться перечитати або передавати в цикл лише id.
При завантаженні Unit of Work зберігає копію оригінальних значень полів. На flush для кожної managed-сутності поточні значення порівнюються з копією, і різниця стає change set для UPDATE. Тому зміна властивості через звичайний сеттер достатня, а обʼєкти, створені поза EntityManager, не відстежуються, поки їх не persist-нули.
new: обʼєкт створений, Doctrine про нього не знає. managed: у identity map, зміни відстежуються. detached: був managed, але після clear або detach звʼязок втрачено, зміни ігноруються, потрібен merge через перечитання. removed: помічений на видалення, DELETE піде на flush.