4

原始问题

我正在 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:
宝马 7 系
Crysler 300
道奇 Dart
梅赛德斯 C 级
沃尔沃 XC90


实际输出:

John Doe:
宝马 7 系
道奇 Dart
道奇 Dart
沃尔沃 XC90
沃尔沃 XC90

相关实体在这里发生了奇怪的重复。具体而言,子实体#3被复制为#2,#5被复制为#4等,子实体#2、#4等根本不加载。这种模式是一致的和可重复的。

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

附加说明

如果查询所有游乐设施(即司机和汽车之间的关联)并执行 fetch-join,则会出现同样的问题。这是一个重要案例,因为@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:梅赛德斯
John Doe:宝马
Erika Mustermann:宝马
Erika Mustermann:克莱斯勒


实际输出:

John Doe:克莱斯勒
John Doe:梅赛德斯
John Doe:梅赛德斯
Erika Mustermann:宝马
Erika Mustermann:宝马

再一次,结果的数量是正确的,但关联是混乱的。

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

4

3 回答 3

3

这是 Doctrine 中的一个错误。不幸的是,相关的 Doctrine 源代码非常难看,所以我不确定我的错误修复是最好的,但它确实解决了问题。为了完整起见,我从我的错误报告中复制了解释:


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

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

在这个特定的示例中,Car 实体在每次hydrateRowData()调用 DriverRide 实体之前被水合。当该方法查找 Car 实体的父对象时,它第一次什么也没找到(因为 DriverRide 根本还没有被处理),并且在每次后续调用中,都会找到对上一次水合的 DriverRide 对象的引用方法被调用(因为尚未对当前行处理 DriverRide,并且$resultPointers仍然保留对前一行处理结果的引用)。

仅当向 DriverRide 添加其他字段时,该错误才会消失,因为这样做会导致 DriverRide 实体在hydrateRowData(). 记录重复的发生是因为该方法的其他一些奇怪的部分导致子实体被标识为未获取加入(因此延迟加载)每隔一次调用该方法(不计算第一次),所以那些时间(第三,第五等)孩子和父母之间的联系恰好是正确的。

我认为根本问题是$resultPointers每次hydrateRowData()调用都不是局部变量也不是重新初始化。我想不出在任何情况下您都需要引用由前一行数据中的数据水合的对象,因此我建议在此方法开始时简单地重新初始化此变量。那应该可以修复错误。

于 2016-09-01T16:57:49.337 回答
0

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

所以,这就是我所做的。与其手动定义DriverRide实体,不如让 Doctrine 使用many-to-many映射为您定义它,如此所述。

实体将如下所示:

/**
 * @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());
}
于 2016-08-31T22:47:55.000 回答
-1

您必须按品牌对其进行分组,否则它将为您提供所需的结果 按品牌分组

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();
于 2016-08-31T12:22:40.393 回答