原始问题
我正在Doctrine中进行获取联接,使用一个有复合键且没有其他定义字段的联接表,并将不正确的数据加载到我的实体中。这是教条中的错误,还是我做错了什么?
下面是一个简单的例子来说明这个问题。
创建三个实体:
/**
* @ORMEntity
* @ORMTable(name="driver")
*/
class Driver
{
/**
* @ORMId
* @ORMColumn(type="integer")
* @ORMGeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORMColumn(type="string", length=255);
*/
private $name;
/**
* @ORMOneToMany(targetEntity="DriverRide", mappedBy="driver")
*/
private $driverRides;
function getId() { return $this->id; }
function getName() { return $this->name; }
function getDriverRides() { return $this->driverRides; }
}
/**
* @ORMEntity
* @ORMTable(name="driver_ride")
*/
class DriverRide
{
/**
* @ORMId
* @ORMManyToOne(targetEntity="Driver", inversedBy="driverRides")
* @ORMJoinColumn(name="driver_id", referencedColumnName="id")
*/
private $driver;
/**
* @ORMId
* @ORMManyToOne(targetEntity="Car", inversedBy="carRides")
* @ORMJoinColumn(name="car", referencedColumnName="brand")
*/
private $car;
function getDriver() { return $this->driver; }
function getCar() { return $this->car; }
}
/**
* @ORMEntity
* @ORMTable(name="car")
*/
class Car
{
/**
* @ORMId
* @ORMColumn(type="string", length=25)
* @ORMGeneratedValue(strategy="NONE")
*/
private $brand;
/**
* @ORMColumn(type="string", length=255);
*/
private $model;
/**
* @ORMOneToMany(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系
结晶器300
道奇飞镖
梅赛德斯C级
沃尔沃XC90
实际输出:
John Doe:
宝马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连接的查询
$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();
}
预期输出:
约翰·多伊:克莱斯勒
约翰·多伊:梅赛德斯
无名氏:宝马
Erika Mustermann:宝马
Erika Mustermann:克莱斯勒
实际输出:
约翰·多伊:克莱斯勒
约翰·多伊:梅赛德斯
约翰·多伊:梅赛德斯
Erika Mustermann:宝马
Erika Mustermann:宝马
同样,结果的数量是正确的,但关联是混淆的。
这是原始问题的简化测试用例,不受任何WHERE或GROUP BY子句的约束。
这是Doctrine中的一个错误。不幸的是,相关的Doctrine源代码非常丑陋,所以我不确定我的错误修复是否是最好的,但它确实解决了问题。为了完整起见,我将从我的错误报告中复制解释:
此错误的主要原因位于ObjectHydrator
类中。在hydrateRowData()
方法中,$resultPointers
用于存储对每个实体类型的最近水合对象的引用。当子实体需要链接到其父实体时,此方法在$resultPointers
中查找对父实体的引用,如果找到引用,则将子实体链接到该父实体。
问题是$resultPointers
是一个实例/对象(而不是局部/方法)变量,每次调用hydrateRowData()
时都不会重新初始化,因此它可能会保留对上次调用方法时而不是当前调用时已水合的实体的引用。
在这个特定的例子中,每次调用hydrateRowData()
时,Car实体在DriverRide实体之前被水合。当该方法查找Car实体的父实体时,它第一次一无所获(因为DriverRide根本没有被处理),并且在每次后续调用中,查找对上次调用该方法时已水合的DriverRide对象的引用(因为尚未对当前行处理DriverRide,并且$resultPointers
仍然保留对前一行处理结果的引用)。
当向DriverRide添加额外字段时,该错误就会消失,因为这样做恰好会导致DriverRide实体在hydrateRowData()
中先于Car实体进行处理。记录的重复发生是因为该方法的其他一些奇怪部分导致子实体每隔一次调用该方法时(不包括第一次)都被标识为未获取连接(并且因此延迟加载),因此在这些时候(第三次、第五次等),子实体和父实体之间的链接恰好正确。
我认为根本问题是$resultPointers
既不是局部变量,也不是每次调用hydrateRowData()
时重新初始化的。我想不出在任何情况下,您需要引用一个对象,该对象已用前一行数据中的数据水合,因此我建议在该方法开始时简单地重新初始化该变量。这应该可以修复错误。
这是一个很好的问题。我花了一些时间试图找出答案,但我只通过修改实体映射就达到了预期的结果——为什么会出现最初的问题,我仍然不明白,但我会继续调查。
所以,这就是我所做的。不要手动定义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 %sn", $car->getBrand(), $car->getModel());
}
你必须按品牌对其进行分组,否则它会给你带来想要的结果按品牌分组
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();