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

DDC-211: Exception is thrown after many calls to flush() #2796

Closed
doctrinebot opened this issue Dec 15, 2009 · 4 comments
Closed

DDC-211: Exception is thrown after many calls to flush() #2796

doctrinebot opened this issue Dec 15, 2009 · 4 comments
Labels
Milestone

Comments

@doctrinebot
Copy link

Jira issue originally created by user baumgartl:

Two classes having a many-to-many association:

namespace Entity;

/****
 * @Entity
 * @Table(name="user")
*/
class User
{
    /****
     * @Id
     * @Column(name="id", type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /****
     * @Column(name="name", type="string")
     */
    protected $name;

    /****
    * @ManyToMany(targetEntity="Group")
    *   @JoinTable(name="user_groups",
    *       joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
    *       inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")}
    *   )
    */
    protected $groups;

    public function **construct() {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function setName($name) { $this->name = $name; }

    public function getGroups() { return $this->groups; }
}
namespace Entity;

/****
 * @Entity
 * @Table(name="groups")
 */
class Group
{
    /****
     * @Id
     * @Column(name="id", type="integer")
     * @GeneratedValue(strategy="AUTO")
     */
    protected $id;

    /****
     * @Column(name="name", type="string")
     */
    protected $name;

    /****
    * @ManyToMany(targetEntity="User", mappedBy="groups")
    */
    protected $users;

    public function **construct() {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }

    public function setName($name) { $this->name = $name; }

    public function getUsers() { return $this->users; }
}

Fill the database with some data:

$em = EntityManager::create($connectionOptions, $config);

$user = new \Entity\User();
$user->setName('John Doe');

$em->persist($user);
$em->flush();

$groupNames = array('group 1', 'group 2', 'group 3', 'group 4');
foreach ($groupNames as $name) {

    $group = new \Entity\Group();
    $group->setName($name);
    $em->persist($group);
    $em->flush();

    if (!$user->getGroups()->contains($group)) {
        $user->getGroups()->add($group);
        $group->getUsers()->add($user);
        $em->flush();
    }
}

After the user was added to the third group, an exception is thrown:

PHP Fatal error:  Uncaught exception 'PDOException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '1-1' for key 'PRIMARY'' in /lib/Doctrine/DBAL/Connection.php:623
Stack trace:
#0 /lib/Doctrine/DBAL/Connection.php(623): PDOStatement->execute(Array)
#1 /lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php(121): Doctrine\DBAL\Connection->executeUpdate('INSERT INTO use...', Array)
#2 /lib/Doctrine/ORM/Persisters/AbstractCollectionPersister.php(101): Doctrine\ORM\Persisters\AbstractCollectionPersister->insertRows(Object(Doctrine\ORM\PersistentCollection))
#3 /lib/Doctrine/ORM/UnitOfWork.php(306): Doctrine\ORM\Persisters\AbstractCollectionPersister->update(Object(Doctrine\ORM\PersistentCollection))
#4 /lib/Doctrine/ORM/EntityManager.php(288): Doctrine\ORM\UnitOfWork->commit()
#5 [...] in /lib/Doctrine/DBAL/Connection.php on line 623

Using the EchoSqlLogger a strange SQL statement becomes visible (after the user was added to "group 3"):

INSERT INTO groups (name) VALUES (?)
array(1) {
  [1]=>
  string(7) "group 4"
}
DELETE FROM user*groups WHERE user_id = ? AND group*id = ?
array(2) {
  [0]=>
  int(1)
  [1]=>
  int(3)
}
INSERT INTO user*groups (user_id, group*id) VALUES (?, ?)
array(2) {
  [0]=>
  int(1)
  [1]=>
  int(1)
}

Where does the delete statement come from? There is none of that in the code.

Interestingly the example works fine if the user is just added to three groups. If flush() is just called once at the end of the script, everything is fine too.

Many calls to flush() are expected to slow down execution time and/or increase memory consumption, but should work properly.

@doctrinebot
Copy link
Author

Comment created by nicokaiser:

The delete statement seems to come from \Doctrine\ORM\PersistentCollection#getDeleteDiff(), which returns the entities from the association that should be deleted (in this case this happens iff you delete associations between M:N entities):

This is done by an *array_udiff($this->_snapshot, $this->_coll->toArray(), array($this, '_compareRecords'))*, where _compareRecords is nothing more than $a === $b.

I modified getDeleteDiff() so it displays what it does and pasted the output here: http://pastie.org/744175
(the DIFF sections are only the DeleteDiffs). After successfully adding 3 Categories (303, 304, 305) Doctrine suddenly decides that the association Group[134]/Category[305] should be deleted, and then Group[134]/Category303 is re-inserted, which produces an exception of course.

The full log with SQL is here http://pastie.org/744183 (here the Group has id 138 instead of 134).

@doctrinebot
Copy link
Author

Comment created by romanb:

array_udiff is very weird. If anyone can explain me this behavior, I would be delighted:

class Foo {
    public function **construct($val) {
        $this->name = $val;
    }
    public $name;
}

function compare($a, $b) {
    return $a === $b ? 0 : 1;
}

$foo1 = new Foo('one');
$foo2 = new Foo('two');
$foo3 = new Foo('three');
$foo4 = new Foo('four');
$foo5 = new Foo('five');

$snapshot = array();
$coll = array($foo1);
var*dump(array*udiff($snapshot, $coll, 'compare'));
// => array(0) { }

$snapshot = array($foo1);
$coll = array($foo1, $foo2);
var*dump(array*udiff($snapshot, $coll, 'compare'));
// => array(0) { }

$snapshot = array($foo1, $foo2);
$coll = array($foo1, $foo2, $foo3);
var*dump(array*udiff($snapshot, $coll, 'compare'));
// => array(0) { }

$snapshot = array($foo1, $foo2, $foo3);
$coll = array($foo1, $foo2, $foo3, $foo4);
var*dump(array*udiff($snapshot, $coll, 'compare'));
// => array(1) { [2]=>  object(Foo)#3 (2) { ["name"]=>  string(5) "three" } } 

$snapshot = array($foo1, $foo2, $foo3, $foo4);
$coll = array($foo1, $foo2, $foo3, $foo4, $foo5);
var*dump(array*udiff($snapshot, $coll, 'compare'));
// => array(2) { [1]=>  object(Foo)#2 (2) { ["name"]=>  string(3) "two" } [3]=>  object(Foo)#4 (2) { ["name"]=>  string(4) "four" } } 

The array_udiff behavior is the reason for this problem. I just dont understand it.

According to the manual: "Returns an array containing all the values of array1 ($snapshot in this case) that are not present in any of the other arguments. "

@doctrinebot
Copy link
Author

Comment created by romanb:

This should now be fixed. We're using array_udiff_assoc instead which behaves as expected and is faster. Also, this is a necessary preparation for DDC-213. This can now lead to join-table elements being deleted and reinserted when they change position in the collection, however this is expected and indeed needed for DDC-213. I don't see this as an issue for "unordered" collections.

@doctrinebot
Copy link
Author

Issue was closed with resolution "Fixed"

@doctrinebot doctrinebot added this to the 2.0-ALPHA4 milestone Dec 6, 2015
@doctrinebot doctrinebot added the Bug label Dec 7, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant