【问题标题】:Doctrine fetch join fails to hydrate correctlyDoctrine fetch join 无法正确补水
【发布时间】:2016-04-18 21:13:06
【问题描述】:

原始问题

我正在 Doctrine 中进行 fetch 连接,使用具有复合键且没有其他定义字段的连接表,并将 不正确的数据 加载到我的实体中。这是 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();
}

预期输出:

约翰·多伊:
宝马 7 系
克莱斯勒 300
道奇飞镖
梅赛德斯C级
沃尔沃 XC90


实际输出:

约翰·多伊:
宝马 7 系
道奇飞镖
道奇飞镖
沃尔沃 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:宝马
Erika Mustermann:宝马
Erika Mustermann:克莱斯勒


实际输出:

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

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

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

【问题讨论】:

  • 您找到解决方案了吗?我昨天遇到了同样的问题,它让我忙了几个小时却没有在我的代码中发现错误?我也得到奇怪的重复和缺失的关联。
  • 需要考虑的一点:如果在初始查询中使用getArrayResult() 而不是getSingleResult(),那么$driver 实际上将包含正确的汽车,以正确的顺序。
  • 我几乎可以肯定这是 Doctrine 中的一个错误。我在 4 个多月前提交了 bug report,然后提交了 pull request 并进行了简单的错误修复。遗憾的是,他们都没有收到 Doctrine 项目维护者的任何回应。

标签: doctrine-orm eager-loading hydration


【解决方案1】:

这是 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() 时都重新初始化。我想不出在任何情况下您都需要引用由前一行数据中的数据水合的对象,因此我建议在此方法开始时简单地重新初始化此变量。这应该可以修复错误。

【讨论】:

【解决方案2】:

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

所以,这就是我所做的。与其手动定义 DriverRide 实体,不如让 Doctrine 使用 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());
}

【讨论】:

  • 谢谢。我知道如果使用多对多关联不会出现问题。就我而言,在将多对多关联转换为两个一对多关联后,我偶然发现了这个问题。这是必要的,因为我想在关联中存储一个附加属性。似乎需要两件事来触发这个问题:(a)一个多对多关联被分解成两个一对多关联,中间有一个中间实体(b)中间实体有一个组合的主键.
  • 我同时相信我已经接近了这个问题。我们说的是Doctrine Docs(“Join table with meta data”)的“Composite and Foreign Keys as Primary Key”一章的用例3。但是,在第 28 章“限制和已知问题”中明确指出,外键必须始终指向关联实体的主键,否则可能会出现有趣的错误。但这正是我们这里所拥有的:多对多端点(汽车和司机)仅指向 jon 表主键的一半。
  • 抱歉,忘记了我上一条关于“指向连接表主键的一半”部分的评论。我在脑海中混淆了关联的拥有和反面。当然,外键分别指向cardriver的主键,这些都是完整的主键。我的错!但是,如果连接表 driver_ride 获得独立的主键(例如,只是一个普通的 int)并且不使用组合的主键,问题就会消失。
【解决方案3】:

您必须按品牌对其进行分组,否则它会给您想要的结果 按品牌分组

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

【讨论】:

  • 分组在这里没有任何帮助。在问题的后半部分查看我的附加测试用例。您修改后的查询返回相同的结果纯属巧合,因为示例数据库仅包含一位驾驶员在每辆车上最多只乘坐一次的记录。但是,一旦您有更多的司机和更多的游乐设施,结果就不再一样了。您的查询在语义上与原始查询不同。所以这不是解决方案。此外,请更正您的第一句话。这甚至没有意义。
  • 你能给我看看sql吗,给我看看生成的sql getSql() @user2690527
  • SELECT r0_.id AS id_0, r0_.name AS name_1, r1_.brand AS brand_2, r1_.model AS model_3, r2_.driver_id AS driver_id_4, r2_.car AS car_5, FROM driver_ride r2_ INNER JOIN driver r0_ ON r2_.driver_id = r0_.id INNER JOIN car r1_ ON r2_.car = r1_.brand
  • 不应该是inner Join,应该是leftJoin
  • 您的 querybuilder 正在选择表单驱动程序,而您的 sql 正在从 driver_ride 中选择,这很奇怪,
猜你喜欢
  • 1970-01-01
  • 2013-11-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-11-02
  • 2011-08-19
  • 2023-03-28
  • 1970-01-01
相关资源
最近更新 更多