[Symfony][Doctrine] Single Table Inheritanceを使ってエンティティに基底クラスを持たせる
はじめに
[Symfony][Doctrine] MappedSuperclassを使ってエンティティに基底クラスを持たせる
こちらの記事で MappedSuperclass を使う方法を紹介しましたが、今回は Single Table Inheritance バージョンです。
以下の要件(↑の記事と同じ)を例に使い方を説明してみます。
-
労働者というベースクラスを継承した会社員エンティティとフリーランスエンティティを作りたい
やり方
まず、以下のような感じで 労働者 抽象クラスを作成します。
/**
* @ORM\Entity(repositoryClass=WorkerRepository::class)
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"employee" = Employee::class, "freelancer" = Freelancer::class})
*/
abstract class Worker
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
}
-
@ORM\InheritanceType("SINGLE_TABLE")によってSingle Table Inheritanceを使用することを宣言しています -
@ORM\DiscriminatorColumn(name="type", type="string")によってdiscriminator(識別子)とするカラムを設定しています- カラム名は
type、typeは"string"と設定しています - もちろんこれらは自由に決められます
- カラム名は
-
@ORM\DiscriminatorMap({"employee" = Employee::class, "freelancer" = Freelancer::class})によって、discriminatorカラムの値と子クラスのマッピングを設定しています- 省略することも可能で、その場合は
Workerを継承しているクラスがすべて設定され、値は子クラスのクラス名を小文字にしたものになるようです(未確認🙏)
- 省略することも可能で、その場合は
あとは普通のエンティティと同じように 会社員 フリーランス エンティティを作って、 労働者 抽象クラスを継承してあげればOKです。
/**
* @ORM\Entity(repositoryClass=EmployeeRepository::class)
*/
class Employee extends Worker
{
/**
* @ORM\Column(type="integer")
*/
private $salary;
}
/**
* @ORM\Entity(repositoryClass=FreelancerRepository::class)
*/
class Freelancer extends Worker
{
/**
* @ORM\Column(type="integer")
*/
private $sales;
}
これで、以下のように1つのテーブルで 会社員 と フリーランス の2つのエンティティを表現できるようになります。
| id | type | name | salary | sales |
|---|---|---|---|---|
| 1 | employee | 山田太郎 | 300000 | NULL |
| 2 | freelancer | 鈴木一郎 | NULL | 500000 |
リレーションシップもいい感じ
OneToMany
MappedSuperclass を使った継承は、他のクラスから基底クラスを直接参照されることを想定しておらず、他のエンティティから基底クラスに対してManyToOneでリレーションシップを張ることができませんでした が、 Single Table Inheritance ではそれも問題なく対応可能です。
過去記事の例を借りて、「 労働者 が 仕事 を所有する」というリレーションシップを設定してみましょう。
/**
* @ORM\Entity(repositoryClass=WorkerRepository::class)
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"employee" = Employee::class, "freelancer" = Freelancer::class})
*/
abstract class Worker
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
+
+ /**
+ * @ORM\OneToMany(targetEntity=Job::class, mappedBy="worker")
+ */
+ protected $jobs;
}
/**
* @ORM\Entity(repositoryClass=JobRepository::class)
*/
class Job
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Worker::class, inversedBy="jobs")
*/
private $worker;
}
これで特に問題なく動作します👍
ManyToOne
労働者 からのManyToOneもいい感じにできます。
例えば「 案件 が 労働者 を所有する」というリレーションシップを設定してみましょう。
/**
* @ORM\Entity(repositoryClass=WorkerRepository::class)
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\DiscriminatorMap({"employee" = Employee::class, "freelancer" = Freelancer::class})
*/
abstract class Worker
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=255)
*/
protected $name;
+
+ /**
+ * @ORM\ManyToOne(targetEntity=Matter::class, inversedBy="workers")
+ */
+ protected $matter;
}
class Matter
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\OneToMany(targetEntity=Worker::class, mappedBy="matter")
*/
private $workers;
}
これで、 Matter::$workers には、その 案件 に所属している 労働者 が全員入ってきます👍
種類ごとにManyToOne
さらに、 案件 から 会社員 と フリーランス を別々に取得できるようにするのも超簡単に実装できます。
class Matter
{
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\OneToMany(targetEntity=Worker::class, mappedBy="matter")
*/
private $workers;
+
+ /**
+ * @ORM\OneToMany(targetEntity=Employee::class, mappedBy="matter")
+ */
+ private $employees;
+
+ /**
+ * @ORM\OneToMany(targetEntity=Freelancer::class, mappedBy="matter")
+ */
+ private $freelancers;
}
/**
* @ORM\Entity(repositoryClass=EmployeeRepository::class)
*/
class Employee extends Worker
{
+ /**
+ * @ORM\ManyToOne(targetEntity=Matter::class, inversedBy="employees")
+ */
+ protected $matter;
+
/**
* @ORM\Column(type="integer")
*/
private $salary;
}
/**
* @ORM\Entity(repositoryClass=FreelancerRepository::class)
*/
class Freelancer extends Worker
{
+ /**
+ * @ORM\ManyToOne(targetEntity=Matter::class, inversedBy="freelancers")
+ */
+ protected $matter;
+
/**
* @ORM\Column(type="integer")
*/
private $sales;
}
このように、 targetEntity を子クラスにしてリレーションシップを書くだけです。簡単!
Repository
もちろん、Repositoryクラスもそれぞれのエンティティに対応するレコードだけを返してくれます。
| Repositoryクラス | 返すエンティティ |
|---|---|
WorkerRepository |
Employee および Freelancer
|
EmployeeRepository |
Employee のみ |
FreelancerRepository |
Freelancer のみ |
便利ですね〜!
⚠️注意点
種類ごとにManyToOneのリレーションシップを張るには、上記のとおり $matter プロパティを子クラスでオーバーライドする必要があります。
このとき、 $matter プロパティの可視性が private だと、順参照はできても逆参照がされません。
つまり、 Matter::$employees や Matter::$freelancers にはちゃんと Employee Freelancer のコレクションが入ってくれるのですが、 Employee::$matter や Freelancer::$matter は null になってしまいます。
Doctrineのコードまでは追ってないので理屈は知りません…詳しい方いらっしゃったらぜひ 教えて ください🙏
まとめ
- Doctrineの Single Table Inheritance を使えばエンティティに基底クラスを持たせることができる
-
MappedSuperclassを使うパターンと違ってリレーションシップもいい感じ
Discussion