fetch join using composite key on join entity fails to hydrate correctly #5762

Closed
jakestay opened this Issue Apr 7, 2016 · 2 comments

Projects

None yet

2 participants

@jakestay
Contributor
jakestay commented Apr 7, 2016

When doing a fetch join to hydrate an entity, using a join table with a composite key and no other defined fields, the hydration incorrectly creates duplicate associated entities.

This issue may possibly be related to #2548. I cannot find any more recent related bug reports.

Steps to reproduce:
Create these three entities and their corresponding database tables:

/**
 * @ORM\Entity
 * @ORM\Table(name="driver")
 */
class Driver
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @ORM\OneToMany(targetEntity="DriverRide", mappedBy="driver")
     */
    private $driverRides;

    function getId() { return $this->id; } 
    function getName() { return $this->name; } 
    function getDriverRides() { return $this->driverRides; }
}
/**
 * @ORM\Entity
 * @ORM\Table(name="driver_ride")
 */
class DriverRide
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Driver", inversedBy="driverRides")
     * @ORM\JoinColumn(name="driver_id", referencedColumnName="id")
     */
    private $driver;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Car", inversedBy="carRides")
     * @ORM\JoinColumn(name="car", referencedColumnName="brand")
     */
    private $car;

    function getDriver() { return $this->driver; }
    function getCar() { return $this->car; }
}
/**
 * @ORM\Entity
 * @ORM\Table(name="car")
 */
class Car
{
    /**
     * @ORM\Id
     * @ORM\Column(type="string", length=25)
     * @ORM\GeneratedValue(strategy="NONE")
     */
    private $brand;

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

    /**
     * @ORM\OneToMany(targetEntity="DriverRide", mappedBy="car")
     */
    private $carRides;

    function getBrand() { return $this->brand; }
    function getModel() { return $this->model; }
    function getCarRides() { return $this->carRides; }
}

Populate the database tables with this data:

INSERT INTO car (brand, model) VALUES ('BMW', '7 Series');
INSERT INTO car (brand, model) VALUES ('Crysler', '300');
INSERT INTO car (brand, model) VALUES ('Mercedes', 'C-Class');
INSERT INTO car (brand, model) VALUES ('Volvo', 'XC90');
INSERT INTO car (brand, model) VALUES ('Dodge', 'Dart');

INSERT INTO driver (id, name) VALUES (1, 'John Doe');

INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Crysler');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Mercedes');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Volvo');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (1, 'Dodge');

Use this code to hydrate a Driver entity and display its contents:

$qb = $em->createQueryBuilder();
$driver = $qb->select('d, dr, c')
   ->from('Driver', 'd')
   ->leftJoin('d.driverRides', 'dr')
   ->leftJoin('dr.car', 'c')
   ->where('d.id = 1')
   ->getQuery()->getSingleResult();

print '<p>' . $driver->getName() . ':';
foreach ($driver->getDriverRides() as $ride) {
    print '<br>' . $ride->getCar()->getBrand() . ' ' . $ride->getCar()->getModel();
}

Expected output:
John Doe:
BMW 7 Series
Crysler 300
Dodge Dart
Mercedes C-Class
Volvo XC90

Actual output:
John Doe:
BMW 7 Series
Dodge Dart
Dodge Dart
Volvo XC90
Volvo XC90

There is a strange duplication going on here with the associated entities. Note that the problem disappears if there are additional fields defined in the join entity (DriverRide), or if all the associated entities (Car) have already been previously loaded from the database.

@jakestay
Contributor

I think the main cause of this bug is located in the ObjectHydrator class. In the hydrateRowData() method, $resultPointers is used by to store a reference to the most recently hydrated object of each entity type. When a child entity needs to be linked to its parent, this method looks in $resultPointers to find a reference to the parent and, if it finds one, links the child to that parent.

The problem is that $resultPointers is an instance/object (rather than local/method) variable that does not get reinitialized every time hydrateRowData() is called, and so it may retain a reference to an entity that was hydrated the previous time the method was called rather than the current time.

In this particular example, the Car entity is hydrated before the DriverRide entity each time hydrateRowData() is called. When the method looks for the parent of the Car entity, it finds nothing the first time (because DriverRide has not yet been processed at all) and, on every subsequent call, finds a reference to the DriverRide object that was hydrated the previous time the method was called (because DriverRide has not yet been processed for the current row, and $resultPointers still retains a reference to the result of processing for the previous row).

The bug disappears when additional fields are added to DriverRide only because doing so happens to cause the DriverRide entity to be processed before the Car entity in hydrateRowData(). The duplication of records happens because some other weird part of this method causes the child entity to be identified as not fetch joined (and so lazy loaded) every other time the method is called (not counting the first time), and so those times (the third, fifth, etc.) the link between child and parent happens to turn out correctly.

I believe that the fundamental problem is that $resultPointers is neither a local variable nor reinitialized each time hydrateRowData() is called. I cannot think of any scenario in which you would need a reference to an object that was hydrated with data from the previous row of data, so I would recommend simply reinitializing this variable at the beginning of this method. That should fix the bug.

@jakestay jakestay added a commit to jakestay/doctrine2 that referenced this issue Apr 12, 2016
@jakestay jakestay add functional test and bug fix for issue #5762 b90da34
@kamazee kamazee added a commit to kamazee/doctrine2 that referenced this issue Aug 14, 2016
@jakestay @kamazee jakestay + kamazee add functional test and bug fix for issue #5762 595d92e
@kamazee kamazee added a commit to kamazee/doctrine2 that referenced this issue Aug 14, 2016
@jakestay @kamazee jakestay + kamazee add functional test and bug fix for issue #5762 b3a2ed4
@kamazee kamazee added a commit to kamazee/doctrine2 that referenced this issue Aug 29, 2016
@kamazee kamazee Remove irrelevant accessors (#5762) 5da1221
@kamazee kamazee added a commit to kamazee/doctrine2 that referenced this issue Aug 29, 2016
@kamazee kamazee Use ::class const instead of FQCN string (#5762) f98e542
@Ocramius Ocramius added a commit that referenced this issue Sep 7, 2016
@Ocramius Ocramius Merge branch 'fix/#5975-fix_hydrating_fetch_join_with_composite_pk-2.…
…5' into 2.5

Close #5975
Close #5762
Close #5776
a2c23fb
@Ocramius Ocramius closed this in 2814d6e Sep 7, 2016
@Ocramius
Member
Ocramius commented Sep 7, 2016

Fixed in #5975

@Ocramius Ocramius self-assigned this Sep 7, 2016
@Ocramius Ocramius added the Bug label Sep 7, 2016
@Ocramius Ocramius added this to the 2.5.5 milestone Sep 7, 2016
@yvoyer yvoyer added a commit to yvoyer/doctrine2 that referenced this issue Nov 21, 2016
@jakestay @yvoyer jakestay + yvoyer add functional test and bug fix for issue #5762 906a94b
@yvoyer yvoyer added a commit to yvoyer/doctrine2 that referenced this issue Nov 21, 2016
@kamazee @yvoyer kamazee + yvoyer Remove irrelevant accessors (#5762) 367662d
@yvoyer yvoyer added a commit to yvoyer/doctrine2 that referenced this issue Nov 21, 2016
@kamazee @yvoyer kamazee + yvoyer Use ::class const instead of FQCN string (#5762) d4b4695
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment