こんにちは!下田です。

Symfony Advent Calendar 11日目の記事です。若干の時空間異常が起きているようですが気にしないでおきます。

さて今回はみんな大好き(?)Inheritance Mappingの話です。

2015年の記事ではありますが、弊社ブログでも過去に紹介しております。

リレーションと組み合わせて使うとすごい

今日紹介したかったのはこちらです。「リレーションと組み合わせるとすごい」という話です。

以下、簡単な例で説明します。

前提

Department(部署)クラスとEmployee(社員)クラスがあり、部署クラスに複数の社員クラスがOneToManyで紐付いている状況を考えます。

Departmentクラス

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * 部署クラス
 * @ORM\Entity
 */
class Department
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @var Collection<Employee>
     * @ORM\OneToMany(targetEntity="Employee",mappedBy="department", fetch="EAGER")
     */
    private $employees;

    public function __construct()
    {
        $this->employees = new ArrayCollection();
    }

    /**
     * @return Collection
     */
    public function getEmployees(): Collection
    {
        return $this->employees;
    }
}

Employeeクラス

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 社員クラス
 * @ORM\Entity
 */
abstract class Employee
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(type="integer")
     */
    private $id;

    /**
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="employees")
     * @ORM\JoinColumn(name="department_id", referencedColumnName="id")
     */
    private $department;
}

但し、社員クラスはRegularEmployee(正社員),ContractEmployee(契約社員)の2種類があり、Inheritance Mappingで表現されているものとします。

Employeeクラスにmapping設定を書いて、

/**
 * @ORM\Entity
+ * @ORM\InheritanceType("SINGLE_TABLE")
+ * @ORM\DiscriminatorColumn(name="type", type="string")
+ * @ORM\DiscriminatorMap({"regular" = "RegularEmployee", "contract" = "ContractEmployee"})
 */
abstract class Employee
{
}

RegularEmployee,ContractEmployeeクラスを定義します。

<?php


namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 正社員
 * @ORM\Entity
 */
class RegularEmployee extends Employee
{
}
<?php


namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * 契約社員
 * @ORM\Entity
 */
class ContractEmployee extends Employee
{
}

図にするとこんな関係ですね。

class


さてこのとき、部署クラスの getEmployee() で取得できるのは

  • (正社員,契約社員の集合である)社員クラスのCollection

ですが、

  • 正社員のみのCollection
  • 契約社員のみのCollection

をそれぞれ取得したい場合も出てくると思います。さて、どう実装すればよいでしょうか?


結論から述べると、普通にMappingアノテーションのみで定義できちゃうんです。 やってみます。

<?php

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\DepartmentRepository")
 */
class Department
{
    // ...

    /**
     * 社員の集合
     * @var Collection<Employee>
     * @ORM\OneToMany(targetEntity="Employee",mappedBy="department", fetch="EAGER")
     */
    private $employees;

    /**
     * 正社員のみの集合
     * @var Collection<RegularEmployee>
     * @ORM\OneToMany(targetEntity="RegularEmployee",mappedBy="department", fetch="EAGER")
     */
    private $regularEmployees;

    /**
     * 契約社員のみの集合
     * @var Collection<ContractEmployee>
     * @ORM\OneToMany(targetEntity="ContractEmployee",mappedBy="department", fetch="EAGER")
     */
    private $contractEmployees;

    public function __construct()
    {
        $this->employees = new ArrayCollection();
        $this->regularEmployees = new ArrayCollection();
        $this->contractEmployees = new ArrayCollection();
    }

    /**
     * @return Collection
     */
    public function getEmployees(): Collection
    {
        return $this->employees;
    }

    /**
     * @return Collection
     */
    public function getRegularEmployees(): Collection
    {
        return $this->regularEmployees;
    }

    /**
     * @return Collection
     */
    public function getContractEmployees(): Collection
    {
        return $this->contractEmployees;
    }
}

こんな感じで、Employee/RegularEmployee/ContractEmployeeをそれぞれプロパティとして定義しておくと、きちんと所望のクラス別に分けて取得することができます。

正/契約社員クラスに対しての OneToManyを定義したので、正/契約社員クラス側にも ManyToOneを書いておきましょう。

class RegularEmployee extends Employee
{
    /**
     * 所属部署
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="regularEmployees")
     */
    private $department;
}
class ContractEmployee extends Employee
{
    /**
     * 所属部署     
     * @ORM\ManyToOne(targetEntity="Department", inversedBy="contractEmployees")
     */
    private $department;
}

動作確認

実際に取得できるか確かめてみます。一旦Repositoryを作っておいて、

<?php

namespace App\Repository;

use App\Entity\Department;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class DepartmentRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Department::class);
    }
}
<?php

namespace App\Repository;

use App\Entity\Employee;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class EmployeeRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Employee::class);
    }
}

DBを更新し、実際にデータを入れてみます。

INSERT INTO `department` (`id`)
VALUES (1);

INSERT INTO `employee` (`id`, `type`, `department_id`)
VALUES
	(1, 'regular', 1),
	(2, 'regular', 1),
	(3, 'regular', 1),
	(4, 'contract', 1),
	(5, 'contract', 1),
	(6, 'contract', 1);

departmentテーブル

id
1

Department(部署)はid=1の一つだけです。

employeeテーブル

id department_id type
1 1 regular
2 1 regular
3 1 regular
4 1 contract
5 1 contract
6 1 contract

正社員,契約社員がそれぞれ3名ずつ、合計6名の社員がDepartment(部署) id=1にぶら下がっている形です。

これでデータの準備は完了です。

さて、確認方法は何でもよいのですが、DepartmentControllerをシュッと作って、dumpして見てみましょう。

<?php

namespace App\Controller;

use App\Entity\Employee;
use App\Entity\RegularEmployee;
use App\Entity\ContractEmployee;
use App\Entity\Department;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DepartmentController extends AbstractController
{
    /**
     * @Route("/department", name="department")
     */
    public function index()
    {
        /** @var Department $department */
        $department = $this->get('doctrine')->getRepository(Department::class)->find(1);

        dump($department->getEmployees());
        dump($department->getRegularEmployees());
        dump($department->getContractEmployees());

        // ...
    }
}

https://127.0.0.1:8000/department にアクセスしてみます。

おお!

  • (正社員,契約社員の集合である)社員クラスのCollection
  • 正社員のみのCollection
  • 契約社員のみのCollection

をそれぞれ取得できているようですね。Doctrineは賢い!

Repository使ってもすごい

ついでに、部署からのリレーションではなくRepositoryから取得する場合を考えます。先程作ったコレですね。

<?php

namespace App\Repository;

use App\Entity\Employee;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;

class EmployeeRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Employee::class);
    }
}

さて、正/契約社員をどう出し分けたら良いでしょうか?

…typeで判別する?

…RegularEmployeeRepository,ContractEmployeeRepositoryをそれぞれ作る必要がある?


正解はこれです。

$employeeRepo = $this->get('doctrine')->getRepository(Employee::class);
$regularEmployeeRepo = $this->get('doctrine')->getRepository(RegularEmployee::class);
$contractEmployeeRepo = $this->get('doctrine')->getRepository(ContractEmployee::class);

EmployeeRepositoryさえあれば、getRepository()する時に具象クラス名を指定すれば、各々の具象クラスに対してのRepositoryとして働いてくれます。

やってみます。さっきのDepartmentControllerを書き換えて、

<?php

namespace App\Controller;
class DepartmentController extends AbstractController
{
    /**
     * @Route("/department", name="department")
     */
    public function index()
    {
        $employee = $this->get('doctrine')->getRepository(Employee::class)->findAll();
        $regularEmployee = $this->get('doctrine')->getRepository(RegularEmployee::class)->findAll();
        $contractEmployee = $this->get('doctrine')->getRepository(ContractEmployee::class)->findAll();
        
        dump($employee);
        dump($regularEmployee);
        dump($contractEmployee);

        // ...
    }
}

この状態で https://127.0.0.1:8000/department を見てみましょう。

 2019-12-18 9.52.51.png (143.0 kB)

きちんと区別して取得できていますね。

(でもこれ、Controllerだからできる事であって、RepositoryをDIする利用法と相性悪いような気もしますが…)

おわりに

今回はInheritance Mappingについての話でした。

ついついテーブル設計側(DB側)に着目してしまいがちですが、利用するアプリケーション側の挙動もきちんと考えられていて、Doctrineはすごいなぁと。

みんな大好き(?)と冒頭では疑問形で書きましたが、この記事で少しDoctrineのことを好きになってもらえれば幸いです。

今回の例をgithubに上げておいたので、ご興味ある方はぜひ触ってみて下さい!