🎻

[Symfony][Doctrine] Single Table Inheritanceを使ってエンティティに基底クラスを持たせる

2020/07/21に公開

はじめに

[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::$employeesMatter::$freelancers にはちゃんと Employee Freelancer のコレクションが入ってくれるのですが、 Employee::$matterFreelancer::$matternull になってしまいます。

Doctrineのコードまでは追ってないので理屈は知りません…詳しい方いらっしゃったらぜひ 教えて ください🙏

まとめ

  • Doctrineの Single Table Inheritance を使えばエンティティに基底クラスを持たせることができる
  • MappedSuperclass を使うパターンと違ってリレーションシップもいい感じ
GitHubで編集を提案

Discussion