Incorrect commit order when inserting multiple entities #6331

Closed
opened 2026-01-22 15:31:04 +01:00 by admin · 16 comments
Owner

Originally created by @AssortedParrot on GitHub (Oct 12, 2019).

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 (OneToOne User, 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 (OneToOne User, 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 (OneToOne UploadedFile, 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 the lastUploadedFile field set to NULL)
  • Insert the UploadedFile object (the owner field can now be set to the ID of the User we just inserted)
  • Update the User object (set lastUploadedFile to the ID of the UploadedFile 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 and lastDownloadedBy fields in the UploadedFile 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).

Originally created by @AssortedParrot on GitHub (Oct 12, 2019). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 2.6.4 #### Summary <!-- Provide a summary describing the problem you are experiencing. --> In some cases, Doctrine will insert entities into the database in the wrong order, causing constraint violations for non-nullable foreign keys. #### Current behavior <!-- What is the current (buggy) 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` (OneToOne `User`, *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` (OneToOne `User`, 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` (OneToOne `UploadedFile`, 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 <!-- Provide steps to reproduce the bug. If possible, also add a code snippet with relevant configuration, entity mappings, DQL etc. Adding a failing Unit or Functional Test would help us a lot - you can submit one in a Pull Request separately, referencing this bug report. --> The test case mentioned above can be found in [this repository](https://github.com/AssortedParrot/doctrine-incorrect-commit-order). 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 <!-- What was the expected (correct) behavior? --> I would expect the following behavior: - Insert the `User` object first (with the `lastUploadedFile` field set to `NULL`) - Insert the `UploadedFile` object (the `owner` field can now be set to the ID of the `User` we just inserted) - Update the `User` object (set `lastUploadedFile` to the ID of the `UploadedFile` 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` and `lastDownloadedBy` fields in the `UploadedFile` 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](https://github.com/doctrine/orm/blob/0c2ccec9151fd0b44a60f9cc149e2a107d380799/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php#L143), 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).
admin added the Bug label 2026-01-22 15:31:04 +01:00
admin closed this issue 2026-01-22 15:31:04 +01:00
Author
Owner

@SenseException commented on GitHub (Oct 15, 2019):

Thank you for reporting this issue and providing a reproducible code example. The nullable configuration in the mapping doesn't seem to influence the order and make the not null constraint happen. I'm not sure if this should be checked in CommitOrderCalculator though. Do you have a suggestion on how/where to introduce a fix for this?

@SenseException commented on GitHub (Oct 15, 2019): Thank you for reporting this issue and providing a reproducible code example. The nullable configuration in the mapping doesn't seem to influence the order and make the not null constraint happen. I'm not sure if this should be checked in `CommitOrderCalculator` though. Do you have a suggestion on how/where to introduce a fix for this?
Author
Owner

@AssortedParrot commented on GitHub (Oct 17, 2019):

The nullable configuration in the mapping doesn't seem to influence the order and make the not null constraint happen.

I'm not sure I understand. In my tests, changing the lastDownloadedBy field in UploadedFile to not nullable will indeed change the commit order, avoiding the problem.

Do you have a suggestion on how/where to introduce a fix for this?

Unfortunately, not really. I'm lacking a deeper understanding of the commit order calculation process. To be precise, I don't understand why UnitOfWork#getCommitOrder() adds nullable associations as dependencies in the CommitOrderCalculator. Are nullable associations relevant for the commit order?

If you try to INSERT an entity that contains a nullable reference to another, not-yet-inserted entity, the reference can just be set to NULL during the insertion, and then later updated once the referenced entity is inserted. (In fact, that's precisely what's happening with the lastUploadedFile field if this issue is worked around.)

So my first thought would be to only add non-nullable associations as dependencies in the CommitOrderCalculator, but this approach causes a considerable number of tests to fail, so I did not pursue it further.

And at this point I'm out of ideas. Would be nice if someone who knows more about the commit order calculation than me could chime in.

@AssortedParrot commented on GitHub (Oct 17, 2019): > The nullable configuration in the mapping doesn't seem to influence the order and make the not null constraint happen. I'm not sure I understand. In my tests, changing the `lastDownloadedBy` field in `UploadedFile` to *not* nullable will indeed change the commit order, avoiding the problem. > Do you have a suggestion on how/where to introduce a fix for this? Unfortunately, not really. I'm lacking a deeper understanding of the commit order calculation process. To be precise, I don't understand why `UnitOfWork#getCommitOrder()` adds nullable associations as dependencies in the `CommitOrderCalculator`. Are nullable associations relevant for the commit order? If you try to `INSERT` an entity that contains a nullable reference to another, not-yet-inserted entity, the reference can just be set to `NULL` during the insertion, and then later updated once the referenced entity is inserted. (In fact, that's precisely what's happening with the `lastUploadedFile` field if this issue is worked around.) So my first thought would be to only add non-nullable associations as dependencies in the `CommitOrderCalculator`, but this approach causes a considerable number of tests to fail, so I did not pursue it further. And at this point I'm out of ideas. Would be nice if someone who knows more about the commit order calculation than me could chime in.
Author
Owner

@Tetragramat commented on GitHub (May 5, 2020):

This bug exist at least 6 years and was reported multiple times #7006, #6499, #5538 and #4230

@Tetragramat commented on GitHub (May 5, 2020): This bug exist at least 6 years and was reported multiple times #7006, #6499, #5538 and #4230
Author
Owner

@kacperjurak commented on GitHub (Sep 5, 2020):

Today it was happened to me. Lost 3 hours. At the end just set column to nullable, but it's a workaround.

@kacperjurak commented on GitHub (Sep 5, 2020): Today it was happened to me. Lost 3 hours. At the end just set column to nullable, but it's a workaround.
Author
Owner

@kmsomebody commented on GitHub (Nov 14, 2020):

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.

@kmsomebody commented on GitHub (Nov 14, 2020): 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.
Author
Owner

@Cartman34 commented on GitHub (Jan 21, 2021):

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...

@Cartman34 commented on GitHub (Jan 21, 2021): 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...
Author
Owner

@rvanlaak commented on GitHub (May 20, 2021):

What about solely fixing this for the OneToOne relationships and cascade persist behavior? That could be a partial fix for the bigger problem, by having an impact that's as low as possible.

Or, checking for such configurations on the join column and throw an exception with a more clear message that setting the joincolumn in specific cases is not supported? These cases will already lead to exceptions in the first place, so giving a clear error message would nog be a BC break either. That saves users from spending hours on debugging.

@rvanlaak commented on GitHub (May 20, 2021): What about solely fixing this for the `OneToOne` relationships and cascade persist behavior? That could be a partial fix for the bigger problem, by having an impact that's as low as possible. Or, checking for such configurations on the join column and throw an exception with a more clear message that setting the joincolumn in specific cases is not supported? These cases will already lead to exceptions in the first place, so giving a clear error message would nog be a BC break either. That saves users from spending hours on debugging.
Author
Owner

@mesolaries commented on GitHub (Jun 22, 2021):

Still having this issue in a simple ManyToOne relationship. I'm using doctrine/orm: 2.9.3. Any reasons why it's still not fixed?

@mesolaries commented on GitHub (Jun 22, 2021): Still having this issue in a simple `ManyToOne` relationship. I'm using `doctrine/orm: 2.9.3`. Any reasons why it's still not fixed?
Author
Owner

@rvanlaak commented on GitHub (Jun 22, 2021):

@mesolaries could you provide some minimal code for your entities on which the error occurs, so a unit test can get created to verify this problem (and thereafter a possible fix)? I've made an attempt for OneToOne in #8703

@rvanlaak commented on GitHub (Jun 22, 2021): @mesolaries could you provide some minimal code for your entities on which the error occurs, so a unit test can get created to verify this problem (and thereafter a possible fix)? I've made an attempt for `OneToOne` in #8703
Author
Owner

@mesolaries commented on GitHub (Jun 22, 2021):

Here's a minimal example code where error occurs:

// ...

/**
 * @ORM\Entity(repositoryClass=ApplicationRepository::class)
 * @ORM\Table(name="application.application")
 */
class Application
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $domain;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $secret;

    /**
     * @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="application", orphanRemoval=true)
     */
    private $applicationPeople;

    public function __construct()
    {
        $this->applicationPeople = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getDomain(): ?string
    {
        return $this->domain;
    }

    public function setDomain(string $domain): self
    {
        $this->domain = $domain;

        return $this;
    }

    public function getSecret(): ?string
    {
        return $this->secret;
    }

    public function setSecret(string $secret): self
    {
        $this->secret = $secret;

        return $this;
    }

    /**
     * @return Collection|ApplicationPerson[]
     */
    public function getApplicationPeople(): Collection
    {
        return $this->applicationPeople;
    }

    public function addApplicationPerson(ApplicationPerson $applicationPerson): self
    {
        if (!$this->applicationPeople->contains($applicationPerson)) {
            $this->applicationPeople[] = $applicationPerson;
            $applicationPerson->setApplication($this);
        }

        return $this;
    }

    public function removeApplicationPerson(ApplicationPerson $applicationPerson): self
    {
        if ($this->applicationPeople->removeElement($applicationPerson)) {
            // set the owning side to null (unless already changed)
            if ($applicationPerson->getApplication() === $this) {
                $applicationPerson->setApplication(null);
            }
        }

        return $this;
    }
}

// ...

/**
 * @ORM\Entity(repositoryClass=PersonRepository::class)
 * @ORM\Table(name="auth.person")
 */
class Person implements UserInterface
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=180)
     */
    private $email;

    /**
     * @ORM\Column(type="string", length=180)
     */
    private $username;

    /**
     * @ORM\Column(type="json")
     */
    private $roles = [];

    /**
     * @var string The hashed password
     * @ORM\Column(type="string", nullable=true)
     */
    private $password;

    /**
     * @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="person", orphanRemoval=true)
     */
    private $applicationPeople;

    public function __construct()
    {
        $this->applicationPeople = new ArrayCollection();
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    /**
     * A visual identifier that represents this user.
     *
     * @see UserInterface
     */
    public function getUsername(): string
    {
        return (string)$this->username;
    }

    public function setUsername(string $username): self
    {
        $this->username = $username;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getRoles(): array
    {
        $roles = $this->roles;
        // guarantee every user at least has ROLE_USER
        $roles[] = 'ROLE_USER';

        return array_unique($roles);
    }

    public function setRoles(array $roles): self
    {
        $this->roles = $roles;

        return $this;
    }

    /**
     * @see UserInterface
     */
    public function getPassword(): string
    {
        return (string)$this->password;
    }

    public function setPassword(?string $password): self
    {
        $this->password = $password;

        return $this;
    }

    /**
     * Returning a salt is only needed, if you are not using a modern
     * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml.
     *
     * @see UserInterface
     */
    public function getSalt(): ?string
    {
        return null;
    }

    /**
     * @see UserInterface
     */
    public function eraseCredentials()
    {
        // If you store any temporary, sensitive data on the user, clear it here
        // $this->plainPassword = null;
    }

    /**
     * @return Collection|ApplicationPerson[]
     */
    public function getApplicationPeople(): Collection
    {
        return $this->applicationPeople;
    }

    public function addApplicationPerson(ApplicationPerson $applicationPerson): self
    {
        if (!$this->applicationPeople->contains($applicationPerson)) {
            $this->applicationPeople[] = $applicationPerson;
            $applicationPerson->setPerson($this);
        }

        return $this;
    }

    public function removeApplicationPerson(ApplicationPerson $applicationPerson): self
    {
        if ($this->applicationPeople->removeElement($applicationPerson)) {
            // set the owning side to null (unless already changed)
            if ($applicationPerson->getPerson() === $this) {
                $applicationPerson->setPerson(null);
            }
        }

        return $this;
    }
}

// ...

/**
 * @ORM\Entity(repositoryClass=ApplicationPersonRepository::class)
 * @ORM\Table(name="application.application_person")
 */
class ApplicationPerson
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Application::class, inversedBy="applicationPeople")
     * @ORM\JoinColumn(nullable=false)
     */
    private $application;

    /**
     * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="applicationPeople")
     * @ORM\JoinColumn(nullable=false)
     */
    private $person;

    /**
     * @ORM\Column(type="string", length=32)
     */
    private $number;

    /**
     * @ORM\Column(type="string", length=64, nullable=true)
     */
    private $cidr;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getApplication(): ?Application
    {
        return $this->application;
    }

    public function setApplication(?Application $application): self
    {
        $this->application = $application;

        return $this;
    }

    public function getPerson(): ?Person
    {
        return $this->person;
    }

    public function setPerson(?Person $person): self
    {
        $this->person = $person;

        return $this;
    }

    public function getNumber(): ?string
    {
        return $this->number;
    }

    public function setNumber(string $number): self
    {
        $this->number = $number;

        return $this;
    }

    public function getCidr(): ?string
    {
        return $this->cidr;
    }

    public function setCidr(?string $cidr): self
    {
        $this->cidr = $cidr;

        return $this;
    }
}

Creating entities and flush:

// ...

// Creating Person entity
$person = new Person();

$person
    ->setEmail($email)
    ->setPassword($password)
    ->setUsername($username)
;

$this->em->persist($person);

// Creating Application entity
$application = new Application(); // Important: I'm creating a new entity

$application
    ->setDomain($domain)
    ->setSecret(StringGenerator::random(64))
;

$this->em->persist($application); // Important: I'm persisting a newly created entity

// Creating ApplicatonPerson entity 
$applicationPerson = new ApplicationPerson();

$applicationPerson
    ->setPerson($person)
    ->setApplication($application) // Important: I'm setting persisted entity here
    ->setNumber($number)
;

$this->em->persist($applicationPerson);

$this->em->flush();

// ...

Getting error on flush operation:

An exception occurred while executing 'INSERT INTO application.application_person (id, number, cidr, application_id, person_id) VALUES (?, ?, ?, ?, ?)' with params [1, "1000", null, null, 1]:

SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "application_id" violates not-null constraint
DETAIL: Failing row contains (1, 1000, null, null, 1).

@rvanlaak

@mesolaries commented on GitHub (Jun 22, 2021): Here's a minimal example code where error occurs: ```php // ... /** * @ORM\Entity(repositoryClass=ApplicationRepository::class) * @ORM\Table(name="application.application") */ class Application { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=255) */ private $domain; /** * @ORM\Column(type="string", length=255) */ private $secret; /** * @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="application", orphanRemoval=true) */ private $applicationPeople; public function __construct() { $this->applicationPeople = new ArrayCollection(); } public function getId(): ?int { return $this->id; } public function getDomain(): ?string { return $this->domain; } public function setDomain(string $domain): self { $this->domain = $domain; return $this; } public function getSecret(): ?string { return $this->secret; } public function setSecret(string $secret): self { $this->secret = $secret; return $this; } /** * @return Collection|ApplicationPerson[] */ public function getApplicationPeople(): Collection { return $this->applicationPeople; } public function addApplicationPerson(ApplicationPerson $applicationPerson): self { if (!$this->applicationPeople->contains($applicationPerson)) { $this->applicationPeople[] = $applicationPerson; $applicationPerson->setApplication($this); } return $this; } public function removeApplicationPerson(ApplicationPerson $applicationPerson): self { if ($this->applicationPeople->removeElement($applicationPerson)) { // set the owning side to null (unless already changed) if ($applicationPerson->getApplication() === $this) { $applicationPerson->setApplication(null); } } return $this; } } ``` ```php // ... /** * @ORM\Entity(repositoryClass=PersonRepository::class) * @ORM\Table(name="auth.person") */ class Person implements UserInterface { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string", length=180) */ private $email; /** * @ORM\Column(type="string", length=180) */ private $username; /** * @ORM\Column(type="json") */ private $roles = []; /** * @var string The hashed password * @ORM\Column(type="string", nullable=true) */ private $password; /** * @ORM\OneToMany(targetEntity=ApplicationPerson::class, mappedBy="person", orphanRemoval=true) */ private $applicationPeople; public function __construct() { $this->applicationPeople = new ArrayCollection(); } public function getId(): ?int { return $this->id; } public function getEmail(): ?string { return $this->email; } public function setEmail(string $email): self { $this->email = $email; return $this; } /** * A visual identifier that represents this user. * * @see UserInterface */ public function getUsername(): string { return (string)$this->username; } public function setUsername(string $username): self { $this->username = $username; return $this; } /** * @see UserInterface */ public function getRoles(): array { $roles = $this->roles; // guarantee every user at least has ROLE_USER $roles[] = 'ROLE_USER'; return array_unique($roles); } public function setRoles(array $roles): self { $this->roles = $roles; return $this; } /** * @see UserInterface */ public function getPassword(): string { return (string)$this->password; } public function setPassword(?string $password): self { $this->password = $password; return $this; } /** * Returning a salt is only needed, if you are not using a modern * hashing algorithm (e.g. bcrypt or sodium) in your security.yaml. * * @see UserInterface */ public function getSalt(): ?string { return null; } /** * @see UserInterface */ public function eraseCredentials() { // If you store any temporary, sensitive data on the user, clear it here // $this->plainPassword = null; } /** * @return Collection|ApplicationPerson[] */ public function getApplicationPeople(): Collection { return $this->applicationPeople; } public function addApplicationPerson(ApplicationPerson $applicationPerson): self { if (!$this->applicationPeople->contains($applicationPerson)) { $this->applicationPeople[] = $applicationPerson; $applicationPerson->setPerson($this); } return $this; } public function removeApplicationPerson(ApplicationPerson $applicationPerson): self { if ($this->applicationPeople->removeElement($applicationPerson)) { // set the owning side to null (unless already changed) if ($applicationPerson->getPerson() === $this) { $applicationPerson->setPerson(null); } } return $this; } } ``` ```php // ... /** * @ORM\Entity(repositoryClass=ApplicationPersonRepository::class) * @ORM\Table(name="application.application_person") */ class ApplicationPerson { /** * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") */ private $id; /** * @ORM\ManyToOne(targetEntity=Application::class, inversedBy="applicationPeople") * @ORM\JoinColumn(nullable=false) */ private $application; /** * @ORM\ManyToOne(targetEntity=Person::class, inversedBy="applicationPeople") * @ORM\JoinColumn(nullable=false) */ private $person; /** * @ORM\Column(type="string", length=32) */ private $number; /** * @ORM\Column(type="string", length=64, nullable=true) */ private $cidr; public function getId(): ?int { return $this->id; } public function getApplication(): ?Application { return $this->application; } public function setApplication(?Application $application): self { $this->application = $application; return $this; } public function getPerson(): ?Person { return $this->person; } public function setPerson(?Person $person): self { $this->person = $person; return $this; } public function getNumber(): ?string { return $this->number; } public function setNumber(string $number): self { $this->number = $number; return $this; } public function getCidr(): ?string { return $this->cidr; } public function setCidr(?string $cidr): self { $this->cidr = $cidr; return $this; } } ``` Creating entities and flush: ```php // ... // Creating Person entity $person = new Person(); $person ->setEmail($email) ->setPassword($password) ->setUsername($username) ; $this->em->persist($person); // Creating Application entity $application = new Application(); // Important: I'm creating a new entity $application ->setDomain($domain) ->setSecret(StringGenerator::random(64)) ; $this->em->persist($application); // Important: I'm persisting a newly created entity // Creating ApplicatonPerson entity $applicationPerson = new ApplicationPerson(); $applicationPerson ->setPerson($person) ->setApplication($application) // Important: I'm setting persisted entity here ->setNumber($number) ; $this->em->persist($applicationPerson); $this->em->flush(); // ... ``` Getting error on flush operation: ``` An exception occurred while executing 'INSERT INTO application.application_person (id, number, cidr, application_id, person_id) VALUES (?, ?, ?, ?, ?)' with params [1, "1000", null, null, 1]: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "application_id" violates not-null constraint DETAIL: Failing row contains (1, 1000, null, null, 1). ``` @rvanlaak
Author
Owner

@rvanlaak commented on GitHub (Jun 30, 2021):

@mesolaries I've added the minimum of your code to PR #8703

Locally (when running on MySQL) these tests pass, so any idea why you got the exception?

@rvanlaak commented on GitHub (Jun 30, 2021): @mesolaries I've added the minimum of your code to PR #8703 Locally (when running on MySQL) these tests pass, so any idea why you got the exception?
Author
Owner

@mesolaries commented on GitHub (Jun 30, 2021):

@rvanlaak I use Postgres 12. If you add an additional flush() operation after persisting the Application entity (flushing 2 times in total), the errors disappear. In the example I use only one flush() operation at the end. I think for some reason doctrine trying to insert ApplicationPerson entity before Application entity.

Doctrine INSERT order that causes the exception:

  1. Insert Person entity
  2. Insert ApplicationPerson entity (Exception thrown -> null value in column application_id)
  3. Insert Application entity (Not reaching)

The correct INSERT order that should actually be:

  1. Insert Person entity
  2. Insert Application entity
  3. Insert ApplicationPerson entity with ids of Person and Application entities
@mesolaries commented on GitHub (Jun 30, 2021): @rvanlaak I use Postgres 12. If you add an additional `flush()` operation after persisting the `Application` entity (flushing 2 times in total), the errors disappear. In the example I use only one `flush()` operation at the end. I think for some reason doctrine trying to insert `ApplicationPerson` entity before `Application` entity. Doctrine *INSERT* order that causes the exception: 1. Insert `Person` entity 2. Insert `ApplicationPerson` entity (Exception thrown -> `null` value in column `application_id`) 3. Insert `Application` entity (Not reaching) The correct *INSERT* order that should actually be: 1. Insert `Person` entity 2. Insert `Application` entity 3. Insert `ApplicationPerson` entity with `id`s of `Person` and `Application` entities
Author
Owner

@rvanlaak commented on GitHub (Jun 30, 2021):

The test has one flush: https://github.com/doctrine/orm/pull/8703/files#diff-31dcd3f7ddcb30c3ac344bcef0059e3e921a9f258de7c46d30b9faeff49a0e4aR49-R58

Can hereby verify that locally the same test also passes when running on Postgres.

@rvanlaak commented on GitHub (Jun 30, 2021): The test has one flush: https://github.com/doctrine/orm/pull/8703/files#diff-31dcd3f7ddcb30c3ac344bcef0059e3e921a9f258de7c46d30b9faeff49a0e4aR49-R58 Can hereby verify that locally the same test also passes when running on Postgres.
Author
Owner

@mesolaries commented on GitHub (Jun 30, 2021):

@rvanlaak I don't have any special cases that causes the error. @Cartman34 have the same issue with ManyToOne relationship: https://github.com/doctrine/orm/issues/7866#issuecomment-764557132

@mesolaries commented on GitHub (Jun 30, 2021): @rvanlaak I don't have any special cases that causes the error. @Cartman34 have the same issue with ManyToOne relationship: https://github.com/doctrine/orm/issues/7866#issuecomment-764557132
Author
Owner

@rvanlaak commented on GitHub (Jul 21, 2021):

@Cartman34 does PR #8703 describe your scenario as well?

@rvanlaak commented on GitHub (Jul 21, 2021): @Cartman34 does PR #8703 describe your scenario as well?
Author
Owner

@mpdude commented on GitHub (Feb 27, 2023):

Please help: Give #10547 a try and let me know if it fixes this issue

@mpdude commented on GitHub (Feb 27, 2023): Please help: Give #10547 a try and let me know if it fixes this issue
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6331