基礎Ruby on Rails Chapter9 ActiveRecordの活用／コールバック

前 [基礎Ruby on Rails Chapter8 アクション・コールバック／マイアカウントページの作成](https://qiita.com/tseno/items/46065bc132b189c37ccb)
次 [基礎Ruby on Rails Chapter9 ActiveRecord／スコープ／ページネーション](https://qiita.com/tseno/items/894e107e58a58e4ff8db)

# ニュース記事の表示と編集

## Articleモデルの作成

- ニュース記事を表示するためのモデル`article`を作成する。

```sh
$ bin/rails g model article
      invoke  active_record
      create    db/migrate/20180929222512_create_articles.rb
      create    app/models/article.rb
```

- マイグレーションスクリプトができるので、カラムを定義する。

```rb:db/migrate/20180929222512_create_articles.rb
class CreateArticles < ActiveRecord::Migration[5.2]
  def change
    create_table :articles do |t|
      t.string :title, null: false  # タイトル
      t.text :body, null: false  # 記事
      t.datetime :released_at, null:false  # 掲載開始日時
      t.datetime :expired_at  # 掲載終了日時
      t.boolean :member_only, null: false, default: false  # 会員のみフラグ
      t.timestamps null:false
    end
  end
end
```

- マイグレーションを実行する。

```sh
$ bin/rails db:migrate
```

- シードデータを読み込ませるための設定をする。
- `articles`を追記する。（現在の開発モード（develop等）の下にファイルがあればシードデータを投入する）

```rb:db/seeds.rb（一部）
table_names = %w(members articles)
（省略）
```

- シードデータの新規作成をする。
- `8.days.ago.advance(days: idx)`は、現在日時の8日前のidx日後を返す。

```rb:db/seeds/development/articles.rb
body =
    "Morning Gloryが4対2でSunflowerに勝利。\n\n" +
        "2回表、6番渡辺の二塁打から7番山田、8番高橋の連続タイムリーで2点先制。" +
        "9回表、ランナー一二塁で2番田中の二塁打で2点を挙げ、ダメを押しました。\n\n" +
        "投げては初先発の山本が7回を2失点に抑え、伊藤、中村とつないで逃げ切りました。"

0.upto(9) do |idx|
  Article.create(
      title: "練習試合の結果#{idx}",
      body: body,
      released_at: 8.days.ago.advance(days: idx),
      expired_at: 2.days.ago.advance(days: idx),
      member_only: (idx % 3 == 0)
  )
end

0.upto(29) do |idx|
  Article.create(
      title: "Article#{idx+10}",
      body: "blah, blah, blah...",
      released_at: 100.days.ago.advance(days: idx),
      expired_at: nil,
      member_only: false
  )
end
```

- 開発用のシードデータを投入する。

```sh
$ bin/rails db:rebuild
```

## バリデーションの追加

- タイトル、本文、掲載開始日時は空ではならない。
- タイトルは80文字以下。
- 本文は2000文字以下。

```rb:app/models/article.rb
class Article < ApplicationRecord
  validate :title, :body, :released_at, presence: true
  validate :title, length: { maximum: 80 }
  validate :body, length: { maximum: 2000 }
end
```

- ロケールファイルに、articleのモデル名とカラム名を日本語で追加。

```yml:config/locales/ja.yml（一部）
ja:
  activerecord:
    models:
      member: 会員情報
      article: ニュース記事
    attributes:
#（省略）
      article:
        title: タイトル
        body: 本文
        released_at: 掲載開始日時
        expired_at: 掲載終了日時
        member_only: 会員限定
    errors:
#（省略）
```

- バリデーションが正しく機能していることを確認する。

```sh
$ bin/rails c
irb(main):004:0> article = Article.first
  Article Load (0.3ms)  SELECT  "articles".* FROM "articles" ORDER BY "articles"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<Article id: 1, title: "練習試合の結果0", body: "Morning Gloryが4対2でSunflowerに勝利。\n\n2回表、6番渡辺の二塁打か ら7番山...", released_at: "2018-09-21 23:06:51", expired_at: "2018-09-27 23:06:51", member_only: true, created_at: "2018-09-29 23:06:51", updated_at: "2018-09-29 23:06:51">
irb(main):008:0> article.title = "A" * 80
=> "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
irb(main):009:0> article.valid?
=> true
irb(main):011:0> article.title = "A" * 81
=> "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
irb(main):013:0> article.valid?
=> false
irb(main):014:0> article.errors.full_messages_for(:title)
=> ["タイトルは80文字以内で入力してください"]
irb(main):015:0>
```

## ルーティングの設定

- `articles`をルーティングに設定する。複数形に気を付ける。
- 7つのアクションなのでオプションは指定しない。

```rb:config/routes.rb（一部）
#（省略）
  resource :session, only: [:create, :destroy]
  resource :account, only: [:show, :edit, :update]
  resource :password, only: [:show, :edit, :update]
  resources :articles
end
```

- `routes`コマンドで、ルーティングを確認する。

```sh
$ bin/rails routes -c article
      Prefix Verb   URI Pattern                  Controller#Action
    articles GET    /articles(.:format)          articles#index
             POST   /articles(.:format)          articles#create
 new_article GET    /articles/new(.:format)      articles#new
edit_article GET    /articles/:id/edit(.:format) articles#edit
     article GET    /articles/:id(.:format)      articles#show
             PATCH  /articles/:id(.:format)      articles#update
             PUT    /articles/:id(.:format)      articles#update
             DELETE /articles/:id(.:format)      articles#destroy
```

## ArticlesControllerの作成

- `bin/rails g controller articles`でコントローラを作成する。

```sh
$ bin/rails g controller articles
      create  app/controllers/articles_controller.rb
      invoke  erb
      create    app/views/articles
```

- アクセス制限する。indexとshowはログインしていない状態でも見えるようにする。

```rb:app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :login_required, except: [:index, :show]
end
```

### indexアクション

- 掲載開始、終了日時は考えずに、まずは全てを新しいもの順に表示する。

```rb:app/controllers/articles_controller.rb（一部）
  def index
    @article = Article.order(released_at: :desc)
  end
```

- indexのviewを作成する。new、show、edit、destoroyアクションへのリンクも付ける。

```erb:app/views/articles/index.html.erb
<% @page_title = "ニュース記事一覧" %>
<h1><%= @page_title %></h1>

<div class="toolbar"><%= link_to "新規作成", :new_article %></div>

<% if @articles.present? %>
  <table class="list">
    <thead>
    <tr>
      <th>タイトル</th>
      <th>日時</th>
      <th>操作</th>
    </tr>
    </thead>
    <tbody>
      <% @articles.each do |article| %>
        <tr>
          <td><%= link_to article.title, article %></td>
          <td><%= article.released_at.strftime("%Y/%m/%d %H:%M") %></td>
          <td>
            <%= link_to "編集", [:edit, article] %> |
            <%= link_to "削除", article, method: :delete, data: { confirm: "本当に削除しますか？" } %>
          </td>
        </tr>
      <% end %>
    </tbody>
  </table>
<% else %>
  <p>ニュースがありません。</p>
<% end %>
```

- ヘッダから`articles`へのindexのリンクを付ける。

```erb:app/views/shared/_header.html.erb（一部）
    <%= menu_link_to "ニュース", :articles %>
```

- ヘッダのリンクをクリックすると、indexが表示された。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/4af807d2-7ce0-6a54-7bc2-60028b1a8732.png)

### showアクション

```rb:app/controllers/articles_controller.rb（一部）
  def show
    @article = Article.find(params[:id])
  end
```

- showのviewを作成する。
- `simple_format`は文字列を次のルールで変換する。
    - 2個以上の連続する改行を段落とみなし、`<p>`タグで囲む。
    - 単独の改行は`<br>`タグにする。
    - 危険なタグを取り除く。（`<script>`など）
    - HTMLで特殊な意味を持つ記号をエスケープする。（`&<>`）

```erb:app/views/articles/show.html.erb
<% @page_title = @article.title %>

<h1><%= @page_title %></h1>

<% if current_member %>
  <div class="toolbar"><%= link_to "編集", [:edit, @article] %></div>
<% end %>

<table class="attr">
  <tr>
    <th width="100">タイトル</th>
    <td><%= @article.title %></td>
  </tr>
  <tr>
    <th>本文</th>
    <td><%= simple_format(@article.body) %></td>
  </tr>
  <tr>
    <th>掲載開始日時</th>
    <td><%= @article.released_at.strftime("%Y/%m/%d %H:%M") %></td>
  </tr>
  <tr>
    <th>掲載終了日時</th>
    <td><%= @article.expired_at.try(:strftime, "%Y/%m/%d %H:%M") %></td>
  </tr>
  <tr>
    <th>会員限定</th>
    <td><%= @article.member_only? ? "○" : "－" %></td>
  </tr>
</table>
```

### newアクションとeditアクション

- コントローラのnewアクションとeditアクションを作成する。

```rb:app/controllers/articles_controller.rb（一部）
  def new
    @article = Article.new
  end

  def edit
    @article = Article.find(params[:id])
  end
```

- newアクションのviewを作成する。部分テンプレート`_form`を使用する。

```erb:app/views/articles/new.html.erb
<% @page_title = "ニュース記事の新規登録" %>
<h1><%= @page_title %></h1>

<%= form_for @article do |form| %>
  <%= render "form", form: form %>
  <div><%= form.submit %></div>
<% end %>
```

- editアクションのviewを作成する。部分テンプレート`_form`を使用する。

```erb:app/views/articles/edit.html.erb
<% @page_title = "ニュース記事の編集" %>
<h1><%= @page_title %></h1>

<p><%= link_to "記事の詳細に戻る", @article %></p>

<%= form_for @article do |form| %>
  <%= render "form", form: form %>
  <div><%= form.submit %></div>
<% end %>
```

- 部分テンプレート`_form`を作成する。

```erb:app/views/articles/_form.html.erb
<%= render "shared/errors", obj: @article %>

<table class="attr">
  <tr>
    <th><%= form.label :title %></th>
    <td><%= form.text_field :title, size: 20 %></td>
  </tr>
  <tr>
    <th><%= form.label :body %></th>
    <td><%= form.text_area :body, rows: 10, cols: 45 %></td>
  </tr>
  <tr>
    <th><%= form.label :released_at, for: "article_released_at_li" %></th>
    <td><%= form.datetime_select :released_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %></td>
  </tr>
  <tr>
    <th><%= form.label :expired_at, for: "article_expired_at_at_li" %></th>
    <td><%= form.datetime_select :expired_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %></td>
  </tr>
  <tr>
  <tr>
    <th><%= Article.human_attribute_name(:member_only) %></th>
    <td>
      <%= form.check_box :member_only %>
      <%= form.label :member_only %>
    </td>
  </tr>
</table>
```

- 記事が編集可能なことを確認する。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/e4bfe3b9-ab32-414f-d0bd-2b2a0c5c406a.png)


### createアクションとupdateアクションとdestroyアクション

- createアクションとupdateアクションとdestroyアクションを実装する。

```rb:app/controllers/articles_controller.rb（一部）
  def create
    @article = Article.new(params[:article])
    if @article.save
      redirect_to @article, notice: "ニュース記事を登録しました。"
    else
      render "new"
    end
  end

  def update
    @article = Article.find(params[:id])
    @article.assign_attributes(params[:article])
    if @article.save
      redirect_to @article, notice: "ニュース記事を更新しました。"
    else
      render "edit"
    end
  end

  def destroy
    @article = Article.find(params[:id])
    @article.destroy
    redirect_to :articles
  end
```

# Active Recordコールバック

## Active Recordコールバックとは

- アクション・コールバックと同様に、Active Recordコールバックはバリデーションや保存の前後に処理を行うことができる。
- モデルにコールバックを設定する書き方は2種類ある。
- ブロックで登録する方法

```rb
class Article < ApplicationRecord
  before_save do
    # 保存する前に行う処理
  end
end
```

- プライベートメソッドのシンボルを登録する方法

```rb
class Article < ApplicationRecord
  before_save :do_something

  private def do_something
    # 保存する前に行う処理
  end
end
```

|メソッド|実行されるタイミング|
|:--|:--|
|before_validation|バリデーション前|
|after_validation|バリデーション後|
|before_save|保存前（新規作成、更新の両方）|
|before_create|新規作成前|
|before_update|更新前|
|after_update|更新後|
|after_create|新規作成後|
|after_save|保存後（新規作成、更新の両方）|

- before_validationとafter_validationは、onオプションに、:createまたは:updateを指定すると、新規作成または更新の場合だけコールバックを行う。

```rb
  before_validation on: :create do
    # 新規作成のバリデーションの前に行う処理
  end
```

## no_expiration属性

- 上記で作成した掲載終了日時は、空にすることができない。
- そこで、「掲載終了日時を設定しない」チェックボックスを追加して、チェックされている場合掲載終了日時を保存しない。
- チェックボックスに対応する属性`no_expiration`は、articleテーブルのカラムではなく、クラスの属性として実装する。
- no_expirationのゲッターは、expired_atがnilがとうか。
- no_expirationのセッターは、trueか1の時trueにして、それ以外はfalseにする。

```rb:app/models/article.rb（一部）
  def no_expiration
    expired_at.nil?
  end

  def no_expiration=(val)
    @no_expiration = val.in?([true, "1"])
  end
```

- テーブルのカラムにないフィールドを作るには、フィールド名と同名のアクセサメソッドをモデルクラスに加えて属性を作る。

## コールバックを使って掲載終了日時を消す

- validationの前に、`@no_expiration`がtrueの場合は、expired_atにnilを入れる。

```rb:app/models/article.rb（一部）
  before_validation do
    self.expired_at = nil if @no_expiration
  end
```

## フォームの書き換え

- 掲載終了日時を設定しないのチェックボックスを追加する。

```erb:app/views/articles/_form.html.erb（一部）
  <tr>
    <th><%= form.label :expired_at, for: "article_expired_at_at_li" %></th>
    <td>
      <div>
        <%= form.check_box :no_expiration %>
        <%= form.label :no_expiration %>
      </div>
      <div>
        <%= form.datetime_select :expired_at, start_year: 2000, end_year: Time.current.year + 1, use_month_numbers: true %>
      </div>
    </td>
  </tr>
```

- ロケールデータに、no_expirationを追加する。

```rb:config/locales/ja.yml（一部）
        expired_at: 掲載終了日時
        no_expiration: 掲載終了日時を設定しない
```

- チェックボックスが追加され、保存すると空になることを確認。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/403e633e-722f-e9d5-40f0-5f82acfb1c16.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/88686/8503ebca-d3f3-c90a-882d-0009441d1be7.png)


参考
[改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)](https://www.amazon.co.jp/%E6%94%B9%E8%A8%824%E7%89%88-Ruby-Rails-IMPRESS-KISO/dp/429500460X/ref=tmm_pap_swatch_0?_encoding=UTF8&qid=1536666358&sr=1-1)