雑記

2000|01|
2003|05|06|07|08|09|10|11|12|
2004|01|02|03|04|05|06|07|09|10|11|12|
2005|01|02|03|04|05|06|07|08|09|10|11|12|
2006|01|02|03|04|05|06|07|08|09|10|11|
2007|01|03|04|05|06|07|08|09|10|11|12|
2008|01|02|03|04|05|06|07|09|11|
2009|02|03|05|06|07|08|10|11|12|
2010|01|03|04|05|06|07|08|09|10|
2011|05|06|09|10|
2012|03|07|09|12|
2013|01|02|04|05|07|08|10|11|
2014|04|05|08|10|12|
2015|01|05|
2016|09|
2024|06|
2025|01|

2009-02-08 [長年日記]

[Rails] like式とプレースホルダと'%'

Railsでlike式を使った検索を行う場合、

Foo.find(:conditions => ["text like ?", "%"+params[:word]+"%"])

という方法が常套手段のようだが、PostgreSQLな環境では、フォームのword欄に'む%り'のような文字列を入れられると、「じゅげむじゅげむごこうのすりきれ」といったエントリにもマッチしてしまった。これは非常によろしくない。

で、安易に、

Foo.find(:conditions => ["text like ?", "%"+params[:word].gsub(/%/, '\%')+"%"])

とかエスケープしようとすると、プレースホルダの変換が効いてしまい、

SELECT * From foos WHERE (text like '%む\\%り%') ...

てな具合に'\'がエスケープされたSQLが発行されてうまくいかない。

ソースを当たってActiveRecord::ConnectionAdapters::Quoting.quote_string(s)らしいという所まではつきとめ、書き換えたら一応うまくいったが、INSERTとかにも使われていそうなので、副作用が怖い。

とかなんとか半日ぐらい唸っていたら、突然へんなのが降りてきた。

まず、コントローラに以下のメソッドを追加

 def escape_for_like(s, chr)
   s.downcase.gsub(/\[/, chr+'[').gsub(/%/, chr+'%').gsub(/_/, chr+'_')
 end

で、以下のようなfindを書く

 Foo.find(:conditions => ["lower(text) like E? ESCAPE 'A'", "%"+escape_for_like(params[:word], 'A')+"%"])

解説すると、

  • lower()とdowncaseでデータと検索キーをすべて英小文字化
  • 大文字の'A'をエスケープ文字とすることでプレースホルダーでの変換を回避
  • '%', '['および'_'をエスケープしてparams[:word]内の余計な解釈を回避

というわけで、マニュアルに

  • '%', '['および'_'といった記号の検索はできません

とか、

  • 検索ではワイルドカードが使えます。'%'を入れると…

とかへんてこりんな仕様を書かずに、一言

  • 英大文字と小文字は区別されません

と書くだけで済む。かな。

追記

さらに

 def escape_for_like(s, chr)
   s.gsub(chr, chr*2).gsub(/\[/, chr+'[').gsub(/%/, chr+'%').gsub(/_/, chr+'_')
 end

で、

 Foo.find(:conditions => ["text like E? ESCAPE 'A'", "%"+escape_for_like(params[:word], 'A')+"%"])

とやれば、大文字小文字交じりでもOKだった。


2009-02-16 [長年日記]

[Rails] 「1つのフォームで複数のsubmitボタン」をちょっとエレガント(?)に

[03/06 追記] flash.keep を追加してフラッシュエントリも保持するように修正

あるデータを表示して、それを確定するか破棄するか選択するようなアプリケーションを考える。

コントローラのメソッドは以下のような感じ。

  show -+- commit
        |
        +- reject

app/controller/application.rb に以下の2メソッドを追加

 # submit ボタンのリダイレクト
 def redirect_to_submit_action
   raise if params[:submit].size != 1
   session[:stored_params] ||= Hash.new
   params[:orig_action] = params[:action]
   params[:action] = params[:submit].keys.first
   h = params.hash.to_s
   session[:stored_params][h] = params
   flash.keep
   redirect_to :action => params[:action], :id => params[:id], :k => h
 rescue
   session[:stored_params].delete(h) if h
   render :text => '<p>Redirect error</p>', :layout => 'application'
   return false
 end

 # パラメータの復元
 def recover_params
   h = params[:k] or return false
   if session[:stored_params] && session[:stored_params][h]
     params.replace(session[:stored_params][h])
     session[:stored_params].delete(h)
   end
 end

redirect_to_submit_actionでのredirectを、単に

   redirect_to params

のように書いてしまっても一応動くが、その場合ブラウザのロケーションバーに他のパラメータがずらずらと並んでしまい、よろしくない。

対象となるコントローラでは次のようにフィルタを設定する。

 class FooController < ApplicationController
    before_filter :recover_params, :only => [ :commit, :reject ]
    before_filter :redirect_to_submit_action, :only => :dummy_redirect
  :
    def commit
       # commit 処理
    end

    def reject
       # reject 処理
    end
 end

app/view/foo/show.rhtml のフォームでは:action に dummy_redirectを指定する。

 <%- form_tag :action => 'dummy_redirect' do -%>
   :
 <%= submit_tag '確定', :name => 'submit[commit]' %>
 /
 <%= submit_tag '破棄', :name => 'submit[reject]' %>
 <%- end -%>

これで、[確定]ボタンをクリックすればcommitが、[破棄]ボタンをクリックするとrejectが呼ばれる。