フィルターの実装
前回の続き。
処理の流れ
- フィルター機能を追加した見た目は、以下のようなイメージを目指す。
- フィルターフォームには、以下のような名前を設定しておく。
- パラメーターからは以下の情報を取得することができる。(例えば、「ファイルが、200701 を含む」のような条件になる。)
params[:filter_column] | フィルター対象の列タイトル |
params[:filter_item] | 検索キーワード |
params[:filter_id] | 演算条件を表すid(例:...を含む、...から始まる、等) |
- 3つのパラメーターを取得できたら、最終的にpaginateのオプションを以下のように設定したいのだ。(「ファイルが、200701 を含む」の場合)
@csv_pages, @csvs = paginate :csvs, :per_page => 10, :conditions => "file_name LIKE '%200701%'"
この、3つのパラメータをpaginateのオプションに変換するところに工夫が必要だ。
パラメーターからSQLへの変換テーブルを作成する。
いろいろ悩んだ結果、ifで条件判断するのはやめて、データベース上に変換テーブルを登録してしまった。
- filter_sqlsテーブルは、「...を含む」とか「...と等しい」などの演算条件params[filter_id]を見て、SQLに変換する方法を知っている。
- filter_setsテーブルは、filter_sqlを意味のあるグループでまとめている。
- filter_sqlの「空白である」「空白以外」などは複数のfilter_setに所属する可能性があるので、二つのテーブルは多:多関連になる。
filter_setsテーブル
id | name | 意味 | 所有するfilter_sqlsのid |
---|---|---|---|
1 | free_string | 文字列のフリーワード検索 | 1-5, 18-20 |
2 | free_value | 数値の大小比較 | 6-11, 18-20 |
3 | free_datetime | 日付の比較 | 12-20 |
4 | select | プルダウンリスト用 | 6, 7 |
5 | select_boolean | true、false選択用 | 6のみ |
filter_sqlsテーブル
- item列とoption列の内容は、Ruby式として実行することにした。
- optionは、BETWEENのように2つの引数が必要だったり、NULLを正しく取り扱うために条件を追加したりと、特例に対応するための項目。
id | name | not_op | operator | wild1 | item | wild2 | option | sort |
---|---|---|---|---|---|---|---|---|
1 | ...を含む | LIKE | % | % | 1000 | |||
2 | ...から始まる | LIKE | % | 2000 | ||||
3 | ...で終わる | LIKE | % | 3000 | ||||
4 | ...と同じ | LIKE | 4000 | |||||
5 | ...を含まない | NOT | LIKE | % | % | 5000 | ||
6 | ...と等しい | = | 6000 | |||||
7 | ...と等しくない | <> | "OR #{column} IS NULL" | 7000 | ||||
8 | ...より大きい | > | 8000 | |||||
9 | ...以上 | >= | 9000 | |||||
10 | ...より小さい | 10000 | ||||||
11 | ...以下 | <= | 11000 | |||||
12 | ...と等しい(日時) | BETWEEN | start_at_period(item) | "AND #{end_at_period(item)}" | 12000 | |||
13 | ...と等しくない(日時) | NOT | BETWEEN | start_at_period(item) | "AND #{end_at_period(item)} OR #{column} IS NULL" | 13000 | ||
14 | ...より大きい(日時) | > | end_at_period(item) | 14000 | ||||
15 | ...以上(日時) | >= | start_at_period(item) | 15000 | ||||
16 | ...より小さい(日時) | start_at_period(item) | 16000 | |||||
17 | ...以下(日時) | <= | end_at_period(item) | 17000 | ||||
18 | 空白である | LIKE | %Q() | "OR #{column} IS NULL" | 18000 | |||
19 | 空白以外 | LIKE | "_%" | 19000 | ||||
20 | LIKE | "%" | "OR #{column} IS NULL" | 900 |
:conditionsオプションとの対応をチェック
サンプル
id | name | not_op | operator | wild1 | item | wild2 | option |
---|---|---|---|---|---|---|---|
1 | ...を含む | LIKE | % | % | |||
18 | 空白である | LIKE | %Q() | "OR #{column} IS NULL" |
以下のように変数を設定すると...
-
-
- id = params[:filter_id]
- f = FilterSql.find(id)
- column = params[:filter_column]
- item = eval(f.item || '') || item || '%'
- option = eval(f.option || '')
-
paginateの:conditionsオプションは、次のように表現できる。
:conditions => "#{f.not_op} #{column} #{f.operator} '#{f.wild1}#{item}#{f.wild2}' #{option}"
- column="file_name", item="200701", id=1(...を含む)で試してみる。
:conditions => "file_name LIKE '%200701%'"
- column="file_name", item="200701", id=18(空白である)で試してみる。
:conditions => "file_name LIKE '' OR file_name IS NULL"
うまくいった!
マイグレーションの設定
class CreateFilterSqls < ActiveRecord::Migration def self.up create_table :filter_sqls do |t| t.column :name, :string t.column :not_op, :string t.column :operator, :string t.column :wild1, :string t.column :item, :string t.column :wild2, :string t.column :option, :string t.column :sort, :integer end end def self.down drop_table :filter_sqls end end
class CreateFilterSets < ActiveRecord::Migration def self.up create_table :filter_sets do |t| t.column :name, :string end create_table :filter_sets_filter_sqls, :id => false do |t| t.column :filter_set_id, :integer t.column :filter_sql_id, :integer end end def self.down drop_table :filter_sets drop_table :filter_sets_filter_sqls end end
多:多関連の設定
class Filtersql < ActiveRecord::Base has_and_belongs_to_many :filtersets end
- :order => "sort ASC"オプションを設定することで、filter_sqlを、sortフィールドの昇順で取得するようになる。
class Filterset < ActiveRecord::Base has_and_belongs_to_many :filtersqls, :order => "sort ASC" end
コントローラーの設定
- app/controllers/csvs_controller.rb
- human_compair_sql(column, item, id)というメソッドを定義して、そこでSQLに変換することにした。変換条件はfilter_sqlsテーブルに設定してあるので、とてもシンプルになった!
class CsvsController < ApplicationController ...(途中省略)... def human_compair_sql(column, item, id) f = Filtersql.find(id) option = eval(f.option || '') item = eval(f.item || '') || item || '%' "#{f.not_op} #{column} #{f.operator} '#{f.wild1}#{item}#{f.wild2}' #{option}" end def list # 配列の設定 @date_input_sample = '<span id="date_input_sample">(入力例: 2007/4/1 , 4/1 , 6:10)</span>' @column_titles = %w{file_name file_comment editable file_size created_at file_updated_at management_section_id user_id} # :conditionsの設定 @filter_column = params[:filter_column] || @column_titles[0] filter_item = params[:filter_item] filter_id = params[:filter_id] || 20 @conditions = human_compair_sql(@filter_column, filter_item, filter_id) # :orderの設定 @next_direction = (params[:sort_direction] == 'asc') ? 'desc' : 'asc' @order = "#{params[:sort_field] || 'id'} #{@next_direction}" @csv_pages, @csvs = paginate :csvs, :per_page => 10, :conditions => @conditions, :order => @order render :action => 'list' end ...(途中省略)...
ビューの設定
- app/views/csvs/_list_form.rhtml
- app/views/csvs/list.rhtmlから呼び出されて、フィルターフォームを描画する。データ形式ごとの違いは、フィールド名の部分テンプレートファイル(_filter_フィールド名.rhtml)で描画する。
<%# [左記条件でフィルタ]ボタンを押すと、id="list_params"の範囲が送信される。(このフォーム全体) %> <div id="list_params"> <%= form_remote_tag :update => 'list_update', :submit => 'list_params', :url => {:action => 'list'} %> <%# [条件をクリア]をクリックすると、id="no_filter_params"の範囲だけ送信される。 %> <span id="no_filter_params"> <%# 並び替え条件を維持する。 %> <%= hidden_field_tag :sort_field ,params[:sort_field] if params[:sort_field] %> <%= hidden_field_tag :sort_direction ,params[:sort_direction] if params[:sort_direction] %> <%# フィルタ列の選択 %> <% options = @column_titles.map{|t| [Csv.human_attribute_name(t), t]} %> <%= select_tag :filter_column, options_for_select(options, params[:filter_column]) %>が </span> <%# フィルタ文字と比較方法を入力 %> <%# 列タイトルごとに部分テンプレートを用意 _filter_フィールド名.rhtml %> <%= render :partial => "filter_#{@filter_column}" %> <%= submit_to_remote 'csv_filter', '左記条件でフィルター', :update => 'list_update', :submit => 'list_params', :url => {:action => 'list'} %> <small> <%= link_to_remote '条件をクリア', :update => 'list_update', :submit => 'no_filter_params', :url => {:action => 'list'} %> </small> <%# フィルタ列を選択するselect_tag :filter_columnを監視して、変化したら、条件をクリアして再描画する。 %> <%= observe_field :filter_column, :update => "list_update", :submit => "no_filter_params", :url => {:action => 'list'} %> <%= end_form_tag %> </div> <!----以下デバッグ用--------------------------------------------------------------> <!----> <small><%= params.inspect %></small> <br /> <small><%= @conditions.inspect %></small> <!---->
フィールドごとの部分テンプレートは、内容が同じでも全てのフィールド分用意してしまった。以下は、その一部。
- app/views/csvs/_filter_file_name.rhtml
- ファイル列のフィルタ入力用。
<%= text_field_tag :filter_item, params[:filter_item] %> <%= select_tag :filter_like_op, options_for_select(@like_op, params[:filter_like_op]) %>
- app/views/csvs/_filter_editable.rhtml
- 編集フラグ列のフィルタ入力用。(「編集可能」「表示のみ」の選択)
<%= select_tag :filter_item, options_for_select([@no_select, ['編集可能', 't'], ['表示のみ', 'f']], params[:filter_item]) %> <%= hidden_field_tag :filter_compare, '=' %>
- app/views/csvs/_filter_user_id.rhtml
- 担当者列のフィルタ入力用。(担当者をセレクトボックスで選択)
<% options = Csv.find(:all, :include => :user).map do |c| ["#{c.user.lastname} #{c.user.firstname}", c.user_id] end.uniq.unshift(@no_select) %> <%= select_tag :filter_item, options_for_select(options, params[:filter_item].to_i) %> <%= select_tag :filter_compare, options_for_select(@compare[0, 2], params[:filter_compare]) %>
以上で、日付以外の項目については、ちゃんと機能するようになった。