■課題_5で学ぶ内容:

Formオブジェクトとは?

デザインパターンの一種で、フォーム機能を一纏めにし、コントローラやビューを介在するものです。 Formオブジェクトは必ずしもActiveRecordモデルと1対1にはなりません。複数のActiveRecordモデルがある場合も、対応するActiveRecordモデルが無い場合でも使用できます。 ユーザー入力機能やフォーム機能をコントローラだけで行おうとすると、FatControllerになってしまいます。そのため、フォーム機能と複数のモデルを介する機能を切り分ける役割がFormオブジェクトなのです。

ActiveModelとは?

ActiveModelは、主にActiveRecordからDBに依存する部分を除いた振る舞いを提供するライブラリです。

主にFormオブジェクトと合わせて使われることが多いです。

ActiveModel::Attributesとは?

※Attributes機能とは? =>attributeとは、オブジェクトが抱えるデータのことで、「属性(attribute)」と呼ぶことがあります。例えば「human」というクラスがあったとしたら、「name」や「job」などの「属性(attribute)」を追加して使用します。

以前はAttributes機能というと、ActiveRecordでしか使えなかったので人によって型変換の書き方がまちまちだったようです。

Rails5.2からはAttributesが、ActiveModelでも使えるようになっているようなので、これから書く私達は下記のように使っていきましょう。

class SearchArticlesForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :category_id, :integer
end

scopeとは?

scopeとは、絞り込み条件をまとめて名前をつけて、カスタムクエリー用メソッドとして使うことができる機能です。

scopeを使うと、繰り返し利用される絞り込み条件をスッキリ読みやすくできます。 定義はモデル内に記述します。

scopeメソッドは、下記の様に>の後に引数を定義することで、引数を渡すことが出来ます。

class モデル名 < ApplicationRecord
  scope :スコープの名前, -> { 条件式 }    # 基本の型

  scope :スコープの名前, -> (引数){ 条件式 }   # 引数を使う場合
end

基本の型では、公開記事を取得する際に、limit(2)によって最大2つまでの取得に固定されています。

取得する記事の上限をその都度変更したい場合は、下記の様に引数countをscopeメソッドに定義します。

class Article < ApplicationRecord  # 引数を使用しない場合
  scope :published, -> { where(published: true).limit(2) }
end

class Article < ApplicationRecord # 引数を使う場合
  scope :published, -> (count){ where(published: true).limit(count) }
end

=============================================================================

[元々の実装されているコードを読み解く]

app/admin/articles_controller.rb

def index
  authorize(Article)

  @search_articles_form = SearchArticlesForm.new(search_params)     # パラメータを受け取り、Formオブジェクトの初期化をして @search_articles_form に代入しています。
  @articles = @search_articles_form.search.order(id: :desc).page(params[:page]).per(25)   # 検索データを持った @search_articles_form にFormオブジェクトで定義してある searchメソッドを実行して@articlesに代入しています。(後方のメソッドはページネーションに関するものです。)
end

def search_params
  params[:q]&.permit(:title, :category_id)           # paramsの中から、"q"=>{ ... }の部分だけを取り出しています。
end
app/forms/search_articles_form.rb

class SearchArticlesForm
  include ActiveModel::Model      # ActiveModel導入
  include ActiveModel::Attributes # attributes導入(ただし、上のコードも合わせて必要)

  attribute :category_id, :integer  # category属性を追加
  attribute :title, :string         # title属性を追加

def search                     # articles_controller.rb で使用
    relation = Article.distinct      # すべてのカラムで値が重複する場合、重複レコードを1つにまとめる事を明示的に表記。(実際にはバリデーションでtitleカラムに一意性であることを掛けているので、結果的に無くても変わらない。)

    relation = relation.by_category(category_id) if category_id.present?   # category_idが存在する場合、relationにby_categoryメソッドを実行。(by_categoryはscopeで定義)

    title_words.each do |word|            # 分割されたtitleのワード配列をeach文で回す。
      relation = relation.title_contain(word)     # 一つ一つのワードを、relationに実行するtitle_containメソッドの引数として渡す。(title_containはscopeで定義)
    end

    relation                      # relationを返す。中身は該当のarticleを含んだ配列が想定されます。(Rubyのメソッドは最後に評価された結果がメソッドの戻り値となる。)
  end

  private

def title_words        # searchメソッド内で使用
    title.present? ? title.split(nil) : []  # titleが存在する場合、titleを先頭と末尾の空白を除き、空白文字に一致する部分で分割する。存在しなければ、空の配列を返す。
  end
end

[checkポイント]
・splitメソッド:
	文字列を指定した区切り文字で分割し、配列で返すメソッドです。第一引数に区切り文字を指定し、第二引数に分割数を指定する事が出来ます。
 区切り文字にnilを指定した場合は、「その文字列の先頭と末尾の空白文字」を除いてくれ、その上で「空白文字に一致する部分」で分割します。

[例]
"文字列".split(区切り文字, 分割数)
"文字列".split(nil)

irb(main):001:0> " abc def g ".split(nil)
=> ["abc", "def", "g"]
app/models/article.rb

scope :by_category, ->(category_id) { where(category_id: category_id) }       # 渡されたcategory_idを検索する
scope :title_contain, ->(word) { where('title LIKE ?', "%#{word}%") }         # 渡されたwordを含むtitleをあいまい検索する