【SQL】テーブル結合 ー外部結合ー
こんにちは、マリンです。
前回の続き、次は外部結合についてです。
内部結合についてはこちらです⬇️
外部結合
外部結合は、結合したテーブルの指定したカラムの値が一致するものに加え、
どちらかのテーブルにしかないレコードも取得します。
どちらかというのは、基準とするテーブル(つまり左か右か)によって変わります。
例に使うテーブル構造を記述しておきます。
[cars]
belongs_to :owner
id | name | owners_id |
---|---|---|
1 | crown | 1 |
2 | vellfire | 1 |
3 | stepwagon | 3 |
4 | legacy | 2 |
5 | elgrand |
[owners]
has_many :cars
id | name |
---|---|
1 | ichiro |
2 | jiro |
3 | saburo |
4 | shiro |
基本構文
SELECT カラム1, カラム2,... FROM テーブル1 LEFT(RIGHT) OUTER JOIN テーブル2 ON 条件
LEFTまたはRIGHTはどちらのテーブルを基準にするかを示しています。
LEFT OUTER JOIN: 左側(テーブル1)を基準結合
RIGHT OUTER JOIN: 右側(テーブル2)を基準に結合
例えばLEFTであればテーブル1が基準となり、
並び順や、前述したこちらのテーブルにしかないレコードも
テーブル1の物が表示されます。
詳細
それでは例を使った詳細です。
なお、railsでの方法には私が職場でよく使用するincludes
をご紹介します。
*内部、外部ともに様々な方法がありますが、
現時点でよく使うもの以外は理解が浅いので、
内部はjoins
、外部はincludes
のみご紹介させていただきます。
[SQL]
SELECT * FROM cars LEFT OUTER JOIN owners ON cars.owner_id = owner.id
LEFT OUTER JOIN
なので、cars
が基準テーブルになります。
[Rails]
Cars.includes(:owner)
こちらが基本形。
includes
は厳密には外部結合ではないようですが、
同等の値を取得することができます。
上記を実行すると
Car Load (1.2ms) SELECT "cars".* FROM "cars" Owner Load (12.8ms) SELECT "owners".* FROM "owners" WHERE "owners"."id" IN (6, 1, 4)
と言うSQLが発行されます。
ご覧の通り、まずcars
の全カラムを呼び出し、
その後上記のowner_id
を元にowners
の該当レコードを取得しています。
ちょっと二度手間感がありますね。
そんな時はreferences
を使います。
includesとreferencesをセットで使う
先ほどの基本形でも取れる値は同じなのですが、
もっとスマートに1回のSQLでinclude先のレコードも取ってくることができます。
それがreferences
メソッドです。
Car.includes(:owner).references(:owner)
これを実行すると
SQL (0.2ms) SELECT "cars"."id" AS t0_r0, "cars"."owner_id" AS t0_r1, "cars"."name" AS t0_r2, "cars"."created_at" AS t0_r3, "cars"."updated_at" AS t0_r4, "cars"."dealership" AS t0_r5, "cars"."display_order" AS t0_r6, "owners"."id" AS t1_r0, "owners"."name" AS t1_r1,"owners"."created_at" AS t1_r2, "owners"."updated_at" AS t1_r3 FROM "cars" LEFT OUTER JOIN "owners" ON "owners"."id" = "cars"."owner_id"
となります。
ご覧の通り、SQLは1回のみでowner
のレコードも取得できています。
上記を見ると、
LEFT OUTER JOIN
になっていますね。
includes
だけではこうはなっていなかったので、
外部結合として使うのであれば、こちらがお作法的には正しいのかな?
なんて思ったりしました。
Ownerのカラムを使う
上記のまま、例えば
Car.includes(:owner).references(:owner).each do |car| car.owner.name end
とするとおそらく
NoMethodError: undefined method 'name' for nil:NilClass
と出てきてしまうと思います。
オーナーのいない車もテーブルに入っているため、
そこのオーナー名が引っかかってしまっているわけですね。
このような時はtry
を使うと便利です。
- try:対象がnilでない場合のみメソッドを呼び出す。
つまりowner ? owner.name
と同じことをやってくれるということです。
try
の方が短く済んで良いかなと思います。
Car.includes(:owner).references(:owner).each do |car| car.owner.try(:name) end
これできちんとオーナーの名前も取得でき、もしオーナーが空の場合には取得しない。
ということが実現できます。
まとめ
今回はincludes
&references
の方法をご紹介しましたが、
他にもeager_load
やpreload
などもあります。
内部結合でも様々な方法がありますので、
ご興味のある方は調べてみてください。
なかなか奥が深い上に咀嚼に時間のかかるテーブル結合でした・・・
自分で色々試しながら理解してきましたが、
まだまだ自信がない部分も多々あります。
何か間違っているところなど御座いましたらぜひご教示ください。
最後に、今回こちらの記事に大変助けられましたのでご紹介しておきます。