【Rails】left_joinsメソッドで定義する左外部結合とは?

Rails

left_joinsメソッドとは、関連するレコードが有る無しに関わらずレコードのセットを取得してくれるメソッドです。

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

left_joinsメソッドは、関連するテーブル同士を左外部結合します。引数には、アソシエーションで定義した関連名を渡します。エイリアスにleft_outer_joinsメソッドがあります。

left_joinsメソッドの使い方

この章では、left_joinsメソッドの使い方について解説します。

基本的な使い方
リンクをコピーしました

left_joinsメソッドは、結合したテーブルから指定したモデルのデータのみ取得します。下記の「飼い主を管理するownersテーブル」と「飼い猫を管理するcatsテーブル」を結合して確認します。

ownersテーブルとcatsテーブル

Ownerモデルを指定してleft_joinsメソッドを使うと、下記の様に結合したテーブルから左側のownersテーブルのデータを取得します。

left_joinsメソッドによって、左側のデータを取得する例

コンソール | left_joinsメソッドでOwnerモデルのデータを取得
1
2
3
4
5
6
7
8
9
Owner.left_joins(:cats)
SELECT  `owners`.* FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` LIMIT 11

=> #<ActiveRecord::Relation [
#<Owner id: 1, name: "田中", created_at: "2019-12-25 02:43:01", updated_at: "2019-12-25 02:43:01">, 
#<Owner id: 1, name: "田中", created_at: "2019-12-25 02:43:01", updated_at: "2019-12-25 02:43:01">, 
#<Owner id: 2, name: "伊藤", created_at: "2019-12-25 02:43:15", updated_at: "2019-12-25 02:43:15">, 
#<Owner id: 3, name: "高橋", created_at: "2019-12-25 02:43:20", updated_at: "2019-12-25 02:43:20">, 
#<Owner id: 4, name: "加藤", created_at: "2019-12-25 02:43:23", updated_at: "2019-12-25 02:43:23">]>

また後述しますが、左側のテーブルを基準に結合することを左外部結合と言います。
左外部結合は、上記のピンク色の部分の「条件(cats.owner_id = owners.id)が一致したデータ」と緑色の部分の「基準となる左のテーブル(owners)のデータ」を取得します。

left_joinsメソッドの引数
リンクをコピーしました

left_joinsメソッドの引数には、アソシエーションで定義した関連名を指定します。

先ほどのownersテーブルとcatsテーブルのアソシエーションは下記の通りです。(飼い主は沢山の猫を飼っているが、猫は1人の飼い主に属す関係)

owner.rb/cat.rb | それぞれのアソシエーションに定義した関連名を確認する
1
2
3
4
5
6
7
8
9
# owner.rb
class Owner < ApplicationRecord
    has_many :cats # 関連名
end

# cat.rb
class Cat < ApplicationRecord
    belongs_to :owner # 関連名
end

catsテーブルを左側の基準にしてownersテーブルと結合する場合は、Cat.left_joins(:owner)と定義しますが、下記の様に間違ってテーブル名:ownersを渡してしまうとActiveRecord::ConfigurationError が発生します。

コンソール | left_joinsメソッドの引数に関連名ではなく、間違ってテーブル名を渡す例
1
2
3
Cat.left_joins(:owners)
Traceback (most recent call last):
ActiveRecord::ConfigurationError (Can't join 'Cat' to association named 'owners'; perhaps you misspelled it?)

left_joinsメソッドの引数は、テーブル名ではなくアソシエーションの関連名を渡す事に注意してください。

条件を指定する方法
リンクをコピーしました

left_joinsメソッドでテーブルを左外部結合する際に、whereメソッドで条件指定する事が出来ます。whereメソッドのカラム名は、left_joinsメソッドで指定したモデルのカラムを指します。

左外部結合する際に条件を指定する
1
モデル名.left_joins(:関連名).where(カラム名: )

ownerモデルにleft_joinsメソッドでcatsテーブルと左外部結合する際に、飼い主の名前が高橋さんという条件where(name: '高橋')を追加すると、下記の様に結合したテーブルからwhereメソッドで指定した条件を抽出している事が確認出来ます。

whereメソッドによって抽出できるレコード

コンソール|条件を追加して左外部結合する
1
2
3
Owner.left_joins(:cats).where(name: '高橋')
SELECT  `owners`.* FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `owners`.`name` = '高橋' LIMIT 11
=> #<ActiveRecord::Relation [#<Owner id: 3, name: "高橋", created_at: "2019-12-25 02:43:20", updated_at: "2019-12-25 02:43:20">]>

また、catsテーブルの様な結合先のテーブルに対して条件を指定する場合は、下記の様にwhere(結合先のテーブル名: { カラム名: 値 })で指定します。

結合先のテーブルに対して条件を指定
1
モデル名.left_joins(:関連名).where(結合先のテーブル名: { カラム名:  })

それでは、猫を飼っていないオーナーを取得してみます。猫を飼っていないオーナーとは、結合先のcatsテーブルのowner_idカラムがnilになっているレコードが該当するので、下記の様にwhereメソッドで条件を指定します。

コンソール | 猫を飼っていないオーナーを取得する
1
2
3
4
5
Owner.left_joins(:cats).where(cats: { owner_id: nil })
SELECT  `owners`.* FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `cats`.`owner_id` IS NULL LIMIT 11

=> #<ActiveRecord::Relation [ # 返り値
#<Owner id: 4, name: "加藤", created_at: "2019-12-25 02:43:23", updated_at: "2019-12-25 02:43:23">]>

返り値を確認すると、猫を飼っていない加藤さんのデータを取得することが出来ています。また、Ownerモデルに対してleft_joinsメソッドを指定したので、ownerモデルのデータしか取得されていないことも確認できます。

joinsメソッドと同じ指定方法なので、詳しくはwhereメソッドのカラム指定方法2を参考にしてください。

結合先のデータを取得する方法
リンクをコピーしました

left_joinsメソッドは、デフォルトでは指定したモデルのデータしか取得しません。下記は、Ownerモデルがleft_joinsメソッドに指定されている為Ownerモデルのデータしか取得されません。

left_joinsメソッドによって、左側のデータを取得する例

指定したモデルのデータだけではなく、結合先のデータを表示させるには、selectメソッドを併用します。下記の様に文字列を使ってselect('テーブル名.カラム名')と指定する事で、結合先のデータを取得する事が出来ます。

結合先のデータを取得する
1
モデル名.left_joins(:関連名).select('テーブル名.カラム名')

それでは、selectメソッドで「Ownerモデルの全てのデータと結合先のcatsテーブルのspeciesカラムのデータ」を取得していきます。

結合先のデータを取得する
1
2
Owner.left_joins(:cats).select('owners.*, cats.species') 
SELECT  owners.*, cats.name FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` LIMIT 11

下記の様に、selectメソッドで指定したカラムのデータを取得する事が出来ています。試しに上から2番目の田中さんの飼い猫の種類を取得します。

selectメソッドで結合先のテーブルのデータも取得する

コンソール | 上から2番目の田中さんの飼い猫の種類を取得する
1
2
3
4
Owner.left_joins(:cats).select('owners.*, cats.species').first.species
  Owner Load (1.1ms)  SELECT  owners.*, cats.species FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` ORDER BY `owners`.`id` ASC LIMIT 1

=> "スコティッシュ・フォールド"

selectメソッドを使う事でデフォルトの指定モデルだけではなく結合先のデータも取得する事が出来ます。

SQLで理解する左外部結合

この章では、左外部結合についてSQLとテーブルを使って解説します。

左外部結合とは?
リンクをコピーしました

左外部結合とは、外部結合の結合方式の1つです。結合条件に従って複数のテーブルを1つのテーブルとして結合しますが、その際に「結合条件に一致するレコード」と「左側のテーブルにしかないレコード」を取得します。

左外部結合の取得する部分

ここからはleft_joinsメソッドの裏側で発行される左外部結合のSQLについて、サンプルコードとテーブルを使いながら詳しく解説していきます。

RailsはSQLを特に意識する事なく使えますが、コードの裏側で発行されるSQLが分かると、メソッドについてより深く理解する事が出来ます。

左外部結合の特徴
リンクをコピーしました

左外部結合は、「左側のテーブルにしかないレコードを取得して結合する」という大きな特徴があります。

Ownerモデルを指定してleft_joinsメソッドを使うと、下記の様に加藤さんは猫を飼っていませんが、結合先のcatsテーブルの値が有る無しに関わらず結合します。

left_joinsメソッドによって、左側のデータを取得する例

このコードで発行されるSQL文は、下記の通りです。2行目にあるLEFT OUTER JOIN句に注目してください。

SQL | Owner.left_joins(:cats)で発行されるSQL文
1
2
3
SELECT  owners.*  /* 取得するカラム */
FROM owners LEFT OUTER JOIN cats  /* ownersテーブルを左側の基準にしてcatsテーブルと結合する */
ON cats.owner_id = owners.id LIMIT 11    /* テーブル結合条件 */

このLEFT OUTER JOIN句の左側にあるownersテーブルの全てのデータを抽出して、これを基準にON句以降の結合条件で右側のcatsテーブルをチェックして一致するレコードを結合させています。

ownersテーブルを基準に左外部結合

ownersテーブルの全てのデータを基準にして結合条件に一致すれば、catsテーブルのレコードが結合されますが、上記の加藤さんの様に条件に一致せず結合出来なかったとしてもレコードにNULLが入って結合されます。

左外部結合した結果

そして最後に、SELECT文で指定したカラムのデータを結合されたテーブルから取得します。今回はSELECT owners.*となっているので、下記の様にownersテーブルの全てのカラムのデータを取得します。

select文でownersテーブルの全てのカラムのデータを取得

また、加藤さんのcatsテーブルと結合されたレコードにNULLが入ったのは、ownersテーブルのデータが基準となって結合しているからです。もし、ownersとcatsを反対にしてcats LEFT OUTER JOIN ownersにした場合は、下記の様にcatsテーブルの全てのデータを抽出してownersテーブルに結合条件に一致するレコードをチェックしにいきます。

catsテーブルを基準に左外部結合

今回は、catsテーブルに条件に一致しないレコードは無かったので結合結果は下記の様になります。

catsテーブルを基準に左外部結合した結果

catsテーブルのデータを基準にして結合条件をチェックしているので、加藤さんのレコードはもちろん存在しません。

Pikawakaマーク左外部結合のポイント

  1. LEFT OUTER JOIN句の左に位置するテーブルが基準となる
  2. 結合条件に一致するレコードと左のテーブルにしか無いデータも取得する
  3. 左のテーブルにしかない場合は、結合先のレコードにNULLが入る

内部結合と左外部結合の違い
リンクをコピーしました

左外部結合は、条件に一致したレコードに加えて左のテーブルにしかないデータも結合しますが、左のテーブルにしか無いデータが必要な場面とは、一体どの様な場面なのでしょうか?

先ほどのownersテーブルを基準にしてcatsテーブルと結合した時は、左のテーブル(owners)にしか無い加藤さんのデータも結合されてました。

ownersテーブルを基準にcatsテーブルと左外部結合した結果

左のテーブルにしか無いということは、加藤さんには飼い猫はいません。ownersテーブルを基準にcatsテーブルを左外部結合すると「猫を飼っているオーナー」と「猫を飼っていないオーナー」の2種類のオーナーを取得することが出来ます。

結合したcatsテーブルのowner_idがNULLの場合は、猫を飼っていないownerになるので、下記の様にselectメソッドで結合先のcatsテーブルのidカラムのデータを取得します。そして、猫を飼っているか飼っていないかで処理を分ける事が出来ます。

shell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
owners = Owner.left_joins(:cats).select('owners.*, cats.owner_id')
SELECT  owners.*, cats.owner_id FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` LIMIT 11

owners.each do |owner|
  if owner.owner_id
    p "#{owner.name}さんは猫を飼っています"
  else
    p "#{owner.name}さんは猫を飼っていません"
  end
end

# 出力結果
"田中さんは猫を飼っています"
"田中さんは猫を飼っています"
"伊藤さんは猫を飼っています"
"高橋さんは猫を飼っています"
"加藤さんは猫を飼っていません"

これが内部結合になると、下記の様に結合条件に一致するレコードのみを結合します。

ownersテーブルとcatsテーブルを内部結合した結果

つまり、内部結合すると「猫を飼っているオーナー」のデータしか取得する事が出来ないのです。

猫を飼っていないオーナーのみを取得するには、下記の様にLEFT OUTER JOIN句で左外部結合して、whereメソッドで結合先のcatsテーブルのowner_idカラムがnilのデータを抽出します。

コンソール | 猫を飼っていないオーナーを取得する
1
2
3
4
5
Owner.left_joins(:cats).where(cats: { owner_id: nil })
SELECT  `owners`.* FROM `owners` LEFT OUTER JOIN `cats` ON `cats`.`owner_id` = `owners`.`id` WHERE `cats`.`owner_id` IS NULL LIMIT 11

=> #<ActiveRecord::Relation [ # 返り値
#<Owner id: 4, name: "加藤", created_at: "2019-12-25 02:43:23", updated_at: "2019-12-25 02:43:23">]>

返り値は、猫を飼っていない加藤さんのデータを取得する事が出来ています。

Pikawakaマークポイント

  1. 内部結合は、結合条件に一致するレコードのみを結合する
  2. 左外部結合は、結合条件に一致するレコードに加えて、基準となる左側のテーブルにしかないデータも結合する
まとめ
  • 左外部結合とは、結合条件に一致するレコードに加えて左側の軸となるテーブルのレコードを結合する方法です。
  • Railsで左外部結合をするには、left_joinsメソッドで定義します。
  • left_joinsメソッドの引数には、テーブル名ではなく、アソシエーションで定義した関連名を渡します。