Doctrineの継承機能について
こんにちは。小川です。
よくあるオブジェクトリレーショナルマッパーは基本的に1つのテーブルに対して1つのモデルオブジェクトを定義します。このときにモデルオブジェクト間に親子関係を持たせたり、いくつかのモデルに対して抽象クラスを作りたいと思ったことは ありませんか?Doctrineには継承に関する3つのパターンが用意されており、今回はそれをご紹介したいと思います。
まずは3つのパターンの概要を説明します。
・Simple inheritance
単純に1つのテーブルに対して複数のモデルを作成する方法です。
どのレコードがどのモデルに対応するかは特に判断は行いません。
・Concrete inheritance
モデル1つひとつに対してテーブルを作成する方法です。
テーブルごと分けるため、カラムもテーブルごとに定義されます。
・Column Aggregation inheritance
単純に1つのテーブルに対して複数のモデルを作成する方法です。
Simple inheritanceと違い、各レコードに対してモデルに対応するキーを付与してレコードの判断を行います。
では実際のコードを見つつ、詳細について説明していきたいと思います。
◆Simple inheritance
# schema.yml
Log:
columns:
code: string(255)
message: string(255)
ErrorLog:
inheritance:
extends: Log
type: simple
columns:
exception_class: string(255)
ApplicationLog:
inheritance:
extends: Log
type: simple
<?php
// Log.php
class Log extends sfDoctrineRecord
{
public function setTableDefinition()
{
$this->setTableName('log');
$this->hasColumn('code', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('message', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('exception_class', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
}
}
// ErrorLog.php
class ErrorLog extends Log
{
}
// ApplicationLog.php
class ApplicationLog extends Log
{
}
一番単純なSimple inheritanceです。上記はログを残すためのLogテーブルと、ログの種類に分けていくつかの子クラスを作成する例になります。
この方法では取得した際にレコードがどのモデルに対応するのかは判断しないため、あまり使わないのではないかと思います。
上記のようなログの場合に、取得は全部まとめてやるけど作成するときに色々操作を変えたいというときなどに使用するとよいかと思います。
◆Concrete inheritance
AbstractUser:
abstract: true
columns:
email: string(255)
name: string(255)
password: string(255)
User:
inheritance:
extends: AbstractUser
type: concrete
columns:
zip: string(8)
address1: string(255)
address2: string(255)
Admin:
inheritance:
extends: AbstractUser
type: concrete
<?php
// AbstractUser.php
abstract class AbstractUser extends sfDoctrineRecord
{
public function setTableDefinition()
{
$this->setTableName('abstract_user');
$this->hasColumn('email', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('name', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('password', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
}
}
// User.php
class User extends AbstractUser
{
public function setTableDefinition()
{
parent::setTableDefinition();
$this->setTableName('user');
$this->hasColumn('zip', 'string', 8, array(
'type' => 'string',
'length' => '8',
));
$this->hasColumn('address1', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('address2', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
}
}
// Admin.php
class Admin extends AbstractUser
{
public function setTableDefinition()
{
parent::setTableDefinition();
$this->setTableName('admin');
}
}
上記はUserの抽象クラスを作成して、それをベースにしたuserとadminテーブルを作成し、それぞれに対応するUserモデルとAdminモデルを作成する例です。
AbstractUserというモデルが抽象クラスとなります。schemaでabstractという属性を定義していますが、これをつけたモデルは抽象クラスとして定義されます。
先ほどとは違いテーブルが複数できるため、上記の用にテーブルは分けたい場合に使用します。なお親クラスに定義してあるカラムはそれぞれのテーブルに作成されます。
上記の例では抽象クラスを使用していますが、もちろん抽象クラスである必要はありません。とにかくテーブルを分けたい場合はConcrete inheritanceを使用すると思ってください。
◆Column Aggregation inheritance
Product:
columns:
name: string(255)
description: string(255)
price: integer(4)
stock: integer(4)
Book:
inheritance:
extends: Product
type: column_aggregation
columns:
author: string(255)
Wear:
inheritance:
extends: Product
type: column_aggregation
columns:
size: string(255)
brand: string(255)
<?php
// Product.php
class Product extends sfDoctrineRecord
{
public function setTableDefinition()
{
$this->setTableName('product');
$this->hasColumn('name', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('description', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('price', 'integer', 4, array(
'type' => 'integer',
'length' => '4',
));
$this->hasColumn('stock', 'integer', 4, array(
'type' => 'integer',
'length' => '4',
));
$this->hasColumn('type', 'string', 255, array(
'type' => 'string',
'length' => 255,
));
$this->hasColumn('author', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('size', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->hasColumn('brand', 'string', 255, array(
'type' => 'string',
'length' => '255',
));
$this->setSubClasses(array(
'Book' =>
array(
'type' => 'Book',
),
'Wear' =>
array(
'type' => 'Wear',
),
));
}
}
// Book.php
class Book extends Product
{
}
// Wear.php
class Wear extends Product
{
}
Simple inheritanceと同様に単一のテーブルを複数のモデルで共有するColumn Aggregation inheritanceですが、Simple inheritanceとは大きな違いがあります。
Productクラスの定義の中に、typeというカラムが定義されています。これはColumn Aggregation inheritanceがレコードがどのモデルに対応するのかを特定するためのカラムです。setSubClassesというメソッドで実際にどのモデルに対してどの値が入るのかを定義しています。デフォルトでは子クラスのクラス名がtypeカラムに入るようになっているのでBookモデルの場合はBook、Wearモデルの場合はWearという文字が入るようになっています。
またこのtypeに入る値や、typeというカラム名は変更可能です。schema.ymlでは以下のようになります。
Product:
columns:
name: string(255)
description: string(255)
price: integer(4)
stock: integer(4)
Book:
inheritance:
extends: Product
type: column_aggregation
keyField: category
keyValue: 1
columns:
author: string(255)
Wear:
inheritance:
extends: Product
type: column_aggregation
keyField: category
keyValue: 2
columns:
size: string(255)
brand: string(255)
inheritanceの項目の中にkeyFieldという属性でフィールド名を、keyValueという属性で値を定義します。上記はcategoryというカラムが作成され、Bookの場合は1が、Wearの場合は2が入るようになります。
このkeyFieldで指定したカラム(もしくはtypeカラム)は親クラスに自分自身で定義しても問題はありません。
Column Aggregation inheritanceを用いた場合、親テーブルクラスから値を取得してきてもきちんとkeyFieldを判断して適切なクラスへキャストしてくれますので、Simple inheritanceよりも使えるシーンは多いかと思います。調べ切れていないのですが、たぶんkeyFieldカラムがkeyValueと一致するといった単純な内容でしか判断はできないと思います。複雑な条件を用いる場合はpreDqlSelectなどのフックメソッドを用いて判断する方法が思いつきますが、できる限りDoctrineにあわせた設計をしていく方が賢明ではないかと思います。
紹介は以上になります。上記のことは大抵Doctrineのドキュメントに書いてありますので、興味のある方は是非見てみてください。
Doctrine ORM for PHP - Inheritance
継承の他にもDoctrineにはTemplate(Behavior)という抽象化のための機能があります。Templateは既に定義されているモデルに対して機能をMix-inを行うための仕組みです。汎用性はありますがどうしてもRecordを外部から操作せざるを得ないため、Templateだけでは中々やりたいことが実現できないのではないかと思います。
継承を用いた場合はRecordはもちろん、Tableも継承されますので、ファインダメソッドの抽象化なども非常に簡単に行うことができます。是非こういったDoctrineの機能を活用してみてください!