こんにちは!下田です。
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
{
}
図にするとこんな関係ですね。
さてこのとき、部署クラスの 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
を見てみましょう。
きちんと区別して取得できていますね。
(でもこれ、Controllerだからできる事であって、RepositoryをDIする利用法と相性悪いような気もしますが…)
おわりに
今回はInheritance Mappingについての話でした。
ついついテーブル設計側(DB側)に着目してしまいがちですが、利用するアプリケーション側の挙動もきちんと考えられていて、Doctrineはすごいなぁと。
みんな大好き(?)と冒頭では疑問形で書きましたが、この記事で少しDoctrineのことを好きになってもらえれば幸いです。
今回の例をgithubに上げておいたので、ご興味ある方はぜひ触ってみて下さい!