FormObjectを用いた検索機能

FormObject

form_withのmodelオプションにActiveRecord以外のオブジェクトを渡す時(複数のモデルを扱うときなど)のデザインパターンActiveModelをインクルードすることで実現。

  • DBを使わないフォームでも、ActiveRecordを利用した場合と同じお作法を利用できるので可読性が増す
  • 他の箇所に分散されがちなロジックをform object内に集めることができ、凝集度を高められる

ActiveModel

ActiveRecordからDBに依存する部分を除いた振る舞いを提供しているライブラリ。ActiveRecordを継承しないクラスでもActiveRecordと同じようなメソッドが使えるようになる

include ActiveModel::Attributes
attribute :title, :string
# attributeメソッド(属性の型変換)が使える
# 文字列をモデルの期待しているデータ型に変換するメソッド

extend ActiveModel::Callbacks
before_update :reset_me
# コールバックができる
# extendは当該クラスのクラスメソッドとして追加(includeはインスタンスメソッドとして追加)

include ActiveModel::Model
# これひとつで以下のモジュールが使えるようになる
# ActiveModel::Validations  バリデーションを可能にする
# ActiveModel::Translation  翻訳:human_attribute_nameを使用可能にする
# ActiveModel::Naming       モデル名の便利メソッド:model_nameを使用可能にする
# など

ActiveModelによるFormObjectの実装例(書籍の検索)

FormObjectのクラス SearchBooksFormを定義。

# app/forms/search_books_form.rb

class SearchBooksForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :author_id, :integer
  attribute :category_id, :integer
  attribute :title, :string

  def search
    books = Book.all

    books = books.where(article_id: self.article_id)  # selfは省略可
    books = books.where(category_id: self.category_id)
    self.title.split(nil).each do |word|
      books = books.where('title LIKE ?', "%#{word}%")
    end

    books
  end

検索フォームではmodelオプションでSearchBooksFormのインスタンスを渡し、scopeオブションとurlオプションでbooks_controllerにparams[:q]が送られるようにする。

<%= form_with model: @search_articles_form, scope: :q, url:books_path, method: :get do |f| %>
  <%= f.text_field :title %>

コントローラ

# books_controller.rb

def index
  @search_articles_form = SearchArticlesForm.new(search_params) # 検索ワードのattributesを持ったSearchBooksFormインスタンスを定義
  @articles = @search_articles_form.search # 専用のsearchメソッドにより検索
end

def search_params
  params[:q]&.permit(:title, :category_id, :author_id, :body, :tag_id)
end

formobjectに属性値として検索ワードを渡し、searchメソッド内でそれらの属性値にアクセスし、レコードを絞り込んでいる。