Doctrine Hydrator关联实体覆盖问题:如何在不重复执行查询的情况下获取包含首尾OrderItem的订单数组
问题分析:Doctrine QueryBuilder 中OneToOne关联首尾OrderItem的水化异常
先还原下问题场景
我们有order_item表的测试数据:
| id | order_id | status |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 1 | 1 |
| 3 | 1 | 0 |
| 4 | 1 | 1 |
| 5 | 1 | 0 |
实体关联是这么设计的:
Order实体用OneToOne分别关联首个和最后一个OrderItem(属性名orderItemFirst、orderItemLast)- 同时
Order用OneToMany关联所有OrderItem(属性名items) OrderItem也做了对应的反向关联
然后用QueryBuilder构建查询,筛选出status=true的首尾OrderItem时,出现了奇怪的现象:
- 直接调
getResult(),得到的orderItemFirst和orderItemLast居然都是id=4的那个OrderItem - 但调
getArrayResult()就完全正常:orderItemFirst是id=2、status=1的记录,orderItemLast是id=4、status=1的记录 - 更离谱的是,如果先调用
getArrayResult(),再调用getResult(),getResult()的结果就突然正确了
为啥会出现这种诡异的情况?
这本质是Doctrine ORM的Identity Map(实体身份映射)机制在搞事情:
- Identity Map的核心作用:Doctrine会把已经加载过的实体实例缓存起来,用实体的主键作为唯一标识。只要主键相同,就直接复用缓存里的实例,不会重新创建。
- 水化顺序与缓存覆盖的问题:
当你直接调用getResult()时,Doctrine会先执行SQL查询,然后按结果集的顺序逐步把数据水化(转换成实体对象)。在你的SQL结果里,符合status=1的最后一条记录是id=4的OrderItem。
因为orderItemFirst和orderItemLast都是关联到OrderItem类的OneToOne属性,Doctrine在水化时,会先处理结果集中的orderItemLast数据,把id=4的实体存入Identity Map。等处理orderItemFirst对应的id=2数据时,由于查询结果的结构问题(同一行同时包含首尾两个OrderItem的字段),Hydrator(水化器)会错误地把已经缓存的id=4实体赋值给orderItemFirst,而不是重新创建id=2的实体。 - getArrayResult()为啥没问题?
因为getArrayResult()返回的是原始数组,完全绕开了Identity Map和实体水化流程,直接返回SQL查询的原始结果,所以不会有缓存覆盖的问题。 - 先调getArrayResult()再调getResult()为啥就正常了?
因为先调用getArrayResult()时,SQL已经执行过了,Doctrine会把结果集中所有的OrderItem数据(id=2和id=4)都加载到Identity Map里。等后面调用getResult()时,Hydrator能从缓存里找到两个不同的实体,自然就能正确赋值给orderItemFirst和orderItemLast了。
不用重复执行查询的解决办法
办法1:给两个关联设置不同的别名,显式选择字段
在构建QueryBuilder的时候,给首尾两个OrderItem关联设置不同的别名,并且明确指定要查询这两个关联的字段,让Hydrator能清楚区分:
$queryBuilder = $this->createQueryBuilder('o') // 给首个OrderItem关联起别名firstItem,指定筛选条件 ->leftJoin('o.orderItemFirst', 'firstItem', 'WITH', 'firstItem.status = :status') // 给最后一个OrderItem关联起别名lastItem,指定筛选条件 ->leftJoin('o.orderItemLast', 'lastItem', 'WITH', 'lastItem.status = :status') // 显式选择两个关联的实体数据 ->addSelect('firstItem') ->addSelect('lastItem') ->setParameter('status', true); $orders = $queryBuilder->getResult();
这样Doctrine就能准确把id=2的实体赋值给orderItemFirst,id=4的赋值给orderItemLast。
办法2:清空Identity Map后再执行getResult()
在调用getResult()之前,清空EntityManager中OrderItem类的缓存,让Doctrine重新水化所有需要的实体,避免复用错误的缓存:
// 只清空OrderItem的缓存,不影响其他实体的缓存 $this->entityManager->clear(OrderItem::class); $orders = $queryBuilder->getResult();
注意:如果业务中之前已经加载了其他OrderItem实体,这个操作会把它们从缓存中清除,所以要根据业务场景判断是否适用。
办法3:修改实体关联的fetch模式
把orderItemFirst和orderItemLast的关联设置为fetch="EAGER",并确保查询时显式关联:
// Order实体中的关联定义示例 /** * @ORM\OneToOne(targetEntity=OrderItem::class, fetch="EAGER") * @ORM\JoinColumn(name="first_item_id", referencedColumnName="id") */ private $orderItemFirst; /** * @ORM\OneToOne(targetEntity=OrderItem::class, fetch="EAGER") * @ORM\JoinColumn(name="last_item_id", referencedColumnName="id") */ private $orderItemLast;
然后在QueryBuilder中显式关联这两个属性,确保Doctrine在水化时优先加载对应的实体,避免缓存覆盖的问题。
内容的提问来源于stack exchange,提问作者Disstudio
相关产品推荐
相关产品推荐

