Ruby on RailsでのJOIN活用をわかりやすく解説
はじめに
Ruby on Railsでアプリケーションを作るとき、データ同士を結び付けて活用する場面は多いですよね。 記事とコメント、ユーザーと投稿、注文と商品など、いろいろなテーブルを結び付けて表示したい場面があるのではないでしょうか。 そんなときに役立つのが、JOIN というデータベースのテーブル結合機能です。 Railsにおいては、Active Recordのクエリメソッドを使って、JOINをわかりやすい形で書くことができます。 しかし、実務でJOINを使うときにどのメソッドを使えばいいのか、どう書けば意図したデータが取得できるのかが分からないという方もいるかもしれません。 今回は初心者の皆さんにも理解しやすいよう、JOINを使ったActive Recordの活用シーンや書き方を具体例とともに紹介していきます。
この記事を読むとわかること
- JOINを使う理由と、実務で役立つ具体的なシーン
- RailsのActive RecordでJOINを記述する方法
- 等価結合以外の結合や、テーブル別名を活用する方法
- JOIN関連のよくあるトラブルと対策
- 実務での注意点やパフォーマンス面のヒント
JOINを使うメリットとは
RailsのJOINは、複数のテーブルをまとめて検索できる という特長があります。 単純にテーブルを横並びにするだけでなく、リレーションを活かして効率的に必要なデータを取得することができます。
データ取得の効率化
例えば、ユーザー と 投稿 の2つのテーブルがあり、それぞれに紐づくコメント数やタグ情報なども取得したいとします。 もしテーブルを個別にSELECTしていたら、ユーザー情報を取得してから投稿を取得して、さらにコメントやタグを取得して…というふうに何度もクエリを発行する必要があるかもしれません。 JOINを使えば1回のクエリで関連情報をまとめて取り出すことができるので、データ取得の効率化につながります。
実務での活用シーン
JOINを使うシーンとして、たとえば「注文一覧で、注文者の名前や配送先の情報を一覧表示する」といった場面があります。 ECサイトを想像すると、orders テーブルに対して users や addresses など複数のテーブルが紐づくことが多いですよね。 そこでJOINを使って、ひとまとめに表示できるようにすると処理がスムーズになることが多いです。
どのようなJOINがあるか
JOINには主に以下の種類があります。
- INNER JOIN:結合キーが両方のテーブルで一致するデータのみ取得
- LEFT OUTER JOIN:左側のテーブルを基準に、右側のテーブルとの一致がなくても左側の行をすべて取得
- RIGHT OUTER JOIN:右側のテーブルを基準にする結合(あまりRailsでは多用されない)
- FULL OUTER JOIN:両テーブルの全行を取得する(DBの種類によってはサポートしていない場合もあります)
RailsのActive Recordでは、joins や left_joins を使うことで、INNER JOINやLEFT OUTER JOINを簡単に書くことができます。
Active RecordでのJOINの基本
RailsでJOINをする場合、よく使われるのが joins メソッドです。 Active Recordのリレーションメソッドとして提供されているため、テーブル名や関連付け名を指定するだけでシンプルにJOINを実行できます。
基本的な構文
たとえば、PostモデルとUserモデルが「1対多」の関係で、PostがUserに属している場合を考えます。 データベース上では「postsテーブル」に「user_id」カラムがある状態です。
# Post.rb class Post < ApplicationRecord belongs_to :user end # User.rb class User < ApplicationRecord has_many :posts end
このとき、PostをUserの情報と合わせて取得したい場合に、以下のように書きます。
# Postモデルに対してUserテーブルをJOINする例 posts = Post.joins(:user)
これにより、Railsが内部的に SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id"
のようなクエリを発行します。
LEFT OUTER JOINを使う場合
もしLEFT OUTER JOINのように、結合されるデータがなくても左のテーブルの行をすべて取得したい場合は、以下のように書きます。
posts = Post.left_joins(:user)
結果として、ユーザーが存在しない投稿であっても取得することができます。 これもActive Recordがうまくやってくれるので、クエリを書き換える手間が少なくて便利ですね。
JOINで参照するテーブルを増やす
実際のアプリケーションでは、PostとUserだけでなく、さらにCommentやTagなど別の関連テーブルも一緒に取得したい場合がありますよね。 JOINをネストすることで、複数の関連テーブルをまとめて取得することができます。
belongs_toが複数ある例
例えば、Comment モデルが Post に属しており、さらに Post が User に属しているケースを見てみましょう。
# Comment.rb class Comment < ApplicationRecord belongs_to :post end # Post.rb class Post < ApplicationRecord belongs_to :user has_many :comments end # User.rb class User < ApplicationRecord has_many :posts end
CommentモデルからUser情報までたどりたいときは、以下のように書きます。
comments = Comment.joins(post: :user)
これでCommentからPost、さらにUserへとつながるテーブルがJOINされます。 テーブルを多段階に結合できるので、複雑なリレーションも1回のクエリで一気に取得することができます。
has_manyで多段階JOINする
一方で、User モデルからCommentまでをJOINで取得したい場合には、次のように書きます。
users = User.joins(posts: :comments)
これにより、User -> Post -> Comment の順にJOINが実行されます。 INNER JOINの場合は、コメントがない投稿や、投稿がないユーザーは結果に含まれなくなるので注意が必要です。
JOINを使った条件検索
JOINの良いところは、結合先テーブルのカラムを条件にして検索することができる点です。 例えば、ユーザー名を部分一致検索して該当する投稿データを引っ張ってくる、というようなこともシンプルに実現できます。
具体例:ユーザーの名前を条件に検索
Userテーブルのnameカラムから、"Alice" を含む投稿だけを取得してみましょう。
posts = Post.joins(:user) .where("users.name LIKE ?", "%Alice%")
Railsは結合されるテーブルのカラムをSQLのプレースホルダとして扱うため、実行されるクエリは下記のようになります。
SELECT "posts".* FROM "posts" INNER JOIN "users" ON "users"."id" = "posts"."user_id" WHERE (users.name LIKE '%Alice%')
テーブル名の省略に注意
where句を書いているときに、同名のカラムが異なるテーブルに存在することがあります。 その場合、クエリがあいまいになると、データベース側でエラーが発生するか、意図しない結果になる可能性があります。 JOINで複数テーブルを結合するときは、"テーブル名.カラム名" という形で明示する習慣を付けると混乱を防ぎやすいです。
テーブル別名を使う
場合によっては、JOINされるテーブルに別名(エイリアス)を付けたほうが読みやすくなるケースがあります。
Active Recordでも joins
や references
で、SQLのAS句に相当する別名を付けることができます。
Railsでの書き方
実際のSQLでは JOIN users AS u
のように書くことがありますが、Railsの場合はもう少し抽象化されています。
複雑なクエリを書くときや、同じテーブルを複数回JOINする必要があるときなど、Arel を直接使ってエイリアスを設定する方法が考えられます。
ただ、初心者の方にはArelでの記述は少しハードルが高いかもしれませんので、最初は可能な限り普通のリレーション名を利用し、複雑になってきたらArelやSQLフラグメントを検討すると良いでしょう。
Arelを使ったJOIN例
少し応用ですが、参考としてArelを使った書き方を簡単に見てみます。 ここではUserモデルに対して別名"u"を付けてJOINするイメージです。
# Postモデルのテーブルを取得 post_table = Post.arel_table # Userモデルのテーブルを取得 user_table = User.arel_table # Userテーブルに"u"という別名を付けてJOINするArel構文 join_condition = post_table[:user_id].eq(user_table[:id]) join_alias = Arel::Nodes::TableAlias.new(user_table, Arel.sql("u")) relation = Post.joins( post_table.join(join_alias).on(join_condition).join_sources ) posts_with_user_alias = relation.select("posts.*", "u.name AS user_name")
こうしてテーブル別名を活用すると、SQLが複雑になる場合でも可読性をある程度維持できます。 ただし、一気に難度が上がるため、はじめはActive Recordのシンプルな書き方に慣れてからで大丈夫です。
JOINとincludesの違い
Railsで関連テーブルを読み込むとき、joins 以外にも includes や eager_load といったメソッドがありますよね。 これらはN+1問題 を回避する目的でよく用いられ、裏側ではJOINやLEFT JOINが使われることがあります。
includesはJOINではなくプリロード
たとえば、PostとUserを同時に取得したいときに Post.includes(:user)
と書きます。
するとRailsは、投稿データを取得したあと、ユーザー情報を別クエリでもう一度取得するというように、2回のクエリを発行することがあります。
これがいわゆるプリロードの仕組みで、JOINとは少し違ったアプローチを取ります。
eager_loadやpreloadとの使い分け
eager_load
は内部でLEFT OUTER JOINを使い、preload
は2回クエリを発行してデータを取得する、という違いがあります。
基本的に、関連テーブルの条件検索をしたい場合は joins、関連テーブルをまとめて取得してN+1問題を防ぎたいだけなら includes や preload を使うというイメージです。
必要なシーンに応じて使い分けると良いですね。
JOINを使う際の注意点
JOINはとても便利ですが、いくつか気を付けるべきポイントがあります。
結合結果の重複
INNER JOINを使うと、結合先テーブルに紐づくデータの数だけ、メインの行が重複してしまうことがあります。 例えば、1つの投稿にコメントが5件ある場合は、その投稿が5行に重複して並ぶことになるわけです。 この重複データをそのまま扱うと、ビューで重複表示が起こったり、集計に不備が生じたりするので注意が必要ですね。
LEFT JOINではNULLが含まれる可能性
LEFT JOINやLEFT OUTER JOINを使うと、結合先のテーブルが存在しない行については、NULLが入るカラムが発生します。 アプリケーション側でNULLチェックを忘れているとエラーが出たり、不用意にエラーをスローしてしまうこともあります。 結果セットのカラム値がNULLになる場合があることを念頭に置いておきましょう。
JOINした結果、結合先のカラムがNULLになるケースを想定してコードを記述しておくことが大切です。
パフォーマンスに注意
JOINを安易に使いすぎると、テーブル間のデータ量が大きいときにクエリが重くなる可能性があります。 とくに複数の大きなテーブルを結合する場合は、ビューの表示速度が低下するかもしれません。 業務システムでは、インデックスを貼るなどのパフォーマンス対策が求められる場面があります。
JOINにおける実務例
ここでは実務でありがちな例として、ECサイトの受注管理 を考えてみましょう。 orders テーブルに対して users、order_items、products などが関連しているイメージです。
ORDERとUSERをJOINして購入者名を表示
例えば、注文履歴一覧ページで購入者の名前やメールアドレスをまとめて表示したいときは、こんな感じになります。
# orders.rb class Order < ApplicationRecord belongs_to :user has_many :order_items end # users.rb class User < ApplicationRecord has_many :orders end orders = Order.joins(:user).select( "orders.id, orders.total_price, users.name AS purchaser_name, users.email AS purchaser_email" )
これで、注文情報と一緒にユーザーの名前やメールアドレスを1回のクエリで取得できます。
ORDER_ITEMSとPRODUCTSをJOINして商品情報を引き出す
さらに、注文明細(order_items)を商品テーブル(products)とJOINして、商品名や単価を取得したい場合もありますよね。 下記は、注文詳細ページやダッシュボードなどで使いそうなイメージです。
# order_items.rb class OrderItem < ApplicationRecord belongs_to :order belongs_to :product end # products.rb class Product < ApplicationRecord has_many :order_items end order_items_with_products = OrderItem.joins(:product).select( "order_items.id, order_items.quantity, products.name AS product_name, products.price AS product_price" )
こうすることで、order_items と products がJOINされ、結果セットに商品名や価格が含まれます。 実務では、これを更に orders ともJOINして一度に取得する場面があるでしょう。
JOINで複数テーブルを結合する場合のSQLフラグメント
RailsのActive Recordは基本的に抽象化されているので、複雑な条件の結合でもわりとスムーズに書けます。 ただ、どうしても複雑すぎる結合や、特定のDB機能を使った条件を付けるときはSQLフラグメントを使うことがあります。
例:3つ以上のテーブルを結合して複数条件を設定する
たとえば以下のように、joins
の中で文字列を使い、直接SQLを書く方法があります。
order_summary = Order.joins(" INNER JOIN users ON users.id = orders.user_id LEFT JOIN order_items ON order_items.order_id = orders.id INNER JOIN products ON products.id = order_items.product_id ").select(" orders.id AS order_id, users.name AS user_name, products.name AS product_name ").where("products.active = ?", true)
あまりに複雑になる場合は可読性が落ちるため、モデルの関連を活かしたJOINの書き方を優先し、どうしても必要な時のみフラグメントを活用するほうがおすすめです。
JOINを使ったデータ更新や削除
JOINを使ってデータを取得した後に、レコードを更新・削除したい場面もあるかもしれません。 RailsのActive Recordでは、JOINリレーションがかかったオブジェクトをそのまま更新しようとすると思わぬ動きをすることがあります。
Active Recordオブジェクトとしての取り扱い
Post.joins(:user)
などで取得した結果は、基本的には Post モデルのオブジェクト一覧になります。
そのまま update
メソッドなどで更新すると、結合先のテーブルであるUserモデルの情報は更新されません。
JOIN後に取得したActive Recordオブジェクトは、JOINしていない独立のモデル(ここではPost)のインスタンスと考えてください。
結合先テーブルのレコード変更
一方、結合先テーブルのUserレコードなどを更新したい場合は、usersテーブル へ直接アプローチする必要があります。
User.where(...)
のように条件を書いて、該当のUserレコードを更新します。
JOINを使ってデータ取得しているからといって、結合先もまとめて更新できるわけではない点に注意が必要です。
パフォーマンスとインデックスの考え方
JOINを多用すると、DBの負荷が気になってくることはありませんか。 複数テーブルを結合しているので、それぞれの結合キーにインデックスが張られていないと、検索速度が遅くなりやすいです。
主キーや外部キーにインデックスを付ける
Railsのmigrationファイルで、user_id や product_id といった外部キーを設定する際にインデックスを付与することを考えてみてください。
たとえば、t.references :user, foreign_key: true, index: true
のような書き方でマイグレーションを作成すると、JOIN時の検索パフォーマンスが改善する場合があります。
大量データを扱うシーン
実務では、数百万件のデータを抱える大規模テーブルをJOINするケースもありますよね。 そういった場合には、適切なWHERE句 と LIMIT句 を組み合わせて、必要なデータだけを取得する工夫をすることでパフォーマンスをコントロールできます。 ビュー側ではページネーションを使って、一度に大量のレコードを表示しないようにするなどの設計も考えられます。
複数テーブルをJOINする場合は、外部キーにインデックスを付け、必要に応じてWHERE句やLIMIT句を使って絞り込むとパフォーマンスを維持しやすいです。
JOINのよくあるトラブルシューティング
最後に、JOINを使っているとよく起こるトラブルと、その対策を簡単にまとめてみます。
同名カラムの衝突
複数のテーブルに同じ名前のカラムがあるとき、Railsがどちらのカラムを取得するか分からずエラーや想定外の動きが起こることがあります。
この場合、select
メソッドで別名をつけるか、テーブル名を明示して衝突を避けることが大事です。
JOINするたびにオブジェクト数が増える
リレーションをJOINでどんどんつなげていくと、想定以上に結果のレコードが増えてしまうことがあります。
実際にはモデルオブジェクトが重複して返ってきて、View側で繰り返し表示されてしまうケースがあるので、distinct
メソッドなどを使って重複排除を行う方法があります。
posts = Post.joins(:comments).distinct
こうすると、投稿が重複して返ってくるのを防げます。
LEFT JOINでNULLを扱う際の例外
Railsのバージョンによっては、JOINした結果NULLが混ざっていることで予期せぬ例外が発生することがあります。
基本的には、User.joins(:profile)
のような関係を扱う場合に、関連が必須ではないなら left_joins
を使いつつ、NULLチェックを丁寧に行うと安全です。
まとめ
RailsでのJOINは、テーブル同士を結合して複数のデータを一気に扱える便利な仕組みです。 ユーザー情報や投稿情報をまとめて取得する、ECサイトで注文履歴と商品情報をまとめて引き出すなど、実務でも大いに活躍します。
ただし、JOINは使い方を誤ると重複データやパフォーマンスの問題を引き起こす可能性があります。 そのため、joins と includes の使い分けを理解し、必要に応じてLEFT JOINやSQLフラグメントなどを活用することが大切ですね。
また、実務では複数テーブルを扱うときにインデックスを貼ることや、重複データを排除する方法など、いろいろなポイントを意識するとスムーズに開発が進みやすいです。
JOINはテーブルのスキーマ設計やリレーションの理解が深まるほど、より活用の幅が広がります。 初心者の皆さんも、まずは1対多の単純な結合から始めて、慣れてきたら複数テーブルや複雑な結合に挑戦してみてはいかがでしょうか。