cakePHP3で中間テーブルをJOINしてselectした時の、とあるエラーの話

この記事は自分がぶつかったエラーをどう解決したのか、忘れないための戒め兼忘備録です。
エラー解決の役に立つことを保証しません。
そもそもの自分の認識に重大な間違いがある可能性も否定できないです。

(20180214 追記)
長らく当記事はメンテしてませんでしたが、自分が書いた記事の中でなぜかこれが一番Viewを稼いでいるので追記します。
ブックマークチュートリアルでリレーションを多対多にしたい場合は、手前味噌ですが[cakePHPのブックマークチュートリアルを多対多にするレシピ](https://qiita.com/gitboku/items/2aada5379ef39cdf90ef)を見たほうが参考になると思います。

# 文脈
cakePHP3を使っています。
[ブックマークチュートリアル](https://book.cakephp.org/3.0/ja/tutorials-and-examples/bookmarks/intro.html#id3)を自分なりにアレンジしようとしたときの話。

#####・アレンジ内容
BookmarksテーブルとTagsテーブルの関係が多対1なので、多対多の関係にする。

####・アレンジ完了までの道のり
1. Bookmarks_Tagsという中間テーブルを作る。
2. 中間テーブルを用いてView用のTagsデータを取ってくるようにする。
3. 「Tagの選択肢の中から一つ選ぶ」ようになってるので「チェックボックスを用いて自由に選ぶ」ようにViewを変更する。
4. チェックした内容がDBに送られるようにする。

この記事は道のりの2~3くらいの話です。
# エラーが発生するまでの手順

チュートリアルを終えた時点で以下のような二つのテーブルが存在します（idとtitle以外にもカラムはあるけど、話には関係ないので省略します）。
左がbookmarksテーブルで、右がtagsテーブルです。

|id  |title     ||id|title|
|:--:|:--------:|---|:---:|:---:|
|1   |黒バス||1|comic|
|2   |ヒロアカ  ||2|sports|

このbookmarksとtagsは「多対1」の関係で、これを「多対多」の関係に直そうとしています。
そこで、以下の用の中間テーブル"bookmarks_tags"を作成しました。
黒バスは"comic"と"sports"のタグを持ち、ヒロアカは"comic"のタグを持ちます。

|bookmark_id|tag_id|
|:---:|:---:|
|1|1|
|1|2|
|2|1|

bookmarks_tagsテーブルは、DBにマイグレーションでcreateしただけで、モデルやコントローラーは作っていません。
ちなみに、bookmarks_tagsテーブルをcreateしただけで、なぜか勝手にデータが挿入されていました。
おそらくcakePHPがフレームワークの力でどうにかしたのでしょう。

次に、bookmars_idの情報をもとに、tag.titleを取得します。
BookmarksController.phpのviewメソッドの真ん中あたりに以下の行を追記。

```php
$tags = $this->Bookmarks->find('all_tags', [
            'bookmarkId' => $id
        ]);
```
[クエリービルダー](https://book.cakephp.org/3.0/ja/orm/query-builder.html)を参考に、BookmarksTable.php内で、以下のようなメソッドを作成しました。

```php
public function findAllTags(Query $query, int $bookmarkId)
    {
        // SELECT title
        //  FROM tags
        //  JOIN bookmarks_tags
        //  ON tags.id = bookmarks_tags.tag_id
        //  WHERE bookmard_id = $bookmarkId;
        $tags = TableRegistry::get('bookmarks_tags')
        ->find()
        ->select(['title'])
        ->from(['tags'])
        ->join([
            'table' => 'bookmarks_tags',
            'conditions' => 'tags.id = bookmarks_tags.tag_id'
        ])
        ->where(['bookmark_id' => $bookmarkId]);

        return $tags;
    }
```

さて、ここで一旦問題なく動いているかを確認しようとブラウザから`/bookmarker/bookmarks/view/1`にアクセスしました。
すると、以下のようなエラーが……

```
Could this be caused by using Auto-Tables?

Some of the Table objects in your application were created by instantiating "Cake\ORM\Table" instead of any other specific subclass.

This could be the cause for this exception. Auto-Tables are created for you under the following circumstances:

    The class for the specified table does not exist.
    The Table was created with a typo: TableRegistry::get('Atricles');
    The class file has a typo in the name or incorrect namespace: class Atricles extends Table
    The file containing the class has a typo or incorrect casing: Atricles.php
    The Table was used using associations but the association has a typo: $this->belongsTo('Atricles')
    The table class resides in a Plugin but no plugin notation was used in the association definition.


Please try correcting the issue for the following table aliases:

    BookmarksTags

```

# 解決までに踏んだ手順

## エイリアスを追加
エイリアスがどうのこうのと言ってるので、以下のようにJOINを追加して、conditionsも変更

```php
->join([
            'table' => 'bookmarks_tags',
+           'alias' => 'b',
+           'conditions' => 'tags.id = b.tag_id'
        ])
```
何も変わらなかったので戻しました。

## メソッド名を変更
命名規則を間違えたせいでフレームワークに怒られているのかと思い、'all_tags'から'all'のように変更しました。
'findAll'という名前は一般的すぎて使いたくないけど、今は妥協。

```php
// 'all_tags'から'all'に変更
$tags = $this->Bookmarks->find('all', [
```

```php
// メソッド名を'findAllTags'から'findAll'に変更
public function findAll(Query $query, int $bookmarkId)
```
すると、エラー文が以下のように変わりました。

```
Argument 2 passed to App\Model\Table\BookmarksTable::findAll() must be of the type integer, array given
```

## 引数を変更
「array型の引数が渡されました。引数の型はintegerである必要があります。」(意訳)ということで、引数部分を以下のように変更

```php
// 第二引数を'int $bookmarkId'から'array $options'に変更
public function findAll(Query $query, array $options)
...
        // '$bookmarkId'から'$options['bookmarkId']'に変更
        ->where(['bookmark_id' => $options['bookmarkId']]);

```

すると、データベースエラーに変わりました。

```
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'bookmarks_tags.title' in 'field list' 
```

## データベースエラーに対処
エラー分を読むに、以下のSQLが実行されたとのこと。

```sql
SELECT bookmarks_tags.title
 AS `bookmarks_tags__title`
 FROM tags
 INNER JOIN bookmarks_tags
 ON tags.id = bookmarks_tags.tag_id
 WHERE (bookmark_id = :c0 AND Bookmarks.id = :c1)
 LIMIT 1
```
自分が実行してほしいSQLは以下の通りです。

```sql
SELECT title
  FROM tags
  JOIN bookmarks_tags
  ON tags.id = bookmarks_tags.tag_id
  WHERE bookmard_id = $bookmarkId;
```
まずbookmarks_tags.titleを`SELECT`しているのが問題です。
自分がとってきてほしいのはtags.titleだ。
というわけで`get('bookmarks_tags')`の中身を'tags'に変えると、`WHERE`に関するエラーが表示された。

```
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'Bookmarks.id' in 'where clause' 


If you are using SQL keywords as table column names, you can enable identifier quoting for your database connection in config/app.php.

SQL Query:
SELECT tags.title AS `tags__title` FROM tags INNER JOIN bookmarks_tags  ON tags.id = bookmarks_tags.tag_id WHERE (bookmarks_tags.bookmark_id = :c0 AND Bookmarks.id = :c1) LIMIT 1
```
自分はBookmarks.id = :c1を条件に含めた覚えはないが、いったいどこでつけられたのだろうか？
というかbookmarksテーブルにidカラムは存在するはずだが？

#結論：名前は大事
何が原因なのか、すでに気づいた方もいるでしょう。
この問題の原因は、メソッドの名前が`findAll()`であることです。
関数の名前を`findTags()`に変えたらあっさり動きました。
最終的なメソッドは以下のようになりました。

```php
public function findTags(Query $query, array $options = array())
    {
        // SELECT title
        //  FROM tags
        //  JOIN bookmarks_tags
        //  ON tags.id = bookmarks_tags.tag_id
        //  WHERE bookmard_id = $bookmarkId;
        $tagsList = TableRegistry::get('tags')
        ->find('all')
        ->join([
            'table' => 'bookmarks_tags',
            'conditions' => 'tags.id = bookmarks_tags.tag_id',
        ])
        ->where(['bookmark_id' => $options['bookmarkId']]);

        return $tagsList;
    }
```
ちなみに、Controllerは以下のようになりました。"+"がついてる部分がチュートリアルにプラスした部分です。

```php
public function view($id = null)
    {
        $bookmark = $this->Bookmarks->get($id, [
            'contain' => ['Users', 'Tags']
        ]);

+       $tagsList = $this->Bookmarks->find('tags', [
+           'bookmarkId' => $id
+       ]);

        $this->set('bookmark', $bookmark);
+       $this->set('tagsList', $tagsList);
        $this->set('_serialize', ['bookmark']);
    }
```

知ってる人もいるでしょうが、`findAll()`という名前の関数はcakePHPにすでに存在します。
それを上書きする形となっていたため、予期せぬエラーが起きたのでしょう。
油断大敵、今日の一針明日の十針というお話でした　<font color="WhiteSmoke">上手いこと言いたくて、頑張ってことわざ調べました</font>