以前述べたように、Doctrineの最も低いレベルにおいてスキーマはデータベーステーブル用のスキーマメタデータをマッピングするPHPクラスの一式で表現されます。
この章ではPHPコードを使用してスキーマ情報をマッピングする方法を詳しく説明します。
データベースの互換性の問題の1つは多くのデータベースにおいて返されるクエリの結果セットが異なることです。MySQL はフィールドの名前はそのままにします。このことは"SELECT myField FROM ..."形式のクエリを発行する場合、結果セットはmyFieldのフィールドを含むことを意味します。
不幸にして、これはMySQLとその他のいくつかのデータベースだけの挙動です。例えばPostgresはすべてのフィールド名を小文字で返す一方でOracleは大文字ですべてのフィールド名を返します。"だから何?Doctrineを使う際にこれがどのような方法で影響を及ぼすの?"、と疑問に思うかもしれません。幸いにして、この問題を悩む必要はまったくありません。
Doctrineはこの問題を透過的に考慮します。Record派生クラスを定義しmyFieldという名前のフィールドを定義する場合、MySQLもしくはPostgresもしくはOracleその他を使おうが、$record->myField (もしくは$record['myField']、好きな方で)を通してアクセスできることを意味します。
要するに: under_scores(アンダースコア)、camelCase(キャメルケース)もしくは望む形式を使用してフィールドを好きなように名付けることができます。
Doctrineにおいてカラムとカラムのエイリアスは大文字と小文字を区別します。DQLクエリでカラムを使用するとき、カラム/フィールドの名前はモデルの定義のケースにマッチしなければなりません。
Doctrineはカラムのエイリアスを定義する方法を提供します。これはデータベースのロジックからアプリケーションのロジックを分離するのを維持したい場合にとても役に立ちます。例えば データベースフィールドの名前を変更したい場合、アプリケーションで変更する必要のあるのはカラムの定義だけです。
// models/Book.php
class Book extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('bookTitle as title', 'string');
}
}
下記のコードはYAMLフォーマットのサンプルです。YAML Schema Filesの章でYAMLの詳しい情報を読むことができます:
---
# schema.yml
# ...
Book:
columns:
bookTitle:
name: bookTitle as title
type: string
Now データベースのカラムはbookTitleという名前ですがtitleを使用してオブジェクトのプロパティにアクセスできます。
// test.php
// ...
$book = new Book();
$book->title = 'Some book';
$book->save();
Doctrineはすべてのデータ型のデフォルト値をサポートします。デフォルト値がレコードのカラムに付属するとき2つのことを意味します。まずこの値はすべての新しく作成されたRecordに添付されDoctrineがデータベースを作成するときにcreate tableステートメントにデフォルト値を含めます。
// models/generated/BaseUser.php
class User extends BaseUser
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 255, array('default' => 'default username'));
// ...
}
// ...
}
YAMLフォーマットのサンプルコードは次の通りです。YAML Schema Filesの章でYAMLの詳細情報を読むことができます:
---
# schema.yml
# ...
User:
# ...
columns:
username:
type: string(255)
default: default username
# ...
真新しいUserレコードで名前を表示するときデフォルト値が表示されます:
// test.php
// ...
$user = new User();
echo $user->username; // デフォルトのユーザー名
データベースフィールドに保存できる情報用にすべてのDBMSはデータの型の複数の選択肢を提供します。しかしながら、利用可能なデータ型の一式はDBMSによって異なります。
DoctrineによってサポートされるDBMSでインターフェイスを簡略化するために、内在するDBMSにおいてアプリケーションが個別にアクセスできるデータ型の基本セットが定義されました
Doctrineのアプリケーションプログラミングインターフェイスはデータベースオプションを管理する際にデータ型のマッピングを考慮します。それぞれのドライバを使用して内在するDBMSに送るかつDBMSから受け取るものを変換することも可能です。
次のデータ型の例ではDoctrineのcreateTable()メソッドを使います。データ型セクションの最後の配列の例では選んだDBMSでポータブルなテーブルを作成するためにcreateTable()メソッドを使うことがあります(何のDBMSが適切にサポートされているか理解するためにDoctrineのメインドキュメントを参照してくださるようお願いします)。次の例ではインデックスの作成と維持はカバーされないことも注意してください。この章はデータ型と適切な使い方のみを考慮します。
アプリケーションレベルでバリデートされた長さ(Doctrineバリデータでバリデートされた長さ)と同様に、カラムの長さはデータベースレベルで影響があることを気を付けてください。
例 1. 'string'型と長さ3000の'content'という名前のカラムはデータベースレベルの長さ4000を持つ'TEXT'データベースの型になります。しかしながらレコードがバリデートされるとき最大長が3000である'content'カラムを持つことのみ許可されます。
例 2. 多くのデータベースでは'integer'型と長さ1を持つカラムは'TINYINT'になります。
一般的に Doctrineは指定された長さによってどのinteger/string型を使うのか知っているほど賢いです。
Doctrine APIの範囲内で オプションのテーブルデザインに役立つように設計された修飾子が少しあります:
上記の内容に基づいて話しを進めると、特定の使い方のシナリオ用の特定のフィールドの型を作成するために、修飾子がフィールド定義を変更することが言えます。DBMSのフィールド値の定義によって、フィールド上でデフォルトのDBMS NOT NULL Flagをtrueもしくはfalseに設定するためにnotnull修飾子は次の方法で使われます: PostgreSQLにおいて"NOT NULL"の定義は"NOT NULL"に設定される一方で、(例えば)MySQLでは"NULL"オプションは"NO"に設定されます。"NOT NULL"フィールド型を定義するために、定義配列に追加パラメータを追加するだけです(例は次のセクションを参照)。
'sometime' = array(
'type' => 'time',
'default' => '12:34:05',
'notnull' => true,
),
上記の例を利用することで、デフォルトのフィールド演算子も研究できます。フィールド用のデフォルト値はnotnull演算子と同じ方法で設定されます。この値はDBMSがテキストフィールド用にサポートする文字集合、フィールドのデータ型用の他の有効な値に設定されます。上記の例において、"Time"データ型に対して有効な時間である'12:34:05'を指定しました。datetimeと同じく日付と時間を設定するとき、調べて選択したDBMSのエポックの範囲に収まるようにすべきであることを覚えておいてください。さもなければエラーを診断するのが困難な状況に遭遇します!
'sometext' = array(
'type' => 'string',
'length' => 12,
),
上記の例ではデータベースのテーブルで長さ12のフィールドを変更する文字が作られます。長さの定義が省略される場合、Doctrineは指定されたデータ型で許容される最大長を作成されます。これはフィールドの型とインデックス作成において問題を引き起こす可能性があります。ベストプラクティスはすべてもしくは大抵のフィールドに対して長さを定義することです。
論理型は0か1の2つの値のどちらかだけを表します。効率性の観点からDBMSドライバの中には単独の文字のテキストフィールドで整数型を実装するものがあるのでこれらの論理型を整数型として保存されることを想定しないでください。この型のフィールドに割り当てできる3番目の利用可能な値としてnullを使うことで三値論理は可能です。
次のいくつかの例では使ったり試したりすることを想定していません。これらは単にPHPコードもしくはYAMLスキーマファイルを利用してDoctrineの異なるデータ型を使う方法を示すことだけを目的としています。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('booltest', 'boolean');
}
}
YAMLフォーマットでの同じ例です。YAML Schema Filesの章でYAMLの詳細内容を見ることができます:
---
Test:
columns:
booltest: boolean
整数型はPHPの整数型と同じです。それぞれのDBMSが扱える大きさの整数型の値を保存します。
オプションとしてこの型のフィールドは符号なしの整数として作成されますがすべてのDBMSはこれをサポートしません。それゆえ、このようなオプションは無視されることがあります。本当にポータルなアプリケーションはこのオプションの利用可能性に依存すべきではありません。
整数型はカラムの長さによって異なるデータベースの型にマッピングされます。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('integertest', 'integer', 4, array(
'unsigned' => true
)
);
}
}
YAMLフォーマットでの例です。YAML Schema Filesの章っでYAMLの詳細情報を読むことができます:
---
Test:
columns:
integertest:
type: integer(4)
unsigned: true
浮動小数点のデータ型は10進法の浮動小数点数を保存できます。このデータ型は高い精度を必要としない大きなスケールの範囲の数値を表現するのに適しています。スケールと精度に関してデータベースに保存される値の制限は使用されるDBMSに依存します。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('floattest', 'float');
}
}
下記のコードはYAMLフォーマットでの例です。YAML Schema Filesの章でYAMLの詳細情報を読むことができます:
---
Test:
columns:
floattest: float
小数型のデータは固定精度の小数を保存できます。このデータ型は高い正確度と精度を必要とする数値を表現するのに適しています。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('decimaltest', 'decimal');
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を学ぶことができます:
---
Test:
columns:
decimaltest: decimal
他のカラムのlengthを設定するように小数の長さを指定することが可能で3番目の引数でオプションとしてscaleを指定できます:
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('decimaltest', 'decimal', 18, array(
'scale' => 2
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細情報をみることができます:
---
Test:
columns:
decimaltest:
type: decimal(18)
scale: 2
テキストデータ型では長さに対して2つのオプションが利用可能です: 1つは明示的に制限された長さでもう一つはデータベースが許容する限りの大きさの未定義の長さです。
効率の点で制限オプション付きの長さは大抵の場合推奨されます。未定義の長さオプションはとても大きなフィールドを許可しますがインデックスとnullの利用を制限することがあり、その型のフィールド上でのソートを許可しません。
この型のフィールドは8ビットの文字を扱うことができます。文字列の値をこの型に変換することでドライバはDBMSで特別な意味を持つ文字のエスケープを考慮します。
デフォルトではDoctrineは可変長の文字型を使用します。固定長の型が使われる場合、fixed修飾子を通してコントロールできます。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('stringtest', 'string', 200, array(
'fixed' => true
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細情報を見ることができます:
---
Test:
columns:
stringtest:
type: string(200)
fixed: true
これはPHPの'array'型と同じです。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('arraytest', 'array', 10000);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細情報を見ることができます:
---
Test:
columns:
arraytest: array(10000)
Doctrineはオブジェクトをカラム型としてサポートします。基本的にオブジェクトをフィールドに設定可能でDoctrineはそのオブジェクトのシリアライズ/アンシリアライズを自動的に処理します。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('objecttest', 'object');
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細情報を読むことができます:
---
Test:
columns:
objecttest: object
配列とオブジェクト型はデータベースで永続化するときはデータをシリアライズしデータベースから引き出すときはデータをアンシリアライズします
blob(Binary Large OBject)データ型は、通常はファイルに保存されるデータのようにテキストフィールドに大きすぎる未定義の長さのデータを保存することを意味します。
内在するDBMSが"全文検索"として知られる機能をサポートしない限りblobフィールドはエリーの検索句(WHERE)のパラメータを使用することを意味しません。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('blobtest', 'blob');
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
blobtest: blob
clob (Character Large OBject)データ型は、通常はファイルに保存されるデータのように、テキストフィールドで保存するには大きすぎる未定義の長さのデータを保存することを意味します。
blogフィールドがデータのすべての型を保存するのが想定されているのに対してclobフィールドは印字可能なASCII文字で構成されるデータのみを保存することを想定しています。
内在するDBMSが"全文検索"として知られる機能をサポートしない限りclobフィールドはクエリ検索句(WHERE)のパラメータとして使われることが想定されています。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('clobtest', 'clob');
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
clobtest: clob
timestampデータ型は日付と時間のデータ型の組み合わせに過ぎません。timestamp型の値の表記は日付と時間の文字列の値は1つのスペースで連結することで実現されます。それゆえ、フォーマットのテンプレートはYYYY-MM-DD HH:MI:SSです。表される値は日付と時間データ型で説明したルールと範囲に従います。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('timestamptest', 'timestamp');
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
timestamptest: timestamp
timeデータ型はその日の与えられた瞬間の時間を表します。DBMS独自の時間の表記もISO-8601標準に従ってテキストの文字列を使用することで実現できます。
日付の時間用にISO-8601標準で定義されたフォーマットはHH:MI:SSでHHは時間で00から23まででMIとSSは分と秒で00から59までです。時間、分と秒は10より小さな数値の場合は左側に0が詰められます。
DBMSの中にはネイティブで時間フォーマットをサポートするものがありますが、DBMSドライバの中にはこれらを整数もしくはテキストの文字列として表現しなければならないものがあります。ともかく、この型のフィールドによるソートクエリの結果と同じように時間の値の間で比較することは常に可能です。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('timetest', 'time');
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
timetest: time
dateデータ型は年、月と日にちのデータを表します。DBMS独自の日付の表記はISO-8601標準の書式のテキスト文字列を使用して実現されます。
日付用にISO-8601標準で定義されたフォーマットはYYYY-MM-DDでYYYYは西暦の数字(グレゴリオ暦)、MMは01から12までの月でDDは01か31までの日の数字です。10より小さい月の日にちの数字には左側に0が追加されます。
DBMSの中にはネイティブで日付フォーマットをサポートするものがありますが、他のDBMSドライバではこれらを整数もしくはテキストの値として表現しなければならないことがあります。どの場合でも、この型のフィールドによるソートクエリの結果によって日付の間の比較は常に可能です。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('datetest', 'date');
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
datetest: date
Doctrineはunifiedなenum型を持ちます。カラムに対して可能な値はDoctrine_Record::hasColumn()でカラム定義に指定できます。
DBMSに対してネイティブのenum型を使用したい場合次の属性を設定しなければなりません:
$conn->setAttribute(Doctrine_Core::ATTR_USE_NATIVE_ENUM, true);
次のコードはenumの値を指定する方法の例です:
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('enumtest', 'enum', null,
array('values' => array('php', 'java', 'python'))
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
enumtest:
type: enum
values: [php, java, python]
gzipデータ型は存続するときに自動的に圧縮取得されたときに解凍される以外は文字列と同じです。ビットマップ画像など、大きな圧縮率でデータを保存するときにこのデータ型は役に立ちます。
class Test extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('gziptest', 'gzip');
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
Test:
columns:
gziptest: gzip
内部ではgzipカラム型の内容の圧縮と解凍を行うために圧縮系のPHP関数が使われています。
次の定義を考えましょう:
class Example extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('id', 'string', 32, array(
'type' => 'string',
'fixed' => 1,
'primary' => true,
'length' => '32'
)
);
$this->hasColumn('someint', 'integer', 10, array(
'type' => 'integer',
'unsigned' => true,
'length' => '10'
)
);
$this->hasColumn('sometime', 'time', 25, array(
'type' => 'time',
'default' => '12:34:05',
'notnull' => true,
'length' => '25'
)
);
$this->hasColumn('sometext', 'string', 12, array(
'type' => 'string',
'length' => '12'
)
);
$this->hasColumn('somedate', 'date', 25, array(
'type' => 'date',
'length' => '25'
)
);
$this->hasColumn('sometimestamp', 'timestamp', 25, array(
'type' => 'timestamp',
'length' => '25'
)
);
$this->hasColumn('someboolean', 'boolean', 25, array(
'type' => 'boolean',
'length' => '25'
)
);
$this->hasColumn('somedecimal', 'decimal', 18, array(
'type' => 'decimal',
'length' => '18'
)
);
$this->hasColumn('somefloat', 'float', 2147483647, array(
'type' => 'float',
'length' => '2147483647'
)
);
$this->hasColumn('someclob', 'clob', 2147483647, array(
'type' => 'clob',
'length' => '2147483647'
)
);
$this->hasColumn('someblob', 'blob', 2147483647, array(
'type' => 'blob',
'length' => '2147483647'
)
);
}
}
YAMLフォーマットでの同じ例です。YAML Schema Filesの章でYAMLの詳細を読むことができます:
Example:
tableName: example
columns:
id:
type: string(32)
fixed: true
primary: true
someint:
type: integer(10)
unsigned: true
sometime:
type: time(25)
default: '12:34:05'
notnull: true
sometext: string(12)
somedate: date(25)
sometimestamp: timestamp(25)
someboolean: boolean(25)
somedecimal: decimal(18)
somefloat: float(2147483647)
someclob: clob(2147483647)
someblob: blob(2147483647)
上記の例はPgsqlで次のテーブルが作成します:
| カラム | 型 |
|---|---|
| id | character(32) |
| someint | integer |
| sometime | タイムゾーンなしのtime |
| sometext | characterもしくはvarying(12) |
| somedate | date |
| sometimestamp | タイムゾーンなしのtimestamp |
| someboolean | boolean |
| somedecimal | numeric(18,2) |
| somefloat | doubleの精度 |
| someclob | text |
| someblob | bytea |
Mysqlではスキーマは次のデータベーステーブルを作成します:
| フィールド | 型 |
|---|---|
| id | char(32) |
| someint | integer |
| sometime | time |
| sometext | varchar(12) |
| somedate | date |
| sometimestamp | timestamp |
| someboolean | tinyint(1) |
| somedecimal | decimal(18,2) |
| somefloat | double |
| someclob | longtext |
| someblob | longblob |
DoctrineにおいてすべてのレコードのリレーションはDoctrine_Record::hasMany、Doctrine_Record::hasOneメソッドで設定されます。Doctrineはほとんどの種類のデータベースリレーションをサポートします from 一対一のシンプルな外部キーのリレーションから自己参照型のリレーションまでサポートします。
カラムの定義とは異なりDoctrine_Record::hasManyとDoctrine_Record::hasOneメソッドはsetUp()と呼ばれるメソッドの範囲内で設置されます。両方のメソッドは2つの引数を受け取ります: 最初の引数はクラスの名前とオプションのエイリアスを含む文字列で、2番目の引数はリレーションのオプションで構成される配列です。オプションの配列は次のキーを含みます:
| 名前 | オプション | 説明 |
|---|---|---|
| local | No | リレーションのローカルフィールド。ローカルフィールドはクラスの定義ではリンク付きのフィールド。 |
| foreign | No | リレーションの外部フィールド。外部フィールドはリンク付きのクラスのリンク付きフィールドです。 |
| refClass | Yes | アソシエーションクラスの名前。これは多対多のアソシエーションに対してのみ必要です。 |
| owningSide | Yes | 所有側のリレーションを示すには論理型のtrueを設定します。所有側とは外部キーを所有する側です。2つのクラスの間のアソシエーションにおいて所有側は1つのみです。Doctrineが所有側を推測できないもしくは間違った推測をする場合このオプションが必須であることに注意してください。'local'と'foreign'の両方が識別子(主キー)の一部であるときこれが当てはまります。この方法で所有側を指定することは害になることはありません。 |
| onDelete | Yes | Doctrineによってテーブルが適用されるときにonDelete整合アクションが外部キー制約に適用されます。 |
| onUpdate | Yes | Doctrineによってテーブルが作成されたときにonUpdate整合アクションが外部キー制約に適用されます。 |
| cascade | Yes | オペレーションをカスケーディングするアプリケーションレベルを指定する。現在削除のみサポートされる |
最初の例として、Forum_BoardとForum_Threadの2つのクラスがあるとします。リレーションが一対多なので、Forum_Boardは多くのForum_Threadsを持ちます。リレーションにアクセスする際にForum_を書きたくないので、リレーションのエイリアスを使用しエイリアスのThreadsを使用します。
最初にForum_Boardクラスを見てみましょう。これはカラム: 名前, 説明を持ち主キーを指定していないので、Doctrineはidカラムを自動作成します。
hasMany()メソッドを使用することでForum_Threadクラスへのリレーションを定義します。localフィールドがboardクラスの主キーである一方でforeignフィールドがForum_Threadクラスのboard_idフィールドです。
// models/Forum_Board.php
class Forum_Board extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 100);
$this->hasColumn('description', 'string', 5000);
}
public function setUp()
{
$this->hasMany('Forum_Thread as Threads', array(
'local' => 'id',
'foreign' => 'board_id'
)
);
}
}
asキーワードが使われていることに注目してください。このことはForum_BoardがForum_Threadに定義された多数のリレーションを持ちますがThreadsのエイリアスが設定されることを意味します。
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Forum_Board:
columns:
name: string(100)
description: string(5000)
Forum_Threadクラスの内容を少しのぞいて見ましょう。カラムの内容は適当ですが、リレーションの定義方法に注意をはらってください。それぞれのThreadは1つのBoardのみを持つことができるのでhasOne()メソッドを使っています。またエイリアスの使い方とlocalカラムがboard_idである一方で外部カラムはidカラムであることに注目してください。
// models/Forum_Thread.php
class Forum_Thread extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('board_id', 'integer');
$this->hasColumn('title', 'string', 200);
$this->hasColumn('updated', 'integer', 10);
$this->hasColumn('closed', 'integer', 1);
}
public function setUp()
{
$this->hasOne('Forum_Board as Board', array(
'local' => 'board_id',
'foreign' => 'id'
)
);
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Forum_Thread:
columns:
user_id: integer
board_id: integer
title: string(200)
updated: integer(10)
closed: integer(1)
relations:
User:
local: user_id
foreign: id
foreignAlias: Threads
Board:
class: Forum_Board
local: board_id
foreign: id
foreignAlias: Threads
これらのクラスを使い始めることができます。プロパティに既に使用した同じアクセサはリレーションに対してもすべて利用できます。
最初に新しいboardを作りましょう:
// test.php
// ...
$board = new Forum_Board();
$board->name = 'Some board';
boardの元で新しいthreadを作りましょう:
// test.php
// ...
$board->Threads[0]->title = 'new thread 1';
$board->Threads[1]->title = 'new thread 2';
それぞれのThreadはそれぞれのユーザーに関連付ける必要があるので新しいUserを作りそれぞれのThreadに関連付けましょう:
$user = new User();
$user->username = 'jwage';
$board->Threads[0]->User = $user;
$board->Threads[1]->User = $user;
これですべての変更を1つの呼び出しで保存できます。threadsと同じように新しいboardを保存します:
// test.php
// ...
$board->save();
上記のコードを使うときに作成されるデータ構造を見てみましょう。投入したばかりのオブジェクトグラフの配列を出力するためにtest.phpにコードを追加します:
print_r($board->toArray(true));
レコードのデータを簡単にインスペクトできるようにDoctrine_Record::toArray()はDoctrine_Recordインスタンスのすべてのデータを取り配列に変換します。これはリレーションを含めるかどうかを伝える$deepという名前の引数を受け取ります。この例ではThreadsのデータを含めたいので{[true]}を指定しました。
ターミナルでtest.phpを実行すると次の内容が表示されます:
$ php test.php
Array
(
[id] => 2
[name] => Some board
[description] =>
[Threads] => Array
(
[0] => Array
(
[id] => 3
[user_id] => 1
[board_id] => 2
[title] => new thread 1
[updated] =>
[closed] =>
[User] => Array
(
[id] => 1
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-20 16:41:57
[updated_at] => 2009-01-20 16:41:57
)
)
[1] => Array
(
[id] => 4
[user_id] => 1
[board_id] => 2
[title] => new thread 2
[updated] =>
[closed] =>
[User] => Array
(
[id] => 1
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-20 16:41:57
[updated_at] => 2009-01-20 16:41:57
)
)
)
)
Doctrine内部でautoincrementの主キーと外部キーが自動的に設定されることに注意してください。主キーと外部キーの設定に悩む必要はまったくありません!
一対一のリレーションは最も基本的なリレーションでしょう。次の例ではリレーションが一対一であるUserとEmailの2つのクラスを考えます。
最初にEmailクラスを見てみましょう。一対一のリレーションをバインドしているのでhasOne()メソッドを使用しています。Emailクラスで外部キーのカラム(user_id)を定義する方法に注目してください。これはEmailがUserによって所有され他の方法がないという事実に基づいています。実際次の慣習 - 所有側のクラスで外部キーを設置することに従うべきです。
外部キー用に推奨される命名規約は: [tableName]_[primaryKey]です。外部テーブルは'user'で主キーは'id'なので外部キーのカラムは'user_id'と名付けました。
// models/Email.php
class Email extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('address', 'string', 150);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Email:
columns:
user_id: integer
address: string(150)
relations:
User:
local: user_id
foreign: id
foreignType: one
リレーションは自動的に反転して追加されるので、YAMLスキーマファイルを使用するとき反対端(User)でリレーションを指定することは必須ではありません。リレーションはクラスの名前から名付けられます。ですのでこの場合User側のリレーションはEmailと呼ばれmanyになります。これをカスタマイズしたい場合foreignAliasとforeignTypeオプションを使用できます。
EmailクラスはUserクラスとよく似ています。localとforeignカラムはEmailクラスの定義と比較されるhasOne()の定義に切り替えられることに注目してください。
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
$this->hasOne('Email', array(
'local' => 'id',
'foreign' => 'user_id'
)
);
}
}
setUp()メソッドをオーバーライドしてparent::setUp()を呼び出していることに注目してください。これはYAMLもしくは既存のデータベースから生成されたBaseUserクラスがメインのsetUp()メソッドを持ちリレーションを追加するためにUserクラスでこのメソッドをオーバーライドしているからです。
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
User:
# ...
relations:
# ...
Email:
local: id
foreign: user_id
一対多と多対一のリレーションは一対一のリレーションとよく似ています。以前の章で見た推奨される慣習は一対多と多対一のリレーションにも適用されます。
次の例では2つのクラス: UserとPhonenumberがあります。一対多のリレーションとして定義します(ユーザーは複数の電話番号を持つ)。繰り返しますがPhonenumberはUserによって所有されるのでPhonenumberクラスに外部キーを設置します。
// models/User.php
class User extends BaseUser
{
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('Phonenumber as Phonenumbers', array(
'local' => 'id',
'foreign' => 'user_id'
)
);
}
}
// models/Phonenumber.php
class Phonenumber extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('phonenumber', 'string', 50);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id'
)
);
}
}
YAMLフォーマットでの同じです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
User:
# ...
relations:
# ...
Phonenumbers:
type: many
class: Phonenumber
local: id
foreign: user_id
Phonenumber:
columns:
user_id: integer
phonenumber: string(50)
relations:
User:
local: user_id
foreign: id
ツリー構造は自己参照の外部キーのリレーションです。次の定義は階層データの概念の用語では隣接リスト(Adjacency List)とも呼ばれます。
// models/Task.php
class Task extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 100);
$this->hasColumn('parent_id', 'integer');
}
public function setUp()
{
$this->hasOne('Task as Parent', array(
'local' => 'parent_id',
'foreign' => 'id'
)
);
$this->hasMany('Task as Subtasks', array(
'local' => 'id',
'foreign' => 'parent_id'
)
);
}
}
YAMLフォーマットでの同じ例です。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Task:
columns:
name: string(100)
parent_id: integer
relations:
Parent:
class: Task
local: parent_id
foreign: id
foreignAlias: Subtasks
上記の実装は純粋な例で階層データを保存し読み取るための最も効率的な方法ではありません。階層データを扱い推奨方法に関してはDoctrineに含まれるNestedSetビヘイビアを確認してください。
リレーショナルデータベースの背景知識があれば、多対多のアソシエーションを扱う方法になれているかもしれません: 追加のアソシエーションテーブルが必要です。
多対多のリレーションにおいて2つのコンポーネントの間のリレーションは常に集約関係でアソシエーションテーブルは両端で所有されます。ユーザーとグループの場合: ユーザーが削除されているとき、ユーザーが所属するグループは削除されません。しかしながら、ユーザーとユーザーが所属するグループの間のアソシエーションが代わりに削除されています。これはユーザーとユーザーが所属するグループの間のリレーションを削除しますが、ユーザーとグループは削除しません。
ときにはユーザー/グループを削除するときアソシエーションテーブルの列を削除したくないことがあります。リレーションをアソシエーションコンポーネントに設定する(このケースではGroupuser) ことで明示的にこのビヘイビアをオーバーライドできます。
次の例ではリレーションが多対多として定義されているGroupsとUsersがあります。このケースではGroupuserと呼ばれる追加クラスも定義する必要があります。
class User extends BaseUser
public function setUp()
{
parent::setUp();
// ...
$this->hasMany('Group as Groups', array(
'local' => 'user_id',
'foreign' => 'group_id',
'refClass' => 'UserGroup'
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
User:
# ...
relations:
# ...
Groups:
class: Group
local: user_id
foreign: group_id
refClass: UserGroup
多対多のリレーションをセットアップするとき上記のrefClassオプションは必須です。
// models/Group.php
class Group extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('groups');
$this->hasColumn('name', 'string', 30);
}
public function setUp()
{
$this->hasMany('User as Users', array(
'local' => 'group_id',
'foreign' => 'user_id',
'refClass' => 'UserGroup'
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Group:
tableName: groups
columns:
name: string(30)
relations:
Users:
class: User
local: group_id
foreign: user_id
refClass: UserGroup
groupは予約語であることにご注意ください。これがsetTableNameメソッドを使用してテーブルをgroupsにリネームする理由です。予約語がクォートでエスケープされるように他のオプションはDoctrine::ATTR_QUOTE_IDENTIFIER属性を使用して識別子のクォート追加を有功にすることです。
$manager->setAttribute(Doctrine_Core::ATTR_QUOTE_IDENTIFIER, true);
// models/UserGroup.php
class UserGroup extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer', null, array(
'primary' => true
)
);
$this->hasColumn('group_id', 'integer', null, array(
'primary' => true
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
UserGroup:
columns:
user_id:
type: integer
primary: true
group_id:
type: integer
primary: true
リレーションが双方向であることに注目してください。Userは複数のGroupを持ちGroupは複数のUserを持ちます。Doctrineで多対多のリレーションを完全に機能させるためにこれは必須です。
新しいモデルで遊んでみましょう。ユーザーを作成しこれにいくつかのグループを割り当てます。最初に新しいUserインス場合も考えてみましょう。注文テーブルが実在する製品の注文のみが含まれることを保証したい場合を考えます。ですので製品テーブルを参照する注文テーブルで外部キー制約を定義します:
// models/Order.php
class Order extends Doctrine_Record
{
public function setTableDefinition()
{
$this->setTableName('orders');
$this->hasColumn('product_id', 'integer');
$this->hasColumn('quantity', 'integer');
}
public function setUp()
{
$this->hasOne('Product', array(
'local' => 'product_id',
'foreign' => 'id'
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Order:
tableName: orders
columns:
product_id: integer
quantity: integer
relations:
Product:
local: product_id
foreign: id
外部キーを含むクエリを発行するときに最適なパフォーマンスを保証するために外部キーのカラムのインデックスは自動的に作成されます。
Orderクラスがエクスポートされるとき次のSQLが実行されます:
CREATE TABLE orders (
id integer PRIMARY KEY auto_increment,
product_id integer REFERENCES products (id),
quantity integer,
INDEX product_id_idx (product_id)
)
productテーブルに現れないproduct_idでordersを作成するのは不可能です。
この状況においてordersテーブルは参照するテーブルでproductsテーブルはは参照されるテーブルです。同じように参照と参照されるカラムがあります。
Doctrineでリレーションを定義し外部キーがデータベースで作成されるとき、Doctrineは外部キーの名前をつけようとします。ときには、その名前が望んだものとは違うことがあるのでリレーションのセットアップで`foreignKeyName`オプションを使うことで名前をカスタマイズできます。
// models/Order.php
class Order extends Doctrine_Record
{
// ...
public function setUp()
{
$this->hasOne('Product', array(
'local' => 'product_id',
'foreign' => 'id',
'foreignKeyName' => 'product_id_fk'
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAMLの詳細はYAML Schema Filesの章で読むことができます:
---
# schema.yml
# ...
Order:
# ...
relations:
Product:
local: product_id
foreign: id
foreignKeyName: product_id_fk
CASCADE
親テーブルから列を削除もしくは更新しコテーブルでマッチするテーブルを自動的に削除もしくは更新します。ON DELETE CASCADEとON UPDATE CASCADEの両方がサポートされます。2つのテーブルの間では、親テーブルもしくは子テーブルの同じカラムで振る舞うON UPDATE CASCADE句を定義すべきではありません。
SET NULL
親テーブルから列を削除もしは更新し子テーブルで外部キーカラムをNULLに設定します。外部キーカラムがNOT NULL修飾子が指定されない場合のみこれは有効です。ON DELETE SET NULLとON UPDATE SET NULL句の両方がサポートされます。
NO ACTION
標準のSQLにおいて、NO ACTIONはアクションが行われないことを意味し、具体的には参照されるテーブルで関連する外部キーの値が存在する場合、主キーの値を削除するもしくは更新する処理が許可されません。
RESTRICT
親テーブルに対する削除もしくは更新オペレーションを拒否します。NO ACTIONとRESTRICTはON DELETEもしくはON UPDATE句を省略するのと同じです。
SET DEFAULT
次の例においてUserとPhonenumberの2つのクラスのリレーションを一対多に定義します。onDeleteカスケードアクションで外部キーの制約も追加します。このことはuserが削除されるたびに関連するphonenumbersも削除されることを意味します。
上記で示されている整合性制約は大文字小文字を区別しスキーマで定義するときは大文字でなければなりません。下記のコードは削除カスケードが使用されるデータベース削除の例です。
class Phonenumber extends Doctrine_Record
{
// ...
public function setUp()
{
parent::setUp();
// ...
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'CASCADE'
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Phonenumber:
# ...
relations:
# ...
User:
local: user_id
foreign: id
onDelete: CASCADE
外部キーがあるところで整合性制約がおかれていることに注目してください。整合性制約がデータベースのプロパティにエクスポートされるためにこれは必須です。
インデックスは特定のカラムの値を持つ列を素早く見つけるために使われます。インデックスなしでは、データベースは最初の列から始め関連する列をすべて見つけるためにテーブル全体を読み込まなければなりません。
テーブルが大きくなるほど、時間がかかります。テーブルが問題のカラム用のインデックスを持つ場合、データベースはデータをすべて見ることなくデータの中ほどで位置を素早く決定できます。テーブルが1000の列を持つ場合、これは列を1つづつ読み込むよりも少なくとも100倍以上速いです。
インデックスはinsertとupdateを遅くなるコストがついてきます。しかしながら、一般的に SQLのwhere条件で使われるフィールドに対して常にインデックスを使うべきです。
Doctrine_Record::indexを使用してインデックスを追加できます。インデックスをnameという名前のフィールドに追加するシンプルな例です:
次のインデックスの例はDoctrineの環境に実際に追加することは想定されていません。これらはインデックス追加用のAPIを示すためだけを意図しています。
class IndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->index('myindex', array(
'fields' => array('name')
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
IndexTest:
columns:
name: string
indexes:
myindex:
fields: [name]
nameという名前のフィールドにマルチカラムインデックスを追加する例です:
class MultiColumnIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->index('myindex', array(
'fields' => array('name', 'code')
)
);
}
}
YAMLフォーマットでの例は次の通りです。YAML Schema Filesの章でYAMLの詳細を見ることができます:
---
MultiColumnIndexTest:
columns:
name: string
code: string
indexes:
myindex:
fields: [name, code]
同じテーブルで複数のインデックスを追加する例です:
class MultipleIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->hasColumn('age', 'integer');
$this->index('myindex', array(
'fields' => array('name', 'code')
)
);
$this->index('ageindex', array(
'fields' => array('age')
)
);
}
}
YAMLフォーマットでの同じ例です。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
MultipleIndexTest:
columns:
name: string
code: string
age: integer
indexes:
myindex:
fields: [name, code]
ageindex:
fields: [age]
Doctrineは多くのインデックスオプションを提供します。これらの一部はデータベース固有のものです。利用可能なオプションの全リストは次の通りです:
| 名前 | 説明 |
|---|---|
| sorting | 文字列の値が'ASC'もしくは'DESC'の値を取れるか |
| length | インデックスの長さ(一部のドライバのみサポート)。 |
| primary | インデックスがプライマリインデックスであるか。 |
| type | 文字列の値で'unique'、'fulltext'、'gist'もしくは'gin'が許可されるか |
nameカラムでユニークインデックスを作る方法の例は次の通りです。
class MultipleIndexTest extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->hasColumn('code', 'string');
$this->hasColumn('age', 'integer');
$this->index('myindex', array(
'fields' => array(
'name' => array(
'sorting' => 'ASC',
'length' => 10),
'code'
),
'type' => 'unique',
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAMLの詳細はYAML Schema Filesの章で読むことができます:
---
MultipleIndexTest:
columns:
name: string
code: string
age: integer
indexes:
myindex:
fields:
name:
sorting: ASC
length: 10
code: -
type: unique
Doctrineは多くの特別なインデックスをサポートします。これらにはMysqlのFULLTEXTとPgsqlのGiSTインデックスが含まれます。次の例では'content'フィールドに対してMysqlのFULLTEXTインデックスを定義します。
// models/Article.php
class Article extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string', 255);
$this->hasColumn('content', 'string');
$this->option('type', 'MyISAM');
$this->index('content', array(
'fields' => array('content'),
'type' => 'fulltext'
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Article:
options:
type: MyISAM
columns:
name: string(255)
content: string
indexes:
content:
fields: [content]
type: fulltext
テーブルの型をMyISAMに設定していることに注目してください。これはfulltextインデックス型はMyISAMでのみサポートされるためInnoDBなどを使う場合はエラーを受け取るからです。
Doctrine_Recordのcheck()メソッドを使用することで任意のCHECK制約を作成できます。最後の例では価格がディスカウント価格よりも常に高いことを保証するために制約を追加します。
// models/Product.php
class Product extends Doctrine_Record
{
public function setTableDefinition()
{
// ...
$this->check('price > discounted_price');
}
// ...
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Product:
# ...
checks:
price_check: price > discounted_price
生成されるSQL(pgsql):
CREATE TABLE product (
id INTEGER,
price NUMERIC,
discounted_price NUMERIC,
PRIMARY KEY(id),
CHECK (price >= 0),
CHECK (price <= 1000000),
CHECK (price > discounted_price))
データベースの中にはCHECK制約をサポートしないものがあります。この場合Doctrineはチェック制約の作成をスキップします。
Doctrineバリデータが定義で有効な場合はレコードが保存されるとき価格が常にゼロ以上であることも保証されます。
トランザクションの範囲で保存される価格の中にゼロよりも小さいものがある場合、DoctrineはDoctrine_Validator_Exceptionを投げトランザクションを自動的にロールバックします。
Doctrineはさまざまなテーブルオプションを提供します。すべてのテーブルオプションはDoctrine_Record::option関数を通して設定できます。
例えばMySQLを使用しINNODBテーブルを利用したい場合は次のようにできます:
class MyInnoDbRecord extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->option('type', 'INNODB');
}
}
YAMLフォーマットの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を見ることができます:
---
MyInnoDbRecord:
columns:
name: string
options:
type: INNODB
次の例では照合順序と文字集合のオプションを設定します:
class MyCustomOptionRecord extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('name', 'string');
$this->option('collate', 'utf8_unicode_ci');
$this->option('charset', 'utf8');
}
}
YAMLフォーマットでの同じ例です。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
MyCustomOptionRecord:
columns:
name: string
options:
collate: utf8_unicode_ci
charset: utf8
特定のデータベース(Firebird、MySqlとPostgreSQL)でcharsetオプションを設定しても無意味でDoctrineがデータを適切に返すのには不十分であることがあります。これらのデータベースに対して、データベース接続のsetCharset関数を使うこともお勧めします:
$conn = Doctrine_Manager::connection();
$conn->setCharset('utf8');
Doctrineはモデルを定義するときにレコードフィルターを添付する機能を持ちます。レコードフィルターは無効なモデルのプロパティにアクセスするときに起動されます。ですのでこれらのフィルターの1つを使うことを通してプロパティをモデルに追加することが本質的に可能になります。
フィルターを添付するにはこれをモデル定義のsetUp()メソッドに追加することだけが必要です:
class User extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('username', 'string', 255);
$this->hasColumn('password', 'string', 255);
}
public function setUp()
{
$this->hasOne('Profile', array(
'local' => 'id',
'foreign' => 'user_id'
));
$this->unshiftFilter(new Doctrine_Record_Filter_Compound(array('Profile')));
}
}
class Profile extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('first_name', 'string', 255);
$this->hasColumn('last_name', 'string', 255);
}
public function setUp()
{
$this->hasOne('Profile', array(
'local' => 'user_id',
'foreign' => 'id'
));
}
}
上記の例のコードによってUserのインスタンスを使うときProfileリレーションのプロパティに簡単にアクセスできます。次のコードは例です:
$user = Doctrine_Core::getTable('User')
->createQuery('u')
->innerJoin('u.Profile p')
->where('p.username = ?', 'jwage')
->fetchOne();
echo $user->first_name . ' ' . $user->last_name;
first_nameとlast_nameプロパティに問い合わせるときこれらは$userインスタンスに存在しないのでこれらはProfileリレーションにフォワードされます。これは次の内容を行ったこととまったく同じです:
echo $user->Profile->first_name . ' ' . $user->Profile->last_name;
独自のレコードフィルターをとても簡単に書くこともできます。必要なことはDoctrine_Record_Filterを継承しfilterSet()とfilterGet()メソッドを実装するクラスを作ることです。例は次の通りです:
class MyRecordFilter extends Doctrine_Record_Filter
{
public function filterSet(Doctrine_Record $record, $name, $value)
{
// プロパティをトライしてセットする
throw new Doctrine_Record_UnknownPropertyException(sprintf('Unknown record property / related component "%s" on "%s"', $name, get_class($record)));
}
public function filterGet(Doctrine_Record, $name)
{
// プロパティをトライしてゲットする
throw new Doctrine_Record_UnknownPropertyException(sprintf('Unknown record property / related component "%s" on "%s"', $name, get_class($record)));
}
}
これでフィルターをモデルに追加できます:
class MyModel extends Doctrine_Record
{
// ...
public function setUp()
{
// ...
$this->unshiftFilter(new MyRecordFilter());
}
}
`filterSet()`もしくは`filterGet()`がプロパティを見つけられない場合、例外クラスの`Doctrine_Record_UnknownPropertyException`のインスタンスが投げられていることをかならず確認してください。
Doctrineはデータベースとアプリケーションレベルでカスケーディングオペレーションを提供します。このセクションではアプリケーションとデータベースレベルの両方でセットアップする詳細な方法を説明します。
とりわけオブジェクトグラフを扱うとき、個別のオブジェクトの保存と削除はとても退屈です。Doctrineはアプリケーションレベルでオペレーションのカスケード機能を提供します。
デフォルトではsave()オペレーションは関連オブジェクトに既にカスケードされていることにお気づきかもしれません。
Doctrineは2番目のカスケードスタイル: deleteを提供します。save()カスケードとは異なり、deleteカスケードは次のコードスニペットのように明示的に有効にする必要があります:
// models/User.php
class User extends BaseUser
{
// ...
public function setUp()
{
parent::setup();
// ...
$this->hasMany('Address as Addresses', array(
'local' => 'id',
'foreign' => 'user_id',
'cascade' => array('delete')
)
);
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
User:
# ...
relations:
# ...
Addresses:
class: Address
local: id
foreign: user_id
cascade: [delete]
アプリケーションレベルで関連オブジェクトにカスケードされるオペレーションを指定するためにcascadeオプションが使われます。
現在サポートされる値はdeleteのみであることにご注意ください。より多くのオプションは将来のDoctrineのリリースで追加されます。
上記の例において、Doctrineは関連するAddressにUserの削除をカスケードします。次の説明は$record->delete()を通してレコードを削除する際の一般的な手続きです:
1. Doctrineは適用する必要のある削除カスケードが存在するかリレーションを探します。削除カスケードが存在しない場合、3に移動します)。
2. 指定された削除カスケードを持つそれぞれのリレーションに対して、Doctrineはカスケードのターゲットであるオブジェクトがロードされることを確認します。このことはDoctrineは関連オブジェクトがまだロードされていない場合データベースから関連オブジェクトが取得することを意味します。(例外: すべてのオブジェクトがロードされていることを確認するために多くの値を持つアソシエーションはデータベースから再取得されます)。それぞれの関連オブジェクトに対して、ステップ1に進みます)。
3. Doctrineは参照照合性を維持しながらすべての削除を並べ替え最も効果的な方法で実行します。
この説明から1つのことがすぐに明らかになります: アプリケーションレベルのカスケードはオブジェクトレベルで行われ、参加しているオブジェクトが利用可能にすることを行うために1つのオブジェクトから別にオブジェクトにオペレーションがカスケードされることを意味します。
このことは重要な意味を示します:
データベースレベルでカスケードオペレーションはとても効率的にできるものがあります。もっともよい例は削除カスケードです。
次のことを除いて一般的にデータベースレベルの削除カスケードはアプリケーションレベルよりも望ましいです:
データベースレベルの削除カスケードは外部キー制約に適用されます。それゆえこれらは外部キーを所有するリレーション側で指定されます。上記から例を拾うと、データベースレベルのカスケードの定義は次のようになります:
// models/Address.php
class Address extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('user_id', 'integer');
$this->hasColumn('address', 'string', 255);
$this->hasColumn('country', 'string', 255);
$this->hasColumn('city', 'string', 255);
$this->hasColumn('state', 'string', 2);
$this->hasColumn('postal_code', 'string', 25);
}
public function setUp()
{
$this->hasOne('User', array(
'local' => 'user_id',
'foreign' => 'id',
'onDelete' => 'CASCADE'
)
);
}
}
YAMLフォーマットの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を詳しく読むことができます:
---
# schema.yml
# ...
Address:
columns:
user_id: integer
address: string(255)
country: string(255)
city: string(255)
state: string(2)
postal_code: string(25)
relations:
User:
local: user_id
foreign: id
onDelete: CASCADE
Doctrineがテーブルを作成するときonDeleteオプションは適切なDDL/DMLステートメントに翻訳されます。
'onDelete' => 'CASCADE'がAddressクラスで指定されることに注目してください。Addressは外部キー(user_id)を所有するのでデータベースレベルのカスケードは外部キーに適用されます。
現在、2つのデータベースレベルのカスケードスタイルはonDeleteとonUpdateに対してのみです。Doctrineがテーブルを作成するとき両方とも外部キーを所有する側で指定されデータベーススキーマに適用されます。