Doctrine fetch join无法正确保湿

时间:2016-04-18 21:13:06

标签: doctrine-orm eager-loading hydration

原始问题

我正在Doctrine中进行获取连接,使用具有复合键且没有其他已定义字段的连接表,并将不正确的数据加载到我的实体中。这是Doctrine中的错误,还是我做错了什么?

以下是一个说明问题的简单示例。

创建三个实体:

/** 
 * @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; }
}

使用以下数据填充相应的数据库表:

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

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_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');

使用此代码来水合Driver实体并显示其内容:

$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();
}

预期输出:

John Doe:
BMW 7系
Crysler 300
道奇飞镖
梅赛德斯C级
沃尔沃XC90

实际输出:

John Doe:
BMW 7系
道奇飞镖
道奇飞镖
沃尔沃XC90
沃尔沃XC90

这里有关联实体的奇怪重复。具体地,子实体#3被复制为#2,#5被复制为#4等,并且子实体#2,#4等根本没有被加载。这种模式是一致和可重复的。

我的代码有问题吗?为什么Doctrine无法正确地将数据库中的数据映射到实体?

补充说明

如果查询所有游乐设施(也就是驱动程序和汽车之间的关联)并执行提取连接,则会出现同样的问题。这是一个重要的案例,因为@ qaqar-haider的答案对于这种情况不起作用。

假设以下表格数据

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

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 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, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (2, 'BMW');
INSERT INTO driver_ride (driver_id, car) VALUES (2, 'Crysler');

想象一下使用fetch-joins

的以下查询
$qb = $em->createQueryBuilder();
$rides = $qb->select('dr, d, c')
   ->from('DriverRide', 'dr')
   ->leftJoin('dr.driver', 'd')
   ->leftJoin('dr.car', 'c')
   ->getQuery()->getResult();

foreach ($rides as $ride) {
    print $ride->driver->getName() . ': ' . $ride->getCar()->getModel();
}

预期输出:

约翰·多伊:克莱斯勒 John Doe:梅赛德斯
John Doe:宝马 Erika Mustermann:宝马郎 Erika Mustermann:克莱斯勒

实际输出:

约翰·多伊:克莱斯勒 John Doe:梅赛德斯
John Doe:梅赛德斯
Erika Mustermann:宝马郎 Erika Mustermann:宝马

再次,结果的数量是正确的,但这些关联是混合的。

这是原始问题的简化测试用例,不受任何WHERE或GROUP BY子句的约束。

3 个答案:

答案 0 :(得分:3)

这是Doctrine中的一个错误。不幸的是,相关的Doctrine源代码非常难看,所以我不确定my bug fix是最好的,但它确实解决了这个问题。我在这里复制the explanation from my bug report是为了完整性:

此错误的主要原因位于ObjectHydrator类。在hydrateRowData()方法中,$resultPointers用于存储对每种实体类型的最近水合对象的引用。当子实体需要链接到其父实体时,此方法会在$resultPointers中查找对父项的引用,如果找到父项,则将该项链接到该父项。

问题是$resultPointers是一个实例/对象(而不是本地/方法)变量,每次调用hydrateRowData()时都不会重新初始化,因此它可能保留对实体的引用前一次调用该方法而不是当前时间,这是水合的。

在此特定示例中,每次调用hydrateRowData()时,Car实体在DriverRide实体之前都是水合的。当该方法查找Car实体的父实体时,它第一次找不到任何内容(因为DriverRide尚未完全处理),并且在每次后续调用时,都会找到对前一次水合的DriverRide对象的引用调用了方法(因为尚未为当前行处理DriverRide,$resultPointers仍然保留对前一行处理结果的引用。)

当其他字段添加到DriverRide时,该错误消失,因为这样做恰好导致在hydrateRowData()中的Car实体之前处理DriverRide实体。发生重复记录是因为这个方法的其他一些奇怪的部分导致子实体被识别为每次调用该方法时都没有连接(并且因此延迟加载)(不计算第一次),所以这些时间(第三,第五,等等。孩子和父母之间的联系恰好是正确的。

我认为根本问题是$resultPointers既不是局部变量,也不是每次调用hydrateRowData()时都重新初始化。我想不出任何一种情况,你需要引用一个与上一行数据中的数据一起水合的对象,所以我建议在这个方法的开头简单地重新初始化这个变量。这应该可以解决这个问题。

答案 1 :(得分:0)

这是一个很好的问题。我花了一些时间试图找出答案,但我设法只通过修改实体映射来实现预期的结果 - 为什么最初的问题出现我仍然无法理解,但我会继续调查。

所以,这就是我所做的。让Doctrine为您定义DriverRide映射,而不是手动定义many-to-many实体,如here所述。

实体将如下所示:

/**
 * @Entity
 * @Table(name="cars")
 */
class Car
{
    /**
     * @Id
     * @Column(type="string", length=25)
     * @GeneratedValue(strategy="NONE")
     */
    protected $brand;

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

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

    public function setBrand($brand) { $this->brand = $brand; }
    public function setModel($model) { $this->model = $model; }
}

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

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

    // This will create a third table `driver_rides`

    /**
     * @ManyToMany(targetEntity="Car")
     * @JoinTable(
     *     name="driver_rides",
     *     joinColumns={@JoinColumn(name="driver_id", referencedColumnName="id")},
     *     inverseJoinColumns={@JoinColumn(name="car", referencedColumnName="brand")}
     * )
     */
    protected $cars;

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

    function getId() { return $this->id; }
    function getName() { return $this->name; }
    function getCars() { return $this->cars; }

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

    public function addCar(\Car $car) { $this->cars[] = $car; return $this; }
    public function removeCar(\Car $car) { $this->cars->removeElement($car); }
}

查询得到你想要的东西:

$qb = $entityManager->createQueryBuilder();

/** @var $driver Driver */
$driver = $qb->select('d, dr')
    ->from('Driver', 'd')
    ->leftJoin('d.cars', 'dr')  // join with `driver_rides` table
    ->where('d.id = 1')
    ->getQuery()
    ->getSingleResult();

printf("%s:\n", $driver->getName());
foreach ($driver->getCars() as $car) {
    printf("%s %s\n", $car->getBrand(), $car->getModel());
}

答案 2 :(得分:-1)

You have to group it by a brand, othervise it would give you desired results Group By brand

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