エンジニア志望のブログ

Railsチュートリアル第13章

はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境  Ruby: 2.7.2 , Rails: 6.1.4

メモ

マイクロソフトテーブルのインデックス

マイクロポストテーブルのuser_idとcreated_atにインデックスが追加されている

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
    add_index :microposts, [:user_id, :created_at]
  end
end

user_idに関連づけられたマイクロポストを作成時刻の逆順で取り出しやすくするためにインデックスを追加。
また、user_idとcreated_atをひとつの配列に含めているのは、両方のキーを同時に扱う複合キーインデックスを作成するため。

作られたマイクロポストモデル
外部キーとしてuserのidを指定したため
生成されたマイクロソフトモデルにはbelongs_to :userでユーザーモデルに関連づけられている

class Micropost < ApplicationRecord
  belongs_to :user
end

モデルを関連づける

belongs_to:1対1の関係性を持たせる
has_many :1対多の関係性を持たせる


1対1:ひとつの投稿は1人のユーザーによって投稿される
1対多:一人のユーザーは複数の投稿を行うことができる

モデルを関連づけることで使えるメソッド

micropost.user
user.microposts
user.microposts.create
user.microposts.create!
user.microposts.build #newするのと同様
user.microposts.find_by()

紐付いているユーザーを通してマイクロポストを作成
こうして作られたマイクロポストは外部キーであるuser_idは自動的に生成元のユーザーのIDに設定される

デフォルトスコープ

データベースからデータを取り出す際に絞り込みや順序を指定することができる

これはデフォルトでのSQLを変更するため、使用する際には注意が必要とのこと
参考:Railsのdefault_scopeは悪ではない。 - Qiita

Dependent: destroy

1対多の関係の1側の要素が削除された時に同時に関連づけられた多の要素も削除される

ユーザーを削除した際に関連づけられている全ての投稿も削除される

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

request.referrerメソッド

ひとつ前のURLを返す

/homeで何らかのHTTPリクエストを送信してrequest.referrerが呼び出されるとURL /homeが返される

参考:request | Railsドキュメント

SQLインジェクション

SQL文の組み立てに問題があり脆弱性がある場合、この問題を利用して攻撃を行うこと。

参考:安全なウェブサイトの作り方 - 1.1 SQLインジェクション:IPA 独立行政法人 情報処理推進機構

対策
直接SQL文を書かないようにすることでSQLインジェクションの対策をする

Micropost.where("user_id = ?", id)

画像のアップロード

ActiveStorageを用いて画像のアップロードを行う
ファイルをクラウドストレージへのアップロードや、Active Recordオブジェクトに結びつける機能を提供する。
ActiveStorageは画像ファイル、平文、PDFなどを取り扱える
参考:Active Storage の概要 - Railsガイド

準備

1. ActiveStorage用マイグレーションファイルを生成

$ rails active_storage:install

2. マイグレーションファイルを実行

$ rails db:migrate

3. ファイルをモデルに結びつける
has_one_attachedメソッドでアップロードされたファイルとモデルを結びつける。
imageを1対1でモデルと結びつける

class モデル < ApplicationRecord
  ・・
  has_one_attached :image
  ・・
end

1対多で結びつけたい場合has_many_attachedメソッドを利用

4. ファイルを受けつける

<%= f.file_field :image %>

5. ファイルを受け取る
・ストロングパラメータで:imageを許可する
・ActiveStorageのattachメソッドを利用してファイルを受け取る

micropost.image.attach(params[:micropost][:image])

6. ファイルを表示する

    <%= image_tag micropost.image if micropost.image.attached? %>

micropost.image.attached?ではマイクロポストがimageを持っているかを調べる。

画像のバリデーション

Active Storageにはバリデーション機能がネイティブでサポートされていない。
そこで'active_storage_validations'gemを追加して設定を行う。

画像フォーマットのバリデーション

content_type: { in: %w[image/jpeg image/gif image/png],
                message: "must be a valid image format" }

画像サイズのバリデーション

size: { less_than: 5.megabytes,
        message: "should be less than 5MB" }
画像のリサイズ

表示する画像のサイズを設定する。

1. ImageMagickを開発環境にインストール

brew install imagemagick

2. gemを追加

gem 'image_processing',           '1.9.3'
gem 'mini_magick',                '4.9.5'

3. インストール

$ bundle install

4. 変換済み画像を作成
保存されている画像を加工

image.variant(resize_to_limit: [500, 500])

これをimage_tagで表示

テストでfixutureで定義されたファイルを使う

/test/fixturesにテストで利用したい画像を追加する
fixuture_file_uploadメソッドを利用してfixtureで定義されたファイルをアップロードしてこれをテストで利用する

演習

13.1.4

演習1

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

作成日時を降順に設定したため
最後の投稿より最初の投稿の方が新しい

irb(main):001:0> Micropost.first.created_at
   (0.7ms)  SELECT sqlite_version(*)
  Micropost Load (1.0ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> Fri, 16 Jul 2021 00:10:48.658871000 UTC +00:00
irb(main):002:0> Micropost.last.created_at
  Micropost Load (0.5ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" ASC LIMIT ?  [["LIMIT", 1]]
=> Thu, 15 Jul 2021 12:28:01.015111000 UTC +00:00
演習2

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

デフォルトスコープで設定したorder byが追加されている
ORDER BY "microposts"."created_at" DESC

irb(main):003:0> Micropost.first
  Micropost Load (0.8ms)  SELECT "microposts".* FROM "microposts" ORDER BY "microposts"."created_at" DESC LIMIT ?  [["LIMIT", 1]]
=> #<Micropost id: 2, content: "Lorem ipsum", user_id: 1, created_at: "2021-07-16 00:10:48.658871000 +0000", updated_at: "2021-07-16 00:10:48.658871000 +0000">
演習3

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

ユーザーが削除されたら削除されたユーザーに関連付けられていた投稿も削除されていることを確認

irb(main):001:0> user = User.first
   (0.6ms)  SELECT sqlite_version(*)
  User Load (0.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.6778...
irb(main):002:0> user.microposts.first.id
  Micropost Load (0.8ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["user_id", 1], ["LIMIT", 1]]
=> 2
irb(main):003:0> user.destroy
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."user_id" = ? ORDER BY "microposts"."created_at" DESC  [["user_id", 1]]
  Micropost Destroy (0.7ms)  DELETE FROM "microposts" WHERE "microposts"."id" = ?  [["id", 2]]
  User Destroy (1.2ms)  DELETE FROM "users" WHERE "users"."id" = ?  [["id", 1]]
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Example User", email: "example@railstutorial.org", created_at: "2021-07-14 08:39:31.194822000 +0000", updated_at: "2021-07-15 05:42:22.677832000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: true, activation_digest: "$2a$12$zTggchTpdRrUiMmX/Af1DuLf2Zm8ASlZ079q5wV7WuR...", activated: true, activated_at: "2021-07-14 08:39:30.948680000 +0000", reset_digest: "$2a$12$VdwivaGX9p8cfpggYPhIE.DO4hXpXpnGwRhRM41M0lI...", reset_sent_at: "2021-07-15 05:42:22.677743000 +0000">
irb(main):004:0> Micropost.find(2)
  Micropost Load (0.3ms)  SELECT "microposts".* FROM "microposts" WHERE "microposts"."id" = ? ORDER BY "microposts"."created_at" DESC LIMIT ?  [["id", 2], ["LIMIT", 1]]
Traceback (most recent call last):
        1: from (irb):4
ActiveRecord::RecordNotFound (Couldn't find Micropost with 'id'=2)

13.2.1

演習1

7.3.3で軽く説明したように、今回ヘルパーメソッドとして使ったtime_ago_in_wordsメソッドは、Railsコンソールのhelperオブジェクトから呼び出すことができます。このhelperオブジェクトのtime_ago_in_wordsメソッドを使って、3.weeks.agoや6.months.agoを実行してみましょう。

irb(main):002:0> helper.time_ago_in_words(3.weeks.ago)
=> "21 days"
irb(main):003:0> helper.time_ago_in_words(6.month.ago)
=> "6 months"
演習2

helper.time_ago_in_words(1.year.ago)と実行すると、どういった結果が返ってくるでしょうか?

irb(main):006:0> helper.time_ago_in_words(1.years.ago)
=> "about 1 year"
演習3

micropostsオブジェクトのクラスは何でしょうか? ヒント: リスト 13.23内のコードにあるように、まずはpaginateメソッド(引数はpage: nil)でオブジェクトを取得し、その後classメソッドを呼び出してみましょう。

b(main):009:0> microposts = Micropost.paginate(page: nil)
   (3.9ms)  SELECT sqlite_version(*)
  Micropost Load (0.4ms)  SELECT "microposts".* FROM "microposts" /* loading for inspect */ ORDER BY "microposts"."created_at" DESC LIMIT ? OFFSET ?  [["LIMIT", 11], ["OFFSET", 0]]
=> #<ActiveRecord::Relation []>
irb(main):010:0> microposts.class
=> Micropost::ActiveRecord_Relation

13.2.2

演習1

(1..10).to_a.take(6)というコードの実行結果を推測できますか? 推測した値が合っているかどうか、実際にコンソールを使って確認してみましょう。

irb(main):001:0> (1..10).to_a.take(6)
=> [1, 2, 3, 4, 5, 6]
演習2

先ほどの演習にあったto_aメソッドの部分は本当に必要でしょうか? 確かめてみてください。

irb(main):002:0> (1..10).take(6)
=> [1, 2, 3, 4, 5, 6]
演習3

Fakerはlorem ipsum以外にも、非常に多種多様の事例に対応しています。Fakerのドキュメント(英語)を眺めながら画面に出力する方法を学び、実際に架空の大学名やHipster IpsumやChuck Norris facts(参考: チャック・ノリスの真実)を画面に出力してみましょう。(訳注: もちろん日本語にも対応していて、例えば沖縄らしい用語を出力するfaker-okinawaもあります。ぜひ遊んでみてください。)

ドラゴンボールのFakerがあったので試してみました笑

  content = Faker::JapaneseMedia::DragonBall.character

13.2.3

演習2

リスト 13.28にあるテストを変更して、will_paginateが1度のみ表示されていることをテストしてみましょう。ヒント: 表 5.2を参考にしてください。

    assert_select 'div.pagination', count: 1

13.3.1

演習1

なぜUsersコントローラ内にあるlogged_in_userフィルターを残したままにするとマズイのでしょうか? 考えてみてください。
DRY原則に反するため。

13.3.2

演習1

Homeページをリファクタリングして、if-else文の分岐のそれぞれに対してパーシャルを作ってみましょう。

views/static_pages/home.html.erb

<% if logged_in? %>
  <%= render 'static_pages/logged_in_user' %>
<% else %>
  <%= render 'static_pages/not_logged_in_user' %>
<% end %>

views/static_pages/_logged_in_user.html.erb

<div class="row">
  <aside class="user_info">
    <section class="user_info">
      <%= render 'shared/user_info' %>
    </section>
    <section class="micropost_form">
      <%= render 'shared/micropost_form' %>
    </section>
  </aside>
</div>

views/static_pages/_not_logged_in_user.html.erb

<div class="center jumbotron">
  <h1>Welcome to the Sample App</h1>  
  <h2>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </h2> 
  <%= link_to "Sign up now!", signup_path, class: "btn btn-lg btn-primary" %>
</div>

  <%= link_to image_tag("rails.svg", alt: "Rails logo", width: "200px"),
                        "https://rubyonrails.org/" %>
演習2

マイクロポストを投稿した直後に、ブラウザの更新ボタンを押すとエラーが表示されます。なぜエラーが表示されるのでしょうか?その原因を考えてみましょう。

マイクロポストを投稿するとPOSTリクエストが/micropostsに送信される。
micropostsコントローラーでは投稿に失敗した時renderメソッドを用いてhomeページが表示される。しかしrenderでは'static_pages/home'を表示しただけでURLを更新していない。つまり投稿に失敗した場合URLは/micropostsの状態になる。
なので/micropostsの状態でページの更新をすると/micropostsに対してGETリクエストが送信され、現段階ではリクエストに対するルートが指定されていないためルーティングエラーになる。

演習3

もし上記の現象に対応するとしたら、どんな対応方法があるでしょうか?その対応方法を考えてみましょう。(ヒント: 様々な対応方法がありますが、対応方法によっては今後の実装に支障が出ることがあります。ここでは対応方法のアイデア出しに留めておきましょう。)

投稿に失敗した時にURLを更新できれば解決できると思う。
つまり投稿に失敗した時にリダイレクトするように変更すれば対応できると思う。

参考に
qiita.com
POSTした後にrenderするのは良くないとのことです。
後で改良したいと思います。

13.4.1

演習2

リスト 13.64に示すテンプレートを参考に、13.4で実装した画像アップローダーをテストしてください。テストの準備として、まずはサンプル画像をfixtureディレクトリに追加してください(リスト 13.63)。リスト 13.64で追加したテストでは、Homeページにあるファイルアップロードと、投稿に成功した時に画像が表示されているかどうかをチェックしています。なお、テスト内にあるfixture_file_uploadというメソッドは、fixtureで定義されたファイルをアップロードする特別なメソッドです18 。ヒント: image属性が有効かどうかを確かめるときは、11.3.3で紹介したassignsメソッドを使ってください。このメソッドを使うと、投稿に成功した後にcreateアクション内のマイクロポストにアクセスするようになります。

  test "micropost interface" do
    log_in_as(@user)
    get root_path
    assert_select "div.pagination", count: 1
    assert_select "input[type=file]"
    #無効な送信
    assert_no_difference "Micropost.count" do
      post microposts_path, params: { micropost: { content: ""} }
    end
    assert_template 'static_pages/home'
    assert_select 'div#error_explanation'
    assert_select 'a[href=?]', '/?page=2' #正しいページネーションリンク
    #有効な送信
    content = "This micropost really ties the room toghether"
    image = fixture_file_upload('test/fixtures/kitten.jpg', 'image/jpeg')
    assert_difference "Micropost.count", 1 do
      post microposts_path, params: { micropost: { content: content, image: image } }
    end
    assert assigns(:micropost).image.attached?
    assert_redirected_to root_url
    follow_redirect!
    assert_match content, response.body
    #投稿を削除する
    assert_select 'a', text: 'delete' 
    first_micropost = @user.microposts.paginate(page: 1).first
    assert_difference "Micropost.count", -1 do
      delete micropost_path(first_micropost)
    end
    #違うユーザーのプロフィールにアクセス
    get user_path(users(:archer))
    assert_select 'a', text: 'delete', count: 0
  end

おわりに

この章はかなり内容がつまっていたような気がしました。
Railsの力を借りて画像のアップロードまで実装ができて改めてRailsすごいなと思いました。