Web Marina

日々の業務や勉強などで得た知識をアウトプットしていきます。

【SQL】テーブル結合 ー外部結合ー

f:id:song-of-life1352607:20170220105406j:plain

こんにちは、マリンです。

前回の続き、次は外部結合についてです。

内部結合についてはこちらです⬇️

song-of-life.hatenablog.com




外部結合

外部結合は、結合したテーブルの指定したカラムの値が一致するものに加え、

どちらかのテーブルにしかないレコードも取得します。

どちらかというのは、基準とするテーブル(つまり左か右か)によって変わります。


例に使うテーブル構造を記述しておきます。

[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_loadpreloadなどもあります。

内部結合でも様々な方法がありますので、

ご興味のある方は調べてみてください。

なかなか奥が深い上に咀嚼に時間のかかるテーブル結合でした・・・

自分で色々試しながら理解してきましたが、

まだまだ自信がない部分も多々あります。

何か間違っているところなど御座いましたらぜひご教示ください。

最後に、今回こちらの記事に大変助けられましたのでご紹介しておきます。

qiita.com