QA@IT

Rails で同一テーブルに複数回 join する際に明示的 alias を付ける方法はありませんか?

11618 PV

こんにちは。

掲題の通りなのですが、Rails で同一テーブルに複数回 join する場合に自力で join 句を書く以外の方法はあるのでしょうか?色々調べてみたのですが、join 句を書くしか無さそうでしたので気になって質問させて頂きました。

具体例を挙げます。以下のようなモデルがあるとします。

class User < AR::Base
class Manager < User
class Sales < User

class Deal < AR::Base
  belongs_to :manager
  belongs_to :sales

User は単一テーブル継承をしているものとします。ここで Deal を検索する際、その関連先の sales.name および manager.name に対して case insensitive な LIKE 検索をかけたい場合、どのようにすれば綺麗に書くことが出来るでしょうか?

単に片方だけを join したい場合は比較的単純です。

@model.joins(:manager).where("lower(manager.name) LIKE ?", "%#{manager_name.downcase}%")

ところが managersales の順に両方を join しようとすると、Rails だと alias が以下のようになるようです。

  • manager : users
  • sales : sales_deals

どうやら最初に join した方はテーブル名のまま、以降 join したものは「属性名_関連元名」が alias になっているようです。これだと managersales の片方が検索条件として指定されたり、あるいは両方が指定された場合で alias が変化してしまうため、明示的に alias を付けたくなるのですが、alias を明示的に指定する方法は無いでしょうか?もしくは、case insensitive な LIKE 検索を alias を指定せずに実装する方法はあるでしょうか?

現状だと以下のように酷いコードになってしまいます。

@deals = @deals.joins(%(INNER JOIN "users" as "manager" ON "users"."id" = "deals"."manager_id" AND "manager"."type" IN ('Manager'))).where("lower(manager.name) LIKE ?", "%#{params[:manager_name].downcase}%") if params[:manager_name].present?
@deals = @deals.joins(%(INNER JOIN "users" as "sales" ON "users"."id" = "deals"."sales_id" AND "sales"."type" IN ('Sales'))).where("lower(sales.name) LIKE ?", "%#{params[:sales_name].downcase}%") if params[:sales_name].present?

幾ら何でもこれはちょっと酷いなぁと思うのですが、同一テーブルに対して複数の関連を持つモデルで、関連先の属性で LIKE 検索しようと思うとこうなってしまいます。何かもう少し良い方法はないでしょうか?せめて alias の命名戦略をカスタマイズ出来れば良いのですが……。

回答

ActiveRecord では、質問文にあるように SQL を書く以外には alias を指定する方法はなさそうです。直接 Arel を使えば可能です。

deals = Deal.arel_table
managers = User.arel_table.alias("managers")

Deal.joins(
  deals.join(managers)
    .on(
      Arel::Nodes::And.new([
        managers[:id].eq(deals[:manager_id]),
        managers[:type].eq("Manager")
      ])
    )
    .join_sources
    .first
).where("LOWER(managers.name) LIKE ?", "%foo%")
# => SELECT "deals".* FROM "deals" INNER JOIN "users" "managers" ON "managers"."id" = "deals"."manager_id" AND "managers"."type" = 'Manager' WHERE (LOWER(managers.name) LIKE '%foo%')

もっとも、質問文にあるように SQL を直接書いても、ほとんどの場合、問題も起きず、わかりやすいように思います。

私が書くなら、alias を明示して join するスコープと、それを使って絞り込みするクラスメソッドを作りますかね。

class Deal
  scope :with_managers, -> {
    joins(<<-SQL
      INNER JOIN "users" AS "managers"
        ON "managers"."id" = "deals"."manager_id" AND
           "managers"."type" = 'Manager'
    SQL
    )
  }

  def self.search_by_manager_name(q)
    with_managers.where("LOWER(managers.name) LIKE ?", "%#{q.downcase}%")
  end
end

参考:
https://github.com/rails/arel/blob/master/README.markdown
http://stackoverflow.com/questions/5072709/arel-joins-and-rails-queries

編集 履歴 (0)
  • すみません遅くなりました。回答ありがとうございます!arel を直接使えば一応 alias は付けられるんですね。確かに alias を明示して SQL で join する scope をつくって、それを使えば少なくとも join についての知識はそこに集約出来るのでそれでも良いような気はしました。それにしてもこれぐらい ActiveRecord で出来ても良いような気はするのですが、残念……。 -
ウォッチ

この質問への回答やコメントをメールでお知らせします。