🎻

[Symfony][Doctrine] SELECT結果の件数が1件とは限らないリレーションをOneToOneにしても一応動くという話

2020/07/24に公開

知らなかったので共有です。タイトルを見て「何を今さら」と思った人はスルーしてください😅

よくあるOneToManyの例

以下のように、 Parent エンティティが複数の Child エンティティを所有している、という状況を考えます。

class Parent
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\OneToMany(targetEntity=Child::class, mappedBy="parent")
     */
    private $children;
    
    /**
     * @return Collection|Child[]
     */
    public function getChildren(): Collection
    {
        return $this->children;
    }

    public function addChild(Child $child): self
    {
        if (!$this->children->contains($child)) {
            $this->children[] = $child;
            $child->setParent($this);
        }

        return $this;
    }

    public function removeChild(Child $child): self
    {
        if ($this->children->contains($child)) {
            $this->children->removeElement($child);
            // set the owning side to null (unless already changed)
            if ($child->getParent() === $this) {
                $child->setParent(null);
            }
        }

        return $this;
    }
}
class Child
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity=Parent::class, inversedBy="children")
     */
    private $parent;
    
    public function getParent(): ?Parent
    {
        return $this->parent;
    }

    public function setParent(Parent $parent): self
    {
        $this->parent = $parent;

        return $this;
    }
}

よくあるOneToMany/ManyToOneのリレーションシップですね。

これを無造作にOneToOneにしちゃっても普通に動く

これを、何も考えずにリレーションの設定をOneToOne/OneToOneに変えてみましょう。

class Parent
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\OneToOne(targetEntity=Child::class, mappedBy="parent")
     */
    private $child;

    public function getChild(): ?Child
    {
        return $this->child;
    }

    public function setChild(Child $child): self
    {
        $this->child = $child;

        return $this;
    }
}
class Child
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\OneToOne(targetEntity=Parent::class, inversedBy="child")
     */
    private $parent;

    public function getParent(): ?Parent
    {
        return $this->parent;
    }

    public function setParent(Parent $parent): self
    {
        $this->parent = $parent;

        return $this;
    }
}

実はこれ、普通に動くんです😳

$parent->getChild() したときに実行されるSQLは

SELECT
  t0.id AS id_1,
FROM
  profile t0
WHERE
  t0.user_id = ?

こんな感じで LIMIT 1 とかはついてませんが、複数レコード取得された場合でも、PHP側で1件だけが採用されてそのエンティティが返ってきます。知りませんでした。

複数レコードのうちどの1件が返ってくるかはたぶん保証されてないと思いますが、実験してみたところどうやらid昇順で最後の1件が返ってくるっぽいです。

プロファイラを見てみるとエラーは出てる

ただし、プロファイラを覗いてみると、以下のようなエラーが出ていました😓

The mappings App\Entity\Parent#child and App\Entity\Child#parent are inconsistent with each other.

多分 OneToOne なのにDBの child.parent_id にユニーク制約がないことが原因だと思うのですが、試せていないので正確なことは分かりません🙏(詳細知ってる方いたらぜひ DM お待ちしてます🙇)

GitHubで編集を提案

Discussion