先月からSymfony2とDoctrine2を触り始めた新人の永井です。
早速Doctrine2のInheritance Mappingという機能を試したので、紹介をしたいと思います。
1つの親エンティティに対していくつかのサブパターンが存在するようなデータ構造を設計することって良くありますよね。
良く見るクラス図をサンプルにするとこんな感じの構造です。
上記の図の場合だと、
- どんな図形かについては関心がなく色分けだけしたい場合
- 正方形のみを取り扱いたい場合
など、やりたいことがいろんな階層である場合に、こういったデータ構造になることがあるかと思います。
OOPの世界では良くある構造なのですが、継承という概念のないRDB上に再現しようとすると途端に面倒になります。
そこで、Doctrine2のInheritance Mappingの登場です。
今回はMartin Fowler先生のPofEAAで紹介されている2つのパターン
を実装してみたいと思います。
Single Table Inheritance
Single Table Inheritanceは全ての階層構造を一つのテーブルで表現したものです。
Martin Fowler先生のウェブサイトで紹介されている構造を実際に実装してみたいと思います。
具体的には、こんな感じでエンティティクラスを作成していきます。
ディレクトリ構造
├── Entity
│ ├── Player
│ │ ├── Bowler.php
│ │ ├── Cricketer.php
│ │ ├── Footballer.php
│ │ └── Player.php
各クラス
Player.php
<?php
namespace AppBundle\Entity\Player;
use Doctrine\ORM\Mapping as ORM;
/**
* Player
* @ORM\InheritanceType("SINGLE_TABLE")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\Entity
*/
class Player
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=255)
*/
private $name;
//...
}
Cricketer.php
<?php
namespace AppBundle\Entity\Player;
use Doctrine\ORM\Mapping as ORM;
/**
* Cricketer
*
* @ORM\Entity
*/
class Cricketer extends Player
{
/**
* @var integer
*
* @ORM\Column(name="batting_average", type="integer")
*/
private $battingAverage;
//...
}
Bowler.php
<?php
namespace AppBundle\Entity\Player;
use Doctrine\ORM\Mapping as ORM;
/**
* Bowler
*
* @ORM\Entity
*/
class Bowler extends Cricketer
{
/**
* @var integer
*
* @ORM\Column(name="bowling_average", type="integer")
*/
private $bowlingAverage;
//...
}
Footballer.php
<?php
namespace AppBundle\Entity\Player;
use Doctrine\ORM\Mapping as ORM;
/**
* Footballer
*
* @ORM\Entity
*/
class Footballer extends Player
{
/**
* @var string
*
* @ORM\Column(name="club", type="string", length=255)
*/
private $club;
//...
}
実際にスキーマを生成すると、以下のようなSQLが実行されます。
CREATE TABLE player
(
id INT AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
batting_average INT NOT NULL,
bowling_average INT NOT NULL,
club VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
複数のエンティティをマージした結果が反映されて、一つのテーブルにまとめられていますね。
素晴らしい!
実際にレコードを挿入してみる
コントローラーとかで以下の様にデータを挿入してみると・・・
<?php
$manager = $this->getDoctrine()->getManager();
$bowler = new Bowler();
$bowler->setName('野茂英雄');
$bowler->setBattingAverage(100);
$bowler->setBowlingAverage(200);
$manager->persist($bowler);
$cricketer = new Cricketer();
$cricketer->setName('鈴木一郎');
$cricketer->setBattingAverage(150);
$manager->persist($cricketer);
$footballer = new Footballer();
$footballer->setName('三浦知良');
$footballer->setClub('カルテットFC');
$manager->persist($footballer);
$manager->flush();
実行後のテーブルの状況
player
id | name | type | batting_average | bowling_average | club |
---|---|---|---|---|---|
1 | 三浦知良 | footballer | NULL | NULL | カルテットFC |
2 | 鈴木一郎 | cricketer | 150 | NULL | NULL |
3 | 野茂英雄 | bowler | 100 | 200 | NULL |
一つのテーブルで完結している分見通しが良く感じます。
ただ、項目が増えてくると混乱してきそうですね。
また利用されていないカラムにはNULLがセットされていることもわかります。
この様にレコードに関係のないカラムには強制的にNULLが設定される仕様なので、サブクラスのみが利用するカラムにはNOT NULL制約を設定することは出来ません。
Class Table Inheritance
Class Table Inheritanceは、複数テーブルで継承関係を表現したものです。
こちらもSingle Table Inheritanceで実装したものと同じ構造を実装してみます。
Playerエンティティクラスを以下の様に変更します。
<?php
//...
/**
* Player
- * @ORM\InheritanceType("SINGLE_TABLE")
+ * @ORM\InheritanceType("JOINED")
* @ORM\DiscriminatorColumn(name="type", type="string")
* @ORM\Entity(repositoryClass="AppBundle\Entity\PlayerRepository")
*/
これだけでClass Table Inheritanceへの変更は完了です。
簡単ですね。
実際にスキーマを生成すると、以下のようなSQLが実行されます。
CREATE TABLE player
(
id INT AUTO_INCREMENT NOT NULL,
name VARCHAR(255) NOT NULL,
type VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE cricketer
(
id INT NOT NULL,
batting_average INT NOT NULL,
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE bowler
(
id INT NOT NULL,
bowling_average INT NOT NULL,
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
CREATE TABLE footballer
(
id INT NOT NULL,
club VARCHAR(255) NOT NULL,
PRIMARY KEY(id)
)
DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE = InnoDB;
ALTER TABLE cricketer ADD CONSTRAINT FK_EABF45F3BF396750 FOREIGN KEY (id) REFERENCES player (id) ON DELETE CASCADE;
ALTER TABLE bowler ADD CONSTRAINT FK_FE38CAFDBF396750 FOREIGN KEY (id) REFERENCES player (id) ON DELETE CASCADE;
ALTER TABLE footballer ADD CONSTRAINT FK_DA9711BABF396750 FOREIGN KEY (id) REFERENCES player (id) ON DELETE CASCADE;
注目は
- それぞれのサブテーブルのidがplayerテーブルのidを外部キーとしている。
- bowlerテーブルには、継承しているcricketerテーブルの情報は含まれていない
- DELETE CASCADEが設定されている
といったところでしょうか。
実際にレコードを挿入してみる
Single Table Inheritanceの時と全く同じ内容でレコードを追加してみます。
実行後のテーブルの状況
player
id | name | type |
---|---|---|
1 | 三浦知良 | footballer |
2 | 鈴木一郎 | cricketer |
3 | 野茂英雄 | bowler |
cricketer
id | batting_average |
---|---|
2 | 150 |
3 | 100 |
bowler
id | bowling_average |
---|---|
3 | 200 |
footballer
id | club |
---|---|
1 | カルテットFC |
Bowlerのデータが、継承元であるCricketerやPlayerそれぞれのテーブルに分けて挿入されていることが確認できます。
素晴らしいですね。
Single Table Inheritanceと比較して各テーブルのカラム数が減って見通しが良くなっているように感じます。
ただ、サブクラスを増やす毎にJOINするテーブルが増えていくことになるので、Single Table Inheritanceと比較するとパフォーマンス面での不安はある様に思います。
DiscriminatorMap
Doctrine2の公式サイトのサンプルコードには、アノテーションによる設定が記載されています。
<?php
namespace MyProject\Model;
/**
* @Entity
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name="discr", type="string")
* @DiscriminatorMap({"person" = "Person", "employee" = "Employee"}) // DiscriminatorMapの設定箇所
*/
class Person
{
// ...
}
しかし、同じ名前空間で作成されているというルールに基づいて各クラスが作成されていれば、省略することも可能です。
サブクラスが増えてくるとアノテーションに記載する量も増えてきてしまうので、この様なオプションは助かりますね。
最後に
これらを自分で全て管理しようとするとかなり複雑になるのが予想されますので、Doctrine2が提供してくれるのはとてもありがたいなと思います。
また、Single Table InheritanceパターンやClass Table Inheritanceパターンといったライブラリやフレームワークに依存しない汎用的なデザインパターンが採用されているのも、好感が持てます。
日本語の情報が多くないですが、利用する機会が多い機能かと思いますので、是非利用してみてください。