ArelでのOR検索ついでにごにょごにょいじってみた
at 2015-12-04 05:39 (UTC)
1つのキーワードで複数テーブルの複数カラムをあいまい検索ってよくある話ですね。
Arelを使って OR の LIKE 検索って冗長になりがちだけどそこそこパターン化出来そうだなーとつらつらとコード書いてみた。
こんなテーブルがあるとします
create_table :users do |t|
t.string :name
t.string :email
end
create_table :belongings do |t|
t.integer :user_id
t.string :company_name
t.string :dept_name
end
1つのキーワードであらゆる属性をあいまい検索したい場合、Arelを使えばこう書けます
left join も Arel で書けますが、冗長なので eager_load で代用してます
match_key = "%#{keyword}%"
name_matches = User.arel_table[:name].matches(match_key)
email_matches = User.arel_table[:email].matches(match_key)
company_name_matches = Belonging.arel_table[:company_name].matches(match_key)
dept_name_matches = Belonging.arel_table[:dept_name].matches(match_key)
User.eager_load(:belonging).where(name_matches.or(email_matches).or(company_name_matches).or(dept_name_matches))
or
のメソッドチェーンがなんかやだね。こうすれば対応カラムが増えた時 matchers
の要素を増やすだけで済むぞ
match_key = "%#{keyword}%"
matchers = [User.arel_table[:name].matches(match_key),
User.arel_table[:email].matches(match_key),
Belonging.arel_table[:company_name].matches(match_key),
Belonging.arel_table[:dept_name].matches(match_key)]
condition = matchers.inject { |cond, matcher| cond.or(matcher) }
User.eager_load(:belonging).where(condition)
これならシンプルに inject(&:sym)
でいけるね
match_key = "%#{keyword}%"
matchers = [User.arel_table[:name].matches(match_key),
User.arel_table[:email].matches(match_key),
Belonging.arel_table[:company_name].matches(match_key),
Belonging.arel_table[:dept_name].matches(match_key)]
User.eager_load(:belonging).where(matchers.inject(&:or))
matchers
も定型的なので冗長だよね
class_columns_set = { User => [:name, :email], Belonging => [:company_name, :dept_name] }
matchers = class_columns_set.map do |cls, cols|
cols.map { |col| cls.arel_table[col].matches("%#{keyword}%") }
end.flatten
User.eager_load(:belonging).where(matchers.inject(&:or))
なんかシンプルなパターンなら切り出せそう
class SimpleFinder
def initialize(base_class, class_columns_set)
@base_class = base_class
@class_columns_set = class_columns_set
end
def find(keyword)
matchers = @class_columns_set.map do |cls, cols|
cols.map { |col| cls.arel_table[col].matches("%#{keyword}%") }
end.flatten
foreign_tables = @class_columns_set.keys.reject { |cls| cls == @base_class }.map(&:table_name)
@base_class.eager_load(*foreign_tables).where(matchers.inject(&:or))
end
end
SimpleFinder.new(User, User => [:name, :email], Belonging => [:company_name, :dept_name]).find('foobar')
実際は SimpleFinder
までやると適用できるパターンが限定されるので、その手前が落とし所な気もする。
人によっては inject(&:sym)
使うぐらいが一番可読性がいいって意見もありそう。
Arelを使って OR や LIKE をするメリットは scope
にして merge
した時に壊れないって記述をよく見ますが、こういう風に動的に対応箇所を増やせるように持っていくのも楽というのもメリットですね。文字列で where
内を書いていたらなかなかこうはできない。あ、こんな処理を他にもたくさん書かなければいけない場合は Squeel
入れたほうがいいと思います。
全然関係ないけど、gist って編集時はインスタンス変数に色つけてくれるのに閲覧時には色つけてくれないの何でだろ
[追記] たまたま Ruby on Rails Advent Calendar 2015 が空いてたので飛び入りしました。