homeASCIIcasts

251: MetaWhereとMetaSearch 

(view original Railscast)

Other translations: En Es

Other formats:

Written by Naomi Fujimoto

下の図は、product(商品)の一覧を表示する簡単なRails 3アプリケーションのスクリーンショットです。このページには、商品を名前で絞り込むための検索フォームがあります。

「video」を検索

検索はProductsController内のindexアクションで実行されます。コードは以下のようになります。

/app/controllers/products_controller.rb

def index
  @products = Product.where("name LIKE ?", "%#{params[:search]}%")
end

SQL句を含むwhereメソッドで、商品テーブルで名前が"LIKE (検索語)"の条件に一致するものを検索します。 検索条件がイコールよりも複雑なため、単純に条件をハッシュで渡すことができません。同じことが「より小さい」「より大きい」などの比較演算を行う場合にも当てはまります。ActiveRecordの場合、これらすべてでSQL句が必要になります。

そのような場合に、もしコードの中にSQL句を埋め込みたくないということであれば、MetaWhereというgemが役に立つかも知れません。MetaWhereでは、条件句に渡される引数の中に演算子のメソッドを使えるので、 検索式をこのように書くことができます。

Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)

MetaWhereは、このコードから次のSQL文を生成します。

SELECT "articles".* FROM "articles" 
WHERE ("articles"."title" LIKE 'Hello%')
AND ("articles"."created_at" > '2010-04-12 18:39:32.592087')

MetaWhereを使えば、検索式にSQL句を使わずにハッシュ形式で条件を指定することができます。このアプローチはDataMapperとMongoidのしくみと似ています。これをActiveRecordで利用できることで利便性が向上します。MetaWhereを使うのがいかに簡単かを見るために、productアプリケーションを修正して検索機能を組み込むことにしましょう。

まずアプリケーションのGemfileにgemの情報を追加します。

/Gemfile

# Edit this Gemfile to bundle your application's dependencies.
source 'http://gemcutter.org'

gem "rails", "3.0.3"
gem "sqlite3-ruby", :require => "sqlite3"
gem "meta_where"

bundleコマンドを実行し、 gemがインストールされたことを確認します。インストールができたら、ProductsControllerの検索条件のSQL句を書き換えます。

/app/controllers/products_controller.rb

def index
  @products = Product.where(:name.matches => ↵
    "%#{params[:search]}%")
end

この変更を行った後もアプリケーションの動作は以前と変わらず、与えられた条件に対して同じ検索結果を返します。

次にコンソール画面で、より複雑な例を見てみましょう。例として、価格が5ポンド未満の全ての商品を検索します。

ruby-1.9.2-p0 > Product.where(:price.lt => 5)
+----+-----------------------+--------+-------------+------------+
| id | name                  | price  | released_at | updated_at |
+----+-----------------------+--------+-------------+------------+
| 6  | 1 Pint of Milk        | 0.49   | 2010-06-06  | 2011-01-31 |
| 7  | Porridge Oats         | 1.99   | 2010-07-07  | 2011-01-31 |
+----+-----------------------+--------+-------------+------------+
2 rows in set

さらにパイプ記号で区切られた複数のハッシュを使って OR条件を追加できます。以下の検索では、価格が5ポンド未満か、あるいは名前に「video」の文字列を含む全ての商品を表示しています。

ruby-1.9.2-p0 > Product.where({:price.lt => 5} | ↵
  {:name.matches => "%video%"})
+----+-----------------------+--------+-------------+------------+
| id | name                  | price  | released_at | updated_at |
+----+-----------------------+--------+-------------+------------+ 
| 6  | 1 Pint of Milk        | 0.49   | 2010-06-06  | 2011-01-31 |
| 7  | Porridge Oats         | 1.99   | 2010-07-07  | 2011-01-31 |
| 8  | Video Game Console    | 299.95 | 2010-08-08  | 2011-01-31 |
| 9  | Video Game Disc       | 29.95  | 2010-09-09  | 2011-01-31 |
+----+-----------------------+--------+-------------+------------+ 
4 rows in set

MetaWhereは、その他にもorderメソッドにいくつか便利な機能を追加します。商品をreleased_atの日付でソートして最新の商品を最初に表示したい場合、次のように記述します。

ruby-1.9.2-p0 > Product.order(:released_at.desc)
+----+-----------------------+--------+-------------+------------+
| id | name                  | price  | released_at | updated_at |
+----+-----------------------+--------+-------------+------------+ 
| 9  | Video Game Disc       | 29.95  | 2010-09-09  | 2011-01-31 |
| 8  | Video Game Console    | 299.95 | 2010-08-08  | 2011-01-31 |
| 7  | Porridge Oats         | 1.99   | 2010-07-07  | 2011-01-31 |
| 6  | 1 Pint of Milk        | 0.49   | 2010-06-06  | 2011-01-31 |
| 5  | Oak Coffee Table      | 279.99 | 2010-05-05  | 2011-01-31 |
| 4  | Black Leather Sofa    | 499.99 | 2010-04-04  | 2011-01-31 |
| 3  | Stereolab T-Shirt     | 12.49  | 2010-03-03  | 2011-01-31 |
| 2  | DVD Player            | 79.99  | 2010-02-02  | 2011-01-31 |
| 1  | All-New Log For Girls | 29.95  | 2010-01-01  | 2011-01-31 |
+----+-----------------------+--------+-------------+------------+
9 rows in set

検索条件に別の記法を使うこともできます。これを使用できるように設定を有効化します。次のように指定します。

MetaWhere.operator_overload!

この設定が有効になっていれば、gtltというメソッドの代わりに標準のRubyの演算子を使用できます。この記法を使って価格が5ポンド未満の全ての商品を検索するには、以下のように指定します。

ruby-1.9.2-p0 > Product.where(:price < 5)
+----+-----------------------+--------+-------------+------------+
| id | name                  | price  | released_at | updated_at |
+----+-----------------------+--------+-------------+------------+
| 6  | 1 Pint of Milk        | 0.49   | 2010-06-06  | 2011-01-31 |
| 7  | Porridge Oats         | 1.99   | 2010-07-07  | 2011-01-31 |
+----+-----------------------+--------+-------------+------------+
2 rows in set

この方法で、検索条件を簡単を指定できるようになります。

MetaWhere gemには他にも多くの機能があります。詳しくはドキュメントを読むことをおすすめします。

MetaSearch

今回のエピソードの後半では、同じ作者によるもう一つのgemであるMetaSearchを見てみましょう。このgemは、フォームを介してモデルに対して簡単に検索を行う方法を提供します。モデルのsearchメソッドを呼び出し、フォームから入力された検索条件を渡します。それによって条件に一致するレコードを取り出すことができます。一つの例を下に示します。

def index
  @search = Article.search(params[:search])
  @articles = @search.all
end

ビューのコードでは、各フィールドの名前が検索機能を定義しています。例えばフォームのtitle_containsというテキストフィールドは、titleに指定された値が含まれているレコードを検索することを意味しています。

では今回のアプリケーションで、現状の検索フォームを、MetaSearchを使うように置き換えてみましょう。最初に、MetaWhereのときと同様に、Gemfileにgemの情報を追加します。

/Gemfile
# Edit this Gemfile to bundle your application's dependencies.
source 'http://gemcutter.org'

gem "rails", "3.0.3"
gem "sqlite3-ruby", :require => "sqlite3"
gem "meta_where"
gem "meta_search"

bundleコマンドを実行してgemをインストールします。次にProductsControllerの検索用のコードを対応するMetaSearchのコードに置き換えます。元のコードは次のとおりです。

/app/controllers/products_controller.rb

def index
  @products = Product.where(:name.matches => ↵
    "%#{params[:search]}%")
end

これを次のように書き換えます。

/app/controllers/products_controller.rb

def index
  @search = Product.search(params[:search])
  @products = @search.all
end

Product.searchを呼び出してフォームからの引数を渡し、searchインスタンスを生成します。次に@search.allを呼び出して、一致する商品のリストを得ます。これがSQL文を使って商品を検索します。条件を追加したい場合は、allの代わりにrelationを使用するとスコープが返されます。自動改ページが必要な場合はpaginateを使えますが、今回はallで十分です。

indexテンプレートで、検索フォームを書き換えてMetaSearchに必要なフィールド名を指定します。元のフォームではform_tagが使われていますが、それをform_forに置き換えてsearchオブジェクトを指定します。検索語が名前(name)に含まれる(contains)商品を検索したいので、フォームに追加するフィールドの名称はname_containsとします。

/app/views/products/index.html

<%= form_for @search do |f| %>
  <p>
    <%= f.label :name_contains %>
    <%= f.text_field :name_contains %>
  </p>
  <p class="button"><%= f.submit "Search" %></p>
<% end %>

ここでproductページをリロードすると、新しいフォームが表示され、前と同じ検索を行うと同じ結果が返ってきます。

同じ商品がMetaWhereから返される

この方法が便利な点は、モデルやコントローラを修正することなくビュー上でフィールドをいくつでも追加できるところです。例えば、値段の範囲も指定できるようにしたい場合、次のようにフォームにprice_gteprice_lteという2つのフィールドを追加するだけです。

/app/views/products/index.html.erb

<%= form_for @search do |f| %>
  <p>
    <%= f.label :name_contains %>
    <%= f.text_field :name_contains %>
  </p>
  <p>
    <%= f.label :price_gte, "Price ranges from" %>
    <%= f.text_field :price_gte, :size => 8 %>
    <%= f.label :price_lte, "to" %>
    <%= f.text_field :price_lte, :size => 8 %>
  </p>
  <p class="button"><%= f.submit "Search" %></p>
<% end %>

再度ページをリロードすると、2つの新しいフィールドが表示されます。これで、商品を値段の範囲で検索するか、名前と値段の両方を指定して検索できるようになりました。名前に「video」が含まれて値段が10ポンドから30ポンドまでの商品をすると、該当する1件の商品が検索されます。

商品名と値段の範囲で検索する

ここでのポイントは、フォーム上のフィールドの名前で、検索の条件が決められるという点です。フォームに設定するフィールドには他にも多くのオプションを使用できます。詳しくはドキュメントを参照してください。

MetaSearchのもう一つの機能として、指定したフィールドで結果を並び替えることができます。sort_linkメソッドが使えるので、これを利用します。このメソッドは2つの引数(searchオブジェクトとコラム名)を取るので、次のようにソートのためのフィールドを追加します。

/app/views/products/index.html.erb

<p>
  Sort by:
  <%= sort_link @search, :name %>
  <%= sort_link @search, :price %>
  <%= sort_link @search, :released_at %>
</p>

ページをリロードするとリンクが表示され、クリックすると商品をソートできます。ソートと同時に検索による絞り込みもできます。

商品を値段でソートする

セキュリティ

MetaSearchを利用するときに留意する点として、フォームを書き換えることでデータベーステーブルのすべての列が検索可能になってしまうということがあげられます。これは連結先のレコードにも当てはまるため、重要なデータを含む連結レコードがある場合にMetaSearchを使用する際には注意が必要です。

この問題を解決するために、モデルにセキュリティ関連のメソッドを追加して、検索対象とするフィールドを制限することができます。これについては、ここではこれ以上触れませんが、MetaSearchのサイトで詳細を見ることができます。MetaSearchを一般公開されているサイトで使用している場合は、ぜひこの方法をおすすめします。

MetaWhereとMetaSearchの紹介は以上です。同じような機能を持つSearchlogicというgemもありますが、Rails 3には対応していません。Searchlogicの機能が気に入っているがRails 3で動作するものがほしいという場合、MetaWhereとMetaSearchは一見の価値があるでしょう。