0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

railsチュートリアル13章備忘録(前編)

Posted at

##ユーザーのマイクロポスト

$ git checkout -b user-microposts

これまで見てきた4つのリソース

  • ユーザー
  • セッション
  • アカウント有効化
  • パスワードリセット

このうち「ユーザー」というリソースだけが、Active RecordによってDBのテーブルと紐付いている。
全ての準備が整ったので、新規リソース「マイクロポスト」を追加する。
-> ユーザーが短いメッセージを投稿できるようにするためのもの

この章でやること

  • Micropostデータモデルを作成
  • Userモデルとhas_manyおよびbelongs_toメソッドを使って関連付けを行う
  • 結果を処理し表示するために必要なフォームと、その部品を作成(画像のアップロードも実装)。

##Micropostモデル
Micropostリソースの最も本質的な部分を表現するMicropostモデルを作成。

特徴

  • データ検証とUserモデルの関連付けを含む
  • 完全にテストされる
  • デフォルトの順序を持つ
  • また親であるユーザーが破棄された場合には自動的に破棄されるようにする。

基本的なモデル

マイクロポストの内容を保存するcontent属性と、
特定のユーザーとマイクロポストを関連付けるuser_id属性の2つの属性だけを持つ。

microposts
id integer
content text
user_id integer
created_at datetime
updated_at datetime

マイクロポストの投稿は、String型ではなくText型の方がメリット多い。

  • 一応String型でも255文字までは格納できるため140文字制限を満たせる
  • Text型の方が表現豊かなマイクロポストを実現できる。
  • String用のテキストフィールドではなくてText用のテキストエリアを使うため、より自然な投稿フォームが実現可能。
  • Text型の方が将来における柔軟性に富んでいて、例えばいつか国際化をするときに、言語に応じて投稿の長さを調節可能。
  • Text型を使っていても本番環境でパフォーマンスの差は出ません。

Micropostモデルを生成する
$ rails generate model Micropost content:text user:references

①ApplicationRecordを継承したモデルapp/models/micropost.rbが作られ、
中にユーザーと1対1の関係であることを表すbelongs_toのコードも追加されている。
これはuser:referencesという引数も含めていたから。

②micropostsテーブルを作成するためのマイグレーションファイルを生成する。
Userモデルとの最大の違いはreferences型を利用している点。
-> 自動的にインデックスと外部キー参照付きのuser_idカラム追加。UserとMicropostを関連付ける下準備をしてくれる。

Userモデルのときと同じで、Micropostモデルのマイグレーションファイルでも
t.timestampsという行(マジックカラム)が自動的に生成される。
これにより、created_atとupdated_atというカラムが追加される。

インデックスが付与されたMicropostのマイグレーション

db/migrate/[timestamp]_create_microposts.rb
class CreateMicroposts < ActiveRecord::Migration[6.0]
  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    #user_idに関連付けられたすべてのマイクロポストを作成時刻の逆順で取り出しやすくなる
    add_index :microposts, [:user_id, :created_at] 
  end
end

DBを更新
$ rails db:migrate

Micropostのバリデーション

基本的なモデルを作成したので、次は要求される制限を実現するためのバリデーションを追加する。

Micropostモデルを作成したときに、マイクロポストは投稿したユーザーのid(user_id)を持たせるようにした。
これを使って、慣習的に正しくActive Recordの関連付けを実装していく。

まずはMicropostモデル単体を(テスト駆動開発で)動くようにする。

Micropostの初期テスト内容

  • setupのステップで、fixtureのサンプルユーザーと紐付けた新しいマイクロポストを作成。
  • 作成したマイクロポストが有効かどうかをチェック。
  • マイクロポストはユーザーのidを持っているべきなので、user_idの存在性のバリデーションに対するテスト。

これらの要素をまとめたMicropostの有効性に対するテスト

test/models/micropost_test.rb
equire 'test_helper'

class MicropostTest < ActiveSupport::TestCase

  def setup
    @user = users(:michael)
    # 一応動くがこのコードは慣習的に正しくない
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)
  end
  # 現実に即しているかどうかをテスト
  test "should be valid" do
    assert @micropost.valid?
  end
  # user_idが存在しているかどうかをテスト
  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end

テストをパスさせるためapp/models/micropost.rbに、
validates :user_id, presence: trueを追加。

テスト成功。$ rails test:models

次に、マイクロポストのcontent属性に対するバリデーションを追加

  • user_id属性と同様に、content属性も存在する必要。
  • マイクロポストが140文字より長くならないよう制限を加える。
test/models/micropost_test.rb
  test "content should be present" do
    @micropost.content = "   "
    assert_not @micropost.valid?
  end

  test "content should be at most 140 characters" do
    @micropost.content = "a" * 141
    assert_not @micropost.valid?
  end

app/models/micropost.rbに、Micropostモデルのバリデーション
validates :content, presence: true, length: { maximum: 140 }
を追加。

テスト成功。$ rails t

User/Micropostの関連付け

Webアプリケーション用のデータモデルを構築するにあたって、
個々のモデル間での関連付けを十分に考えておくことが重要。

今回の場合は、
それぞれのマイクロポストは1人のユーザーと関連付けられ、
ユーザーは(潜在的に)複数のマイクロポストと関連付けられる。

実装するための一環としてMicropostモデルに対するテストを作成し、
さらにUserモデルにいくつかのテストを追加する。

user.microposts.create
user.microposts.create!
user.microposts.build

慣習的に正しい方法
これらのメソッドは使うと、紐付いているユーザーを通してマイクロポストを作成することができる。
新規のマイクロポストがこの方法で作成される場合、user_idは自動的に正しい値に設定され、
下のように書き換えられる。

@user = users(:michael)
# このコードは慣習的に正しくない
@micropost = Micropost.new(content: "Lorem ipsum", user_id: @user.id)

@user = users(:michael)
# 慣習的に正しい
@micropost = @user.microposts.build(content: "Lorem ipsum")

newメソッドと同様に、buildメソッドはオブジェクトを返すがDBには反映されない。
一度正しい関連付けを定義してしまえば、@micropost変数のuser_idには、
関連するユーザーのid
が自動的に設定される。

user/micropost関連メソッドのまとめ

メソッド 用途
micropost.user Micropostに紐付いたUserオブジェクトを返す
user.microposts Userのマイクロポストの集合を返す
user.microposts.create(arg) userに紐付いたマイクロポストを作成する
user.microposts.create!(arg) 上に同じだが失敗時に例外を発生させる
user.microposts.build(arg) userに紐付いた新しいMicropostオブジェクトを返す
user.microposts.find_by(id: 1) userに紐付いていて、idが1であるマイクロポストを検索する

慣習的に正しい@user.microposts.buildのようなコードを使うためには、
UserモデルとMicropostモデルをそれぞれ更新して、関連付ける必要がある。

Micropostモデルの方では、belongs_to :userというコードが必要。
-> マイグレーションによって自動的に生成されている。

Userモデルの方では、has_many :micropostsと追加する必要がある。
-> 自動的に生成されないので、手動で追加する。

app/models/user.rb
class User < ApplicationRecord
  has_many :microposts

正しく関連付けができたら、先程のsetupメソッドを修正し、慣習的に正しくなる。

ここまで些細なリファクタリングでしかないのでテストは成功。

マイクロポストを改良する

この項で、UserとMicropostの関連付けを改良していく。

  1. ユーザーのマイクロポストを特定の順序で取得できるようにする、
  2. マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにする。

1.デフォルトのスコープ
読み出しの順序に対してuser.micropostsメソッドはデフォルトでは何も保証しない。
ブログやTwitterなどのように、作成時間の逆順で表示するようにする。
これを実装するためには、default scopeというテクニックを使う。

この機能のテストは、見せかけの成功に陥りやすい部分で、
「アプリケーション側の実装が本当は間違っているのにテストが成功してしまう」という罠がある。
正しいテストを書くために、テスト駆動開発で進める。

DB上の最初のマイクロポストが、fixture内のマイクロポスト(most_recent)と同じであるか検証。
マイクロポストの順序付けをテスト。このテストはマイクロポストのfixtureがあるという前提に依存している。

test/models/micropost_test.rb
  test "order should be most recent first" do
    assert_equal microposts(:most_recent), Micropost.first
  end

fixtureについては、ユーザーfixtureと同様に定義可能。

test/fixtures/microposts.yml
orange:
  content: "I just ate an orange!"
  created_at: <%= 10.minutes.ago %>
  user: michael

tau_manifesto:
  content: "Check out the @tauday site by @mhartl: https://tauday.com"
  created_at: <%= 3.years.ago %>
  user: michael

cat_video:
  content: "Sad cats are sad: https://youtu.be/PKffm2uI4dk"
  created_at: <%= 2.hours.ago %>
  user: michael

most_recent:
  content: "Writing a short test"
  created_at: <%= Time.zone.now %>
  user: michael

埋め込みRubyを使ってcreated_atカラムに値を明示的にセットしている点に注目。
created_atカラムはRailsによって自動的に更新されるため基本的には手動で更新できないが、
fixtureファイルの中では更新可能。

テスト失敗。$ rails test test/models/micropost_test.rb

次に、Railsのdefault_scopeメソッドを使ってテストを成功させる。
default_scope・・・DBから要素を取得したときの、デフォルトの順序を指定するメソッド。
特定の順序にしたい場合は、引数にorderを与える。

<例> created_atカラムの順にしたい場合
昇順 : order(:created_at)
降順 : order('created_at DESC')
降順 : order(created_at: :desc) (Rubyの文法で書く)

default_scopeでマイクロポストを順序付ける

app/models/micropost.rb
  default_scope -> { order(created_at: :desc) }

ラムダ式・・・Procやlambda(もしくは無名関数)と呼ばれるオブジェクトを作成する文法
->というラムダ式は、ブロックを引数に取り、Procオブジェクトを返す。
このオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価する。

テストが成功する。$ rails test

2.Dependent: destroy
マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにする。
サイト管理者はユーザーを破棄する権限を持ち、
ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべき。

マイクロポストは、その所有者(ユーザー)と一緒に破棄されることを保証する

app/models/user.rb
  has_many :microposts, dependent: :destroy

dependent: :destroy
-> ユーザーが削除されたときに、そのユーザーに紐付いたマイクロポストも一緒に削除されるようになる。
-> 管理者がシステムからユーザーを削除時、持ち主の存在しないマイクロポストがDBに残る問題を防ぐ。

正しく動くかどうか、テストを使ってUserモデルを検証する。
このテストでは、
(idを紐づけるための)ユーザーと、そのユーザーに紐付いたマイクロポストを作成。

その後、ユーザーを削除して、マイクロポストの数が1つ減っているかどうかを確認する。

dependent: :destroyのテスト

test/models/user_test.rb
  test "associated microposts should be destroyed" do
    @user.save
    @user.microposts.create!(content: "Lorem ipsum")
    assert_difference 'Micropost.count', -1 do
      @user.destroy
    end
  end

テストが成功する。$ rails test

マイクロポストを表示する

Web経由でマイクロポストを作成する方法は、現時点はないが、
表示することと、テストすることなら可能。

ここでは、Twitterのような独立したマイクロポストのindexページは作らずに、
ユーザーのshowページで直接マイクロポストを表示させる。
ユーザープロフィールにマイクロポストを表示させるため、最初にシンプルなERbテンプレートを作成する。
次にサンプルデータ生成タスクにマイクロポストのサンプルを追加して、
画面にサンプルデータが表示されるようにする。
user_microposts_mockup.png
参照:13章 図 13.4: マイクロポストが表示されたプロフィールページのモックアップ

###マイクロポストの描画
本項では、ユーザーのpf画面(show.html.erb)でそのユーザーのマイクロポストを
表示させたり、これまでに投稿した総数も表示させたりしていく。

一度データベースをリセットし、サンプルデータを再生成しておく。
$ rails db:migrate:reset
$ rails db:seed

Micropostのコントローラとビューを作成するために、コントローラを生成
$ rails generate controller Microposts

_micropost.html.erbパーシャルを使ってマイクロポストのコレクションを表示しようとすると
こうなる。

<ol class="microposts">
  <%= render @microposts %>
</ol>

順序無しリストのulタグではなく、順序付きリストのolタグを使用。
これはマイクロポストが特定の順序(新しい→古い)に依存しているため

対応するパーシャル

app/views/microposts/_micropost.html.erb
<li id="micropost-<%= micropost.id %>"> #JSを使って各マイクロポストを操作したくなったときなどに役立つ
  <%= link_to gravatar_for(micropost.user, size: 50), micropost.user %>
  <span class="user"><%= link_to micropost.user.name, micropost.user %></span>
  <span class="content"><%= micropost.content %></span>
  <span class="timestamp">
    Posted <%= time_ago_in_words(micropost.created_at) %> ago.
  </span>
</li>

ヘルパーメソッドtime_ago_in_wordsを使用。
これはメソッド名の表すとおり、「3分前に投稿」といった文字列を出力する。

一度にすべてのマイクロポストが表示されてしまう潜在的問題
<%= will_paginate @microposts %>
最後の課題はマイクロポストの投稿数を表示する
user.microposts.count

マイクロポストをユーザーのshowページ(プロフィール画面)に追加する

app/views/users/show.html.erb
  <div class="col-md-8">
    <% if @user.microposts.any? %>
      <h3>Microposts (<%= @user.microposts.count %>)</h3>
      <ol class="microposts">
        <%= render @microposts %>
      </ol>
      <%= will_paginate @microposts %>
    <% end %>
  </div>
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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?