Symfony2中关联反向端部分加载的危害与数据异常问题
核心原因
你碰到的是Doctrine ORM 2.x(Symfony 2.8默认搭载的版本)持久化集合的标准hydration行为,不属于框架bug:
- 你一开始判断
$form->get('foo')->getData()->getOwners()->getInitialized() === false的结果准确,此时owners是未初始化的懒加载代理集合,正常情况下只有你首次遍历、计数该集合时,才会执行全量查询拉取当前Foo关联的所有Owner记录。 - 当你编写的DQL同时SELECT主实体和带WITH过滤条件的关联实体时,Doctrine的hydrator会直接把过滤得到的关联结果赋值给对应实体的持久化集合,同时把集合标记为已初始化。后续所有对
$foo->getOwners()的调用都不会再触发数据库查询,直接返回这个过滤后的子集,自然和数据库中的真实全量关联数据不一致。
实际危害
风险等级完全取决于后续业务逻辑对该集合的使用方式:
- 低风险场景:仅用该查询结果做定向只读展示,且后续逻辑不再复用这个Foo实例的
owners集合做其他操作,不会产生业务错误。 - 高风险场景:
- 对集合做全量计数、遍历操作时,会得到错误的数值、漏掉未被WITH条件匹配的关联记录,引发数据展示错误
- 基于全量集合做权限校验、状态更新、级联持久化/删除操作时,会出现权限绕过、漏更漏删数据的问题
- 若后续触发实体的级联持久化逻辑,Doctrine会默认当前内存中的子集就是完整关联数据,可能自动删除不在子集中的原有关联记录,造成不可逆的数据丢失
- 该状态仅存在于当前请求的内存对象中,没有任何报错、日志提示,问题隐蔽性极强,排查成本很高。
正确处理方案
根据业务场景选择对应方案即可:
- 方案1:避免过滤查询污染主实体集合
调整DQL写法,给过滤关联设置独立别名,不要直接映射到主实体的关联属性:
查询后不要通过SELECT foo, filteredOwner FROM Foo foo LEFT JOIN foo.owners filteredOwner WITH IDENTITY(filteredOwner.bar) = 1$foo->getOwners()获取过滤结果,直接从结果行中读取filteredOwner对应的数据即可。这种写法不会触发主实体owners集合的初始化,后续其他逻辑调用$foo->getOwners()仍会正常触发懒加载拉取全量数据。 - 方案2:提前初始化全量集合
如果后续逻辑必须使用全量owners数据,在执行过滤DQL前先主动初始化集合:
集合完成全量初始化后,后续DQL查询得到的过滤关联结果不会覆盖已有集合内容,不会出现部分加载问题。$foo->getOwners()->initialize(); - 方案3:查询后重置集合状态
如果执行完过滤DQL后才需要用到全量关联数据,主动重置集合的初始化标记,让后续访问可以重新触发全量查询:// 过滤DQL执行完成后调用 $foo->getOwners()->setInitialized(false); - 方案4:使用集合Criteria做本地过滤
针对单实体的关联过滤需求,优先使用Doctrine集合自带的Criteria过滤能力,不要写带WITH条件的关联查询:
这种方式不会修改原有集合的存储内容,仅返回匹配条件的结果子集,完全不存在集合污染问题。use Doctrine\Common\Collections\Criteria; $criteria = Criteria::create() ->where(Criteria::expr()->eq('bar', 1)); $filteredOwners = $foo->getOwners()->matching($criteria);
内容的提问来源于stack exchange,提问作者Yvon
相关产品推荐
相关产品推荐

