Wrong commit order in some relation cases #5857

Open
opened 2026-01-22 15:20:10 +01:00 by admin · 0 comments
Owner

Originally created by @contentrail on GitHub (Jan 24, 2018).

Issue was found after updating to v.2.6 and associated with improvements in CommitOrderCalculator class.
Here is the example to reproduce.
Objects:

  • Book
  • PCT with mandatory relation to Book (ManyToOne).
    Book has optional relation to the PCT object. Some Bookes has PCT and some not. But all PCT has relation to Book.
  • PCTFee as a child object of PCT (ManyToOne).

db_scheme

/**
 * @ORM\Entity
 * @ORM\Table(name="book")
 */
class Book
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    private $exchangeCode;

    /**
     * @var PCT
     * @ORM\OneToOne(targetEntity="PCT", cascade={"persist", "remove"})
     * @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id")
     */
    private $paymentCardTransaction;

    public function __construct(int $id)
    {
        $this->id = $id;
    }

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

    public function getExchangeCode(): ?string
    {
        return $this->exchangeCode;
    }

    public function setExchangeCode(string $exchangeCode = null): void
    {
        $this->exchangeCode = $exchangeCode;
    }

    public function getPaymentCardTransaction(): ?PCT
    {
        return $this->paymentCardTransaction;
    }

    public function setPaymentCardTransaction(PCT $paymentCardTransaction = null): void
    {
        $this->paymentCardTransaction = $paymentCardTransaction;
    }
}
/**
 * @ORM\Entity
 * @ORM\Table(name="pct")
 */
class PCT
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="NONE")
     */
    private $id;

    /**
     * @var Book
     * @ORM\ManyToOne(targetEntity="Book")
     * @ORM\JoinColumn(name="bookingId", referencedColumnName="id", nullable=false)
     */
    private $book;

    /**
     * @var PCTFee[]
     * @ORM\OneToMany(targetEntity="PCTFee", mappedBy="pct", cascade={"persist", "remove"})
     * @ORM\OrderBy({"id" = "ASC"})
     */
    private $fees;

    public function __construct(int $id, Book $book)
    {
        $this->id = $id;
        $this->book = $book;
        $this->fees = new ArrayCollection();
    }

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

    /**
     * @return PCTFee[]|Collection
     */
    public function getFees(): Collection
    {
        return $this->fees;
    }

    public function addFee(PCTFee $fee)
    {
        $this->fees->add($fee);
    }

    public function removeFee(PCTFee $fee)
    {
        $this->fees->removeElement($fee);
    }

    public function getBook(): Book
    {
        return $this->book;
    }
}
/**
 * @ORM\Entity
 * @ORM\Table(name="pct_fee")
 */
class PCTFee
{
    /**
     * @var int
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var PCT
     * @ORM\ManyToOne(targetEntity="PCT", inversedBy="fees")
     * @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id", nullable=false)
     */
    private $pct;

    public function __construct(PCT $pct)
    {
        $this->pct = $pct;
        $pct->addFee($this);
    }

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

    public function getPCT(): PCT
    {
        return $this->pct;
    }
}

How to reproduce:
Find one Book (or create new).
Create new PCT object with even one PCTFee object for this Book object.

Try to flush.

        $booking = $this->em()->getRepository(Book::class)->find(1);
        if (!$booking) {
            $booking = new Book(1);
            $booking->setExchangeCode('1');
            $this->em()->persist($booking);
        }
        $id = (int) $booking->getExchangeCode();
        $id++;
        $booking->setExchangeCode((string) $id); // Change smth.

        $paymentCardTransaction = new PCT($id, $booking);

        $paymentCardTransactionFee = new PCTFee($paymentCardTransaction);

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

        $this->save();

Error:

An exception occurred while executing 'INSERT INTO pct_fee (paymentCardTransactionId) VALUES (?)' with params [null]:

SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'paymentCardTransactionId' cannot be null

I've try to figure out why and found that CommitOrderCalculator produce wrong commit order: PCTFee, Book, PCT.
It's wrong because PCTFee depends on PCT - PCT must be saved earlier.

New code in CommitOrderCalculator uses weights of relations. But weights is wrong.
We have 2 relations with different weights (nullable and not nullable):

  • PCT to Book with weight 0 (not nullable)
  • Book to PCT with weight 1 (nullable).

Before version 2.6 CommitOrderCalculator has checked both relations. But now it checks only relation with maximum weight!

I've removed code using weights from CommitOrderCalculator:

case self::IN_PROGRESS:
                    if (isset($adjacentVertex->dependencyList[$vertex->hash]) &&
                        $adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight) {
                        $adjacentVertex->state = self::VISITED;

                        $this->sortedNodeList[] = $adjacentVertex->value;
                    }
                    break;

and bug has disappeared.

Originally created by @contentrail on GitHub (Jan 24, 2018). Issue was found after updating to v.2.6 and associated with improvements in CommitOrderCalculator class. Here is the example to reproduce. Objects: - **Book** - **PCT** with mandatory relation to Book (ManyToOne). Book has optional relation to the PCT object. Some Bookes has PCT and some not. But all PCT has relation to Book. - **PCTFee** as a child object of PCT (ManyToOne). ![db_scheme](https://user-images.githubusercontent.com/18163201/35378065-1b7f00d8-01ba-11e8-818b-d8053ed3df34.PNG) ``` /** * @ORM\Entity * @ORM\Table(name="book") */ class Book { /** * @var int * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="NONE") */ private $id; /** * @var string * @ORM\Column(type="string", length=255, nullable=true) */ private $exchangeCode; /** * @var PCT * @ORM\OneToOne(targetEntity="PCT", cascade={"persist", "remove"}) * @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id") */ private $paymentCardTransaction; public function __construct(int $id) { $this->id = $id; } public function getId(): int { return $this->id; } public function getExchangeCode(): ?string { return $this->exchangeCode; } public function setExchangeCode(string $exchangeCode = null): void { $this->exchangeCode = $exchangeCode; } public function getPaymentCardTransaction(): ?PCT { return $this->paymentCardTransaction; } public function setPaymentCardTransaction(PCT $paymentCardTransaction = null): void { $this->paymentCardTransaction = $paymentCardTransaction; } } ``` ``` /** * @ORM\Entity * @ORM\Table(name="pct") */ class PCT { /** * @var int * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="NONE") */ private $id; /** * @var Book * @ORM\ManyToOne(targetEntity="Book") * @ORM\JoinColumn(name="bookingId", referencedColumnName="id", nullable=false) */ private $book; /** * @var PCTFee[] * @ORM\OneToMany(targetEntity="PCTFee", mappedBy="pct", cascade={"persist", "remove"}) * @ORM\OrderBy({"id" = "ASC"}) */ private $fees; public function __construct(int $id, Book $book) { $this->id = $id; $this->book = $book; $this->fees = new ArrayCollection(); } public function getId(): int { return $this->id; } /** * @return PCTFee[]|Collection */ public function getFees(): Collection { return $this->fees; } public function addFee(PCTFee $fee) { $this->fees->add($fee); } public function removeFee(PCTFee $fee) { $this->fees->removeElement($fee); } public function getBook(): Book { return $this->book; } } ``` ``` /** * @ORM\Entity * @ORM\Table(name="pct_fee") */ class PCTFee { /** * @var int * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @var PCT * @ORM\ManyToOne(targetEntity="PCT", inversedBy="fees") * @ORM\JoinColumn(name="paymentCardTransactionId", referencedColumnName="id", nullable=false) */ private $pct; public function __construct(PCT $pct) { $this->pct = $pct; $pct->addFee($this); } public function getId(): ?int { return $this->id; } public function getPCT(): PCT { return $this->pct; } } ``` How to reproduce: Find one Book (or create new). Create new PCT object with even one PCTFee object for this Book object. Try to flush. ``` $booking = $this->em()->getRepository(Book::class)->find(1); if (!$booking) { $booking = new Book(1); $booking->setExchangeCode('1'); $this->em()->persist($booking); } $id = (int) $booking->getExchangeCode(); $id++; $booking->setExchangeCode((string) $id); // Change smth. $paymentCardTransaction = new PCT($id, $booking); $paymentCardTransactionFee = new PCTFee($paymentCardTransaction); $this->em()->persist($paymentCardTransaction); $this->save(); ``` Error: > An exception occurred while executing 'INSERT INTO pct_fee (paymentCardTransactionId) VALUES (?)' with params [null]: > SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'paymentCardTransactionId' cannot be null I've try to figure out why and found that CommitOrderCalculator produce wrong commit order: PCTFee, Book, PCT. It's wrong because PCTFee depends on PCT - PCT must be saved earlier. New code in CommitOrderCalculator uses weights of relations. But weights is wrong. We have 2 relations with different weights (nullable and not nullable): - PCT to Book with weight 0 (not nullable) - Book to PCT with weight 1 (nullable). Before version 2.6 CommitOrderCalculator has checked both relations. But now it checks only relation with maximum weight! I've removed code using weights from CommitOrderCalculator: ``` case self::IN_PROGRESS: if (isset($adjacentVertex->dependencyList[$vertex->hash]) && $adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight) { $adjacentVertex->state = self::VISITED; $this->sortedNodeList[] = $adjacentVertex->value; } break; ``` and bug has disappeared.
admin added the Bug label 2026-01-22 15:20:10 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#5857