2015/12/04

ArelでのOR検索ついでにごにょごにょいじってみた

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
view raw 00_tables.rb hosted with ❤ by GitHub
# 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))
view raw 01_arel_or.rb hosted with ❤ by GitHub
# 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))
view raw 04_finally.rb hosted with ❤ by GitHub
# なんかシンプルなパターンなら切り出せそう
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')
view raw 05_modulize.rb hosted with ❤ by GitHub
実際は SimpleFinder までやると適用できるパターンが限定されるので、04_finally みたいなところが落とし所な気もする。
人によっては 03_inject_with_symbol ぐらいが一番可読性がいいって意見もありそう。
Arelを使って OR や LIKE をするメリットは scope にして merge した時に壊れないって記述をよく見ますが、こういう風に動的に対応箇所を増やせるように持っていくのも楽というのもメリットですね。文字列で where 内を書いていたらなかなかこうはできない
あ、こんな処理を他にもたくさん書かなければいけない場合は Squeel 入れたほうがいいと思います。

全然関係ないけど、gist って編集時はインスタンス変数に色つけてくれるのに閲覧時には色つけてくれないの何でだろ

[追記] たまたま Ruby on Rails Advent Calendar 2015 が空いてたので飛び入りしました。

0 件のコメント :

コメントを投稿