自引用实体用复合主键时触发意外查询的问题排查
问题描述
我有一个Subscription实体,使用instance和rid作为复合主键,同时包含指向自身的parent自引用关联,实体代码如下:
class Subscription { /** * @ORM\Column(name="id", type="uuid", unique=true) * @JMS\Type("string") */ protected $id; /** * @ORM\Id * @ORM\Column(name="instance_id", type="integer") * @JMS\Type("int") */ protected $instance; /** * @ORM\Id * @ORM\Column(name="rid", type="bigint") * @ORM\SequenceGenerator(sequenceName="subscriptions_id_seq", initialValue=1, allocationSize=1) * @JMS\Type("int") */ protected $rid; /** * @ORM\ManyToOne(targetEntity="Subscription") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="parent_int_id", referencedColumnName="rid"), * @ORM\JoinColumn(name="instance_id", referencedColumnName="instance_id") * }) */ protected $parent; ... }
执行查询$subscription = $subscriptionService->findOneBy(['rid' => 147605]);时,Doctrine触发了3条SQL:
- 预期的初始查询:
SELECT t0.id AS id_1, t0.instance_id AS instance_id_2, t0.rid AS rid_3, ... FROM subscriptions t0 WHERE t0.rid = 147605 LIMIT 1;
- 预期的parent关联查询:
SELECT t0.id AS id_1, t0.instance_id AS instance_id_2, t0.rid AS rid_3, ... FROM subscriptions t0 WHERE t0.rid = '890467' AND t0.instance_id = 1 LIMIT 1;
- 意外的查询(会选中该
instance_id下所有行,导致数据库崩溃):
SELECT t0.id AS id_1, t0.instance_id AS instance_id_2, t0.rid AS rid_3, ... FROM subscriptions t0 WHERE t0.instance_id = 1
移除instance作为复合主键可消除问题,但必须保留该结构,求问意外查询的成因和解决办法。
成因分析
- 不完整的主键查询:仅通过
rid(复合主键的一部分)查询实体,Doctrine无法定位唯一实体,只能返回匹配rid的任意一条记录。这种非完整主键查询会导致Doctrine后续的实体管理逻辑出现异常。 - 关联配置的歧义:自引用关联的第二个
JoinColumn复用了实体的主键字段instance_id作为外键的一部分,而非使用独立的外键字段。这种配置让Doctrine在处理关联实体时,错误地将instance_id视为需要批量加载的关联条件,从而触发了全instance_id的查询。
解决方案
1. 使用完整复合主键查询
将查询语句修改为包含完整的复合主键条件,确保Doctrine能准确定位唯一实体,避免后续逻辑异常:
$subscription = $subscriptionService->findOneBy([ 'rid' => 147605, 'instance' => $targetInstanceId // 替换为实际的instance_id值 ]);
2. 修正自引用关联的外键配置
为父实体的instance_id添加独立的外键字段(比如parent_instance_id),修改关联配置如下:
/** * @ORM\ManyToOne(targetEntity="Subscription") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="parent_int_id", referencedColumnName="rid"), * @ORM\JoinColumn(name="parent_instance_id", referencedColumnName="instance_id") * }) */ protected $parent;
同时更新数据库表结构,添加parent_instance_id字段并配置外键约束,避免主键字段和外键字段复用导致的逻辑歧义。
3. 可选:显式指定关联加载模式
如果不需要懒加载parent关联,可以显式设置fetch="EAGER",让Doctrine在初始查询时就加载关联数据,避免后续的懒加载触发异常查询:
/** * @ORM\ManyToOne(targetEntity="Subscription", fetch="EAGER") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="parent_int_id", referencedColumnName="rid"), * @ORM\JoinColumn(name="parent_instance_id", referencedColumnName="instance_id") * }) */ protected $parent;
内容的提问来源于stack exchange,提问作者alexandrin88
相关产品推荐
相关产品推荐

