ActiveRecord は素晴らしいライブラリです。しかし、現実は複雑なデータを取り扱うため、どうしても ActiveRecord 単体では辛くなる時が必ずあります。
この記事を書くきっかけもそんな一幕から始まりました。
□ 目的
users テーブルの name
とemail
で LIKE 検索がしたい!
□ 前提情報( users table )
column_name | data_type |
---|---|
id | integer |
name | string |
string | |
created_at | datetime |
updated_at | datetime |
□ (初めに)単一属性の LIKE 検索
これは、直感的に理解しやすいかと思います。
# email であいまい検索する場合
scope :search_email, ->(params) do
where("email LIKE ?", "%#{params}%")
end
# 検索する属性を動的に変更する場合
scope :search_column, ->(column, params) do
where("#{column} LIKE ?", "%#{params}%")
end
□ 複数属性の LIKE 検索( or
使用 )
ActiveRecord にはor
メソッドがあるので、下記の通りでも実現できます。
けど、これでは同じコードを繰り返して不穏な感じがしますね...
scope :search_columns, ->(params) do
where("name LIKE ?", "%#{params}%").
or(where("email LIKE ?", "%#{params}%"))
end
□ 複数属性の LIKE 検索( Arel 使用 )
複雑な検索には、Arel を使用するケースがあるそうです。
Arel とは、普段使用する ActiveRecord のメソッドを内部で良い感じに変換する用途で使用されます。
# 単一属性で検索したい場合
scope :search_column, ->(column, params) do
where(arel_table[column].matches("%#{params}%"))
end
# 複数属性で検索したい場合( columns に配列で属性を渡して複数条件の OR 検索を実現 )
scope :search_columns, ->(columns, params) do
search_word = "%#{params}%"
conditon = nil
columns.each do |column|
condition = condition.nil? ? arel_table[column].matches(search_word) : condition.or(arel_table[column].matches(search_word))
end
where(condition)
end
- ↓ を見ると matches 以外にもメソッドがあるため、色々な検索方法が出来そうですね。
def matches(other, escape = nil, case_sensitive = false)
Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
end
def matches_regexp(other, case_sensitive = true)
Nodes::Regexp.new self, quoted_node(other), case_sensitive
end
def matches_any(others, escape = nil, case_sensitive = false)
grouping_any :matches, others, escape, case_sensitive
end
def matches_all(others, escape = nil, case_sensitive = false)
grouping_all :matches, others, escape, case_sensitive
end
□ 複数属性の LIKE 検索( SQL 使用 )
ここまで書きましたが、Arel は使わない方が良いです。
複雑になると直感的に理解しづらい感じがします...やはり広く知られている SQL を使用して、実装しましょう。
- 参考 URL
## Case 1 (Array が返り値)
scope :search_columns, ->(params) do
find_by_sql([<<-SQL, "%#{params}%", "%#{params}%"])
SELECT
*
FROM
users
WHERE
email LIKE ?
OR name LIKE ?
SQL
end
## Case 2 (Array が返り値)
scope :search_columns, ->(params) do
find_by_sql([<<-SQL, { search_word: "%#{params}%" }])
SELECT
*
FROM
users
WHERE
email LIKE :search_word
OR name LIKE :search_word
SQL
end
## Case 3 (ActiveRecord::Relation を維持)
scope :search_columns, ->(params) do
where(<<-SQL, search_word: "%#{params}%")
email LIKE :search_word
OR name LIKE :search_word
SQL
end
あとがき
初めは複雑な SQL を避けようとして、Arel に手を染めようとしましたが、伊藤さんの記事を見て思い止まりました( 今思うと結構単純だったけど... )。
他にも.find_by_sql
の説明において、複数テーブルに関わる検索等の複雑性を持つクエリに焦点を当てている記述があること、また、 Arel はあくまで内部 API であり、使用を非推奨化されているとも方々で非推奨の足跡が多いので、サービス開発時には使わない方が良いのでしょう。
Rails エンジニアは SQL が苦手だという風説は、私のような初心者から始まっているのかもしれない...
□ 参考URL
- Arelでクエリを書くのはやめた方が良い5つの理由 - Qiita
- Ruby - 【Rails】Arelを使う使わないについて|teratail
- RailsでActiveRecord/Arelを使って複雑なSELECT文を実行する方法 - Rails Webook
- ActiveRecord::Relation で Arel 使用を非推奨化 | Ruby on Rails 5.2 リリースノート - Railsガイド
- ActiveRecord::Relation で Arel 使用を非推奨化 | 週刊Railsウォッチ(20180323)Rails 5.2.0 RC2リリース、「サーバーレスなRubyが欲しい」、capybara風JSテストフレームワークCypressほか