LoginSignup
0
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-09-30

基礎Ruby on Rails Chapter8 アクション・コールバック/マイアカウントページの作成
基礎Ruby on Rails Chapter9 ActiveRecord/スコープ/ページネーション

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

Articleモデルの作成

  • ニュース記事を表示するためのモデルarticleを作成する。
$ bin/rails g model article
      invoke  active_record
      create    db/migrate/20180929222512_create_articles.rb
      create    app/models/article.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
  • マイグレーションを実行する。
$ bin/rails db:migrate
  • シードデータを読み込ませるための設定をする。
  • articlesを追記する。(現在の開発モード(develop等)の下にファイルがあればシードデータを投入する)
db/seeds.rb(一部)
table_names = %w(members articles)
(省略)
  • シードデータの新規作成をする。
  • 8.days.ago.advance(days: idx)は、現在日時の8日前のidx日後を返す。
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
  • 開発用のシードデータを投入する。
$ bin/rails db:rebuild

バリデーションの追加

  • タイトル、本文、掲載開始日時は空ではならない。
  • タイトルは80文字以下。
  • 本文は2000文字以下。
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のモデル名とカラム名を日本語で追加。
config/locales/ja.yml(一部)
ja:
  activerecord:
    models:
      member: 会員情報
      article: ニュース記事
    attributes:
#(省略)
      article:
        title: タイトル
        body: 本文
        released_at: 掲載開始日時
        expired_at: 掲載終了日時
        member_only: 会員限定
    errors:
#(省略)
  • バリデーションが正しく機能していることを確認する。
$ 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つのアクションなのでオプションは指定しない。
config/routes.rb(一部)
#(省略)
  resource :session, only: [:create, :destroy]
  resource :account, only: [:show, :edit, :update]
  resource :password, only: [:show, :edit, :update]
  resources :articles
end
  • routesコマンドで、ルーティングを確認する。
$ 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でコントローラを作成する。
$ bin/rails g controller articles
      create  app/controllers/articles_controller.rb
      invoke  erb
      create    app/views/articles
  • アクセス制限する。indexとshowはログインしていない状態でも見えるようにする。
app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  before_action :login_required, except: [:index, :show]
end

indexアクション

  • 掲載開始、終了日時は考えずに、まずは全てを新しいもの順に表示する。
app/controllers/articles_controller.rb(一部)
  def index
    @article = Article.order(released_at: :desc)
  end
  • indexのviewを作成する。new、show、edit、destoroyアクションへのリンクも付ける。
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のリンクを付ける。
app/views/shared/_header.html.erb(一部)
    <%= menu_link_to "ニュース", :articles %>
  • ヘッダのリンクをクリックすると、indexが表示された。

image.png

showアクション

app/controllers/articles_controller.rb(一部)
  def show
    @article = Article.find(params[:id])
  end
  • showのviewを作成する。
  • simple_formatは文字列を次のルールで変換する。
    • 2個以上の連続する改行を段落とみなし、<p>タグで囲む。
    • 単独の改行は<br>タグにする。
    • 危険なタグを取り除く。(<script>など)
    • HTMLで特殊な意味を持つ記号をエスケープする。(&<>
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アクションを作成する。
app/controllers/articles_controller.rb(一部)
  def new
    @article = Article.new
  end

  def edit
    @article = Article.find(params[:id])
  end
  • newアクションのviewを作成する。部分テンプレート_formを使用する。
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を使用する。
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を作成する。
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

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

  • createアクションとupdateアクションとdestroyアクションを実装する。
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種類ある。
  • ブロックで登録する方法
class Article < ApplicationRecord
  before_save do
    # 保存する前に行う処理
  end
end
  • プライベートメソッドのシンボルを登録する方法
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を指定すると、新規作成または更新の場合だけコールバックを行う。
  before_validation on: :create do
    # 新規作成のバリデーションの前に行う処理
  end

no_expiration属性

  • 上記で作成した掲載終了日時は、空にすることができない。
  • そこで、「掲載終了日時を設定しない」チェックボックスを追加して、チェックされている場合掲載終了日時を保存しない。
  • チェックボックスに対応する属性no_expirationは、articleテーブルのカラムではなく、クラスの属性として実装する。
  • no_expirationのゲッターは、expired_atがnilがとうか。
  • no_expirationのセッターは、trueか1の時trueにして、それ以外はfalseにする。
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を入れる。
app/models/article.rb(一部)
  before_validation do
    self.expired_at = nil if @no_expiration
  end

フォームの書き換え

  • 掲載終了日時を設定しないのチェックボックスを追加する。
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を追加する。
config/locales/ja.yml(一部)
        expired_at: 掲載終了日時
        no_expiration: 掲載終了日時を設定しない
  • チェックボックスが追加され、保存すると空になることを確認。

image.png
image.png

参考
改訂4版 基礎 Ruby on Rails (IMPRESS KISO SERIES)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0