Help us understand the problem. What is going on with this article?

ラベル機能実装後、検索のテストをしていたらPG::AmbiguousColumn: ERROR

ラベルでの検索も実装し、そのSystem Spec中にタイトルのエラーが発生

System Spec中に……と書いてあるものの、テストを書き色んなパターンを試した結果、見落としていたエラーを発見した、というのが正しいです。

前提

タスク管理アプリを作成中。
タスク名、ステータス(未着手、着手、完了)での検索は実装済み。
今回、タスクにラベル(タグのようなもの)を付けられるようにし、そのラベルでもタスクを検索できるように実装。
その機能のテストを書いている時に起こったエラー。

環境

Rails 5.2.3
Ruby 2.6.5

テーブル構成

TaskテーブルとLabelテーブルがあって、多対多を実現するためのLabelingという中間テーブルがある。

schema.rb(一部略)
  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を書いて実装。

app/models/task.rb(一部略)
  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)じゃだめ
  }
app/controllers/tasks_controller.rb
  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のコード

app/views/tasks/index.html.slim
= 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')

実際のページはこんな感じ

bdee4b6507d645ce831fc2bddd61b40c.png

ラベルでの検索も問題なく実装できていた。ように見えていた。

System Specを書いていると……

ラベルでの検索機能のSystem Spec。ラベルのみでの検索は無事テストを書けたので、タスク名、ステータス、ラベルの3つ全てで検索をするテストを書いてみた。

spec/system/task_spec.rb
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つのデータを用意している。検索した結果、このタスクのみが表示されるはずだった。

スクリーンショット 2020-04-04 21.44.22.png

しかしここで問題発生。
テストが失敗してエラー時のスクリーンショットも見たことないような画面に……。

failures_r_spec_example_groups_tasks_nested_nested_4_nested_マッチしたタスクのみが表示される_200.png

コンソールのログを見てみると、

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の中身を見ると、

app/models/task.rb
  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かを指定してあげる。

app/models/task.rb
scope :search_with_name, -> (name) {
    return if name.blank?
    where('tasks.name LIKE ?', "%#{name}%")
  }

これで無事テストも通ったし、タスク名、ステータス、ラベル全てでの検索をしても問題なく動くようになった。

関連付けしたテーブルで同じカラム名がある場合は注意する!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away