ラベルでの検索も実装し、そのSystem Spec中にタイトルのエラーが発生
System Spec中に……と書いてあるものの、テストを書き色んなパターンを試した結果、見落としていたエラーを発見した、というのが正しいです。
前提
タスク管理アプリを作成中。
タスク名、ステータス(未着手、着手、完了)での検索は実装済み。
今回、タスクにラベル(タグのようなもの)を付けられるようにし、そのラベルでもタスクを検索できるように実装。
その機能のテストを書いている時に起こったエラー。
環境
Rails 5.2.3
Ruby 2.6.5
テーブル構成
TaskテーブルとLabelテーブルがあって、多対多を実現するためのLabelingという中間テーブルがある。
create_table "labelings", force: :cascade do |t|
t.bigint "task_id"
t.bigint "label_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["label_id"], name: "index_labelings_on_label_id"
t.index ["task_id"], name: "index_labelings_on_task_id"
end
create_table "labels", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "tasks", force: :cascade do |t|
t.string "name", null: false
t.text "content", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "deadline", null: false
t.integer "priority", default: 0, null: false
t.integer "status", default: 0, null: false
t.bigint "user_id"
t.index ["name"], name: "index_tasks_on_name"
t.index ["user_id"], name: "index_tasks_on_user_id"
end
検索のロジック
モデルにscopeを書いて実装。
scope :default_order, -> { order(created_at: :desc) } # 作成日を降順に並べるscopeの名前を変更し分かりやすく
scope :sort_deadline, -> { order(deadline: :desc) }
scope :search_with_name, -> (name) {
return if name.blank?
where('name LIKE ?', "%#{name}%")
}
scope :search_with_status, -> (status) {
return if status.blank?
where(status: status)
}
scope :search_with_label, -> (label) {
return if label.blank?
joins(:labels).where('labels.id = ?', label) # where(id: label)じゃだめ
}
def index
@tasks = current_user.tasks
@tasks = @tasks
.search_with_name(params[:name])
.search_with_status(params[:status])
.search_with_label(params[:label])
# 中略
end
検索を実装してるviewのコード
= form_with(method: :get, local: true, url: tasks_path) do |f|
= f.label :name_search, t('.name_search'), value: params[:name]
= f.search_field :name, placeholder: t('.name_search'), class: 'form-control'
= f.label :status_search, t('.status_search'), value: params[:status]
= f.select :status, Task.enum_options_for_select(:status), class: 'form_control', include_blank: true, selected: ''
= f.label :label_search, t('.label_search'), value: params[:label]
= f.select :label, Label.pluck(:name, :id), { include_blank: true }
= f.hidden_field :search, value: true
div.search_button = f.submit(t('.search'), class: 'btn btn-secondary')
実際のページはこんな感じ
ラベルでの検索も問題なく実装できていた。ように見えていた。
System Specを書いていると……
ラベルでの検索機能のSystem Spec。ラベルのみでの検索は無事テストを書けたので、タスク名、ステータス、ラベルの3つ全てで検索をするテストを書いてみた。
context 'タスク名、ステータス、ラベルの全てで検索した場合' do
before do
fill_in 'タスク名で検索', with: "TEST_TASK"
select '未着手', from: :status
select '勉強', from: :label
click_button '検索'
end
example 'マッチしたタスクのみが表示される' do
within ('tbody') do
expect(page).to have_text "TEST_TASK", "未着手", "勉強"
end
end
end
FactoryBotで2つのデータを用意している。検索した結果、このタスクのみが表示されるはずだった。
しかしここで問題発生。
テストが失敗してエラー時のスクリーンショットも見たことないような画面に……。
コンソールのログを見てみると、
Failures:
1) Tasks タスク一覧画面 タスクの検索機能 タスク名、ステータス、ラベルの全てで検索した場合 マッチしたタスクのみが表示される
Failure/Error: - @tasks.each do |task|
ActionView::Template::Error:
PG::AmbiguousColumn: ERROR: column reference "name" is ambiguous
LINE 1: ...ngs"."label_id" WHERE "tasks"."user_id" = $1 AND (name LIKE ...
^
: SELECT "tasks".* FROM "tasks" INNER JOIN "labelings" ON "labelings"."task_id" = "tasks"."id" INNER JOIN "labels" ON "labels"."id" = "labelings"."label_id" WHERE "tasks"."user_id" = $1 AND (name LIKE '%TEST_TASK%') AND "tasks"."status" = $2 AND (labels.id = '1') ORDER BY "tasks"."created_at" DESC LIMIT $3 OFFSET $4
PG::AmbiguousColumn: ERROR
で検索。
ActionView::Template::Error:PG::AmbiguousColumn: ERROR…でRSpecが失敗して困った。 - Qiita
Taskモデルに、TaskモデルとアソシエーションしているLabelモデルをjoinsで結合しています。
それによりorder("created_at desc")が、tasksテーブルにあるcreated_atを指しているのか、labelsテーブルにあるcreated_atを指しているのか、曖昧( = AmbiguousCoulumn )になっていることがエラーの原因でした。
これを見てもう一度コンソールのログを見直すと、column reference "name" is ambiguous
と文句言われてる。そこで気付いた。
LabelテーブルにもTaskテーブルにもnameカラムがあることに。
その上、検索を実装してるscopeの中身を見ると、
scope :search_with_name, -> (name) {
return if name.blank?
where('name LIKE ?', "%#{name}%")
}
と、'name LIKE?'
だけで何のテーブルかを指定していないのが原因そうだ。
解決法
where('name LIKE ?', "%#{name}%")
を、where('tasks.name LIKE ?', "%#{name}%")
に。
何のnameかを指定してあげる。
scope :search_with_name, -> (name) {
return if name.blank?
where('tasks.name LIKE ?', "%#{name}%")
}
これで無事テストも通ったし、タスク名、ステータス、ラベル全てでの検索をしても問題なく動くようになった。
関連付けしたテーブルで同じカラム名がある場合は注意する!