例えば1:nのテーブルがあります。
CREATE TABLE `office` (
`officeCode` varchar(10) NOT NULL,
`city` varchar(50) NOT NULL,
`phone` varchar(50) NOT NULL,
PRIMARY KEY (`officeCode`)
) ENGINE=InnoDB DEFAULT;
CREATE TABLE `employee` (
`employeeNumber` int(11) NOT NULL,
`lastName` varchar(50) NOT NULL,
`firstName` varchar(50) NOT NULL,
`officeCode` varchar(10) NOT NULL,
PRIMARY KEY (`employeeNumber`),
KEY `officeCode` (`officeCode`),
CONSTRAINT `employee_ibfk_2` FOREIGN KEY (`officeCode`) REFERENCES `office` (`officeCode`)
) ENGINE=InnoDB;
officeに対してemployeeが沢山ぶらさがってるようなよくある1:nのテーブルが有るじゃないですか。
employeeのリストでofficeのcityを表示させようとした際、気軽にviewで$model->office->city
なんて書いてしまうと都度officeに大してクエリが走ってしまいますね。そうですn+1問題です。
なのでemployee引くときにJOINして引けばいいですよね。ARにはwith()
'というイーガーローディングのメソッドがあるのでコレを使います。さてここで問題なのがクエリ。Yii2ではこのようなクエリが流れます。
$model->find()->with('office')->all()
SELECT * FROM `employee` LIMIT 20
SELECT * FROM `office` WHERE `officeCode` IN ('1', '6', '4', '2', '3', '7')
※実際にはARがテーブル情報を引いてくるためもう少しクエリが多いです
はて、JOINすれば一発で取ってこれそうなものをわざわざ2回に分けてクエリを発行しています。
じゃあJOINすればいいじゃないとjoinWith()
を使います。
$model->find()->joinWith('office')->all()
SELECT `employee`.* FROM `employee` LEFT JOIN `office` ON `employee`.`officeCode` = `office`.`officeCode` LIMIT 20
SELECT * FROM `office` WHERE `officeCode` IN ('1', '6', '4', '2', '3', '7')
ファッ!?なんでoffice引いてるの!?officeナンデ!?
何故かと言うとsoftark先生が説明されていた通り
注意すべきことが、もう一つあります。それは、joinWith を使ってテーブルを結合しても、イーガーローディングで実行されるクエリが一回だけになったりはしない、ということです。Yii 2 では、メインモデルの取得と、リレーションモデルの取得は、必ず、別のクエリによって行われます。
なんですね。仕様です。Yii2ではそういうものなんです。
と切り捨ててしまうのは簡単ですが、クエリ分割には利点もあります。
JOIN先のレコードに重複がある場合、その分DBからの転送量が減らすことができます。それはレコードが増えるほど顕著になるでしょう。
なおかつインデックスに対するIN句は大概速いです。DBのキャッシュに乗ってればKVSと大差ないレベルです。
そのためクエリは1つふえるものの、クエリのオーバヘッドを相殺しうるメリットが発生しうるのでそう悪くない選択なのかもしれません(個人の感想です)
ちなみにlaravelのORMもイーガーローディングでクエリを分割するアプローチのようです