Redundant UPDATE queries from the second flush() in a row #6504

Closed
opened 2026-01-22 15:34:12 +01:00 by admin · 4 comments
Owner

Originally created by @develancer on GitHub (Jul 22, 2020).

Bug Report

Simple bi-directional OneToMany relation with auto-increment primary key makes UnitOfWork detect non-existing changes.
When performing two flush() calls in a row, subsequent flush results in redundant UPDATE queries.

Q A
BC Break not sure
Version 2.10.2

Summary

Let's say we have a OneToMany relation between TestCollection and TestItem entities. Additionally, TestCollection has a single-column auto-increment primary key (IDENTITY GeneratedValue). We also have cascade persist and remove operations for items.

In a simplest scenario of creating a new collection with a single item

$collection = new TestCollection();
$item = new TestItem($collection, 100);
$item->value = 'TEST';
$collection->items->set(100, $item);
$entity_manager->persist($collection);
$entity_manager->flush();

all seems fine, but if we add an additional flush() just after the first one, it results in the additional UPDATE like

UPDATE test_item SET collection_nid = 5 WHERE collection_nid = 5 AND item_nid = 100

Current behavior

Subsequent flush() operation results in a redundant UPDATE query. Internally, the UnitOfWork seems to detect a difference between $originalData['collection'] which consist of an int(5) (!) and $actualData['collection'] which is a proper entity.

How to reproduce

TestItem:

<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="test_item")
 */
class TestItem
{
	/**
	 * @ORM\Id
	 * @ORM\ManyToOne(targetEntity="TestCollection", inversedBy="items")
	 * @ORM\JoinColumn(name="collection_nid", referencedColumnName="collection_nid")
	 * @var TestCollection
	 */
	public $collection;

	/**
	 * @ORM\Id
	 * @ORM\Column(type="integer", options={"unsigned": true})
	 * @var int
	 */
	public $item_nid;

	/**
	 * @ORM\Column(type="string", nullable=false)
	 * @var string
	 */
	public $value = '';

	public function __construct(TestCollection $collection, int $item_nid)
	{
		$this->collection = $collection;
		$this->item_nid = $item_nid;
	}
}

TestCollection:

<?php

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Table(name="test_collection")
 * @ORM\Entity
 */
class TestCollection
{
	/**
	 * @var int|null
	 *
	 * @ORM\Column(name="collection_nid", type="integer", nullable=false, options={"unsigned": true})
	 * @ORM\Id
	 * @ORM\GeneratedValue(strategy="IDENTITY")
	 */
	public $collection_nid;

	/**
	 * @ORM\OneToMany(targetEntity="TestItem", indexBy="item_nid",
	 *     mappedBy="collection", cascade={"persist", "remove"}, orphanRemoval=true)
	 * @var Collection
	 */
	public $items;

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

Database structure with an additional trigger to detect redundant UPDATE:

CREATE TABLE test_collection (
  collection_nid INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY
);

CREATE TABLE test_item (
  collection_nid INT UNSIGNED NOT NULL,
  item_nid INT UNSIGNED NOT NULL,
  value TEXT NOT NULL,
  PRIMARY KEY (collection_nid, item_nid)
);

DELIMITER :
CREATE TRIGGER test_item_before_update BEFORE UPDATE ON test_item FOR EACH ROW BEGIN
IF NEW.value = OLD.value THEN
signal sqlstate '45000' set message_text = 'redundant update';
END IF;
END:
DELIMITER ;

Expected behavior

Subsequent flush() operation should perform no UPDATE queries.

Originally created by @develancer on GitHub (Jul 22, 2020). ### Bug Report Simple bi-directional OneToMany relation with auto-increment primary key makes UnitOfWork detect non-existing changes. When performing two `flush()` calls in a row, subsequent flush results in redundant UPDATE queries. | Q | A |------------ | ------ | BC Break | not sure | Version | 2.10.2 #### Summary Let's say we have a OneToMany relation between TestCollection and TestItem entities. Additionally, TestCollection has a single-column auto-increment primary key (IDENTITY GeneratedValue). We also have cascade persist and remove operations for items. In a simplest scenario of creating a new collection with a single item ```php $collection = new TestCollection(); $item = new TestItem($collection, 100); $item->value = 'TEST'; $collection->items->set(100, $item); $entity_manager->persist($collection); $entity_manager->flush(); ``` all seems fine, but if we add an additional `flush()` just after the first one, it results in the additional UPDATE like ```sql UPDATE test_item SET collection_nid = 5 WHERE collection_nid = 5 AND item_nid = 100 ``` #### Current behavior Subsequent `flush()` operation results in a redundant UPDATE query. Internally, the UnitOfWork seems to detect a difference between `$originalData['collection']` which consist of an int(5) **(!)** and `$actualData['collection']` which is a proper entity. #### How to reproduce TestItem: ```php <?php use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="test_item") */ class TestItem { /** * @ORM\Id * @ORM\ManyToOne(targetEntity="TestCollection", inversedBy="items") * @ORM\JoinColumn(name="collection_nid", referencedColumnName="collection_nid") * @var TestCollection */ public $collection; /** * @ORM\Id * @ORM\Column(type="integer", options={"unsigned": true}) * @var int */ public $item_nid; /** * @ORM\Column(type="string", nullable=false) * @var string */ public $value = ''; public function __construct(TestCollection $collection, int $item_nid) { $this->collection = $collection; $this->item_nid = $item_nid; } } ``` TestCollection: ```php <?php use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Table(name="test_collection") * @ORM\Entity */ class TestCollection { /** * @var int|null * * @ORM\Column(name="collection_nid", type="integer", nullable=false, options={"unsigned": true}) * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ public $collection_nid; /** * @ORM\OneToMany(targetEntity="TestItem", indexBy="item_nid", * mappedBy="collection", cascade={"persist", "remove"}, orphanRemoval=true) * @var Collection */ public $items; public function __construct() { $this->items = new ArrayCollection(); } } ``` Database structure with an additional trigger to detect redundant UPDATE: ```sql CREATE TABLE test_collection ( collection_nid INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY ); CREATE TABLE test_item ( collection_nid INT UNSIGNED NOT NULL, item_nid INT UNSIGNED NOT NULL, value TEXT NOT NULL, PRIMARY KEY (collection_nid, item_nid) ); DELIMITER : CREATE TRIGGER test_item_before_update BEFORE UPDATE ON test_item FOR EACH ROW BEGIN IF NEW.value = OLD.value THEN signal sqlstate '45000' set message_text = 'redundant update'; END IF; END: DELIMITER ; ``` #### Expected behavior Subsequent `flush()` operation should perform no UPDATE queries.
admin added the Bug label 2026-01-22 15:34:12 +01:00
admin closed this issue 2026-01-22 15:34:12 +01:00
Author
Owner

@Feolius commented on GitHub (Oct 17, 2021):

Having the same issue. However, I figured out that that problem also exists for OneToOne relationship, when foreign key is used as a primary key. Two test entities here:

<?php

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="nodes")
 */
class Node
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue
     */
    private int $nid;

    /**
     * @ORM\OneToOne(targetEntity="NodeField", mappedBy="node")
     */
    private NodeField $field;

    /**
     * @param Collection $field
     * @return Node
     */
    public function setField(NodeField $field): Node
    {
        $this->field = $field;

        return $this;
    }
}
<?php

use Doctrine\ORM\Mapping as ORM;


/**
 * NodeField
 * @ORM\Table(name="node_fields")
 * @ORM\Entity
 */
class NodeField
{
    /**
     * @ORM\Id @ORM\OneToOne(targetEntity="Node", inversedBy="fields")
     * @ORM\JoinColumn(name="nid", referencedColumnName="nid")
     */
    private Node $node;

    /**
     * @param Node $node
     * @return NodeField
     */
    public function setNode(Node $node): NodeField
    {
        $this->node = $node;

        return $this;
    }
}

The following codeblock

<?php
$node = new Node();
$nodeField = new NodeField();
$nodeField->setNode($node);

$entityManager->persist($nodeField);
$entityManager->persist($node);
$entityManager->flush();
// Second flush here.
$entityManager->flush();
$logger = $entityManager->getConfiguration()->getSQLLogger();

foreach ($logger->queries as $query) {
    if (is_array($query['params'])) {
        echo sprintf(
            '<b>query:</b> %s, <b>params:</b> [%s]</br>',
            $query['sql'],
            implode(', ', $query['params'])
        );
    } else {
        echo sprintf(
            '<b>query:</b> %s</br>',
            $query['sql']
        );
    }

}

provides this output

query: "START TRANSACTION"
query: INSERT INTO nodes (nid) VALUES (null), params: []
query: INSERT INTO node_fields (nid) VALUES (?), params: [2]
query: "COMMIT"
query: "START TRANSACTION"
query: UPDATE node_fields SET nid = ? WHERE nid = ?, params: [2, 2]
query: "COMMIT"

So, we have redundant UPDATE query here, which is happening after the second flush() call.

@Feolius commented on GitHub (Oct 17, 2021): Having the same issue. However, I figured out that that problem also exists for OneToOne relationship, when foreign key is used as a primary key. Two test entities here: ```php <?php use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity * @ORM\Table(name="nodes") */ class Node { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue */ private int $nid; /** * @ORM\OneToOne(targetEntity="NodeField", mappedBy="node") */ private NodeField $field; /** * @param Collection $field * @return Node */ public function setField(NodeField $field): Node { $this->field = $field; return $this; } } ``` ```php <?php use Doctrine\ORM\Mapping as ORM; /** * NodeField * @ORM\Table(name="node_fields") * @ORM\Entity */ class NodeField { /** * @ORM\Id @ORM\OneToOne(targetEntity="Node", inversedBy="fields") * @ORM\JoinColumn(name="nid", referencedColumnName="nid") */ private Node $node; /** * @param Node $node * @return NodeField */ public function setNode(Node $node): NodeField { $this->node = $node; return $this; } } ``` The following codeblock ```php <?php $node = new Node(); $nodeField = new NodeField(); $nodeField->setNode($node); $entityManager->persist($nodeField); $entityManager->persist($node); $entityManager->flush(); // Second flush here. $entityManager->flush(); $logger = $entityManager->getConfiguration()->getSQLLogger(); foreach ($logger->queries as $query) { if (is_array($query['params'])) { echo sprintf( '<b>query:</b> %s, <b>params:</b> [%s]</br>', $query['sql'], implode(', ', $query['params']) ); } else { echo sprintf( '<b>query:</b> %s</br>', $query['sql'] ); } } ``` provides this output ``` query: "START TRANSACTION" query: INSERT INTO nodes (nid) VALUES (null), params: [] query: INSERT INTO node_fields (nid) VALUES (?), params: [2] query: "COMMIT" query: "START TRANSACTION" query: UPDATE node_fields SET nid = ? WHERE nid = ?, params: [2, 2] query: "COMMIT" ``` So, we have redundant UPDATE query here, which is happening after the second flush() call.
Author
Owner

@develancer commented on GitHub (Nov 26, 2021):

This bug is still present in 2.10.2, so I updated the specs.

@develancer commented on GitHub (Nov 26, 2021): This bug is still present in 2.10.2, so I updated the specs.
Author
Owner

@derrabus commented on GitHub (Dec 21, 2021):

@develancer Can you test #9244 please?

@derrabus commented on GitHub (Dec 21, 2021): @develancer Can you test #9244 please?
Author
Owner

@develancer commented on GitHub (Dec 28, 2021):

@derrabus The patch does help. Thanks @Feolius

@develancer commented on GitHub (Dec 28, 2021): @derrabus The patch does help. Thanks @Feolius
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6504