##ユーザーのマイクロポスト
$ 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のマイグレーション
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の有効性に対するテスト
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 "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と追加する必要がある。
-> 自動的に生成されないので、手動で追加する。
class User < ApplicationRecord
has_many :microposts
正しく関連付けができたら、先程のsetupメソッドを修正し、慣習的に正しくなる。
ここまで些細なリファクタリングでしかないのでテストは成功。
マイクロポストを改良する
この項で、UserとMicropostの関連付けを改良していく。
- ユーザーのマイクロポストを特定の順序で取得できるようにする、
- マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにする。
1.デフォルトのスコープ
読み出しの順序に対してuser.micropostsメソッドはデフォルトでは何も保証しない。
ブログやTwitterなどのように、作成時間の逆順で表示するようにする。
これを実装するためには、default scope
というテクニックを使う。
この機能のテストは、見せかけの成功に陥りやすい部分で、
「アプリケーション側の実装が本当は間違っているのにテストが成功してしまう」という罠がある。
正しいテストを書くために、テスト駆動開発で進める。
DB上の最初のマイクロポストが、fixture内のマイクロポスト(most_recent)と同じであるか検証。
マイクロポストの順序付けをテスト。このテストはマイクロポストのfixtureがあるという前提に依存している。
test "order should be most recent first" do
assert_equal microposts(:most_recent), Micropost.first
end
fixtureについては、ユーザーfixtureと同様に定義可能。
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でマイクロポストを順序付ける
default_scope -> { order(created_at: :desc) }
ラムダ式・・・Procやlambda(もしくは無名関数)と呼ばれるオブジェクトを作成する文法
->というラムダ式は、ブロックを引数に取り、Procオブジェクトを返す。
このオブジェクトは、callメソッドが呼ばれたとき、ブロック内の処理を評価する。
テストが成功する。$ rails test
2.Dependent: destroy
マイクロポストをユーザーに依存させて、ユーザーが削除されたらマイクロポストも自動的に削除されるようにする。
サイト管理者はユーザーを破棄する権限を持ち、
ユーザーが破棄された場合、ユーザーのマイクロポストも同様に破棄されるべき。
マイクロポストは、その所有者(ユーザー)と一緒に破棄されることを保証する
has_many :microposts, dependent: :destroy
dependent: :destroy
-> ユーザーが削除されたときに、そのユーザーに紐付いたマイクロポストも一緒に削除されるようになる。
-> 管理者がシステムからユーザーを削除時、持ち主の存在しないマイクロポストがDBに残る問題を防ぐ。
正しく動くかどうか、テストを使ってUserモデルを検証する。
このテストでは、
(idを紐づけるための)ユーザーと、そのユーザーに紐付いたマイクロポストを作成。
その後、ユーザーを削除して、マイクロポストの数が1つ減っているかどうかを確認する。
dependent: :destroyのテスト
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テンプレートを作成する。
次にサンプルデータ生成タスクにマイクロポストのサンプルを追加して、
画面にサンプルデータが表示されるようにする。
参照: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タグを使用。
これはマイクロポストが特定の順序(新しい→古い)に依存しているため
対応するパーシャル
<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ページ(プロフィール画面)に追加する
<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>