Ransackの導入って単純だと思っていましたが、躓きました。
タスク一覧画面で、タイトルのキーワードと進捗のステータスでタスクを絞り込み検索する機能を実装しようと考えています。
手順
公式ドキュメントの手順に沿って実装しました。
https://github.com/activerecord-hackery/ransack
gem 'ransack' のインストール
gem 'ransack', '~> 1.8', '>= 1.8.8'
$ bundle install
app/controllers/tasks_controller.rb
の設定
def index
@q = Task.ransack(params[:q])
@tasks = @q.result(distinct: true)
end
ビューファイルの編集
app/views/tasks/index.slim
.search__body
= search_form_for(@q, url:root_path) do |f|
= f.label :title_cont
= f.search_field :title_cont, placeholder: "Input Title"
= f.label :status_id_eq
= f.select :status_id_eq, Task.status_ids.to_a.map{|s|[t("activerecord.enum.status_id.#{s[0]}"), "#{s[0]}"]}
= f.submit
その他
db/schema.rb
はこんな感じです。
ActiveRecord::Schema.define(version: 20180516232642) do
create_table "tasks", force: :cascade do |t|
t.string "title", null: false
t.text "detail", default: "detail", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "deadline"
t.integer "status_id"
end
end
app/models/task.rb
はこんな感じです。
class Task < ApplicationRecord
validates :title, presence: true
validates :detail, presence: true
enum status_id: %i[undefined untouched in_progress done]
end
status_id
の翻訳は以下の通りになります。
config/locale/ja.yml
activerecord:
enum:
status_id:
undefined: "未設定"
untouched: "未着手"
in_progress: "作業中"
done: "完了"
参考程度にapp/helpers/tasks_helper.rb
module TasksHelper
def deadline_format(datetime)
datetime.present? ? datetime.strftime('%Y.%m.%d') : t(:undefined)
end
def status_format(status)
t(status.presence) || t(:undefined)
end
end
何に躓いているのか。
上記の状態で絞り込み検索を行なっても正しい結果を返してくれません。
検索結果はいつもゼロ。
試行錯誤
デバックで流れを確認してみます。
いつもお世話になっているbinding.pry
def index
@q = Task.ransack(params[:q])
@tasks = @q.result(distinct: true)
binding.pry
end
最初のデバックはexit
します。(意味ないんで。)
そして2回目。今回は明日
というキーワードを含み、且つ進捗が未着手
のタスクを探します。
注)未着手=>untouched=>1
未着手はi18n
では"untouched"
, そして "untouched"
はenum
で1
と設定されています。
止まったところで色々と調べます。
4: def index
5: @q = Task.ransack(params[:q])
6: @tasks = @q.result(distinct: true)
7: binding.pry
=> 8: end
[1] pry(#<TasksController>)> @tasks
Task Load (0.7ms) SELECT DISTINCT "tasks".* FROM "tasks" WHERE ("tasks"."title" ILIKE '%明日%' AND "tasks"."status_id" = 0)
=> []
[2] pry(#<TasksController>)>
@tasks
の値
@tasks
は空の配列を返しています。何故?
検索が期待通りの結果を返してくれるならば該当するタスクを表示してくれるはずなのですが。
[2] pry(#<TasksController>)> @tasks.class
=> Task::ActiveRecord_Relation
[3] pry(#<TasksController>)> @q
=> Ransack::Search<class: Task, base: Grouping <conditions: [Condition <attributes: ["title"], predicate: cont, values: ["明日"]>, Condition <attributes: ["status_id"], predicate: eq, values: ["untouched"]>], combinator: and>>
[4] pry(#<TasksController>)> params[:q]
=> <ActionController::Parameters {"title_cont"=>"明日", "status_id_eq"=>"untouched"} permitted: false>
@tasks
のクラスはTask::ActiveRecord_Relation
というのは大丈夫です。
params[:q]
で検索条件をパラメーターとして取得し、それをSQL文
に変換してDBから該当するインスタンスを取得したいのですが、よく見るとAND "tasks"."status_id" = 0
となっていますね。
未着手はenum
で1
と設定しているのに@tasks
にはAND "tasks"."status_id" = 0
と書かれていま。
ここ、怪しいです。
案の定ほかの進捗ステータスで検索してもAND "tasks"."status_id" = 0
でした。
そりゃ、検索結果は0件だ。
何故?
今回タスクの進捗を管理するためにstatus_id
というカラムを用意しました。
status_id
カラムの型はinteger
にして、enum
を使い数値に対応する文字列(半角の英語)を設定しています。そしてその文字列をi18n
で日本語にしてビューで表示するようにしています。
先程の例だと、未着手
=>untouched
=>1
という感じ
(未着手
はi18n
では"untouched"
, そして "untouched"
はenum
で1
と設定されています。)
これで、何が期待した通りに出来ていないのか判明しましたが、
次はどこが原因でそうなったのか調べます。
今度はキーワード明日
、ステータス作業中
で実行し、binding.pry
をしてみます。
(作業中
=>in_progress
=>2
)
4: def index
5: @q = Task.ransack(params[:q])
6: @tasks = @q.result(distinct: true).order('created_at DESC')
7: binding.pry
=> 8: end
[1] pry(#<TasksController>)> params[:q]
=> <ActionController::Parameters {"title_cont"=>"明日", "status_id_eq"=>"in_progress"} permitted: false>
[2] pry(#<TasksController>)> Task.ransack(params[:q])
=> Ransack::Search<class: Task, base: Grouping <conditions: [Condition <attributes: ["title"], predicate: cont, values: ["明日"]>, Condition <attributes: ["status_id"], predicate: eq, values: ["in_progress"]>], combinator: and>>
[3] pry(#<TasksController>)> @q
=> Ransack::Search<class: Task, base: Grouping <conditions: [Condition <attributes: ["title"], predicate: cont, values: ["明日"]>, Condition <attributes: ["status_id"], predicate: eq, values: ["in_progress"]>], combinator: and>>
[4] pry(#<TasksController>)> @q.result(distinct: true)
Task Load (0.6ms) SELECT DISTINCT "tasks".* FROM "tasks" WHERE ("tasks"."title" ILIKE '%明日%' AND "tasks"."status_id" = 0)
=> []
@q
までは期待した検索結果が反映されています。
しかし@q
に対してresult
メソッドを実行すると
返してくれるActiveRecord::Relation
クラスのオブジェクトのSQL文の部分に書かれた
進捗ステータスのidは0になってしまいました。
期待した進捗ステータスのidは2だったのですが…
注)記事内で行なっている検索では期待する検索結果が必ず存在する条件で検索を行なっています。
原因判明
このことをメンターの方に質問をしたところ
以下の記事を教えて頂きました。
http://www.tom08.net/entry/2016/12/05/121746
上記リンクの記事によると、
ransack
はenum
に対応していないので、f.select
で選択したstatus_id
はActiveRecord::Relation
クラスのオブジェクトに変換される際、常に0
になってしまうようです。
なんでもっと早く気づけなかったのか…
enum
のi18n
導入時は頑なにenum_help
を使わなかったのですが、もうダメなようです。
解決
といことでenum_help
を導入。
gem 'enum_help' のインストール
gem 'enum_help', '~> 0.0.15'
$ bundle install
config/locale/ja.yml
を編集
enums:
task:
status_id:
undefined: "未設定"
untouched: "未着手"
in_progress: "作業中"
done: "完了"
ビューファイルの編集
http://319ring.net/blog/archives/3041/
上記リンクの記事を参考にして修正しました。
app/views/tasks/index.slim
.search__body
= search_form_for(@q, url:root_path) do |f|
= f.label :title_cont
= f.search_field :title_cont, placeholder: "#{t(:keyword)}"
= f.label :status_id_eq
= f.select :status_id_eq, Task.status_ids.map{|k, v| [Task.status_ids_i18n[k], v]}
= f.submit
bin/rails c
コマンドでコンソールを立ち上げて値を確認しました。
Running via Spring preloader in process 10882
Loading development environment (Rails 5.1.6)
[1] pry(main)> statuses = Task.status_ids
=> {"undefined"=>0, "untouched"=>1, "in_progress"=>2, "done"=>3}
[2] pry(main)> statuses = Task.status_ids.map{|k,v| [Task.status_ids_i18n[k], v]}
=> [["未設定", 0], ["未着手", 1], ["作業中", 2], ["完了", 3]]
[3] pry(main)>
これで無事ransack
の検索機能が機能するようになりました。
課題
今回の問題を解決するにあたって以下のことが大事だと感じました。
①エラー解決のためのヒントの検索力
今回の問題の解決にかなりの時間を割き原因を探りましたがましたが、結局メンターの方に教えていただいた記事がきっかけで解決することができました。その時、もっと可能性のある原因を推測しそれぞれに対応した適切なキーワードで検索をすれば良かったと感じました。
②ソースコードを読む能力
ソースコードはやはり問題解決の最大のヒントになると思います。今回はたまたま記事を見つけたことで解決できましたが、そうでない場合は実際に何が、どのように動いているのか調べる必要があります。正直まだソースコードを理解できるほどの能力はないですが、ソースコードを理解できようになることをRuby
を学習する一つの目標にしようと思いました。