🎻
[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