Yiiのアクティブレコードについてひとこと言っておく 2


ひとこと言っておくのパート2というよくわからないエントリになりました。

前回はHAS_MANYとBELONGS_TOについて書きました。もう十分言い訳はしたと思うので、今回は言い訳しないことにします。

## MANY_MANYの場合

YiiにはMANY_MANYタイプの関連付けもあります。中間テーブルを使って、元と先のIDを結びつける方法です。

```sql
CREATE TABLE boy (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(128) NOT NULL
);

CREATE TABLE girl (
    id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(128) NOT NULL
);

CREATE TABLE boy_meets_girl (
    boy_id INTEGER NOT NULL KEY REFERENCES boy(id),
    girl_id INTEGER NOT NULL KEY REFERENCES girl(id)
);
```

中間テーブルである `boy_meets_girl` に `INSERT INTO boy_meets_girl (boy_id, girl_id) VALUES (12, 34);` とすれば、id=12のboyとid=34のgirlにリンクができ、この調子で、どれとどれの組み合わせでも、いくつでもリンクできるというのがそれ。ロマンスの神様はリア充なのです。

これ、SELECTの場合は `relations()` で指定できるMANY_MANYだけで問題ないんです。中間テーブル用のモデルもとくに要らない。

```php:Boy.php
<?php
class Boy extends CActiveRecord
{
	public function relations()
	{
		return array(
			'girls' => array(self::MANY_MANY, 'Girl', 'boy_meets_girl(boy_id, girl_id)'),
		);
	}
```

これで、BoyのインスタンスをDBから拾ってきてgirlsプロパティを見たら、リンクされたGirlのインスタンスのリストが取れます。

もちろん女子も男子といっぱい知り合えます。

```php:Girl.php
<?php
class Girl extends CActiveRecord
{
	public function relations()
	{
		return array(
			'boys' => array(self::MANY_MANY, 'Boy', 'boy_meets_girl(girl_id, boy_id)'),
		);
	}
```

`boy_meets_girl(girl_id, boy_id)` と、自分と相手の指定が `Boy` クラスとは逆になっているのがポイントです。

ところがこれ、`CActiveRecord` でリンクを貼る、つまりINSERTを実現するのが思ったよりたいへん。

まずINSERTするには `save()` メソッドが要るので、`boy_meets_girl` 用のモデルクラス `BoyMeetsGirl` が必要です。で、ARで管理しようとすると、`id` という列がないと困ります。これ、スキーマ変更ですよね。めんどくさいなぁ...

さらに、そんなモデルクラスができてしまうと、厳密にはこういう関係になっちゃいますね。

```php:Boy.php
<?php
class Boy extends CActiveRecord
{
	public function relations()
	{
		return array(
			'girlRelations' => array(boy::HAS_MANY, 'BoyMeetsGirl', 'boy_id'),
		);
	}
```

```php:Girl.php
<?php
class Girl extends CActiveRecord
{
	public function relations()
	{
		return array(
			'boyRelations' => array(boy::HAS_MANY, 'BoyMeetsGirl', 'girl_id'),
		);
	}
```

```php:BoyMeetsGirl.php
<?php
class BoyMeetsGirl extends CActiveRecord
{
	public function relations()
	{
		return array(
			'boy' => array(boy::BELONGS_TO, 'Boy', 'boy_id'),
			'girl' => array(boy::BELONGS_TO, 'Girl', 'girl_id'),
		);
	}
```

あれ? MANY_MANY 要らんやんw

そう、データベースの世界でMANY_MANYは、HAS_MANYとBELONGS_TOを使って一般化できるのです。そして、そのほうが適切な場合が多々あります。

たとえば、ゲームのプレイヤーと所有アイテム。誰が何を持っているかという情報には、所有している個数も付けたいですよね。記事と画像の関係にも、記事中の画像表示順序があるかもしれない。`BoyMeetsGirl` の場合は、好感度パラメータとかありえますね。おいどこの恋愛ゲームかという話ですが。

Giiで自動生成した場合でも、あからさまにMANY_MANY関係がありそうなテーブル設計なのに、中間テーブルを挟んでこういう、HAS_MANYとBELONGS_TOの定義を生成してくれます。

MANY_MANYの採用は、本当にその関係に付加情報が付く可能性がないのかを、よく検討してからにしましょう。

## それでもMANY_MANYがいいんだ

で、よく考えた結果、でもやっぱり直接のMANY_MANY関係だけでいい場合があります。そういうときは、中間テーブル用のクラスを設けても複雑になるだけですね。シンプルでいいときはシンプルに。

では実践。

ざっくり言うと、それ用のモデルクラスは作らずに、中間テーブルに互いのIDを保存しちゃえばいいのです。Yiiらしく直接SQLを発行。

```php:Boy.php
<?php
class Boy extends CActiveRecord
{
	public function meetsAGirl($girl)
	{
		$this->commandBuilder->createInsertCommand('boy_meets_girl', array(
			'boy_id'=>$this->id,
			'girl_id'=>$girl->id,
		))->execute();
	}
```

さっくりINSERTできました。

でもこのメソッドには問題があって、それは、相手と自分が確実に `id` を持っている必要があるのにそれをチェックしてないということ。つまり、結び付ける対象がすでにデータベースに存在しないといけない。なので、そのチェックをしないとダメですね。

さらに、すでにリンク関係があるのに同じリンクを貼ろうとしたら…

SQLのUNIQUE制約エラー(あれば)を出さないために、重複チェックは必要です。UNIQUEがなければまあ、エラーが出ずにどんどん重複していくんで、それもいろいろと人間関係的にまずいのですが。

えーと…

```php:Boy.php
<?php
class Boy extends CActiveRecord
{
	public function meetAGirl($girl)
	{
		if ($this->isNewRecord) {
			throw new CDbException("Relation owner is not saved.");
		}
		if ($girl->isNewRecord) {
			throw new CDbException("Relation girl is not saved.");
		}

		$criteria = $this->commandBuilder->createCriteria();
		$criteria->addColumnCondition(array(
			'boy_id'=>$this->id,
			'girl_id'=>$girl->id,
		));
		$alreadyMet = $this->commandBuilder->createCountCommand(
			'boy_meets_girl', $criteria
		)->queryScalar() > 0 ? true : false;

		if($alreadyMet) {
			return;
		}

		$this->commandBuilder->createInsertCommand('boy_meets_girl', array(
			'boy_id'=>$this->id,
			'girl_id'=>$girl->id,
		))->execute();
	}
```

なんだか、いやになってきましたw

この調子で `breakUpWithTheGirl` (つまり別れちゃうって意味)も必要だし、あと、`Girl` から `Boy` への逆向きのAPIの実装も必要ですね。やれやれ、何回同じテーブル名/列名を書かなきゃいけないのかと。

というわけでビヘイビアを作りました。言いたいことはすべてこのコードにあります。

[ManyManySupport.php](https://gist.github.com/4266408)

Yiiの `CActiveRecord` には、リレーションのメタデータをオブジェクトで取得する方法があります。MANY_MANYの定義もそれで調べることができるのです。

http://www.yiiframework.com/doc/api/1.1/CManyManyRelation

これで `relations()` で行った定義から、中間テーブル名、自分のIDの列名、相手のIDの列名をそれぞれ出したら、もうあとのコードはパターンにハメられます。

使い方は簡単。このビヘイビアを `components` あたりに置いて、MANY_MANYがあるモデルで常に使うように指定します。

```php
<?php
	public function behaviors()
	{
		return array(
			'manyManySupport'=>array(
				'class' => 'ManyManySupport',
			),
		);
	}
```

あとは拡張された各引数に、リレーション定義の登録名と、リンクしたい相手のインスタンスを渡せばOK。

```php:Boy.php
<?php
class Boy extends CActiveRecord
{
	public function meet($girl)
	{
		$boy->bindManyMany('girls', $girl);
	}

	public function breakUpWith($girl)
	{
		$boy->unbindManyMany('girls', $girl);
	}
```

次に作るもので使おうと思って作ったので、まだ甘いかもしれないけど、少なくともこのエントリで書いた面倒なコードを一般化できているという点だけでも、役に立つとは思います。

もし仕様が気に入らなかったら、適当に特殊化して使ってください。

ええ、もちろん、車輪を再発明してるんじゃないかという危惧はありますw