内部結合を利用したあいまい検索時にsyntax errorで詰まった話

概要

whereメソッドとjoinメソッドを使用して、内部結合を利用したあいまい検索をした時、syntax errorで詰まった話。

失敗時のコード

掲示板に関して、作成ユーザー、掲示板タグ、掲示板タイトル、掲示板コンテンツ(記事内容)で検索を行おうとして以下のような実装をした所、掲示板コンテンツの部分でsyntax errorが表示された。作成ユーザー、掲示板タグはセレクトボックスによる選択、掲示板タイトル、掲示板コンテンツはフリーワード検索で行えるようにしてある。

# models/board.rb

class Board < ApplicationRecord
  belongs_to :user

  has_many :board_tags
  has_many :contents
  has_many :tags, through: :board_tags

  scope :by_user, ->(user_id) { where(user_id: user_id) }
  scope :by_board_tag, ->(tag_id) { joins(:board_tags).where(board_tags: { tag_id: tag_id }) }
  scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }
  scope :body_contain, ->(word) { joins(:contents).where(contents: { 'contents.body LIKE ?', "%#{word}%" }) }
end
# controllers/boards_controller.rb

class BoardsController < ApplicationController

  def index
    @search_boards = SearchBoards.new(search_params)
    @boards = @search_boards.search.order(id: :desc)
  end

  private

  def search_params
    params[:q]&.permit(:title, :body, :tag_id, :user_id)
  end
# forms/search_boards.rb

class SearchBoards
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :user_id, :integer
  attribute :tag_id, :integer
  attribute :title, :string
  attribute :body, :string

  def search
    relation = Board.distinct

    relation = relation.by_user(user_id) if user_id.present?
    relation = relation.by_board_tag(tag_id) if tag_id.present?

    title_words.each do |word|
      relation = relation.title_contain(word)
    end
    body_words.each do |word|
      relation = relation.body_contain(word)
    end

    relation
  end

  private

  def title_words
    title.present? ? title.split(nil) : []
  end

  def body_words
    body.present? ? body.split(nil) : []
  end
end
# views/boards/index.html.slim

.ul.list-inline
  li
    = form_with model: @search_boards, scope: :q, url: boards_path, method: :get, html: { class: 'form-inline' } do |f|
      => f.select :user_id, User.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
      => f.select :tag_id, Tag.pluck(:name, :id) , { include_blank: true }, class: 'form-control'
      .input-group
        = f.search_field :title, placeholder: "タイトル", class: 'form-control'
      .input-group
        = f.search_field :body, placeholder: "本文", class: 'form-control'
        span.input-group-btn
          = f.submit '検索', class: %w[btn btn-default btn-flat]

エラーを見ると、Boardモデルに書いたscope :body_containの部分で構文エラーが発生していたみたい。

SyntaxError - syntax error, unexpected ',', expecting =>
...ents: {'contents.body LIKE ?', "%#{word}%"}) }
...                              ^
/Users/yoshitaka/Desktop/app/models/article.rb:74: syntax error, unexpected ')', expecting end
...nts.body LIKE ?', "%#{word}%"}) }
...                              ^:
  app/models/board.rb:74:in `'
  app/controllers/boards_controller.rb:7:in `index'

原因

何故こうなったかを調べると、内部結合を利用して結合先のテーブルで文字列検索を実行する時は一致検索時とは異なり、.where(結合先のテーブル名: { }の結合先テーブル部分を書く必要がないことが原因だった。
セレクトボックスで一致検索をする場合、内部結合を利用した一致検索を行うコードは以下のように、whereメソッドの引数に「結合先のテーブル名: { カラム名: 値 }」を指定する事で検索できます。

モデル名.joins(:関連名).where(結合先のテーブル名: { カラム名: 値 })

# article_tagsテーブルのtag_idカラムが1のレコードを全て取得
Board.joins(:board_tags).where(board_tags: { tag_id: 1 }) }

しかしフリーワードであいまい検索をする場合、whereの条件部分に.where(結合先のテーブル名.カラム名 LIKE 〜)といったように、結合先のテーブル名を書くことで結合先のテーブルに対して検索を行なってくれます。

# sentencesテーブルのbodyカラムに「hoge」という文字が入っているレコードを全て取得
Board.joins(:contents).where('contents.body LIKE ?', "%hoge%") }

解決方法

なので、whereの後ろに結合先のテーブル名を書かずに文字列検索用のコードを直後に書いたらエラーが解消した。

scope :body_contain, ->(word) { joins(:contents).where(contents: { 'contents.body LIKE ?', "%#{word}%" }) }
=> scope :body_contain, ->(word) { joins(:contents).where('contents.body LIKE ?', "%#{word}%") }