🎻

[Symfony][Doctrine] STIの基底クラスをOneToManyで持つエンティティをSTIの派生クラスとJOINする

2022/10/18に公開

タイトルを読んだだけではさっぱり分かりませんが、DoctrineのQueryBuilderでちょっと変わったことをやる方法のメモです。

やりたいこと

  • Doctrineの Single Table Inheritance(STI) を使っているエンティティがある
  • このSTIの基底クラスをOneToManyで所有する別のエンティティがある
  • このエンティティを、QueryBuilderを使ってSTIの派生クラスとJOINしたい
  • (そして、派生クラス特有のプロパティに対してWHERE句で絞り込みなどをしたい)

やり方

エンティティの例

適当な例が思い浮かばなかったのでちょっと微妙な例ですが、会員制のECで、店舗と顧客が同じ「ユーザー」という扱いであるようなシステムを想定し、User を継承した ShopCustomer があるとしましょう。

// src/Entity/User.php

#[ORM\Entity(repositoryClass: UserRepository::class)]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn('type')]
#[ORM\DiscriminatorMap([
    'shop' => Shop::class,
    'customer' => Customer::class,
])]
#[ORM\EntityListeners([UserListener::class])]
abstract class User
{
}

そして、ShopCustomer はそれぞれプロフィール情報を持つのですが、それぞれスキーマが異なるため、ShopProfileCustomerProfile という異なるエンティティであるとしましょう。

// src/Entity/Shop.php

#[ORM\Entity(repositoryClass: ShopRepository::class)]
class Shop extends User
{
    #[ORM\OneToOne(targetEntity: ShopProfile::class, mappedBy: 'shop', orphanRemoval: true)]
    protected ?ShopProfile $shopProfile = null;
}
// src/Entity/Customer.php

#[ORM\Entity(repositoryClass: CustomerRepository::class)]
class Customer extends User
{
    #[ORM\OneToOne(targetEntity: CustomerProfile::class, mappedBy: 'customer', orphanRemoval: true)]
    protected ?CustomerProfile $customerProfile = null;
}

さらに、(どういうドメインか謎ですが)複数の User を束ねる Cluster というものを考えます。

// src/Entity/Cluster.php

#[ORM\Entity(repositoryClass: ClusterRepository::class)]
class Cluster
{
    #[ORM\OneToMany(targetEntity: User::class)]
    private Collection $users;
}

QueryBuilderの例

このとき、Cluster::$users に対して Shop Customer をJOINし、さらにその先の ShopProfile CustomerProfile をJOIN(して、ShopProfile CustomerProfile の内容で WHERE などを)したい、というのが今回やりたかったことです。

QueryBuilderでこれをやるには、以下のように書きます。

$qb = $clusterRepository->createQueryBuilder('cl')
    ->leftJoin('cl.users')
    ->leftJoin(Shop::class, 'sh', Doctrine\ORM\Query\Expr\Join::WITH, 'u.id = sh.id') // ここがポイント
    ->leftJoin(Customer::class, 'cu', Doctrine\ORM\Query\Expr\Join::WITH, 'u.id = cu.id') // ここがポイント
    ->leftJoin('sh.shopProfile', 'sp')
    ->leftJoin('cu.customerProfile', 'cp')
    // ->orWhere('sp.shopName like :query')
    // ->orWhere('cp.customerName like :query')
    // みたいな
;

こんなふうに、User とJOINしたあとにさらに ShopCustomer とも WITH でJOINしてしまえば所望の処理を実現できます。

参考

GitHubで編集を提案

Discussion