すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

公開日:  |  最終更新日:

【Rails】 joinsメソッドの使い方 ~ テーブル結合からネストまで学ぶ

ぴっかちゃん
ぴっかちゃん

joinsメソッドとは、関連するテーブル同士を結合(内部結合)してくれるメソッドの事です。関連するテーブルと内部結合したデータを取得する際に便利なメソッドです。

joinsメソッドの基本構文-->
1
モデル名.joins(:関連名)

例えば、以下のようなアソシエーション定義している場合は、Owner.joins(:cats)のようにしてownersテーブルとcatsテーブルを内部結合する事が出来ます。

モデルファイル | OwnerモデルとCatモデルにアソシエーション定義 -->
1
2
3
4
5
6
7
8
9
# owner.rb
class Owner < ActiveRecord::Base
    has_many :cats # これが関連名
end

# cat.rb
class Cat < ActiveRecord::Base
    belongs_to :owner # これが関連名
end
コンソール | ownersテーブルとcatsテーブルをjoinsメソッドで定義-->
1
2
Owner.joins(:cats)
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

joinsメソッドの関連名には、アソシエーションの名前を指定します。関連するテーブル同士の結合はアソシエーションを基本的に利用しますが、joinsメソッドでも同様のことが出来ます。

joinsメソッドの基礎知識

この章では、joinsメソッドの基本的な使い方やテーブル結合について解説します。

テーブル結合とは?

テーブル結合とは、結合条件に従って複数のテーブルを1つのテーブルとして結合させることです。

テーブル結合のイメージ

テーブルの結合の種類には、内部結合(INNER JOIN)や外部結合と呼ばれるものがあります。joinsメソッドは、内部結合でテーブルが結合されています。

テーブル結合を理解するにはSQLの理解がどうしても必要になる為、「SQL・テーブル結合・ActiveRecord」の3つの関係を中心に図解形式で分かり易く解説していきます。

内部結合を理解しよう

内部結合とは、テーブル同士を結合するときに両方のテーブルで結合条件がマッチするレコードのみを取得する結合方法です。条件にマッチしないレコードは削除されます。

内部結合のイメージ

SQLでは、SELECT 文と INNER JOIN 句を組み合わせる事でテーブルを内部結合することが出来ます。INNER JOINの後に「結合するテーブル名」を記述し、ON句以降には「結合条件」を記述します。

SQL | 内部結合の構文
1
2
3
SELECT カラム名 FROM テーブル名
  INNER JOIN 結合するテーブル名 
    ON 結合条件

ON句の結合条件が両テーブルにマッチしたレコードのみを取得して1つのテーブルが作成されます。次の章から具体例を見ながら詳しく解説していきます。

ownersテーブルとcatsテーブルを内部結合してみよう

ネコの飼い主のテーブルがownersテーブル、ネコの名前を扱うテーブルがcatsテーブルの2つのテーブルを例に内部結合を確認していきましょう。テーブルの構成は下記の通りです。

ownersテーブルとcatsテーブルの構成

catsテーブルのowner_idカラムが外部キーとなって対応するownersテーブルのレコードを参照しています。主キーと外部キーが分からない方は、まずはこちらの図とテーブルで理解するアソシエーションをお読み下さい。

それでは、この2つのownersテーブルとcatsテーブルを内部結合してみます。

SQL | ownersテーブルとcatsテーブルを内部結合
1
2
3
SELECT * FROM owners
  INNER JOIN cats 
    ON cats.owner_id = owners.id;

このSQLが発行されると、テーブルは下記の様にベースとなるownersテーブルを左側にしてcatsテーブルを結合します。

ownersテーブルとcatsテーブルの内部結合結果

ON句の結合条件に従って「catsテーブルのowner_idとownerテーブルのidがマッチしているレコード」のみを取得しています。内部結合の場合は、結合条件にマッチしないレコードは削除されて1つのテーブルを構成します。

ownersテーブルには、id=4の加藤さんがいましが、加藤さんはまだ飼い猫が決まっていません。飼い猫がいない状態なのでcatsテーブルのowner_idカラムの値に4が入るレコードは存在しません。

結合条件にマッチしないレコードが削除される例

結合条件は「ownersテーブルのid = catsテーブルのowner_id」でしたが、加藤さんの場合は「id=4にマッチするowner_idがない状態」で結合条件がマッチしないので、加藤さんのレコードが削除されたテーブルが作成されたのです。

joinsメソッドで内部結合してみよう

Railsで関連するテーブルを内部結合するには、joinsメソッドを使います。joinsメソッドの関連名には、アソシエーションの名前を指定します。

joinsメソッドの定義-->
1
モデル名.joins(:関連名)

joinsメソッドで取得するレコードは、関連名で指定したテーブルの方ではなく、モデル名に対応するテーブルのレコードになるので注意して下さい。

ownersテーブルとcatsテーブルをjoinsメソッドで結合しよう

先ほどのownersテーブルとcatsテーブルを例に定義してみましょう。

ownersテーブルとcatsテーブルの構成

「1人のownerは複数のcatsを持つ関係(Owner has_many cats)」なので、Ownerモデルにhas_manyメソッドを定義し、Catモデルにはbelongs_toメソッドを定義します。

モデルファイル | OwnerモデルとCatモデルにアソシエーション定義 -->
1
2
3
4
5
6
7
8
9
# owner.rb
class Owner < ActiveRecord::Base
    has_many :cats # これが関連名
end

# cat.rb
class Cat < ActiveRecord::Base
    belongs_to :owner # これが関連名
end

このhas_manyメソッドやbelongs_toメソッドの引数で指定した関連名(:cats,:owner)をjoinsメソッドの引数で指定することでテーブルを内部結合する事が出来ます。

下記の様に、「Ownerモデルに定義したhas_many :cats」の:catsをjoinsメソッドの引数に指定する事でownersテーブルをベースとしたcatsテーブルを内部結合する事が出来ます。

発行されるSQL文に注目して下さい。

コンソール | ownersテーブルとcatsテーブルをjoinsメソッドで定義-->
1
2
Owner.joins(:cats)
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

上記のコードが実行されると、下記の様に「ownersテーブルのidとcatsテーブルのowner_idの値がマッチしているownersテーブルのレコード」が取得されていることが分かります。つまり、飼い猫のownerのみを取得しています。

ownerモデルにjoinsメソッドを使った結果

joinsメソッドは、モデルのレコードしか取らないのでownersテーブルのレコードをのみ取得した結果になっています。これは、発行されるSQLのSELECT文でownersテーブルの全てのカラムが指定されているからです。

試しにSELECT文を全てのカラムを取得するSELECT *に変更してみましょう。

SELECT文を*へ変更

この様にSELECT文を変更する事で、取得出来るレコードを変更する事が出来ます。後述しますがRailsでは、selectメソッドを利用する事で取得出来るレコードを変更する事が出来ます。

今回は、ownersテーブルをベースにしてcatsテーブルを内部結合しましたが、catsテーブルをベースにしてownersテーブルを結合する事も出来ます。その場合は下記の様なcatsテーブルのレコードを取得することが出来ます。

内部結合したcatsテーブルのレコードを取得する

Catモデルで定義してあるアソシエーションは下記の通りでした。

app/models/cat.rb | Catモデルのアソシエーション定義 -->
1
2
3
class Cat < ActiveRecord::Base
    belongs_to :owner # これが関連名
end

catsテーブルを結合元にして、joinsメソッドでownersテーブルを内部結合させると下記の様になります。

コンソール | Catモデルのアソシエーション定義 -->
1
2
Cat.joins(:owner)
SELECT `cats`.* FROM `cats` INNER JOIN `owners` ON `owners`.`id` = `cats`.`owner_id`

関連名は:ownerの単数形になっています。これは、joinsメソッドの引数には、Catモデルで定義したbelongs_toメソッドの関連名が入るからです。joinsメソッドの引数はテーブル名ではなく、アソシエーションで定義した関連名になるので注意してください。

joinsメソッドの引数を関連名ではなくテーブル名にした場合は、下記の様にエラーがおこります。

コンソール | joinsメソッドの引数を関連名ではなく、テーブル名にした場合 -->
1
2
Cat.joins(:owners)
ActiveRecord::ConfigurationError: Association named 'owners' was not found on Cat; perhaps you misspelled it?
ポイント
  1. joinsメソッドは、モデルのレコードしか取得しません。結合先のテーブルのレコードは取得しないので注意してください。
  2. モデルで定義したアソシエーションの関連名を引数に指定する事でテーブルを結合する事が出来ます。

joinsメソッドで結合したデータを使用する

先ほどのcatsテーブルとownersテーブルをjoinsメソッドで内部結合したデータの使用する例を見ていきたいと思います。

コンソール | catsテーブルとownersテーブルを内部結合する例-->
1
2
@owners = Owner.joins(:cats)
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

@ownersには、猫を飼っているownerのレコードが複数入っています。view側でownerの名前を表示するときに1つ1つのレコードを取り出す為にeachメソッドを使用します。

view側 | joinsメソッドで結合したデータを使用する -->
1
2
3
4
5
6
7
8
9
@owners.each do |owner|
    puts owner.name
end

# 実行結果
田中
田中
伊藤
高橋

eachメソッドを使うことで@owners.each do |owner|ownerにはレコードが1つずつ取り出されるので、ownerに対してカラム名を指定することによって飼い主の名前を表示することが出来ます。

selectメソッドで取得するレコードを変更する

先ほどは、ownersテーブルのidとcatsテーブルのowner_idの値がマッチするownersテーブルのレコードを取得していました。

コンソール | ownersテーブルとcatsテーブルをjoinsメソッドで定義-->
1
2
Owner.joins(:cats)
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

ownerモデルにjoinsメソッドを使った結果

joinsメソッドで取得したレコードに「catsテーブルのnameカラムのレコード」を追加したい場合は、selectメソッドを使って下記の様に記述する事が出来ます。

ownersテーブルとcatsテーブルをjoinsメソッドで定義-->
1
2
3
4
5
6
# selectメソッドの定義
select(:取得したい列)

# ownersテーブルの全てのカラムとcatsテーブルのnameカラムのレコードを取得
Owner.joins(:cats).select('owners.*, cats.name')
SELECT owners.*,cats.name FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

SQLを確認すると、SELECT文にはselectメソッドで指定したカラムが記述されています。このSQLで内部結合されたテーブルの結果は下記の通りです。

catsテーブルのnameカラム列を追加

この様にselectメソッドで取得するカラム列を指定する事も出来ます。

whereメソッドで条件を追加してみよう

joinsメソッドでテーブルを内部結合する際に、whereメソッドを使って条件を抽出してテーブルを結合する事が出来ます。

whereメソッドで条件を追加
1
モデル名.joins(:関連名).where(カラム名: 値)

先ほどのselectメソッドのコードを例にして確認していきます。

コンソール | ownersテーブルとcatsテーブルをjoinsメソッドで定義-->
1
2
Owner.joins(:cats).select('owners.*, cats.name')
SELECT owners.*,cats.name FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id`

catsテーブルのnameカラムを追加したテーブル

selectメソッドでは取得する列を決めていましが、whereメソッドでは条件にあったレコードを取得します。

猫を飼っている田中さんのレコードを取得する場合に、whereメソッドで定義すると下記の様になります。後ほど詳しく解説しますが、whereメソッドで指定してあるidカラムはownersテーブルに対しての条件です。

コンソール | whereメソッドでownersテーブルのid=1を追加-->
1
2
Owner.joins(:cats).select('owners.*, cats.name').where(id: 1)
SELECT owners.*, cats.name FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `owners`.`id` = 1

SQLを確認すると、条件がownersテーブルのid=1のWHERE句が追加されています。このWHERE句が追加された事で条件に従って下記の様にレコードが絞り込まれます。

ownersテーブルの内部結合にwhereメソッド追加

これによって結合されたテーブルの結果は、下記の様にid=1の田中さんのレコードを結合したテーブルになりました。

田中さんのレコードをwhereメソッドで取得した結果

この様にjoinsメソッドと並行してwhereメソッドを使うと、条件を抽出したテーブルを結合する事が出来ます。

whereメソッドのカラム指定方法1

whereメソッドのカラム指定方法には少し注意が必要です。サンプルコードを例に確認してみましょう。下記のコードを実行すると、飼い猫を持つ田中さんのレコードを取得する事が出来ます。

コンソール | whereメソッドでownersテーブルのid=1を追加-->
1
2
Owner.joins(:cats).where(name: "田中")
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `owners`.`name` = '田中'

これで作成されるテーブルは下記の通りです。(※selectメソッドで特にカラムを指定していないので、この場合はownersテーブルの全てのレコードのみを取得します。)

whereメソッドのサンプルテーブル

SQL文のWHERE句を確認すると、「where(name: "田中")」はownersテーブルのnameカラムに対しての条件だという事が分かります。

whereメソッドのカラム指定方法2

先ほどのwhereメソッドの指定方法では、結合元のownersテーブルに対しての条件だけでしたが、結合先のcatsテーブルに対して条件を追加する場合はどの様にすれば良いでしょうか?
それは、whereメソッドの引数に「結合先のテーブル名: { カラム名: 値 }」を指定する事で追加できます。

結合先のテーブルに対して条件を追加する -->
1
モデル名.joins(:関連名).where(結合先のテーブル名: { カラム名:  })

joinsメソッドの引数には、アソシエーションで指定した関連名を指定していましたが、whereメソッドでは「結合先のテーブル名」を指定している事に注意してください。

それでは、「catsテーブルのnameカラムの値が'モモ'」という条件を追加して、飼い猫の名前がモモで飼い主が田中さんのレコードを取得して見ましょう。

コンソール | catsテーブルのカラムに条件を追加 -->
1
2
Owner.joins(:cats).where(cats: { name: "モモ" }, name: "田中")
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `cats`.`name` = 'モモ'

飼い主の名前が "田中"で、飼い猫の名前が"モモ"のレコードは1つだけになるので、この様なテーブルが作成されます。

catsテーブルに対しての条件追加

SQL文のWHERE句を確認すると、「where(cats: { name: "モモ" }」は、where(テーブル名: {カラム名})catsテーブルのnameカラムに対しての条件になっています。一方でname: "田中"の方は、特にテーブル指定してないので、ownersテーブルに対しての条件となります。

joinsメソッドの注意点

冒頭でも少し触れましたが、下記のコードの様に、joinsメソッドの基本的な使い方は、joinsメソッドを実行したOwnerモデルのレコードを取得するもので、あくまでも関連するモデルはwhereメソッドなどで条件を指定して絞り込みをしているだけなので注意してください。

コンソール | catsテーブルのカラムに条件を追加 -->
1
2
Owner.joins(:cats).where(cats: { name: "モモ" }, name: "田中")
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `cats`.`name` = 'モモ'

また、先ほども紹介した様に関連先のモデルのレコードを取得する場合は、selectメソッドを使うことで取得することが出来ます。

ポイント
  1. joinsメソッドは、実行元のモデルの関連したモデルのレコードを取得するメソッドではありません。
  2. 関連するモデルのレコードが欲しい場合は、selectメソッドを使用します。

SQLの知識の必要性

Rails(O/Rマッパー)を使用していると、SQLの知識は要らないと思われがちですが、「無駄なクエリを発行していないか、データの引き出しを間違えていないか」は、裏側で発行されるSQLで確認できます。

他にもパフォーマンス・デバック・複雑な操作の際にもSQLの知識は必要になります。

SQLの知識がなく、「データベースの操作だけできれば良い」と考えているとプログラムの品質や信頼を失ってしまいます。

SQLについて不安がある方は、基礎から一通り学べる以下の参考書を利用すると良いでしょう。

スッキリわかるSQL入門 第2版

発売から数年で不動の定番テキストとなった大人気SQL入門書に、最新のDBに対応した改訂版が登場!

joinsメソッドの応用的な使い方

この章では、joinsメソッドの応用的な使い方について解説します。

複数の関連名の指定

joinsメソッドの引数には、複数の関連名を指定する事が出来ます。

joinsメソッドに複数の関連名を指定する -->
1
モデル名.joins(:関連名, :関連名)

複数の関連名をdogsテーブル追加して確認していきましょう。

dogsテーブルを追加

Catモデルと同様に、複数のdogは、1人の飼い主に属している関係です。(Dog belongs_to Owner)
その場合のアソシエーションは、下記の様になります。

モデルファイル | OwnerモデルとCatモデルにアソシエーション定義 -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# owner.rb
class Owner < ActiveRecord::Base
    has_many :cats
    has_many :dogs
end

# cat.rb
class Cat < ActiveRecord::Base
    belongs_to :owner
end

# dog.rb
class Dog < ActiveRecord::Base
    belongs_to :owner # アソシエーション追加
end

このアソシエーションの関係で「ownersテーブルにcatsテーブルとdogsテーブルを内部結合」するには、下記の様にjoinsメソッドに:cats:dogsの関連名を指定する事で結合が出来ます。

コンソール | 複数の関連名を指定する-->
1
2
Owner.joins(:cats, :dogs)
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` INNER JOIN `dogs` ON `dogs`.`owner_id` = `owners`.`id`

このコードで発行されるSQLは複雑そうに見えますが、今までやってきた知識で理解することが出来ます。

joinsメソッドで取得するのはモデルのレコードなので、ownersテーブルのレコードを最終的に取得しています。

catsテーブルとdogsテーブルをownersテーブルに内部結合

そして、INNER JOIN句でcatsテーブルとdogsテーブルを内部結合していますが、その結合条件は、「ownersテーブルのid = catsテーブルのowner_id」と「ownersテーブルのid = dogsテーブルのowner_id」にマッチしたレコードを取得しています。

最終的には、下記の様なテーブルが作成されます。

catsテーブルとdogsテーブルをownersテーブルに内部結合した最終的なテーブル

これによって、犬と猫を飼っているownerのレコードを取得することができます。

複数の関連名がある場合のwhereメソッドの指定方法

内部結合する際にwhereメソッドを使って条件を抽出してテーブル結合することが出来ましたが、複数の関連名を指定した場合でもwhereメソッドで条件を絞り込むことが出来ます。

先ほど取得した犬と猫を飼っているownerのレコードにwhereメソッドを使って「猫の名前がクロで、犬の名前がシロの飼い主のレコード」を取得していきます。

コンソール | 猫の名前がクロで、犬の名前がシロの飼い主のレコードを取得 -->
1
2
Owner.joins(:cats, :dogs).where(cats: { name: 'クロ'}, dogs: { name: 'シロ' })
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` INNER JOIN `dogs` ON `dogs`.`owner_id` = `owners`.`id` WHERE `cats`.`name` = 'クロ' AND `dogs`.`name` = 'シロ'

where(cats: { name: 'クロ'}, dogs: { name: 'シロ' })を追加したことによって、SQLのWHERE句で「catsテーブルのname='クロ'」と「dogsテーブルのname='シロ'」のレコードを抽出していることが分かります。

whereメソッドを使って猫の名前がクロで、犬の名前がシロの飼い主のレコード」を取得

WHERE句で抽出されたレコードに対してSELECT文で取得するカラムをownersテーブルの全てのカラムと指定しているので、最終的には、下記の様なテーブルが作成されます。

whereメソッドで最終的に取得出来るレコード

先ほどのコードで返り値を確認すると、伊藤さんのレコードを取得することが出来ています。

コンソール | 猫の名前がクロで、犬の名前がシロの飼い主のレコードを取得 -->
1
2
3
Owner.joins(:cats, :dogs).where(cats: { name: 'クロ'}, dogs: { name: 'シロ' })
=> #<ActiveRecord::Relation
 [#<Owner id: 2, name: "伊藤", created_at: "2019-11-25 08:51:27", updated_at: "2019-11-25 08:51:27">]>

これによって、「猫の名前がクロで、犬の名前がシロの飼い主の伊藤さんのレコード」を取得することが出来ました。

今回は、whereメソッドで指定した条件にマッチするレコードを取得することが出来ましたが、条件にマッチするレコードが無かった場合の返り値はどうなるでしょうか?

飼い猫の名前を存在しないアオという名前に変更して確認してみます。

コンソール | 猫の名前を存在しないアオという名前に変更 -->
1
2
Owner.joins(:cats, :dogs).where(cats: { name: 'アオ' }, dogs: { name: 'シロ' })
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` INNER JOIN `dogs` ON `dogs`.`owner_id` = `owners`.`id` WHERE `cats`.`name` = 'アオ' AND `dogs`.`name` = 'シロ'

SQL文は発行されていますが、返り値を確認してみます。

コンソール | 返り値を確認 -->
1
2
Owner.joins(:cats, :dogs).where(cats: { name: 'アオ' }, dogs: { name: 'シロ' })
=> #<ActiveRecord::Relation []>

この様に条件にマッチしない場合は、空のActiveRecord::Relationが返り値となります。

joinsメソッドのネスト

1人の飼い主は複数の猫を飼っていましたが、その猫には複数の子猫がいます。子猫のテーブルをcat_childrenテーブルとします。1匹の猫は複数の子猫を持ち「Cat has many CatChildren」の関係になります。catsテーブルとcat_childrenテーブルの関係は下記の通りです。

catsテーブルとcat_childrenテーブルの関係

さらに複数の猫は1人の飼い主に属しているので、ownersテーブルを含めた3つのテーブルの関係を整理すると下記の様になります。

catsテーブル、ownersテーブル、cat_childrenテーブルの関係性

ownersテーブルとcat_childrenテーブルは、直接の関係はありませんが、cat_childrenテーブルが外部キー(cat_id)を通してcatsテーブルを参照し、catsテーブルが外部キー(owner_id)を通してownersテーブルを参照しています。

この様に連続して参照している様な関連は、joinsメソッドをネストすることによって3つのテーブルを内部結合することが出来ます。

連続して参照している関連名は下記の様に定義します。

joinsメソッドのネストの書き方-->
1
モデル名.joins(関連名1: :関連名2)

それでは、 先ほどのownersテーブルとcatsテーブル、cat_childrenテーブルを例に犬と猫を飼っている飼い主のレコードを取得して見ましょう。

1匹の猫は複数の子猫を持つので「Cat has many CatChildren」の関係なので、それぞれのアソシエーションは下記の様に定義します。

モデルファイル | OwnerモデルとCatモデル,CatChildモデルにアソシエーション定義 -->
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# owner.rb
class Owner < ActiveRecord::Base
    has_many :cats
    has_many :dogs
end

# cat.rb
class Cat < ActiveRecord::Base
    belongs_to :owner
    has_many :cat_children
end

# cat_child.rb
class CatChild < ActiveRecord::Base
    belongs_to :cat
end

そして、このアソシエーションを元に連続して参照している関連をjoinsメソッドで内部結合します。joinsメソッドの引数には、「モデル名.joins(Ownerモデルの関連名: :Catモデルの関連名)」つまり、Owner.joins(cats: :cat_children)を指定します。

コンソール | joinsメソッドのネストの書き方-->
1
2
Owner.joins(cats: :cat_children) #joins(Ownerモデルの関連名: :Catモデルの関連名)
 SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` INNER JOIN `cat_children` ON `cat_children`.`cat_id` = `cats`.`id`

このコードで発行されるSQL文を確認すると、「ownersテーブルのidとcatsテーブルのowner_idがマッチするレコード」と「catsテーブルのidとcat_childrenテーブルのcat_idがマッチしているレコード」を内部結合の条件にしてテーブルが作成されています。

連続して参照するテーブルを内部結合する

この様な結合条件にすることによって、参照先のテーブルの更に関連するテーブルを結合する事ができます。

ネストしている場合のwhereメソッドの指定方法

連続して参照している様な関連でもwhereメソッドを使って条件を抽出することが出来ます。
指定方法は、モデルの関連がネストしているからといってwhereの中もwhere(cats: { name: 'クロ', cat_children: { name: 'クロコ' })の様にネストする必要はありません。

複数の関連名がある場合のwhereメソッドの指定方法と特に変わりません。

結合先のテーブルに対して条件を追加する -->
1
モデル名.joins(:関連名).where(結合先のテーブル名1: { カラム名:  }, 結合先のテーブル名2: { カラム名:  })

それでは、飼い猫の名前がクロで子供の名前がクロコの飼い主のレコードを取得してみましょう。

コンソール | catsテーブルとcat_childrenテーブルに対して条件を追加 -->
1
2
Owner.joins(cats: :cat_children).where(cats: {name: 'クロ'}, cat_children: {name: 'クロコ'})
SELECT `owners`.* FROM `owners` INNER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` INNER JOIN `cat_children` ON `cat_children`.`cat_id` = `cats`.`id` WHERE `cats`.`name` = 'クロ' AND `cat_children`.`name` = 'クロコ'

このコードで発行されるSQLを元に結合されるテーブルは下記の通りです。

ネストする関連にwhereメソッドで条件を絞り込む

WHERE句で抽出されたレコードに対してSELECT文で取得するカラムをownersテーブルの全てのカラムと指定しているので、最終的には、下記の様なテーブルが作成されます。

猫と犬を持つ飼い主

joinsメソッドは、関連するテーブルの内部結合したデータを取得する際に便利なメソッドですが、場合によってはN+1問題が発生してしまう可能性があるので注意してください。

N+1問題が分からないという方は、N+1問題を理解しながらincludesメソッドで解決してみよう!を参考にしてください。

この記事のまとめ

  • joinsメソッドとは、関連するテーブル同士を結合(内部結合)してくれるメソッドのこと
  • 内部結合とは、テーブル同士を結合するときに両方のテーブルで結合条件がマッチするレコードのみを取得する結合方法
  • 内部結合の場合は、結合条件にマッチしないレコードは削除されて1つのテーブルを構成する