В чём разница между persist() и flush() в Doctrine?
persist() переводит новую сущность под наблюдение Doctrine (SQL не выполняется), а flush() применяет все накопленные изменения к базе данных одним батчем в транзакции; уже managed-объекты не требуют persist() — Doctrine трекает их изменения автоматически.
Разница между persist() и flush() в Doctrine
Doctrine ORM реализует паттерн Unit of Work: все изменения накапливаются в памяти и отправляются в базу данных одним SQL-батчем при вызове flush().
persist() — регистрация объекта
Метод EntityManager::persist($entity) переводит объект из состояния new (transient) в состояние managed. После этого Doctrine «следит» за изменениями объекта до момента flush. Никакого SQL не выполняется.
<?php
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
class UserService
{
public function __construct(
private readonly EntityManagerInterface $em,
) {}
public function createUser(string $email, string $name): User
{
$user = new User();
$user->setEmail($email);
$user->setName($name);
// SQL ещё не выполнен! Объект просто «под наблюдением»
$this->em->persist($user);
// Только здесь Doctrine генерирует INSERT INTO users ...
$this->em->flush();
// Теперь $user->getId() доступен
return $user;
}
}
flush() — запись в базу данных
flush() обходит все managed entities, вычисляет изменения (changeset) и выполняет необходимые INSERT/UPDATE/DELETE в одной транзакции. Для новых объектов — INSERT, для изменённых — UPDATE, для удалённых (remove()) — DELETE.
<?php
// flush() обрабатывает ВСЕ изменения, не только свежие persist
$user = $this->em->find(User::class, 1);
$user->setName('Alice'); // Изменение уже managed entity
$newUser = new User();
$newUser->setEmail('bob@example.com');
$this->em->persist($newUser);
// Один flush — один батч SQL:
// UPDATE users SET name = 'Alice' WHERE id = 1
// INSERT INTO users (email) VALUES ('bob@example.com')
$this->em->flush();
Жизненный цикл состояний объекта
new User() --> TRANSIENT (Doctrine не знает об объекте)
persist($user) --> MANAGED (под наблюдением UnitOfWork)
flush() --> MANAGED (и ID проставлен из БД)
remove($user) --> REMOVED (будет DELETE при flush)
flush() --> DETACHED (объект исчез из UnitOfWork)
detach($user) --> DETACHED (ручное отсоединение)
merge($user) --> MANAGED (повторное присоединение, устарело в Doctrine 3)
Когда persist() не нужен
Если объект уже в состоянии MANAGED (получен через find(), getRepository()->findOne() или из коллекции с cascade), изменения автоматически трекаются без вызова persist:
<?php
$product = $this->em->find(Product::class, 42); // MANAGED
$product->setPrice(99.99); // автоматически трекается
// persist() здесь лишний (но не вреден)
$this->em->flush(); // UPDATE products SET price = 99.99 WHERE id = 42
Cascade persist
Связанные сущности автоматически persist'ятся, если в маппинге указан cascade: ['persist']:
<?php
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Order
{
#[ORM\OneToMany(
targetEntity: OrderItem::class,
mappedBy: 'order',
cascade: ['persist', 'remove'],
orphanRemoval: true,
)]
private Collection $items;
public function addItem(OrderItem $item): void
{
$item->setOrder($this);
$this->items->add($item);
// persist($item) не нужен — cascade сделает это
}
}
// Использование:
$order = new Order();
$order->addItem(new OrderItem('Widget', 10));
$this->em->persist($order);
$this->em->flush(); // INSERT order + INSERT order_item
Подводные камни
- Вызов
flush()безpersist()для новой сущности не даёт ошибки — объект просто игнорируется Doctrine. flush()оборачивает все операции в транзакцию; если один INSERT упал с constraint violation, откатываются все изменения текущего flush.- Частый
flush()внутри цикла убивает производительность — лучше один flush после цикла или использовать batch-вставку через DBAL. - После
clear()(очистка UnitOfWork) все ранее managed объекты становятся DETACHED — изменения в них не будут сохранены при следующем flush. - cascade: ['persist'] на ManyToMany-связях может каскадно персистить сотни объектов неожиданно для разработчика.
- В тестах с
EntityManagerв памяти (без реальной БД) flush проходит без ошибок даже при нарушении уникальных индексов — проверяйте с реальной БД.
Common mistakes
- Сводить persist vs flush к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Symfony 7/8 строит request lifecycle вокруг HttpKernel, events, routing, controller resolver и response listeners.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет persist vs flush через конкретную точку lifecycle в Symfony.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.