Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2.16.0 - Issue with persisting entities #10869

Closed
dmaicher opened this issue Aug 3, 2023 · 39 comments · Fixed by #10915
Closed

2.16.0 - Issue with persisting entities #10869

dmaicher opened this issue Aug 3, 2023 · 39 comments · Fixed by #10915
Labels

Comments

@dmaicher
Copy link
Contributor

dmaicher commented Aug 3, 2023

BC Break Report

Q A
BC Break yes
Version 2.16.0

Summary

I have a case in our test suite where entities are not persisted properly anymore. This worked fine on 2.15.

The insert statement does not bind any parameters and fails:

PDOException: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' at line 1

/var/www/app/symfony/vendor/doctrine/dbal/src/Driver/PDO/Statement.php:121
/var/www/app/symfony/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:69
/var/www/app/symfony/vendor/doctrine/dbal/src/Logging/Statement.php:98
/var/www/app/symfony/vendor/doctrine/dbal/src/Driver/Middleware/AbstractStatementMiddleware.php:69
/var/www/app/symfony/vendor/symfony/doctrine-bridge/Middleware/Debug/Statement.php:68
/var/www/app/symfony/vendor/doctrine/dbal/src/Statement.php:190
/var/www/app/symfony/vendor/doctrine/dbal/src/Statement.php:249
/var/www/app/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php:280
/var/www/app/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1177
/var/www/app/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:442
/var/www/app/symfony/vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:403

The logs (see parameters: []):

10:11:57 DEBUG     [doctrine] Executing statement: INSERT INTO widget_settings (hash, enabled, language, orientation, position, widget_content, tooltip_content, header_color, detail_urls, left_title, use_left_title, right_title, use_right_title, logo, use_logo, is_primary, booking_btn_color, widget_name, booking_urls, review_config_id, account_id, parent_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) (parameters: [], types: [])

How to reproduce

I cannot provide a small reproducer yet. I had a look and somehow inside the BasicEntityPersister the call to prepareInsertData($entity) with that entity returns an empty array.

The setup part of the test code that triggers the error looks like this:

        $account = $this->fixtures->createAccount();
        $this->em->persist($account);
        $this->em->persist($this->fixtures->createWidgetSettings($account, false));
        $this->em->persist($this->fixtures->createWidgetSettings($account, false));

        $this->em->flush();

The issue seems to come from the second $this->em->persist($this->fixtures->createWidgetSettings($account, false)); line 🤔

@mpdude does this information help at all to pinpoint the issue? I can try to provide a small reproducer

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

I did some more digging and this seems to be caused by some event listener that does stuff on postPersist and then calls $em->flush() itself.

@derrabus
Copy link
Member

derrabus commented Aug 3, 2023

Can you create a reproducer for this particular issue?

@derrabus derrabus added the Bug label Aug 3, 2023
@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

Similar report here: #10547 (comment)

Not sure if it is allowed/supported to call flush() from a postPersist listener?

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

Not sure if it is allowed/supported to call flush() from a postPersist listener?

yeah thats the question 😄 Nothing is mentioned in the docs

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

Well actually it says

Changes in here are not relevant to the persistence in the database, but you can use these events to alter non-persistable items, like non-mapped fields, logging or even associated classes that are not directly mapped by Doctrine.

@derrabus
Copy link
Member

derrabus commented Aug 3, 2023

Since postPersist is triggered during a flush operation, it's at least a very bad idea to call flush() from there. 🙈

@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

b42cf99 migth be related, it makes a change to UnitOfWork::executeInserts():

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

I will rewrite this listener logic on our end. So feel free to close this issue. I think its very tricky to support all those weird edge cases 😛

@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

Should we prevent flush() reentrance with an exception? Seems its behaviour has been changed during the course of #10547, but I am not sure if it was ever supposed to work/be supported in the first place. (@beberlei WDYT?)

I am really afraid what might happen when the UoW has computed its lists of insertions, field updates etc. and then, during the course of the flush, some event listener calls flush() again, causing the same structures to be updated. Fairly sure this never had defined semantics and/or test coverage.

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

So I had a look at rewriting the listener logic and its not easy 😢 I would need to hook into the flush process and persist + flush some additional entities but only after all previous flushes are done and entities have IDs.

Currently I don't see any way of achieving this then if I cannot flush with any of the events. Even postFlush seems to not really allow it as there is some cleanup stuff happening afterwards.

@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

Assuming the postFlush could be deferred to after the final cleanups... would it work then?

@dmaicher
Copy link
Contributor Author

dmaicher commented Aug 3, 2023

Assuming the postFlush could be deferred to after the final cleanups... would it work then?

Indeed I just tested it: that would work 😊

@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

@beberlei You documented in b6c3fc5 that flush() cannot be safely called from postFlush listeners.

Do you remember any particular reason for that apart from potential recursion issues?

@tasselchof
Copy link

@mpdude I will try to check it, but as I see it from my mobile phone your reproducer is for single key reference, I am using composite keys. Product offer is partitioned by $shop and $id. This might be the difference we are looking for.

@mpdude
Copy link
Contributor

mpdude commented Aug 3, 2023

@tasselchof maybe you meant to comment on #10868?

@tasselchof
Copy link

@tasselchof maybe you meant to comment on #10868?

Yes, sorry once more, didn’t saw it on mobile.

@alexander-schranz
Copy link
Contributor

I tried to create a simple reproducer but currently failing with it, the thing I did not yet tried is when the entity is a ResolvedTargetListener related.

Atleast the flush on postPersist didn't have any issues here:

https://github.com/alexander-schranz/reproducer-doctrine-orm-issue-10869/blob/3ec32bb9c2a2b149a29027a302d071e45f54b6c4/src/EventListener/AccountNumberEventListener.php#L32

This test currently runs fine:

https://github.com/alexander-schranz/reproducer-doctrine-orm-issue-10869/blob/3ec32bb9c2a2b149a29027a302d071e45f54b6c4/tests/Repository/AccountRepositoryTest.php#L28

So maybe really more related to ResolvedTargetListener but would need to test that, if somebody got time before me try to reproduce it on a simpler installation maybe can try it to create on top.

@jmperruchini
Copy link

jmperruchini commented Aug 6, 2023

hello everyone, I have the same problem since the update of symfony 6.3.3..It worked very well before, I used postPersist to retrieve the generated ID and trace by creating a log with a flush ..

public function postPersist(PostPersistEventArgs $args): void
{
if($args->getObject() instanceof Log){
return;
}
$log = new Log();
$log->setActionLog('create');
$log->setModuleLog('personnel');
$log->setEntiteIdLog($args->getObject()->getId()));
$args->getObjectManager()->persist($log);
$args->getObjectManager()->flush();
}

with the following result (incomplete)=>

An exception occurred while executing a query: SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '?, ?, ?, ?, ?, ?, ?, ?, ?)' at line 1
Doctrine\DBAL\Exception\ SyntaxErrorException
Show exception properties

Doctrine\DBAL\Exception\SyntaxErrorException {#5000
-query: Doctrine\DBAL\Query {#4993
-sql: "INSERT INTO log (module_log, action_log, entite_type_log, entite_id_log, utilisateur_ip_log, libelle_log, created_at, utilisateur_id_log, patient_id_log) VALUES ▶"
-params: []
-types: []
}
}

the error only exists for entities with associations !!

I tried to change the logic using onflush and postflush, but I can't get the newly created entity iD.
I am blocked !!

mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 9, 2023
This PR prevents reentrant calls to `UnitOfWork::commit()` with a clear exception message.

Reentrance happens, for example, when users call `EntityManager::flush()` from within `postPersist` or similar event listeners. Since doctrine#10547, it causes strange-looking, non-helpful error messages (doctrine#10869).

Instead of treating this as a bug and trying to fix it, my suggestion is to fail with a clear exception message instead.

Reasons:

I assume the UoW was never designed to support reentrant `flush()` calls in the first place. So, trying to make (or keep) this working might be a rabbit hole.

The assumption is based on the following:

* Not a single test in the ORM test suite covers or even triggers `flush()` reentrance – otherwise, such a test would have failed with the changes suggested here.
* Documentation for e. g. [`preFlush`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/reference/events.html#preflush) or [`postFlush`](https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/reference/events.html#preflush) explicitly mentions that `flush()` must not be called again. I don't know why this is not also stated for e. g. `postPersist`. But why would it be safe to call `flush()` during `postPersist`, in the middle of a transaction, when there are reasons against calling it in `postFlush`, just before final cleanups are made?
* Last but not least, entity insertions, computed changesets etc. are kept in fields like `UnitOfWork::$entityChangeSets` or `UnitOfWork::$originalEntityData`. It's all to easy to mess up these states when we re-compute changesets mid-way through a `flush()` – and again, if that were anticipated, I'd assume to find any kind of test coverage.
@mpdude
Copy link
Contributor

mpdude commented Aug 10, 2023

@dmaicher this code

https://github.com/alexander-schranz/reproducer-doctrine-orm-issue-10869/blob/3ec32bb9c2a2b149a29027a302d071e45f54b6c4/src/EventListener/AccountNumberEventListener.php#L32

linked in this comment uses a postPersist listener to make changes to the entity that has just been persisted, and then tries to flush() again.

Is your use case the same? Or are you changing/adding other objects than the one the postPersist has been dispatched for?

@mpdude
Copy link
Contributor

mpdude commented Aug 10, 2023

@alexander-schranz I wanted to suggest you could try using onflush, since that can make changes to entity values just in time (https://www.doctrine-project.org/projects/doctrine-orm/en/2.16/reference/events.html#onflush).

But then I realized you need the database-provided ID to fill in the missing object value, so you have to INSERT first before the update can be made.

mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 10, 2023
@dbannik
Copy link
Contributor

dbannik commented Aug 16, 2023

workaround

version 2.16.1

cause flush as often as possible
Before
image
After
image

@dbannik
Copy link
Contributor

dbannik commented Aug 16, 2023

i see a double insert
though there should only be one
image

@dbannik
Copy link
Contributor

dbannik commented Aug 16, 2023

The problem arises when postPersist event is caused and in it there is a flash

@jkgroupe
Copy link

jkgroupe commented Aug 16, 2023

Hello, I have the same issue but only when I'm trying to persist a oneToMany collection and only with postPersist (I don't have this issue with postUpdate).

I don't have this issue with my others listeners which using flush on postPersist

My controller

$receipt = new Receipt();
$receipt->setTitle('Test');

$period = new Period();
$period->setStartAt(new \DateTime);
$period->setEndAt(new \DateTime);

$receipt->addPeriod($period);

$this->entityManager->persist($receipt);
$this->entityManager->flush();

My entity

/**
 * @ORM\Table(name="receipts")
 */
class Receipt implements ReceiptInterface
{
    /**
     * @var Collection<Period>
     * @ORM\OneToMany(targetEntity="App\Domain\Receipt\Period", mappedBy="receipt", cascade={"persist", "remove"}, fetch="EXTRA_LAZY", orphanRemoval=true)
     * @ORM\OrderBy({"orderNumber" = "ASC"})
     */
    protected Collection $periods;
}

My Listener

#[AsEntityListener(event: Events::postPersist, method: 'postPersist', entity: Receipt::class)]
final class ReceiptListener 
{
     public function postPersist(Receipt $receipt, LifecycleEventArgs $args): void
     {
          if ($report = $receipt->getReport()) {
              $report->updateAmounts();
          }

          $args->getObjectManager()->flush();
     }
}

Hope it helps

dbannik added a commit to dbannik/orm that referenced this issue Aug 16, 2023
mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 17, 2023
mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 17, 2023
This PR addresses the issue brought up in doctrine#10869. It happens when users use event listeners – `postPersist` in particular – to make changes to entities and/or persist new entities and finally call `EntityManager::flush()`, while the `UnitOfWork` is currently within the commit phase.

There is a discussion in doctrine#10900 whether this kind of reentrance should be deprecated in 2.x and prevented in 3.x. But, in order to prevent complete breakage with the 2.16.0 update, this PR tries to apply a band-aid 🩹.

A few things changed inside the UoW commit implementation in 2.16, and for sure this PR does not restore all details of the previous behavior. Take it with a grain of salt.

Here's the details.

The issue appears when `UoW::commit()` is performing entity insertions, and `postPersist` listener causes `commit()` reentrance when there are pending insertions left.

In that situation, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty.

The entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to the reported failure down the road. The mitigation is to check for this condition and skip such entities.

Before doctrine#10547 (pre-2.16), things worked a bit differently:

The UoW did not build a list of entities (objects) as the commit order, but a sequence of _classes_ to process.

For every entity _class_, it would find all entity instances in `UoW::$entityInsertions` and pass them to the persister in a batch. The persister, in turn, would execute the `INSERT` statements for _all_ those entities _before_ it dispatched the `postInsert` events.

That means when a `postInsert` listener was notified pre-2.16, the UoW was in a state where 1) all insertions for a particular class had been written to the database already and 2) the UoW had not yet gathered the next round of entities to process.
@BrandonGillis
Copy link

Hello, I have the same issue when upgrading to 2.16.x, as stated in the previous comment flush() shouldn't be used inside postPersist event, what would be the correct way to persist and flush another class entity that relies on the postPersist event to get the entity database-ID ?

@cbichis
Copy link

cbichis commented Aug 18, 2023

I confirm the same issue, on 2.16.x persisting of secondary entities doesnt seems to work. Which works on 2.15.x.

On 2.16.x no changes made on postPersist (other entities on which persist is called) doesnt seems to be flushed without the flush. And with the flush there is error regarding missing parameters.

Maybe this is related to?

doctrine/dbal#5895

@codedge
Copy link

codedge commented Aug 20, 2023

I run into this problem when using an ApiPlatform state processor.
By persisting and flushing two entities, it generates one more INSERT statement with empty parameters.

public function process($data, Operation $operation, array $uriVariables = [], array $context = []): Transaction
{
    $entityB = $this->entityBRepository->findOneBy(['refId' => $data->refId]);
    if ($entityB == null) {
        $entityB = (new EntityB())->setRefId($data->refId);
        $this->entityManager->persist($entityB);
    }

    $entity = new EntityA();
    $entity
        ->setEntityB($entityB)
        ->setReference($data->reference)
        ->setDate(new DateTimeImmutable($data->tDate))
    ;

    $this->entityManager->persist($entity);
    $this->entityManager->flush();

    return $entity;
}

@Seb33300
Copy link
Contributor

Same issue here.

In the symfony profiler I can see the INSERT query properly executed the first time with all ? replaced by parameters, and then, trying to execute the same query a second time without replacing ?.

@Seb33300
Copy link
Contributor

Reading previous comments, this may be related to listeners?
In our case we use https://github.com/doctrine-extensions/DoctrineExtensions on our entities (Timestampable)

mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 21, 2023
….0 (Take 2)

The changes from doctrine#10547, which landed in 2.16.0, cause problems for users that call `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and UoW::$entityInsertions being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as doctrine#10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in doctrine#10906 (comment).

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. doctrine#10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 21, 2023
….0 (Take 2)

The changes from doctrine#10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as doctrine#10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in doctrine#10906 (comment).

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. doctrine#10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
@mpdude
Copy link
Contributor

mpdude commented Aug 21, 2023

Everyone affected by this please try #10915 and report whether it helps

@yblatti
Copy link

yblatti commented Aug 22, 2023

Same problem for me.
In a symfony app, I use EntityListeners postPersist for multiple things : taking snapshots, updating ElasticSearch indices...
For all of this, I use symfony messenger, with doctrine transport : I dispatch a message on the bus in the postPersist(), now it fails every time.

Everyone affected by this please try #10915 and report whether it helps

I tried it and it works for me !

mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 22, 2023
….0 (Take 2)

The changes from doctrine#10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as doctrine#10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in doctrine#10906 (comment).

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. doctrine#10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 22, 2023
….0 (Take 2)

The changes from doctrine#10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as doctrine#10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in doctrine#10906 (comment).

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. doctrine#10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
mpdude added a commit to mpdude/doctrine2 that referenced this issue Aug 23, 2023
….0 (Take 2)

The changes from doctrine#10547, which landed in 2.16.0, cause problems for users calling `EntityManager::flush()` from within `postPersist` event listeners.

* When `UnitOfWork::commit()` is re-entered, the "inner" (reentrant) call will start working through all changesets. Eventually, it finishes with all insertions being performed and `UoW::$entityInsertions` being empty. After return, the entity insertion order, an array computed at the beginning of `UnitOfWork::executeInserts()`, still contains entities that now have been processed already. This leads to a strange-looking SQL error where the number of parameters used does not match the number of parameters bound. This has been reported as doctrine#10869.

* The fixes made to the commit order computation may lead to a different entity insertion order than previously. `postPersist` listener code may be affected by this when accessing generated IDs for other entities than the one the event has been dispatched for. This ID may not yet be available when the insertion order is different from the one that was used before 2.16. This has been mentioned in doctrine#10906 (comment).

This PR suggests to address both issues by dispatching the `postPersist` event only after _all_ new entities have their rows inserted into the database. Likewise, dispatch `postRemove` only after _all_ deletions have been executed.

This solves the first issue because the sequence of insertions or deletions has been processed completely _before_ we start calling event listeners. This way, potential changes made by listeners will no longer be relevant.

Regarding the second issue, I think deferring `postPersist` a bit until _all_ entities have been inserted does not violate any promises given, hence is not a BC break. In 2.15, this event was raised after all insertions _for a particular class_ had been processed - so, it was never an "immediate" event for every single entity. doctrine#10547 moved the event handling to directly after every single insertion. Now, this PR moves it back a bit to after _all_ insertions.
@Herz3h
Copy link

Herz3h commented Jan 16, 2024

I'm using gedmo tree and after persisting a tree, it is persisted in reverse. I had to revert to 2.15.5 for the bug to disappear. 2.17.2 didn't work for me.

@yblatti
Copy link

yblatti commented Jan 16, 2024

I'm using gedmo tree and after persisting a tree, it is persisted in reverse. I had to revert to 2.15.5 for the bug to disappear. 2.17.2 didn't work for me.

Have a look at #11086, it's planned for 2.17.3.

@Herz3h
Copy link

Herz3h commented Feb 26, 2024

Even upgrading to 2.18.1 doesn't fix the problem of reverse persist.

@mpdude
Copy link
Contributor

mpdude commented Feb 26, 2024

@Herz3h can you provide a reproduce case?

@faton-joe
Copy link

faton-joe commented Apr 23, 2024

Having this issue also with the latest stable version 2.19.4. The case is similar to this that might help to replicate.
Saving related entities.


       $category = new Category();
        $category->setName('Computer Peripherals');

        $product = new Product();
        $product->setName('Keyboard');
        $product->setPrice(19.99);
        $product->setDescription('Ergonomic and stylish!');

        // relates this product to the category
        $product->setCategory($category);

        $entityManager->persist($category);
        $entityManager->persist($product);
        $entityManager->flush();

Now in case that I do flush() before setting the relation $product->setCategory($category); this will go through, but I don't think this is the solution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.