Symfony Doctrine实现类似Laravel的Has One Through关联问题
在Symfony Doctrine中实现类似Laravel Has One Through的关联方案
针对你的locked_users表(含user_id和user_type字段)关联members、customers表的需求,先分析你之前方案的问题,再给出两种可行的实现方式:
问题根源分析
- 多关联共享外键方案:你在
LockedUser实体中同时定义Customer和Member关联,两者都绑定user_id字段,Doctrine无法区分关联逻辑,且单条locked_users记录仅对应一种用户类型,这种设计本身不符合业务逻辑。 - 继承映射方案:父类
LockedUser同时标注@MappedSuperclass和@Entity(二者互斥);@UniqueEntity配置的字段在父类中不存在;子类重复定义@Table注解(单表继承中子类无需指定表名),这些错误导致映射失效。
方案一:正确配置单表继承
利用Doctrine单表继承,将不同用户类型的锁定记录映射为不同子类,既符合业务逻辑,又能正确关联对应表。
1. 父类LockedUser(基础实体)
/** * @ORM\Entity(repositoryClass="Customize\Repository\LockedUserRepository") * @ORM\Table(name="mtb_locked_user", uniqueConstraints={@ORM\UniqueConstraint(name="user_type_unique", columns={"user_id", "user_type"})}) * @ORM\InheritanceType("SINGLE_TABLE") * @ORM\DiscriminatorColumn(name="user_type", type="string", length=255) */ class LockedUser extends \Eccube\Entity\AbstractEntity { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @ORM\Column(type="integer") */ protected $user_id; // 其他公共字段(如锁定时间、原因等) }
2. 子类LockedCustomer(关联Customer)
/** * @ORM\Entity * @ORM\DiscriminatorValue("customer") */ class LockedCustomer extends LockedUser { /** * @var Customer|null * * @ORM\ManyToOne(targetEntity="Eccube\Entity\Customer") * @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false) */ private ?Customer $customer = null; public function getCustomer(): ?Customer { return $this->customer; } public function setCustomer(?Customer $customer): self { $this->customer = $customer; $this->user_id = $customer?->getId(); return $this; } }
3. 子类LockedMember(关联Member)
/** * @ORM\Entity * @ORM\DiscriminatorValue("member") */ class LockedMember extends LockedUser { /** * @var Member|null * * @ORM\ManyToOne(targetEntity="Eccube\Entity\Member") * @ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false) */ private ?Member $member = null; public function getMember(): ?Member { return $this->member; } public function setMember(?Member $member): self { $this->member = $member; $this->user_id = $member?->getId(); return $this; } }
使用方式:创建对应子类实例并设置关联对象,Doctrine会自动填充user_id和user_type字段:
// 锁定客户 $lockedCustomer = new LockedCustomer(); $lockedCustomer->setCustomer($customerEntity); $entityManager->persist($lockedCustomer); // 锁定会员 $lockedMember = new LockedMember(); $lockedMember->setMember($memberEntity); $entityManager->persist($lockedMember); $entityManager->flush();
方案二:条件关联(无需继承)
若不想使用继承,可在单个LockedUser实体中为关联添加WHERE条件,根据user_type区分关联对象。
/** * @ORM\Entity(repositoryClass="Customize\Repository\LockedUserRepository") * @ORM\Table(name="mtb_locked_user", uniqueConstraints={@ORM\UniqueConstraint(name="user_type_unique", columns={"user_id", "user_type"})}) */ class LockedUser extends \Eccube\Entity\AbstractEntity { /** * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ private $id; /** * @ORM\Column(type="integer") */ private $user_id; /** * @ORM\Column(type="string", length=255) */ private $user_type; /** * @var Customer|null * * @ORM\ManyToOne(targetEntity="Eccube\Entity\Customer") * @ORM\JoinColumn(name="user_id", referencedColumnName="id") * @ORM\Where("user_type = 'customer'") */ private ?Customer $customer = null; /** * @var Member|null * * @ORM\ManyToOne(targetEntity="Eccube\Entity\Member") * @ORM\JoinColumn(name="user_id", referencedColumnName="id") * @ORM\Where("user_type = 'member'") */ private ?Member $member = null; public function setCustomer(?Customer $customer): self { $this->customer = $customer; $this->user_id = $customer?->getId(); $this->user_type = 'customer'; return $this; } public function setMember(?Member $member): self { $this->member = $member; $this->user_id = $member?->getId(); $this->user_type = 'member'; return $this; } // 其他getter/setter }
使用方式:设置关联对象时需同步指定user_type,Doctrine会根据条件加载对应关联。
方案对比
- 单表继承:面向对象设计更清晰,不同用户类型的业务逻辑可拆分到子类,适合后续扩展不同的锁定规则。
- 条件关联:实体结构单一,配置简单,适合业务逻辑差异较小的场景。
内容的提问来源于stack exchange,提问作者Dương Việt Hoà
相关产品推荐
相关产品推荐

