SymfonyMiddleTechnical

В чём разница между 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.

Sources

Related topics