orm: Incorrect commit order when inserting multiple entities
Bug Report
Q | A |
---|---|
BC Break | no |
Version | 2.6.4 |
Summary
In some cases, Doctrine will insert entities into the database in the wrong order, causing constraint violations for non-nullable foreign keys.
Current behavior
As a simple test case with two entities, consider an application where users can upload files: it would have a User
and an UploadedFile
entity.
Other than its ID, an UploadedFile
has two fields:
owner
(OneToOneUser
, not nullable): so we can keep track of which user uploaded the file. This is not nullable because all files need to belong to a valid user.lastDownloadedBy
(OneToOneUser
, nullable): because for some reason we want to keep track of who last downloaded a file. This may be null in case nobody downloaded the file yet.
A User
only has one field:
lastUploadedFile
(OneToOneUploadedFile
, nullable): stores the last file the user uploaded, or null if the user didn’t upload a file yet
From the above mapping it can be seen that the User
must be inserted before the UploadedFile
can be inserted: no UploadedFile
can exist without a User
.
In my test case, a User
and an UploadedFile
are created, and all fields are populated. Then, $em->persist()
is called on the UploadedFile
and then the User
. During the $em->flush()
, Doctrine will try to insert the UploadedFile
first, which will fail (because the owner
field is set to NULL
since the User
is not inserted yet,) causing an exception.
How to reproduce
The test case mentioned above can be found in this repository. To reproduce it, clone the repository, run composer install
and then run php test.php
. It will create a SQLite database in the current directory, create the schema and then try to insert a User
and an UploadedFile
object into the database.
The SQL logs show that Doctrine is trying to INSERT
the UploadedFile
object first, which fails. It will then roll back the transaction and throw an exception.
Expected behavior
I would expect the following behavior:
- Insert the
User
object first (with thelastUploadedFile
field set toNULL
) - Insert the
UploadedFile
object (theowner
field can now be set to the ID of theUser
we just inserted) - Update the
User
object (setlastUploadedFile
to the ID of theUploadedFile
we just inserted)
Additional information
It should be noted that I found many small things that, when changed, caused Doctrine to exhibit the expected behavior:
- Changing the order in which the
owner
andlastDownloadedBy
fields in theUploadedFile
entity are defined - Changing the order of the two
$em->persist()
- Changing the
lastDownloadedBy
field to not nullable
I believe this has something to do with the way the CommitOrderCalculator
traverses the entity graph - changing the order of the fields or of the persist calls might cause it to walk the edges in a different order, resulting in a different commit order. Changing the nullability of the fields affects the weight that is assigned to the edges, which also has an effect on the commit order. However, I don’t quite understand this part, so I was not able to investigate this further.
The code in the test case repository uses SQLite, but I could also reproduce this on MySQL (MariaDB 10.3.18).
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 5
- Comments: 16 (7 by maintainers)
Commits related to this issue
- more reliable CommitOrderCalculator - backtracks to last optional edge on loop - uses required flags instead of weights - tests from PR 8349 anf for Issue #7866 — committed to jhony1104/doctrine-orm by jhony1104 2 years ago
- more reliable CommitOrderCalculator - backtracks to last optional edge on loop - uses required flags instead of weights - tests from PR 8349 anf for Issue #7866 fixed some mistakes some line changes — committed to jhony1104/doctrine-orm by jhony1104 2 years ago
There are cases in which the only workaround is to call flush after each persist. Bulk-updating one-to-one relations will fail due to the unique constraint, if not committed in the correct order. This issue really needs a fix, very urgently. Calling flush multiple times is not an ideal solution.
This bug exist at least 6 years and was reported multiple times #7006, #6499, #5538 and #4230
@Cartman34 does PR #8703 describe your scenario as well?
I am having this issue in a simple case of ManyToOne relationship. Parent is required but child is inserted before with a null parent (but the parent is provided, I checked) If A > B > C, A is referenced by B, C by B and child is owning relation (it contains the parent’s id). Parent does not contain any information about children, the relation is not reversed. For my current case, the inserting order is C, A, B. It should be A, B, C. I persisted data in the right order, each entity is persisted manually but Doctrine is using it wrong…
Today it was happened to me. Lost 3 hours. At the end just set column to nullable, but it’s a workaround.