先月からSymfony2とDoctrine2を触り始めた新人の永井です。
早速Doctrine2のInheritance Mappingという機能を試したので、紹介をしたいと思います。

1つの親エンティティに対していくつかのサブパターンが存在するようなデータ構造を設計することって良くありますよね。
良く見るクラス図をサンプルにするとこんな感じの構造です。

sample-class-diagram

上記の図の場合だと、

  • どんな図形かについては関心がなく色分けだけしたい場合
  • 正方形のみを取り扱いたい場合

など、やりたいことがいろんな階層である場合に、こういったデータ構造になることがあるかと思います。
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パターンといったライブラリやフレームワークに依存しない汎用的なデザインパターンが採用されているのも、好感が持てます。
日本語の情報が多くないですが、利用する機会が多い機能かと思いますので、是非利用してみてください。