コントローラーを肥大化させずにRailsモデルを検索・フィルタリングする方法
Railsのコントローラーで検索、ソート、フィルタリングを実装するのは、意外と面倒な作業です。ElasticSearchやSolrは強力な検索エンジンですが、小規模なアプリケーションにとっては依存関係が大きすぎる場合があります。
幸い、Railsにはスコープ(scope)という仕組みが組み込まれており、シンプルな検索・フィルタリング・ソートに必要な機能の多くをこれだけで実現できます。スコープチェーンを活用すれば、大きな依存関係を導入したり、繰り返しの多い検索コードを自前で書いたりすることなく、必要な機能を構築できます。
スコープを使った検索
例として、商品一覧をテーブル形式で表示するRESTfulコントローラーの#indexアクションを考えてみましょう。各商品には「有効(active)」「保留中(pending)」「無効(inactive)」のいずれかのステータスがあり、単一のロケーションに紐づき、名前を持っています。
これらの商品を条件で絞り込みたい場合は、次のようなスコープを定義します。
class Product < ActiveRecord::Base
scope :filter_by_status, -> (status) { where status: status }
scope :filter_by_location, -> (location_id) { where location_id: location_id }
scope :filter_by_starts_with, -> (name) { where("name like ?", "#{name}%")}
end
それぞれのスコープはProductクラスのクラスメソッドとして定義され、取得結果を絞り込むために利用できます。
@products = Product.filter_by_status("active").filter_by_starts_with("Ruby") # => 名前が「Ruby」で始まるすべての有効な商品
コントローラーでは、これらのスコープを使って結果をフィルタリングできます。
def index
@products = Product.where(nil) # 匿名スコープを作成
@products = @products.filter_by_status(params[:status]) if params[:status].present?
@products = @products.filter_by_location(params[:location]) if params[:location].present?
@products = @products.filter_by_starts_with(params[:starts_with]) if params[:starts_with].present?
end
これで、「Ruby」で始まる名前を持つ有効な商品だけを表示できるようになりました。
https://example.com/products?status=active&starts_with=Ruby
このままだとコードが散らかってしまう
条件が増えるたびにコードが冗長で反復的になっていくのが分かるでしょう。幸い、Rubyを使っているので、ループにまとめてすっきりさせることができます。
def index
@products = Product.where(nil)
filtering_params(params).each do |key, value|
@products = @products.public_send("filter_by_#{key}", value) if value.present?
end
end
private
# 商品リストのフィルタリングに使用できるパラメータ名の一覧
# (ホワイトリストとして機能するため、ここへの登録を忘れないこと)
def filtering_params(params)
params.slice(:status, :location, :starts_with)
end
より再利用しやすいソリューションへ
さらに一歩進んで、このコードをモジュールに切り出せば、フィルタリング機能が必要な任意のモデルで使い回せるようになります。
module Filterable
extend ActiveSupport::Concern
module ClassMethods
def filter(filtering_params)
results = self.where(nil)
filtering_params.each do |key, value|
results = results.public_send("filter_by_#{key}", value) if value.present?
end
results
end
end
end
class Product
include Filterable
...
end
def index
@products = Product.filter(params.slice(:status, :location, :starts_with))
end
これで、モデル側1行+コントローラー側1行だけで、モデルの検索・フィルタリングが実現できました。とても簡単ですね。ソートについても、組み込みのorderクラスメソッドで実現できますが、独自のスコープを書くことをおすすめします。その方が入力値のサニティチェックを行えるからです。
手間を省くために、Filterableモジュールはgistとして公開しています。ぜひ自分のプロジェクトでも試してみてください。筆者自身もこの方法で多くの時間とコード量を節約できています。
更新: Jan Sandbrinkさんの指摘に感謝します。filtering_paramsでのパラメータのホワイトリスト登録を忘れてしまうことは珍しくありません。忘れると、アプリケーションが深刻なセキュリティ問題にさらされる可能性があります。
そこで、status、location、starts_withといった名前に代えて、filter_by_status、filter_by_location、filter_by_starts_withという名前に変更しました。こちらの方が意図が明確で、安全性も高まります。
重要な注意点:セキュリティリスク
パラメータをスコープに渡す方法は、Webアプリに基本的な検索・フィルタリング機能を追加する手軽な手段です。しかし、ユーザーから送られてきたものを何でも受け入れてしまうと、アプリに深刻なセキュリティ脆弱性が生じる恐れがあります。
特に、orderはSQLインジェクションに対して脆弱です。パラメータでソート順を指定する場合は、ユーザーが送信するカラム名を必ず検証し、安全であることが確認できる値のみを許可してください。
Rails SQL Injectionの公式サイトでは、どのActiveRecordメソッドが脆弱かを詳しく学べるので、アプリのセキュリティ維持に役立ててください。
動画での学習がお好みの方へ
真っ新な状態からアプリを作成し、検索・フィルタリング機能を追加するまでの全工程を、専用のスクリーンキャストで視聴できます。アプリの作成、サンプルデータの投入、検索フォームの追加と接続までを丁寧に解説します。さらに、動画と一緒にソースコードも入手できるので、ご自身のRailsアプリにシンプルな検索・フィルタリングを実装する際の参考資料として活用できます。
スクリーンキャストの詳細はこちら!
-
ElasticsearchでSQLデータベースに検索・可視化機能を追加する方法
NoSQLデータストアが注目を集める昨今ですが、リレーショナルデータベースやSQLベースのデータベースは今なお現役で活躍しています。実際、私たちが関わるほぼすべてのお客様の環境では、MongoDB、Redis、Elasticsearchなどと並んで、MySQL、PostgreSQL、MS SQL Serverが利用されています。リレーショナルデータベースから別のデータストアへデータを複製する最も簡単な方法についてのご相談は少なくありません。システム移行のための場合もあれば、全文検索や可視化といった機能を既存のリレーショナルデータに追加したいというケースもあります。幸い、Elasticsearc
-
Bingの検索履歴を確認・削除する方法をわかりやすく解説
Microsoftアカウントでログインした状態でBingを使用すると、実行した検索はすべて自動的に履歴として記録されます。この履歴は、過去に調べた情報をもう一度確認したいときに便利な反面、検索内容には個人的な情報が含まれやすいため、プライバシー面で気になるという人も少なくありません。この記事では、Bingの検索履歴を確認する方法と、記録の停止・削除の手順を詳しく解説します。Bingの検索履歴を確認する方法検索履歴を確認する最も簡単な方法は、Bingの公式サイトに直接アクセスすることです。トップページ右上にあるハンバーガーメニュー(三本線のアイコン)をクリックし、表示されたドロップダウンメニュー