🎻
[Symfony][Doctrine] STIの基底クラスをOneToManyで持つエンティティをSTIの派生クラスとJOINする
タイトルを読んだだけではさっぱり分かりませんが、DoctrineのQueryBuilderでちょっと変わったことをやる方法のメモです。
やりたいこと
- Doctrineの Single Table Inheritance(STI) を使っているエンティティがある
- このSTIの基底クラスをOneToManyで所有する別のエンティティがある
- このエンティティを、QueryBuilderを使ってSTIの派生クラスとJOINしたい
- (そして、派生クラス特有のプロパティに対してWHERE句で絞り込みなどをしたい)
やり方
エンティティの例
適当な例が思い浮かばなかったのでちょっと微妙な例ですが、会員制のECで、店舗と顧客が同じ「ユーザー」という扱いであるようなシステムを想定し、User を継承した Shop と Customer があるとしましょう。
// 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
{
}
そして、Shop と Customer はそれぞれプロフィール情報を持つのですが、それぞれスキーマが異なるため、ShopProfile と CustomerProfile という異なるエンティティであるとしましょう。
// 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したあとにさらに Shop や Customer とも WITH でJOINしてしまえば所望の処理を実現できます。
Discussion