Rails-tutorialのまとめ(第13章ユーザーのマイクロポスト 主に演習)

その12から続く

第13章ユーザーのマイクロポスト

ユーザーというリソースだけが、
Active Recordによってデータベース上のテーブルと紐付いています。
全ての準備が整った今、
ユーザーが短いメッセージを投稿できるようにするためのリソース「マイクロポスト」を追加していきます。

Userモデルとhas_manyおよびbelongs_toメソッドを使って関連付けを行い、さらに、結果を処理し表示するために必要なフォームとその部品を作成します

13.1 Micropostモデル

この新しいMicropostモデルもデータ検証とUserモデルの関連付けを含んでいます。
以前のモデルとは違って、
今回のマイクロポストモデルは完全にテストされ、
デフォルトの順序を持ち、また親であるユーザーが破棄された場合には自動的に破棄されるようにします。

13.1.1 基本的なモデル

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

マイクロポストの投稿にString型ではなくText型を使っている。
Text型の方が表現豊かなマイクロポストを実現できるから
Text型の方が将来における柔軟性に富んでいて
Text用のテキストエリアを使うため、
より自然な投稿フォームが実現できます。

rails generate model Micropost content:text user:references

ApplicationRecordを継承したモデルが作られます。
ただし、今回は生成されたモデルの中に、
ユーザーと1対1の関係であることを表す。
belongs_toのコードも追加されています。
user:referencesという引数も含めていたからです。

このgenerateコマンドはmicropostsテーブルを作成するための
マイグレーションファイルを生成します。

Userモデルとの最大の違いはreferences型を利用している点です。
これを利用すると、自動的にインデックスと外部キー参照付きのuser_idカラムが追加され、
UserとMicropostを関連付けする下準備をしてくれます。

インデックスが付与されたMicropostのマイグレーション
db/migrate/[timestamp]_create_microposts.rb

  def change
    create_table :microposts do |t|
      t.text :content
      t.references :user, foreign_key: true

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end

user_idとcreated_atカラムにインデックスが付与されていることに注目こうすることで、
user_idに関連付けられたすべてのマイクロポストを
作成時刻の逆順で取り出しやすくなります。

user_idとcreated_atの両方を1つの配列に含めている点にも注目です。
こうすることでActive Recordは、両方のキーを同時に扱う複合キーインデックス(Multiple Key Index) を作成します。

rails db:migrate

演習

1:RailsコンソールでMicropost.newを実行し、インスタンスを変数micropostに代入してください。
その後、user_idに最初のユーザーのidを、contentに
“Lorem ipsum” をそれぞれ代入してみてください。
この時点では、micropostオブジェクトのマジックカラム (created_atとupdated_at)には何が入っているでしょうか?

>> micropost = Micropost.new
=> #<Micropost id: nil, content: nil, user_id: nil, created_at: nil, updated_at: nil>
>> micropost.user_id = User.first.id
User Load (0.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> 1
>> micropost.content = "Lorem ipsum"
=> "Lorem ipsum"
>> micropost.created_at
=> nil
>> micropost.updated_at
=> nil

2:先ほど作ったオブジェクトを使って、micropost.userを実行してみましょう。
どのような結果が返ってくるでしょうか? また、micropost.user.nameを実行した
場合の結果はどうなるでしょうか?

micropost.user
User Load (1.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2020-04-07 13:46:42", updated_at: "2020-04-09 06:04:49", password_digest: "$2a$10$GANEfKFrSAstHXyU9iKuCuZCqGiIkWq8G9omIW2kfFD...", remember_digest: nil, admin: true, activation_digest: "$2a$10$PRCrEV3JLxiTFRfnbnQSquram12Njfl4cs9ieMebt3a...", activated: true, activated_at: "2020-04-07 13:46:41", reset_digest: "$2a$10$rqLaoPfGBIsYQ83ULnk3JO4ozC9kCB1tw2Z9dF55ysh...", reset_sent_at: "2020-04-09 06:04:49">

micropostにidを付与することで、userとnameがきちんと紐づいている。

3:先ほど作ったmicropostオブジェクトをデータベースに保存してみましょう。
この時点でもう一度マジックカラムの内容を調べてみましょう。
今度はどのような値が入っているでしょうか?

micropost.save
(0.1ms) SAVEPOINT active_record_1
SQL (2.1ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2020-04-09 13:48:05.586960"], ["updated_at", "2020-04-09 13:48:05.586960"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> micropost.created_at
=> Thu, 09 Apr 2020 13:48:05 UTC +00:00
>> micropost.updated_at
=> Thu, 09 Apr 2020 13:48:05 UTC +00:00

13.1.2 Micropostのバリデーション

新しいMicropostの有効性に対するテスト green
test/models/micropost_test.rb
require '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
  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
end

 

マイクロポストのuser_idに対する検証 green
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
end
green
rails test:models
Micropostモデルのバリデーションに対するテスト red
test/models/micropost_test.rb
require '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
  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
  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
end
Micropostモデルのバリデーション green
app/models/micropost.rb
class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end
green
rails test

演習

Railsコンソールを開き、user_idcontentが空になっているmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

動作確認するだけ

コンソールを開き、今度はuser_idが空でcontentが141文字以上のmicropostオブジェクトを作ってみてください。このオブジェクトに対してvalid?を実行すると、失敗することを確認してみましょう。また、生成されたエラーメッセージにはどんな内容が書かれているでしょうか?

動作確認するだけ

13.1.3 User/Micropostの関連付け

belongs_to/has_many関連付けを使うことで
↓に示すようなメソッドをRailsで使えるようになります
このメソッドになっていることに注意してください。

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

という書き方が、次のように書き換えられます。

@user = users(:michael)
@micropost = @user.microposts.build(content: "Lorem ipsum")

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

 

    メソッド                                    用途
micropost.user                 Micropostに紐付いたUserオブジェクトを返す

user.microposts                Userのマイクロポストの集合をかえす

user.microposts.create(arg)    userに紐付いたマイクロポストを作成する

user.microposts.create!(arg)   userに紐付いたマイクロポストを作成する (失敗時に例外を発生)

user.microposts.build(arg)     userに紐付いた新しいMicropostオブジェクトを返す

user.microposts.find_by(id: 1) userに紐付いていて、idが1である
マイクロポストを検索する

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

一方、Userモデルの方では、has_many :micropostsと追加する必要があります。 ここは自動的に生成されないので、手動で追加してください

マイクロポストがユーザーに所属する (belongs_to) 関連付け
app/models/micropost.rb

class Micropost < ApplicationRecord
  belongs_to :user
  validates :user_id, presence: true
  validates :content, presence: true, length: { maximum: 140 }
end

ユーザーがマイクロポストを複数所有する (has_many) 関連付け green
app/models/user.rb

#追加
has_many :microposts

慣習的に正しくマイクロポストを作成する green
test/models/micropost_test.rb

require 'test_helper'
class MicropostTest < ActiveSupport::TestCase
  def setup
    @user = users(:michael)
    @micropost = @user.microposts.build(content: "Lorem ipsum")
  end
  test "should be valid" do
    assert @micropost.valid?
  end
  test "user id should be present" do
    @micropost.user_id = nil
    assert_not @micropost.valid?
  end
  .
  .
  .
end

演習

1:データベースにいる最初のユーザーを変数userに代入してください。そのuserオブジェクトを使ってmicropost = user.microposts.create(content: “Lorem ipsum”)を実行すると、どのような結果が得られるでしょうか?

micropost = user.microposts.create(content: "Lorem ipsum")

SQL (6.6ms) INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["content", "Lorem ipsum"], ["user_id", 1], ["created_at", "2020-04-10 00:33:04.704864"], ["updated_at", "2020-04-10 00:33:04.704864"]]

=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2020-04-10 00:33:04", updated_at: "2020-04-10 00:33:04">

2:先ほどの演習課題で、データベース上に新しいマイクロポストが追加されたはずです。user.microposts.find(micropost.id)を実行して、
本当に追加されたのかを確かめてみましょう。また、先ほど実行した
micropost.idの部分をmicropostに変更すると、結果はどうなるでしょうか?

user.microposts.find(micropost.id)
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? AND "microposts"."id" = ? LIMIT ? [["user_id", 1], ["id", 1], ["LIMIT", 1]]

=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1,
created_at: "2020-04-10 00:33:04", updated_at: "2020-04-10 00:33:04">

ArgumentError (You are passing an instance of ActiveRecord::Base to `find`. Please pass the id of the object by calling `.id`.)

3:user == micropost.userを実行した結果はどうなるでしょうか?
また、user.microposts.first == micropost を実行した結果は
どうなるでしょうか? それぞれ確認してみてください。

user == micropost.user
=> true
>> user.microposts.first == micropost
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> true

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

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

デフォルトのスコープ

user.micropostsメソッドはデフォルトでは読み出しの順序に対して何も保証しませんが、
最も新しいマイクロポストを最初に表示するようにしてみましょう。
これを実装するためには、default scopeというテクニックを使います。

まずデータベース上の最初のマイクロポストが、
fixture内のマイクロポスト (most_recent) と同じであるか検証するテストを書いていきましょう。

マイクロポストの順序付けをテストする red
test/models/micropost_test.rb

require 'test_helper'

  class MicropostTest < ActiveSupport::TestCase
.
.
.
  test "order should be most recent first" do
  assert_equal microposts(:most_recent), Micropost.first
 end
end
マイクロポスト用のfixture
test/fixtures/microposts.yml

orange:
content: "I just ate an orange!"
created_at: <%= 10.minutes.ago %>

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

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

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

StandardError: No fixture named 'most_recent' found for
fixture set 'microposts'

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

rails test test/models/micropost_test.rb

次に、Railsのdefault_scopeメソッドを使ってこのテストを成功させます。
このメソッドは、データベースから要素を取得したときの、
デフォルトの順序を指定するメソッドです。特定の順序にしたい場合
は、default_scopeの引数にorderを与えます。
例えば、created_atカラムの順にしたい場合は次のようになります。

default_scopeでマイクロポストを順序付ける
app/models/micropost.rb

#追加
default_scope -> { order(created_at: :desc) }

ラムダ式 (Stabby lambda) という文法を使っています。これは、Procやlambda(もしくは無名関数)と呼ばれるオブジェクトを作成する文法です。
->というラムダ式は、ブロックを引数に取り、Procオブジェクトを返します。
このオブジェクトは、callメソッドが呼ばれたとき、
ブロック内の処理を評価します。この構文をコンソールで確かめてみましょう。

-> { puts "foo" }
=> #<Proc:0x007fab938d0108@(irb):1 (lambda)>
>> -> { puts "foo" }.call
foo
=> nil
green
rails test

ユーザーが削除された場合、ユーザーのマイクロポストも同様に削除されるべきです。

この振る舞いは、has_manyメソッドにオプションを渡してあげることで実装できます

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

class User < ApplicationRecord
has_many :microposts, dependent: :destroy

dependent: :destroyというオプションを使うと、
ユーザーが削除されたときに、そのユーザーに紐付いた
(そのユーザーが投稿した)マイクロポストも一緒に削除されるようになります

dependent: :destroyのテスト green
test/models/user_test.rb

require 'test_helper'

class UserTest < ActiveSupport::TestCase

  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
  .
  .
  .
  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
end

演習

1:Micropost.first.created_atの実行結果と、Micropost.last.created_atの実行結果を比べてみましょう。

Micropost.first.created_at
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" ASC LIMIT ? [["LIMIT", 1]]
=> Tue, 29 Jan 2019 07:36:23 UTC +00:00

Micropost.last.created_at
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."id" DESC LIMIT ? [["LIMIT", 1]]
=> Fri, 10 Apr 2020 00:33:04 UTC +00:00

2:Micropost.firstを実行したときに発行されるSQL文はどうなっているでしょうか? 同様にして、Micropost.lastの場合はどうなっているでしょうか?
それぞれをコンソール上で実行したときに表示される文字列が、SQL文になります。

Micropost.first
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ? [["LIMIT", 1]]
=> #<Micropost id: 2, content: "test", user_id: 2, created_at: "2020-04-10 01:43:31", updated_at: "2021-04-10 01:43:31">

Micropost.last
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ? [["LIMIT", 1]]
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2020-04-10 00:33:04", updated_at: "2021-04-10 00:33:04">

firstはDesk降順で取り出してlastはASC昇順で取り出している。

3:データベース上の最初のユーザーを変数userに代入してください。
そのuserオブジェクトが最初に投稿したマイクロポストのidは
いくつでしょうか?
次に、destroyメソッドを使ってそのuserオブジェクトを削除してみてください。削除すると、そのuserに紐付いていた
マイクロポストも削除されていることを
Micropost.findで確認してみましょう

>> user = User.first
User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]]
=> #<User id: 1, name: "YUUKI", email: "yuukitetsuyanet@gmail.com", created_at: "2019-01-27 11:45:56", updated_at: "2019-01-28 10:06:18", password_digest: "$2a$10$RtF8nSd22GLENhTmlBTkPeaqyv7f3UeR6UecG...", remember_digest: nil, admin: false, ¥: nil, activation_digest: "$2a$10$f3kgl8ZWwRuixqBNDohcb.qkPwxw1Pq16gM...", activated: true, activated_at: "2019-01-27 11:46:05", reset_digest: "$2a$10$cs997A4KK0w9hg6cv82z0gw06pZOzIX3x/p...", reset_sent_at: "2019-01-28 10:05:13">
>> user.microposts.first
Micropost Load (0.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2019-01-29 07:36:23", updated_at: "2019-01-29 07:36:23">
>> user_f = user.microposts.first
Micropost Load (0.1ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."id" ASC LIMIT ? [["user_id", 1], ["LIMIT", 1]]
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2019-01-29 07:36:23", updated_at: "2019-01-29 07:36:23">
>> user_f.destroy
(0.1ms) begin transaction
SQL (2.2ms) DELETE FROM "microposts" WHERE "microposts"."id" = ? [["id", 1]]
(6.1ms) commit transaction
=> #<Micropost id: 1, content: "Lorem ipsum", user_id: 1, created_at: "2019-01-29 07:36:23", updated_at: "2019-01-29 07:36:23">>> Micropost.find(1)
Micropost Load (0.2ms) SELECT "microposts".* FROM "microposts" WHERE "microposts"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find Micropost with 'id'=1
from (irb):37

13.2に続く

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です