🎻

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

2020/07/18に公開

やりたいこと

会社員フリーランス という2種類の 労働者 をDoctrineのエンティティとして表現したいとします。

労働者 エンティティに 労働形態 のようなプロパティを持たせるのが一番普通のやり方だと思いますが、色々な理由から 労働者 というベースクラスを継承した 会社員 エンティティと フリーランス エンティティを作りたいという状況があり得ます。

このような実装は、Doctrineの MappedSuperclass という機能を使うことで実現できます。

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

こちらの記事で Single Table Inheritance を使った方法も紹介しています。

やり方

まず、以下のように @MappedSuperclass アノテーションをつけて 労働者 抽象クラスを作成します。

/**
 * @ORM\MappedSuperclass(repositoryClass=WorkerRepository::class)
 */
abstract class Worker
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $name;
}

あとは普通のエンティティと同じように 会社員 フリーランス エンティティを作って、 労働者 抽象クラスを継承してあげれば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;
}

簡単ですね👍

他のエンティティから基底クラスに対してManyToOneでリレーションシップを張ることはできない

他のエンティティから基底クラスに対してリレーションシップを張りたくなることがあると思いますが、 基底クラスに対するManyToOneのリレーションは機能しないので要注意です。

例えば、「 労働者仕事 を所有する」といったリレーションシップの設定を以下のように書くと、 doctrine:migrations:diff でマイグレーションスクリプトの生成には成功するので、一見上手くいくような気がしてしまいます。

  /**
   * @ORM\MappedSuperclass(repositoryClass=WorkerRepository::class)
   */
  abstract class Worker
  {
      /**
       * @ORM\Id()
       * @ORM\GeneratedValue()
       * @ORM\Column(type="integer")
       */
-     protected $id;
+     private $id;

      /**
       * @ORM\Column(type="string", length=255)
       */
      protected $name;
+
+     /**
+      * @ORM\OneToMany(targetEntity=Job::class, mappedBy="worker")
+      */
+     private $jobs;
  }

リレーションシップを張る場合は id プロパティの可視性は private にしておかないと doctrine:migrations:diff したときに

Column name `id` referenced for relation from App\Entity\Job towards App\Entity\Worker does not exist.

というエラーになります。

/**
 * @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;
}

しかし、実際にDBのスキーマを確認してみれば、これが残念ながら機能しないことがすぐに分かります。

具体的には、

  • employee
  • freelancer
  • job

という3つのテーブルが作られ、 job テーブルに worker_id というカラムが作られます。

が、当たり前ですがこの worker_id カラムには 外部キー制約が設定されません。worker というテーブルはないので)

なので、例えば worker_id カラムに 1 が入っていたとして、それが employee テーブルの id=1 を表すのか、 freelancer テーブルの id=1 を表すのかは判別不可能なので、機能しません。

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

こちらの記事で Single Table Inheritance を使った方法を紹介しています。こちらの方法ならリレーションシップを持たせることも可能なので、参考にしてみてください。

まとめ

  • Doctrineの MappedSuperclass を使えばエンティティに基底クラスを持たせることができる
  • 他のエンティティから基底クラスへのManyToOneリレーションシップは機能しないので要注意
GitHubで編集を提案

Discussion