LoginSignup
13
12

More than 3 years have passed since last update.

[CakePHP]助けて!hasManyで結合したテーブルが取得できないの![MySQL]

Last updated at Posted at 2019-12-04

テーブル

ゲームから見てメーカーは多対1の関係(belongsTo)レビューは1対多(hasMany)の関係。

GamesTable.php
class GamesTable extends Table
{
    public function initialize(array $config)
    {
        $this->hasMany('Reviews');

        $this->belongsTo('Makers');
    }
}

selectしようとしたらなんかエラー出たんだけど!

SQLを発行するときに、データをselectするのはいい考えですね。
クエリビルダーはselectメソッドを使わない場合、全てのカラムがselectされますが、
パフォーマンスを考えると必要なカラムだけ取得したいものです。

ReviewController.php
<?php

namespace App\Controller;

use App\Controller\AppController;
use Cake\ORM\TableRegistry;

class GameController extends AppController
{
    public function index()
    {
        $reviews_table = TableRegistry::get('Games');
        $review = $reviews_table->find()
            ->contain('Makers', 'Reviews')
            ->select([
                'title' => 'Games.title',
                'price' => 'Games.price',
                'name' => 'Makers.name',
                'comment' => 'Reviews.comment'
            ]);
    }        
}

しかし、Reviews.commentなんてカラムはないよって怒られます。
でも同じアソシエーションしてるMakersテーブルからは持ってこれてるじゃん!なんで!?

これは、belongsToとhasManyとで発行しているSQLの違いにあります。
クエリオーオブジェクトをdebug()すると、データベースで発行してるSQLを見ることができます。

SELECT Games.title AS title, Games.price AS price, Makers.name AS name
FROM Games
LEFT JOIN Makers 
ON Games.id = Makers.Game_id;

SELECT Reviews.comment
FROM Revies
WHERE Game_id IN (1, 2, 3); /* このIN句の数字は上記のSQLで発行されたGamesテーブル主キー*/

だいたいこんな感じのSQLが発行されます。
belongsToで関連付けられたテーブルは、LEFT JOINが使用されていることがわかりますね。

hasManyで関連付けれたテーブルは、複数のSQLに分割して発行されます。

ちなみに4つのアソシエーションがどのようなSQLを発行するかはこんな感じ。

アソシエーション種別 発行されるSQL
hasOne LEFT JOIN
belongsTo LEFT JOIN
hasMany 分割
belongsToMany 分割

つまりどういうことだってばよ!?

クエリビルダーで使ったselectメソッドは、1つ目のSQL文に対して適応されます。
一目瞭然ですが、1つ目に発行されたSQL分にはReviews.commentなんてカラムはかけらも見当たらないですね。

だからCakePHPちゃんは「お前の指定したカラム見つかれねえんだけど!!」とお怒りだったわけです。

それじゃあhasManyやbelongsToManyでSELECTしたいときは?

containにクロージャーを渡してあげます。

$review = $reviews_table->find()
   ->contain(['Makers', 'Reviews' => function ($q) {
       return $q->select([
            'Game_id',
            'comment'
       ]);
   }])
   ->select([
       'title' => 'Games.title',
       'price' => 'Games.price',
       'name' => 'Makers.name',
    ]);
]);

ここで注意したいのは、関連テーブルをselectする際には、外部キーを確実にselectする必要がある。ということです。
外部キーは規約にしたがってると、ここではGame_idになります。

公式にもそう書いてありますね。

関連によってフェッチされるフィールドを限定する場合、外部キーの列が確実に select されなければなりません 。外部キーのカラムが select されない場合、関連データが 最終的な結果の中に無いということがおこります。

って書いてあるけど外部キーがselectされてなかったエラー吐いてくれます。

主キー側も(binding key)もselectされてなければならない!!

それよりもやっかいなのはこっち。
主キーつまり、Gamesテーブルのidをselectしていないと、エラーを吐かないでただただhasMany関連が空で取得されます。
結構気づきくい。

まあこれは、hasManyで発行されるSQLを見ればなんでそうなるかなんとなく理解できると思います。

最終的にクエリビルダーはこうなる。

$review = $reviews_table->find()
   ->contain(['Makers', 'Reviews' => function ($q) {
       return $q->select([
            'Game_id'
            'comment' => 'Reviews.comment'
       ]);
   }])
   ->select([ 
       'id',
       'title' => 'Games.title',
       'price' => 'Games.price',
       'name' => 'Makers.name',
    ]);
]);

もう一個ハマリポイント

主キーをselectしなきゃいけないけど、MakersテーブルとはLEFT JOINしてるから、idはアンビシャスになるんだろうなーって思いつつこんな書き方したら、

->select([ 
   'id' => 'Games.id'
   'title' => 'Games.title',
   'price' => 'Games.price',
   'name' => 'Makers.name',
]);

だめだった。これもhasManyを取得できないです。
selectしなかった時にSQLログを見ると、Games.id AS Games__idみたいな書き方されてたから、

->select([ 
   'Games__id' => 'Games.id'
   'title' => 'Games.title',
   'price' => 'Games.price',
   'name' => 'Makers.name',
]);

って感じにしたらうまくいきました。

というか、そもそもさっきみたいにidをselectしてもアンビシャスにならないっぽいですね。

13
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
12