Create a gist now

Instantly share code, notes, and snippets.

CakePHP2でModel->find()でjoinsを使ってみた
$option = array();
$option['recursive'] = -1; 
$option['joins'][] = array(
    'type' => 'LEFT',   //LEFT, INNER, OUTER
    'table' => 'posts',
    'alias' => 'Post',    //下でPost.user_idと書くために
    'conditions' => '`User`.`id`=`Post`.`user_id`',
);
$option['conditions'] = array('Post.isPrivate' => 1);
$this->User->find('all', $option)

User hasMany Postの場合 User->find() だとPostはクエリーは別個に発行されます。そのためconditionsでPostのフィールドを使えません。 joinsを使う事で、上記のようにプライベートな投稿をしているユーザーを見つける事が出来ます。

注意点

上記までで素直に使えれば使いやすいけど joins を使うとCakePHPのモデルの構造から大きく外れてしまいます。

1対多の関係でLEFT JOINしたらレコードは重複する

User.id = 1が3つPostを持っていたら、LEFT JOINするとレコードは3つになります。User.id = 1+それぞれのPostをくっ付けたレコードです。CakePHPはどうやら、これをうまい事処理して、いつも通りのUser hasMany Post的な配列にはしてくれません。そのまま重複したレコードをUser->find()の結果として返してきます。

CakePHPは色々とドキュメントに書いてない機能やオプションが多いので、うまい事やってくれる方法、もしかしたら有るかもしれない。というか有ってほしい。

array(
    (int) 0 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        )
    ),
    (int) 1 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        )
    ),
    (int) 2 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        )
    )
)
$option = array();
$option['recursive'] = -1; 
$option['joins'][] = array(
    'type' => 'LEFT',
    'table' => 'posts',
    'alias' => 'Post',
    'conditions' => '`User`.`id`=`Post`.`user_id`',
);
$option['conditions'] = array('User.id' => 1, 'Post.isPrivate' => 1);
$this->User->find('all', $option);

アソシエーションとの相性が悪い

相性が悪いというか、上記の問題があるので、CakePHPのアソシエーションを使った取り方とは、カテゴリーが違うのかも。$option['recursive'] = -1 を設定しておかないと、重複した3つのUserレコード毎に3つのPostを持たせた配列を生成します。ただ、postsテーブルへのクエリーは1個にまとめてくれてはいます。

array(
    (int) 0 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    ),
    (int) 1 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    ),
    (int) 2 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    )
)
$option = array();
// $option['recursive'] = -1; <- コメントアウト
$option['joins'][] = array(
    'type' => 'LEFT',
    'table' => 'posts',
    'alias' => 'Post',
    'conditions' => '`User`.`id`=`Post`.`user_id`',
);
$option['conditions'] = array('User.id' => 1, 'Post.isPrivate' => 1);
$this->User->find('all', $option);

クエリーをまとめるとは

CakePHPは、例えばUserが3人、それぞれ3レコードずつ持っているとしても、hasManyアソシエーションで取得する場合、usersテーブルへ1クエリー、その結果のUser.idをWHERE INに使って、postsテーブルに1クエリーの2クエリーでデータを取得しています。

実はPostのデータが取れてない

$option['fields'] = array('*');
$option['fields'] = array('Post.*');
$option['fields'] = array('User.id', 'User.username', 'Post.title', 'Post.isPrivate');

fields でちゃんと指定しないとJOINしたテーブルのデータがとれません。

array(
    (int) 0 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '1',
            'isPrivate' => '1'
        )
    ),
    (int) 1 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '2',
            'isPrivate' => '1'
        )
    ),
    (int) 2 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '3',
            'isPrivate' => '1'
        )
    )
)
$option = array();
$option['recursive'] = -1; 
$option['joins'][] = array(
    'type' => 'LEFT',
    'table' => 'posts',
    'alias' => 'Post',
    'conditions' => '`User`.`id`=`Post`.`user_id`',
);
$option['conditions'] = array('User.id' => 1, 'Post.isPrivate' => 1);
$option['fields'] = array('*'); //<- 追加
$this->User->find('all', $option);

fieldsを指定しアソシエーションが有効だとひどい

上記2つが混ざった様な配列が出来上がり、データとして使いにくそうです。 ただ、クエリーは変わらず2クエリーだったりします。

array(
    (int) 0 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '1',
            'isPrivate' => '1',
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    ),
    (int) 1 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '2',
            'isPrivate' => '1',
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    ),
    (int) 2 => array(
        'User' => array(
            'password' => '*****',
            'id' => '1',
            'username' => 'foo',
        ),
        'Post' => array(
            'id' => '3',
            'isPrivate' => '1',
            (int) 0 => array(
                [maximum depth reached]
            ),
            (int) 1 => array(
                [maximum depth reached]
            ),
            (int) 2 => array(
                [maximum depth reached]
            )
        )
    )
)
$option = array();
// $option['recursive'] = -1; <- コメントアウト
$option['joins'][] = array(
    'type' => 'LEFT',
    'table' => 'posts',
    'alias' => 'Post',
    'conditions' => '`User`.`id`=`Post`.`user_id`',
);
$option['conditions'] = array('User.id' => 1, 'Post.isPrivate' => 1);
$option['fields'] = array('*'); //<- 追加
$this->User->find('all', $option);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment