kuma0319のブログ

webエンジニアになるためのアウトプットブログ

Ruby on Railsチュートリアル第8章

Railsチュートリアルの第8章を進めていきます。

第8章(基本的なログイン機構)

セッション

  • HTTPはRESTの原則であるステートレス性(前のやり取りの情報に依存せず独立)を持つ。
  • セッションと呼ばれる半永続的な接続をコンピュータ間に設ける。そのセッションの情報を保持するのがcookeis。
  • セッションの一連の流れは、RESTアクションと紐づける。ログインフォーム→new、ログイン→create、ログアウト→destroy
  • セッションの情報はusersがparams[:user]でネストされたハッシュによって属性を取り出した場合と同様に、params[:session]で中身を取り出せる。
セッションとユーザー登録フォームの違い
  • ログインセッションのフォームはユーザー登録フォームのビューがEmailとPasswordになっただけ。
  • ユーザー登録フォームのエラーメッセージは、ActiveRecordのエラーメッセージを表示したが、今回はflashを使用する必要がある。(∵ActiveRecordオブジェクトでは無いため)
  • セッションにはsessionモデルが無く、@session変数も当然無いため、form_with(model: @user)のような記述が出来ず、form_with(url: login_path, scope: :session)でscopeでモデルを指定。
ログインセッション

ログイン(create)を実装。find_byでネストされたハッシュからemailでユーザー検索し、if文でuserが存在していれば論理積で認証。

[app/controllers/sessions_controller.rb]
class SessionsController < ApplicationController

  def new
  end

  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      # エラーメッセージを作成する
      render 'new', status: :unprocessable_entity
    end
  end

  def destroy
  end
end
エラーのフラッシュメッセージ

ユーザー登録のエラーは以下のように_error_messageパーシャルでActiveRecordのメッセージをそのまま利用していた。

    <% @user.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>

これが出来ないので、ユーザー登録の成功(:seuccess)と同様にフラッシュメッセージを用意。

[app/controllers/sessions_controller.rb]
  def create
    user = User.find_by(email: params[:session][:email].downcase)
    if user && user.authenticate(params[:session][:password])
      # ユーザーログイン後にユーザー情報のページにリダイレクトする
    else
      flash[:danger] = 'Invalid email/password combination'
      render 'new', status: :unprocessable_entity
    end
  end

但し、通常のflashはリダイレクトの際に使用されるため、flash.nowを使用する。

ログイン失敗のテスト

統合テストでログイン失敗時のテストをフラッシュメッセージの動作と併せて実施。

[test/integration/users_login_test.rb]
class UsersLoginTest < ActionDispatch::IntegrationTest

  test 'login with invalid information' do
    get login_path                                    #ログインパスを取得。
    assert_template 'sessions/new'                    #sessionのnewページが表示されているか。
    post login_path, params: {session: { email: ' ',
                            password: 'invalid' }}    #ログインパスにpostで無効な情報を送信。
    assert_template 'sessions/new'                    #sessionのnewページがレンダリングされているか?
    assert_response :unprocessable_entity             #HTTPレスポンスが返ってきているか。
    assert_not flash.empty?                           #フラッシュメッセージが表示されているか?
    get root_path                                     #適当な別の処理を実行。
    assert flash.empty?                               #フラッシュメッセージが消えているか?
  end

end

ログイン

sessionメソッドとcokkiesメソッド

sessionメソッドを扱うので9章のcokkiesメソッドも先回りして簡単にまとめておきます。

sessionメソッド

一連のセッション用に必要なデータを残すためのメソッドでハッシュとして扱える。
一時cookiesに暗号化済みのデータを保管する。 ブラウザを閉じると、一時cookiewsの有効期限は終了する。

cookiesメソッド

ブラウザ終了時にも残しておきたいデータを保存しておくためのメソッドでハッシュとして扱える。
sessionメソッドと違ってオプションのexpires(有効期限)の指定で、永続的なセッションを作ることが可能

reset_sessionメソッド

セッションメソッドを使用したlog inメソッドを使用し、ログイン動作を実装する場合、必ずreset_sessionメソッドを組み込む。
reset_sessionメソッドは、セッションをリセットすることでセッション固定攻撃を防ぐRailsの組み込みメソッド。

current_userメソッド

current_userメソッドの部分で重要な情報が書かれていました。
まず完成形のcurrent_userメソッドは以下のようになります。

  # 現在ログイン中のユーザーを返す(いる場合)
  def current_user
    if session[:user_id]
      @current_user ||= User.find_by(id: session[:user_id])
    end
  end

idでの検索のため、find_byでは無く、findでよいような気がしますが、ここではIDが無効な場合であっても例外を発生するのではなく、nilを返してほしい部分。
これは、findの場合指定されたidに対応するユーザーを毎回探そうとして(データベースへリクエスト)例外を発生するが、find_byは条件に合致する最初のデータを返しそれをキャッシュとして保持しておくため、current_userが何回呼び出されたとしても一回の呼出しで済む。
このようなメソッド呼び出しの結果を変数に保存(インスタンス化)し、それを再利用する手法はメモ化といい高速化させるテクニック。
更に、「| | =」という書き方は自己代入で、左辺がnilまたはfalseであれば右辺を代入する。

digestメソッド

ここのdigestメソッドも複雑で三項演算子については9章で詳細が出ますのでそのタイミングでまとめます。
ここでは、テストユーザー用のfictureにpassword_digest属性を追加したいが、Bcryptパスワードのハッシュ化のコスト(セキュリティの強度)は最低値でよいため、最低コストでパスワードをハッシュ化したものをdigestメソッドで定義している。このdigestメソッドはクラスメソッド(クラス自身の持つメソッド)で定義。

テストからfixtureデータを参照

次のようにテストで記述した場合、@userはusers.ymlを参照し、:micaelというシンボルをkeyとしてユーザーを参照する。

  def setup
    @user = users(:michael)
  end
ログイン成功のテスト

統合テストでログイン成功時のテストを実施。
ここで前章のfollow_redirect!の動作を忘れておりました。最初はfollow_redirect!の動作を記述しておらず、templateが何もないといったエラーとなったのですが、実際にページに移動するfollow_redirect!の動作が必要みたいです。

  test "login with valid information" do
    post login_path, params: { session: { email:    @user.email,
                                          password: 'password' } }
    assert_redirected_to @user
    follow_redirect!
    assert_template 'users/show'
    assert_select "a[href=?]", login_path, count: 0
    assert_select "a[href=?]", logout_path
    assert_select "a[href=?]", user_path(@user)
  end

ログアウト

ログアウト処理は、通常reset_sessionをおこなえばok。但し、セキュリティ上@current_user = nilまで入れておく方がbetter。

[app/helpers/sessions_helper.rb]
  # 現在のユーザーをログアウトする
  def log_out
    reset_session
    @current_user = nil   # 安全のため
  end

destroyアクションのリダイレクト時には、Turboに対応するようにHTTPステータスコードを追加(ここだとsee other)しておく。

第8章ー演習ー

8.1.1

①GET login_pathとPOST login_pathとの違いを説明できますか? 少し考えてみましょう。
(解答) GET login_pathはログインフォームの表示、POST login_pathはログインフォームからデータを送信(ログイン処理)で違う。

②ターミナルのパイプ機能を使ってrails routesの実行結果とgrepコマンドをつなぐことで、Usersリソースに関するルーティングだけを表示できます。同様にして、Sessionsリソースに関する結果だけを表示させてみましょう。現在、いくつのSessionsリソースがあるでしょうか? (ヒント: パイプやgrepの使い方が分からない場合は 『コマンドライン編』の 「grepで検索する」を参考にしてみてください。)
(解答)sessionsリソースは3つのみ。
usersリソースのルーティング

 $ rails routes | grep users
                                  signup GET    /signup(.:format)                                                                                 users#new
                                   users GET    /users(.:format)                                                                                  users#index
                                         POST   /users(.:format)                                                                                  users#create
                                new_user GET    /users/new(.:format)                                                                              users#new
                               edit_user GET    /users/:id/edit(.:format)                                                                         users#edit
                                    user GET    /users/:id(.:format)                                                                              users#show
                                         PATCH  /users/:id(.:format)                                                                              users#update
                                         PUT    /users/:id(.:format)                                                                              users#update
                                         DELETE /users/:id(.:format)                                                                              users#destroy

sessionsリソースのルーティング

$ rails routes | grep sessions
                                   login GET    /login(.:format)                                                                                  sessions#new
                                         POST   /login(.:format)                                                                                  sessions#create
                                  logout DELETE /logout(.:format)                                                                                 sessions#destroy

8.1.2

①リスト 8.4で定義したフォームで送信すると、Sessionsコントローラのcreateアクションに到達します。Railsはこれをどうやって実現しているでしょうか? 考えてみてください。(ヒント:表 8.1とリスト 8.5の1行目に注目してください。)
(解答) form_withメソッドはHTTPリクエストのPOSTを指定する。今回はセッションにおけるlogin_pathを指定しているため、sessionsのcreateアクション(POST)に到達する。

8.1.3

Railsコンソールを使って、表 8.2のそれぞれの式が合っているか確かめてみましょう. まずはuser = nilの場合を、次にuser = User.firstとした場合を確かめてみてください。(ヒント: 必ず論理値オブジェクトとなるように、4.2.2で紹介した!!のテクニックを使ってみましょう。例: !!(user && user.authenticate('foobar')))
(解答)

irb(main):001:0> user = User.first
  User Load (1.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=>                                                                     
#<User:0x00007fa595312eb8                                              
...                                                                    
irb(main):002:0> !!(user && user.authenticate('foobar'))
=> true
irb(main):003:0> user = nil
=> nil
irb(main):004:0> !!(user && user.authenticate('foobar'))
=> false

8.1.5

①8.1.4の処理の流れが正しく動いているかどうか、ブラウザで確認してみてください。特に、flashがうまく機能しているかどうか、フラッシュメッセージの表示後に違うページに移動することを忘れないでください。
(解答)期待通りに動作した。

8.2.1

①有効なユーザーで実際にログインし、ブラウザでcookiesの情報を調べてみてください。このとき、sessionの値はどうなっているでしょうか? ブラウザでcookiesを調べる方法が分からなければ、今こそググってみるときです!(コラム 1.2)
(解答)暗号化されためちゃくちゃ長い文字列になっていた。

②上の演習課題と同様に、Expires(有効期限)の値について調べてみてください。
(解答)Max-ageがSessionとなっていたため、セッション終了時まで。

8.2.2

Railsコンソールを使って、User.find_by(id: ...)で対応するユーザーが検索に引っかからなかったとき、nilを返すことを確認してみましょう。
(解答)

irb(main):003:0> User.find_by(id: 5)
  User Load (24.0ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 5], ["LIMIT", 1]]
=> nil  

②先ほどと同様に、今度は:user_idキーを持つsessionハッシュを作成してみましょう。リスト 8.17に記したステップに従って、||=演算子がうまく動くことも確認してみましょう。
(解答)

irb(main):004:0> session = {}
=> {}
irb(main):005:0> session[:user_id] = nil
=> nil
irb(main):006:0> @current_user ||= User.find_by(id: session[:user_id])
  User Load (2.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" IS NULL LIMIT ?  [["LIMIT", 1]]
=> nil                                                                    
irb(main):007:0> session[:user_id] = User.first.id
  User Load (1.5ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ?  [["LIMIT", 1]]
=> 1                                                                      
irb(main):008:0> @current_user ||= User.find_by(id: session[:user_id])
  User Load (0.6ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
=>                                                                        
#<User:0x00007f768e09e708                   
...                                         
irb(main):009:0> @current_user ||= User.find_by(id: session[:user_id])
=> 
#<User:0x00007f768e09e708                   
...
irb(main):010:0> @current_user
=> 
#<User:0x00007f768e09e708
 id: 1,
 name: "Rails Tutorial",
 email: "example@railstutorial.org",
 created_at: Sat, 15 Apr 2023 12:17:28.353917000 UTC +00:00,
 updated_at: Sat, 15 Apr 2023 12:17:28.353917000 UTC +00:00,
 password_digest: "[FILTERED]">

8.2.5

確認とJacaScript側のリファクタリングなので飛ばします。

8.2.6

①リスト 8.15の8行目にあるif userから後ろをすべてコメントアウトすると、ユーザー名とパスワードを入力して認証しなくてもテストがパスしてしまうことを確認してください(リスト 8.37)。パスしてしまう理由は、リスト 8.9では「メールアドレスは正しいがパスワードが誤っている」ケースをテストしていないからです。このテストがないのは重大な手抜かりですので、テストスイートで正しいメールアドレスをUsersのログインテストに追加して、この手抜かりを修正してください(リスト 8.38)。テストが red (失敗)することを確認し、それから先ほどの8行目以降のコメントアウトを元に戻すと green (パス)することを確認してください。この演習の修正は重要なので、この後の 8.3のメインのコードにも修正を反映してあります。
(解答) このテストを追加することで、コメントアウトしたらちゃんとレッドになった。

[test/integration/users_login_test.rb]
  test "login with valid email/invalid password" do
    get login_path
    assert_template 'sessions/new'
    post login_path, params: { session: { email:    @user.email,
                                          password: "invalid" } }
    assert_response :unprocessable_entity
    assert_template 'sessions/new'
    assert_not flash.empty?
    get root_path
    assert flash.empty?
  end

②safe navigation演算子(または“ぼっち演算子”)と呼ばれる&.を用いて、リスト 8.15の8行目の論理値(boolean値)の条件式を、リスト 8.3919 のようにシンプルに変えてください。Rubyのぼっち演算子を使うと、obj && obj.methodのようなパターンをobj&.methodのように凝縮した形で書けます。変更後も、リスト 8.38のテストがパスすることを確認してください。
(解答) 変更後もパスする。

8.2.7

①リスト 8.40のlog_inの行をコメントアウトすると、テストスイートは red になるでしょうか? それとも green になるでしょうか? 確認してみましょう。
(解答) REDになる

 FAIL UsersSignupTest#test_valid_signup_information (2.17s)
        Expected false to be truthy.
        test/integration/users_signup_test.rb:32:in `block in <class:UsersSignupTest>'

②現在使っているテキストエディタの機能を使って、リスト 8.40をまとめてコメントアウトできないか調べてみましょう。また、コメントアウトの前後でテストスイートを実行し、コメントアウトすると red に、コメントアウトを元に戻すと green になることを確認してみましょう。(ヒント: コメントアウト後にファイルを保存することを忘れないようにしましょう。また、テキストエディタコメントアウト機能については『テキストエディタ編』の 「コメントアウト機能」などを参照してみてください。)
(解答) 出来る。

8.3

①ブラウザで「Log out」リンクをクリックしたときに、Webサイトのレイアウトが正しく切り替わることを確認してください。このログアウト時のレイアウト切り替えと、リスト 8.46のテストの末尾3行にどのような対応関係があるかを考えて答えてください。
(解答) 正しく切り替わる。ログイン状態で表示されていた「logout」や「user」リンクが表示されなくなり、「signup」リンクがあることを確認。

②リスト 8.46にある2つのテストの内容が少し増えてしまいました。このままだとテストのメンテナンスがやりにくくなる可能性があります。テストを分割する戦略はいくつか考えられますが、その1つは、関連するテストごとにRubyのクラス(4.4)を別途作る方法です。クラスを作ると、テストに必要なsetupメソッドの中で関連する部分を継承(4.4.2)で再利用できます。 ここで重要なのはリスト 8.48でハイライトされているsuperです。superは、現在のクラスのスーパークラス(クラス階層の1つ上にあるクラス)にある、対応するsetupメソッドを呼び出します。

この演習問題のトピックはやや高度なので、基本的にリスト 8.48のテストコードが green になることを確認できればOKです。しかし、このテストコードを理解してみたい方はぜひじっくり読んでみてください。リファクタリング前のリスト 8.46と注意深く見比べてみると、以前よりもずっと多くのことを発見できるようになった自分に驚くことでしょう。テストのリファクタリングについては、この後の 11.3.3、 12.3.3、 13.3.5で詳しく扱います。
(解答) GREENになる。

第8章ーまとめー

  • ブラウザ上に一時的に情報を保持する方法としてcookiesがある。メソッドとしてはsessionメソッドとcookiesメソッドでcookiesメソッドは永続化も可能。
  • 通常flashはリダイレクト時に使用されるため、それ以外のrenderメソッドなどの場合はflash.nowとする。
  • Railsの組み込みメソッドのreset_sessionメソッドはセッション固定攻撃の対策として必ず入れておく。
  • findメソッドはidに合致するデータを毎回検索するが、find_byは合致する最初のデータを検索しキャッシュに保持する。そのため以降検索無しに使いまわせる。
  • テスト時にリダイレクト先のテストまで記述する場合はfollow redirect!で実際に移動する必要がある。

第8章終わり🐻

Ruby on Railsチュートリアル第7章

Railsチュートリアルの第7章を進めていきます。

第7章(ユーザー登録)

ユーザーの表示

ここはdebug機能の紹介や、gravatorで見た目を整える部分なので特にまとめず、演習のみして飛ばします。

ユーザー登録フォーム

form_withメソッド
  • HTMLフォーム要素(テキストフィールド、ラジオボタン、パスワードフィールドなど)に対応するメソッドが呼び出されると、@userの属性を設定するために特別に設計されたHTMLを返す。
  • <%= form_with(model: @user) do |f| %>の記述では、form_withのオプションとしてmodel: @userを渡してる。こうすることで、Userクラスの変数@user(新しいユーザー)なのでRailsがPOSTメソッドを実行すべきと判断出来る。
  • emailはtype="email"を使用した方が、一般的なtextフィールドよりも操作感がよく適している。
  • passwordはtype="password"とすることで、入力文字がドット表記になる。
  • formタグ内にはCross-Site Request Forgery(CSRF)を阻止するための真正性トークン(authenticity token)も生成される。

ERB形式で以下の記述の場合

<%= f.label :name %>
<%= f.text_field :name %>

以下のHTMLが返される。

  <label for="user_name">Name</label>
  <input type="text" name="user[name]" id="user_name" />

ユーザー登録失敗

createアクション

resources :usersのルーティングで、/usersへのPOSTリクエストはcreateアクションに対応する。

コントローラにcreateアクションを追加。
status: :unprocessable_entityは後々Turboのレンダリングに必要。

[app/controllers/users_controller.rb]
  def create
    @user = User.new(params[:user])    # 実装は終わっていないことに注意!
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new', status: :unprocessable_entity
    end
  end
params[:user]について

paramsハッシュには各リクエストの情報が含まれる。更にparamsハッシュは入れ子のハッシュが含まれ、userハッシュに各属性に対応する値が保存されている。
createアクションの@user = User.new(params[:user])は、この入れ子のハッシュを展開すると下記のような形態となる。

@user = User.new(name: "Foo Bar", email: "foo@invalid",
                 password: "foo", password_confirmation: "bar")

このような複数情報を扱える機能をマスアサインメントと呼ぶ。但し、マスアサインメントにはセキュリティ上の注意が必要。

Strong parameters
  • マスアサインメントは複数の属性を更新できるが、その反面悪意ある情報まで更新される脆弱性がある。
  • これの対策がStrong Parametersで必須パラメータと許可済みパラメータを指定可能。
  • Strong Parametersは、通常補助メソッドの形で使用し、更に外部に公開する必要は無いため、private配下に記述する。
  def create
    @user = User.new(user_params)
    if @user.save
      # 保存の成功をここで扱う。
    else
      render 'new', status: :unprocessable_entity
    end
  end

  private

    def user_params
      params.require(:user).permit(:name, :email, :password,
                                   :password_confirmation)
    end
end
エラーメッセージの表示
  • 現時点ではエラーが発生してもユーザー側へ表示されないため、そういう場合はエラーメッセージのパーシャルを作成し、それを該当ページでrenderする。
  • Railsの慣習として、複数のビューで使われるパーシャルは専用のディレクトリ「shared」に配置する。
  • pluralizeヘルパーは、不規則系を含む種々の単語を複数形に変換可能。エラーメッセージでいうと、「1 error」、「5 errors」といった感じ。

ユーザー登録成功

redirect_to @userについて

createアクションの成功時の動作が無いため、対応する動作を作成する。通常はリダイレクトするのが普通。

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to @user #redirect_to user_url(@user)
    else
      render 'new', status: :unprocessable_entity
    end
  end

ここのredirect_to @userは明らかに分かりにくいのですが、以下のサイトが非常に丁寧にまとめられており、分かりやすかったです。

qiita.com

  1. Railsでは通常相対パスを指定するが、リダイレクトの場合は完全なURLを返す絶対パスでないといけない。
  2. そのためパスはredirect_to(user_url(@user.id))となる。
  3. 引数URLがモデルオブジェクトの場合は、Railsはidを自動で返してくれるため、.idは省略可能。
  4. この段階でredirect_to(user_url(@user))となっており、メソッドに渡す場合の()は省略可能なことは4章の通り。
  5. redirect_to user_url(@user)となり、redirect_to @userまで省略可能。
flashメッセージ

flash変数はハッシュのように取り扱うため、flash[:success] = "Welcome to the Sample App!"と記述すると、:successがkeyで、メッセージがvalueとなる。
flash変数に代入されたメッセージはリダイレクト直後に表示され、keyがあれば対応するメッセージを返す。

[app/views/layouts/application.html.erb]
<% flash.each do |message_type, message| %>
  <div class="alert alert-<%= message_type %>"><%= message %></div>
<% end %>

alert-<%= message_type %>の部分は、keyであるmessage_typeの種類によって適用させるCSSを変えている。

デプロイ

TLS設定
  • TLSはローカルのサーバーからネットワークにデータを送信する前に情報を暗号化する技術。
  • Webホスト側がこのように振る舞うことに依存しない方がよいため、ローカルで設定しておく。
  • production.rbファイルのコードの一部config.force_sslをtrueに変更するだけ。
  • 自分のドメインでこれを利用する場合、TLS/SSL証明書を購入して設定する必要があるが、Renderでは利用可能になっている。
  • カスタムドメインTLSをCloudflareで扱う場合は、Railsアプリ側でTLS/SSLを強制的に有効にする設定はしない
本番環境用のwebサーバー設定(Puma)

Pumaは、大量のリクエストを受信できる能力を備えたサーバー 1. config/puma.rbの内容を書き換える。 Getting Started with Ruby on Rails on Render | Render 2. RenderのStart Commandbundle exec puma -C config/puma.rbに置き換える。

本番環境用のデータベース(PostgreSQL)

config/database.ymlのproductionセクションをイジるだけ。

第7章ー演習ー

7.1.1

①ブラウザで/aboutにアクセスし、デバッグ情報が表示されていることを確認してください。このページを表示するとき、どのコントローラとアクションが使われていたでしょうか?paramsの内容から確認してみましょう。
(解答) #<ActionController::Parameters {"controller"=>"static_pages", "action"=>"about"} permitted: false>

Railsコンソールを開き、データベースから最初のユーザー情報を取得し、変数userに格納してください。その後、puts user.attributes.to_yamlを実行すると何が表示されますか? ここで表示された結果と、yメソッドを使ったy user.attributesの実行結果を比較してみましょう。
(解答) どちらもuserオブジェクトの情報がyaml形式で表示される。

7.1.2

①ERBを使って、マジックカラム(created_atとupdated_at)の値をshowページに表示してみましょう(リスト 7.4)。
(解答)ERBで追加したら、「Taro Yamada, michael@example.com, 2023-04-14 16:18:32 UTC, 2023-04-14 16:30:58 UTC」のようにタイムスタンプが表示された。

[app/views/users/show.html.erb]
<%= @user.name %>, <%= @user.email %>, <%= @user.created_at %>, <%= @user.updated_at %>

②ERBを使って、Time.nowの結果をshowページに表示してみましょう。ページを更新すると結果がどう変わるかも確認してみてください。
(解答) 現在時刻が表示される。「Taro Yamada, michael@example.com, 2023-04-14 16:18:32 UTC, 2023-04-14 16:30:58 UTC, 2023-04-15 03:49:51 +0000」

7.1.3

①showアクションの中にdebuggerを差し込み(リスト 7.6)、ブラウザから/users/1にアクセスしてみましょう。次にRailsコンソールを開き、putsメソッドを使ってparamsハッシュの中身をYAML形式で表示してみましょう。(ヒント: 7.1.1.1の演習を参考にしてください。その演習ではdebugメソッドで表示したデバッグ情報を、どのようにしてYAML形式で表示していたでしょうか?)
(解答)

(ruby) puts params.to_yaml
--- !ruby/object:ActionController::Parameters
parameters: !ruby/hash:ActiveSupport::HashWithIndifferentAccess
  controller: users
  action: show
  id: '1'
permitted: false
nil

②newアクションの中にdebuggerを差し込み、/users/newにアクセスしてみましょう。@userの内容はどのようになっているでしょうか? 確認してみてください。
(解答) nil

(ruby) @user
nil

7.1.4

①(任意)Gravatar上にアカウントを作成し、あなたのメールアドレスと適当な画像を紐付けてみてください。メールアドレスをMD5ハッシュ化して、紐付けた画像がちゃんと表示されるかどうか試してみましょう。
(解答) 飛ばします。

②7.1.4で定義したgravatar_forヘルパーをリスト 7.12のように変更して、sizeをオプション引数として受け取れるようにしてみましょう。正しく変更すると、gravatar_for user, size: 50のように呼び出せるようになります。重要: 改善したヘルパーはこの後の10.3.1で実際に使うので、忘れずに実装しておきましょう。
(解答)変更のみ

③オプション引数は今でもRubyコミュニティで一般的に使われていますが、Ruby 2.0から導入された新機能「キーワード引数(Keyword Arguments)」でも実現できます。先ほど変更したリスト 7.12を、リスト 7.13のように置き換えてもうまく動くことを確認してみましょう。この2つの実装方法はどこが違うのかを考えてみてください。
(解答) うまくいく。オプション引数の場合だと、引数を渡す順番によって意図しない結果を生み出す可能性がある。一方キーワード引数は、keyとvalueのハッシュ形式で表現し、値を取り出すときもハッシュで取り出すため順序に依存しない。

7.2.1

①試しに、リスト 7.15のform_withにあるブロック変数fをすべてfoobarに置き換えてみて、結果が変わらないことを確認してみてください。確かに結果は変わりませんが、変数名をfoobarとするのはあまり良い変更ではなさそうですね。その理由について考えてみてください。
(解答) 結果は変わらない。変数fはformのfだと認識出来るが、foobarだと変数の意味が通じなくなるため。

7.2.2

①『HTML編』ではHTMLをすべて手動で書き起こしていますが、formタグは使っていません。その理由を考えてみてください。
(解答) 入力や送信のフォームを作成していなかった?

####7.3.2 ①/signup?admin=1にアクセスすると、paramsの中にadmin属性が含まれていることをデバッグ情報で確認してみましょう。
(解答) 含まれていることを確認。 #<ActionController::Parameters {"admin"=>"1", "controller"=>"users", "action"=>"new"} permitted: false>

####7.3.3 ①最小文字数を5に変更すると、エラーメッセージも自動的に更新されることを確かめてみましょう。
(解答)確認作業なので飛ばします。

②未送信のユーザー登録フォーム(図 7.14)のURLと、送信済みのユーザー登録フォーム(図 7.20)のURLを比べてみると、URLが違っています。その理由を考えてみてください。
(解答)未送信のユーザー登録フォームは、HTTPリクエストGETで/signupパスに対応するnewページを表示している。送信済みのユーザー登録フォームは、HTTPリクエストPOSTでcreateアクションに対応する。ここでは無効な情報でサインアップした場合newテンプレートがrenderされるが、createアクションは/usersのURLに対応するためURLが異なっている。

7.3.4

①リスト 7.20で実装したエラーメッセージに対するテストを書いてみてください。どこまで細かくテストするかはお任せします。リスト 7.25にテンプレートを用意しておいたので、参考にしてください。
(解答) エラーメッセージパーシャルのdivタグ内のid、classを指定。

[test/integration/users_signup_test.rb]
    assert_select 'div#error_explanation'
    assert_select 'div.alert'
    assert_select 'div.alert-danger'

7.4.1

①有効な情報を送信し、ユーザーが実際に作成されたことを、Railsコンソールを使って確認してみましょう。
(解答) 作成出来た。

irb(main):001:0> User.second
  User Load (1.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? OFFSET ?  [["LIMIT", 1], ["OFFSET", 1]]                                                      
=>                                                              
#<User:0x00007f864a41f500                                       
 id: 2,                                                         
 name: "test user",                                             
 email: "testesttest@test.com",                                 
 created_at: Sat, 15 Apr 2023 11:56:15.406868000 UTC +00:00,    
 updated_at: Sat, 15 Apr 2023 11:56:15.406868000 UTC +00:00,    
 password_digest: "[FILTERED]">        

②リスト 7.26のredirect_to @userをredirect_to user_url(@user)に書き換えても、同じ結果になることを確認してみましょう。
(解答) 飛ばします。

7.4.2

Railsコンソールを開いて、文字列内の式展開(4.2.1)でシンボルを呼び出してみましょう。例えば"#{:success}"といったコードを実行すると、どんな値が返ってきますか? 確認してみてください。
(解答)

irb(main):002:0> "#{:success}"
=> "success"

②上の演習で試した結果を参考に、リスト 7.28のflashがどのような結果になるか考えてみてください。
(解答) 値のみが返ってくる。

7.4.3

飛ばします。

7.4.4

①7.4.2で実装したflashに対するテストを書いてみてください。どのくらい細かくテストするかはお任せします。リスト 7.32に最小限のテンプレートを用意しておいたので、参考にしてください((コードを書き込む)の部分を適切なコードに置き換えると完成します)17 。
(解答) flashがemptyで無いことを確認。

[test/integration/users_signup_test.rb]
  #有効なサインアップ
  test "valid signup information" do
    assert_difference 'User.count', 1 do
      post users_path, params: { user: { name:  "Example User",
                                         email: "user@example.com",
                                         password:              "password",
                                         password_confirmation: "password" } }
    end
    follow_redirect!                      #createアクションのredirect_to @userへ移動。
    assert_template 'users/show'          #リダイレクト先のusers/showページ。
    assert_not flash.empty?               #flashが表示されていることを確認。
  end

②本文中でも指摘したように、flash用のHTML(リスト 7.29)は読みにくくなってしまっています。これを、より読みやすくしたリスト 7.33のコードに変更してみましょう。変更が終わったらテストスイートを実行し、正常に動作することを確認してください。なお、このコードでは、Railsのcontent_tagというヘルパーを使っています。
(解答) 正しく動く。

③リスト 7.26のリダイレクトの行をコメントアウトすると、テストが失敗することを確認してみましょう。
(解答) リダイレクトされないエラー。

ERROR UsersSignupTest#test_valid_signup_information (1.55s)
Minitest::UnexpectedError:         RuntimeError: not a redirect! 204 No Content
            test/integration/users_signup_test.rb:29:in `block in <class:UsersSignupTest>'

④リスト 7.26で、@user.saveの部分をfalseに置き換えたとしましょう(バグを埋め込んでしまったと仮定してください)。このとき、assert_differenceのテストではどのようにしてこのバグを検知するでしょうか? テストコードを追って考えてみてください。
(解答) assert_differenceの予期していた値と異なるといったエラーが発生。

 FAIL UsersSignupTest#test_valid_signup_information (1.54s)
        "User.count" didn't change by 1.
        Expected: 1
          Actual: 0
        test/integration/users_signup_test.rb:23:in `block in <class:UsersSignupTest>'

 FAIL UsersSignupTest#test_invalid_signup_information (1.58s)
        Expected at least 1 element matching "div#error_explanation", found 0..
        Expected 0 to be >= 1.
        test/integration/users_signup_test.rb:16:in `block in <class:UsersSignupTest>'

7.5.4

①ブラウザから本番環境にアクセスし、TLSの鍵マークがかかっているか、URLがhttpsになっているかどうかを確認してみましょう。
(解答) 鍵マークが付いてTLS設定が有効化されている。

②本番環境でユーザーを作成してみましょう。Gravatarの画像は正しく表示されているでしょうか?
(解答)飛ばします。

第7章ーまとめー

  • paramsハッシュはネストされたハッシュが複数あり、複数の属性を同時に更新できるマスアサインメント機能を有する。
  • マスアサインメントはセキュリティ上のリスクもあるため、Strong Parametersで特性の属性のみ更新可能とする。
  • form_withでActiveRecordに対応したフォームを生成可能。
  • Railsにおけるパスの指定は通常相対パスで示し、リダイレクトの際のみ絶対パスで示す。パスの引数がモデルオブジェクトの場合はidであることを明示的に示す必要は無く省略できる。
  • flash変数はリダイレクトの直後にkeyに対応するメッセージが表示される。
  • 本番環境ではセキュアな環境とするために、TLSを導入する。

第7章終わり🐻

Ruby on Railsチュートリアル第6章

Railsチュートリアルの第6章を進めていきます。

第6章(ユーザーのモデルを作成する)

Userモデル

モデルの作成

rails gでモデルを作成。モデルは単数形。db:migrateも実行。

$ rails generate model User name:string email:string
$ rails db:migrate

モデル生成の際には同時にマイグレーションファイルも生成される。
create_tableメソッドを呼び、テーブル名はデータの集まりなので複数形のusers。

class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
カラム名 データ型
id integer
name string
email string
created_at datetime
updated_at datetime
Active Recordの操作(作成、保存、更新、削除)
  • User.newで新規ユーザーオブジェクトをメモリ上に作成。Active Recordの設計で、引数無しの場合はnil
  • validメソッドで有効性の検証が可能。有効かどうかなだけで、DBへ保存されていることを確認する訳ではない。
  • saveメソッドでデータベースへ保存。この際にモデルに対応するid属性とtimestampも変更される。戻り値は真偽値。
  • createnewsaveを兼ねたメソッド。戻り値は真偽値ではなく、オブジェクト自身を返す。
  • createの逆はdestory。削除されたオブジェクトはメモリ上には残っている。
  • saveの前であれば、データベースの情報を元にreloadでオブジェクトを再読み込み可能。
  • 更新と保存を一括でおこなう操作がupdate。更にupdate_attributeは特定の属性のみであればバリデーションにかからず更新が可能。
Active Recordの操作(検索)

検索で使用される、find、find_by、whereの違いについてまとめておきます。

  • User.findは引数に指定したidのユーザーを探す。idが分かっている場合に使用。該当するidのユーザーがいない場合は例外ActiveRecord::RecordNotFoundが発生する。
  • User.find_byは引数に単一の属性を指定してユーザーを検索する。idもしくはid以外が分かっている場合(メールアドレスなど)で使用。返ってくる結果は最初の1件のみ。該当するユーザーがいない場合はnilが返ってくる。
  • whereは引数に一つ以上の属性を指定してユーザーを検索する。また、該当する全ての結果。また、find_byと微妙に違う点として戻り値の形式が異なっており、whereの結果に対して追加でクエリ出来る。

ユーザーの検証

有効性の検証

valid?メソッドを使用。

[test/models/user_test.rb]
require "test_helper"

class UserTest < ActiveSupport::TestCase

  #@userを使えるようにsetupメソッドを実行
  def setup
    @user = User.new(name: "Example User", email: "user@example.com")
  end

  #ユーザーの有効性を検証
  test "should be valid" do
    assert @user.valid?
  end
end
存在性の検証

名前が存在していることを検証するためのテストを先に書く。

[test/models/user_test.rb]
  #ユーザーの存在性を検証
  test "name should be present" do
    @user.name = '   '
    assert_not @user.valid?
  end

ユーザーモデルにnameの存在性バリデーションを追加。
validates :name, presence: trueの書き方は、validatesメソッドに対して属性として:name、オプションとしてpresence: trueを渡している。presence: trueはハッシュであるが、最後の引数のため{}は省略が可能。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name, presence: true
end

emailの存在性検証も全く同じ。

長さの検証

名前は50文字、メールアドレスはStringの上限の255文字としてバリデーションを設定。
まずはテストを記述する。

[test/models/user_test.rb]
  #ユーザー名の長さを検証
  test "name length" do
    @user.name = 'a' * 51
    assert_not @user.valid?
  end

  #メールアドレスの長さを検証
  test "email length" do
    @user.email = 'a' * 256
    assert_not @user.valid?
  end

モデルに長さのバリデーションを追加。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name, presence: true, length: { maximum: 50 }
  validates :email, presence: true, length: { maximum: 255 }
end
ハッシュの省略について

ここの書き方がだいぶ混乱したのでまとめておきます。
ハッシュには、最後の引数である場合はハッシュの{}を省略可能という法則があるが、validates :name, presence: true, length: { maximum: 50 }は、第1引数が:name、第2引数がpresence: true、第3引数がlength: { maximum: 50 }となるため、第2引数のpresence: trueには{}が必要なのでは無いかと思いました。
ただ、よく考えるとvalidates( :name, {presence: true, length: { maximum: 50 }}という記述になるので、第1引数は:nameの属性で、第2引数がオプションハッシュの{presence: true, length: { maximum: 50 }}になるということですね。(多分)
この記述は4章のstylesheet_link_tag "application", "data-turbo-track": "reload"の書き方と全く同じとなりますね。特にこれまで気にせず読んでいませんでしたが、改めて考えるとrubyの省略記法には慣れが必要そうです。

フォーマットの検証

まずはメールアドレスに対するテストを記述。

[test/models/user_test.rb]
  #メールアドレスのフォーマット検証、正しいメールアドレス
  test "email validation should accept valid addresses" do
    valid_addresses = %w[user@example.com USER@foo.COM A_US-ER@foo.bar.org
                         first.last@foo.jp alice+bob@baz.cn]
    valid_addresses.each do |valid_address|
      @user.email = valid_address
      assert @user.valid?, "#{valid_address.inspect} should be valid"
    end
  end

  #メールアドレスのフォーマット検証、不正なメールアドレス
  test "email validation should reject invalid addresses" do
    invalid_addresses = %w[user@example,com user_at_foo.org user.name@example.
                           foo@bar_baz.com foo@bar+baz.com]
    invalid_addresses.each do |invalid_address|
      @user.email = invalid_address
      assert_not @user.valid?, "#{invalid_address.inspect} should be invalid"
    end
  end

メールフォーマットに対するバリデーションを追加。正規表現を使用。VALID_EMAIL_REGEXは定数。
正規表現を試せるwebサイトのRubular(https://rubular.com/)を使用するとよい。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX }
end
一意性の検証

メールアドレスの一意性検証に関する前提
一意性をテストする際は、メモリ上だけでなく、実際にデータベースへレコードを登録している必要がある。そのためメソッドにはsaveupdateを加える。 メールアドレスは大文字と小文字は区別せず、全て小文字として扱うことが通例。

メールアドレスの重複についてテスト。dupメソッドではオブジェクトのコピーを作成可能。

  test "email addresses should be unique" do
    duplicate_user = @user.dup    #dupメソッドでコピーを作成。
    @user.save                    #@userをDBへ保存。
    assert_not duplicate_user.valid?
  end

モデルに一意性のバリデーションを追加。uniqueness: { case_sensitive: false }とすることで、大文字と小文字を区別しない検証が可能。

[app/models/user.rb]
class User < ApplicationRecord
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end
データベースのレベルでは一意性を保証していない問題

モデルに一意性のバリデーションを追加した場合、基本的には重複したメールアドレスはバリデーションで弾かれるが、同時に複数のリクエストが来た場合に、複数のリクエストが同時に同じ値を返してしまいバリデーションエラーとならずに重複してデータベースに保存される可能性がある。
emailカラムにインデックス(index)を追加し、そのインデックスに一意性の制約を付けるだけで解決可能。
更にindexは索引機能のようなもので、データベースに対する全表スキャンの対策にもなる。

マイグレーションファイルを生成(rails g migration)し、add_indexメソッドでユーザーのemailに対して一意性を制約する。

[db/migrate/[timestamp]_add_index_to_users_email.rb]
class AddIndexToUsersEmail < ActiveRecord::Migration[7.0]
  def change
    add_index :users, :email, unique: true
  end
end

これまでのテストの時点では、データベース間での一意性検証は無かったためテストパスしていたが、テスト用データfixtureを編集しておく。
また、実質的にデータベースレベルでメールアドレスの一意性を保つためには、そもそもメールアドレスは全て小文字として扱う方が理にかなっている。そのため、before_savedowncase化しておく。

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end

セキュアなパスワード

ユーザーの認証は、パスワードの送信→ハッシュ化→データベース内のハッシュ化された値との比較、という手順でおこなわれる。
ハッシュ化は、暗号化とが類似しているが明確に異なる。暗号化は元に戻す複合化が可能であるが、ハッシュ化は元に戻せず不可逆的な処理(∵ソルト化(ランダム)ハッシュを追加している)。

has_secure_password
  • ハッシュ化したパスワードを、password_digest属性に保存できる。
  • 2つの仮想的な属性(passwordとpassword_confirmation)が使える。(データベースカラムに存在はしていない)
  • authenticateメソッド(引数のパスワードをハッシュ化した値とpassword_digestを比較)が使える。真偽値を返し、trueであればオブジェクトも返す。 *パスワードのハッシュ化にはbcryptgemを使用する必要がある。

password_digestをカラムに追加するマイグレーションを生成する。末尾をto_usersとすることでテーブルが認識出来る。

$ rails generate migration add_password_digest_to_users password_digest:string

モデルにhas_secure_passwordを追加する。

[app/models/user.rb]
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name, presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
  has_secure_password
end

has_secure_passwordの仮想的なデータ属性であるpasswordとpassword_confirmationをtest userに追加しておく。

[test/models/user_test.rb]
  def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end
パスワードの最小文字数設定

空で無いことと、6文字以上であることをテスト。ここの空白はただの空パスワードでは無く、空文字が数文字分ある。(has_secure_password自体が完全な空文字に対する存在性のバリデーションが付いている。)

 #パスワードが空白でないことを検証
  test "password should be present (nonblank)" do
    @user.password = @user.password_confirmation = " " * 6
    assert_not @user.valid?
  end

  #パスワードが空6文字以上であることを検証
  test "password should have a minimum length" do
    @user.password = @user.password_confirmation = "a" * 5
    assert_not @user.valid?
  end

emailのときと同様に、存在性と文字数のバリデーションをモデルに追加。

[app/models/user.rb]
validates :password, presence: true, length: { minimum: 6 }

第6章ー演習ー

6.1.1

Railsはdb/ディレクトリの中にあるschema.rbというファイルを使っています。これはスキーマ(schema)と呼ばれるデータベースの構造を追跡するために使われます。そこで、自分の環境にあるdb/schema.rbの内容を調べ、その内容とマイグレーションファイル(リスト 6.2)の内容を比べてみてください。 (解答) マイグレーションファイルの中身と対応している。force: :cascadeの部分は既に存在するテーブルがあった場合は、それを削除して再度テーブル生成するオプション。

[db/schema.rb]
ActiveRecord::Schema[7.0].define(version: 2023_04_14_080953) do
  create_table "users", force: :cascade do |t|
    t.string "name"
    t.string "email"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

end

②ほぼすべてのマイグレーションは、元に戻すことが可能です(少なくとも本チュートリアルにおいてはすべてのマイグレーションを元に戻すことができます)。元に戻すことを「ロールバック(rollback)と呼び、Railsではdb:rollbackというコマンドで実現できます。 $ rails db:rollback 上のコマンドを実行後、db/schema.rbの内容を調べてみて、ロールバックが成功したかどうか確認してみてください(コラム 3.1ではマイグレーションに関する他のテクニックもまとめているので、参考にしてみてください)。なお上のコマンドでは、データベースからusersテーブルを削除するためにdrop_tableコマンドを内部で呼び出しています。これがうまくいくのは、drop_tableとcreate_tableがそれぞれ対応していることをchangeメソッドが知っているからです。この対応関係を知っているため、ロールバック用の逆方向のマイグレーションを簡単に実現することができるのです。なお、あるカラムを削除するような不可逆なマイグレーションの場合は、changeメソッドの代わりに、upとdownのメソッドを別々に定義する必要があります。詳細については、Railsガイドの「Active Record マイグレーション」を参照してください。 (解答) 確かにロールバックされた。

[db/schema.rb]
ActiveRecord::Schema[7.0].define(version: 0) do
end

③もう一度rails db:migrateコマンドを実行し、db/schema.rbの内容が元に戻ったことを確認してください。 (解答) 再度schemaの内容が戻った。

6.1.2

Railsコンソールを開き、User.newでUserクラスのオブジェクトが生成されること、そしてそのオブジェクトがApplicationRecordを継承していることを確認してみてください。(ヒント: 4.4.4で紹介したテクニックを使ってみてください。)

②同様の方法で、ApplicationRecordがActiveRecord::Baseを継承していることも確認してみてください。 (解答) ①と②の解答

irb(main):001:0> user = User.new
=> #<User:0x00007f014e8145d8 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> user.class
=> User(id: integer, name: string, email: string, created_at: datetime, updated_at: datetime)
irb(main):003:0> user.class.superclass
=> ApplicationRecord(abstract)
irb(main):004:0> user.class.superclass.superclass
=> ActiveRecord::Base

6.1.3

①user.nameとuser.emailが、どちらもStringクラスのインスタンスであることを確認してみてください。 (解答)

irb(main):005:0> user.name.class
=> String
irb(main):007:0> user.email.class
=> String

②created_atとupdated_atは、どのクラスのインスタンスでしょうか? (解答) ActiveSupport::TimeWithZone

irb(main):008:0> user.created_at.class
=> ActiveSupport::TimeWithZone
irb(main):009:0> user.updated_at.class
=> ActiveSupport::TimeWithZone

6.1.4

①nameを使ってユーザーオブジェクトを検索してみてください。また、find_by_nameメソッドが使えることも確認してみてください(これはfind_byの古い書き方で、古いRailsアプリケーションでよく見かけられます)。 (解答)

irb(main):012:0> User.find_by(name: 'Taro Yamda')
  User Load (0.5ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Taro Yamda"], ["LIMIT", 1]]                                  
=> nil                                                          
irb(main):013:0> User.find_by_name('Taro Yamda')
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ?  [["name", "Taro Yamda"], ["LIMIT", 1]]                                  
=> nil         

②実用的な目的のため、User.allはまるで配列のように扱うことができますが、実際には配列ではありません。User.allで生成されるオブジェクトを調べ、ArrayクラスではなくUser::ActiveRecord_Relationクラスであることを確認してみてください。 (解答)

irb(main):015:0> User.all.class
=> User::ActiveRecord_Relation

③User.allに対してlengthメソッドを呼び出すと、その長さ(データの件数)を求められることを確認してみてください(4.2.2)。なおRubyには、そのクラスを詳しく知らなくてもオブジェクトをどう扱えば良いか何となく見当がつく、という特徴があります。これはダックタイピング(duck typing)と呼ばれ、よく次のような格言で言い表されています「もしアヒルのような容姿で、アヒルのように鳴くのであれば、それはもうアヒルだろう」10 。 (解答) ユーザーを1件しか登録していなため、lengthは1。

irb(main):016:0> User.all.length
  User Load (0.2ms)  SELECT "users".* FROM "users"
=> 1    

6.1.5

①userオブジェクトへの代入を使ってname属性を使って更新し、saveで保存してみてください。
(解答)

irb(main):017:0> user.name = 'Ichiro Sato'
=> "Ichiro Sato"
irb(main):018:0> user
=> 
#<User:0x00007fba0cbe4858                                       
 id: 1,                                                         
 name: "Ichiro Sato",                                           
 email: "t.yamada@example.com",                                 
 created_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00,    
 updated_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00>    
irb(main):019:0> user.save
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Update (0.3ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Ichiro Sato"], ["updated_at", "2023-04-14 09:17:36.928435"], ["id", 1]]
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1        
=> true           

②今度はupdateを使って、email属性を更新および保存してみてください。
(解答)

irb(main):020:0> user.update(email: 'i.sato@example.com')
  TRANSACTION (0.2ms)  SAVEPOINT active_record_1
  User Update (0.3ms)  UPDATE "users" SET "email" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["email", "i.sato@example.com"], ["updated_at", "2023-04-14 09:18:59.317175"], ["id", 1]]
  TRANSACTION (0.2ms)  RELEASE SAVEPOINT active_record_1        
=> true                                                         
irb(main):021:0> user
=> 
#<User:0x00007fba0cbe4858                                       
 id: 1,                                                         
 name: "Ichiro Sato",                                           
 email: "i.sato@example.com",                                   
 created_at: Fri, 14 Apr 2023 08:40:55.614896000 UTC +00:00,    
 updated_at: Fri, 14 Apr 2023 09:18:59.317175000 UTC +00:00> 

③同様に、マジックカラムであるcreated_atも直接更新できることを確認してみてください。(ヒント: 1.year.agoで更新すると便利です。これはRails流の時間指定の1つで、現在の時刻から1年前の日時を算出してくれます。)
(解答)確かに1年前になった。

irb(main):022:0> user.update(created_at: 1.years.ago)
  TRANSACTION (0.1ms)  SAVEPOINT active_record_1
  User Update (0.2ms)  UPDATE "users" SET "created_at" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["created_at", "2022-04-14 09:20:09.696234"], ["updated_at", "2023-04-14 09:20:09.703575"], ["id", 1]]                                                         
  TRANSACTION (0.1ms)  RELEASE SAVEPOINT active_record_1                   
=> true                                                                    
irb(main):023:0> user
=> 
#<User:0x00007fba0cbe4858                                                  
 id: 1,                                                                    
 name: "Ichiro Sato",                                                      
 email: "i.sato@example.com",                                              
 created_at: Thu, 14 Apr 2022 09:20:09.696234000 UTC +00:00,               
 updated_at: Fri, 14 Apr 2023 09:20:09.703575000 UTC +00:00>  

6.2.1

Railsコンソールを開いて、新しく生成したuserオブジェクトが有効(valid)であることを確認してみましょう。
(解答)

irb(main):024:0> user.valid?
=> true

②6.1.3で生成したuserオブジェクトについても、有効かどうかを確認してみましょう。
(解答) 作っていないのでパス。

6.2.2

①nameもemailも空の新しいユーザーuを作成し、作成した時点では有効ではない(invalid)ことを確認してください。なぜ有効ではないのでしょうか? エラーメッセージを確認してみましょう。
(解答) nameもemailも空だとバリデーションエラー。

irb(main):001:0> u = User.new
  TRANSACTION (0.9ms)  begin transaction
=> #<User:0x00007f3b35fdf3c8 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> u.valid?
=> false
irb(main):003:0> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}

②u.errors.messagesを実行すると、ハッシュ形式でエラーが取得できることを確認してください。emailに関するエラー情報だけを取得したい場合、どうやって取得すれば良いでしょうか?
(解答) シンボルで指定。

irb(main):003:0> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
irb(main):004:0> u.errors.messages[:email]
=> ["can't be blank"]

6.2.3

①長すぎるname属性とemail属性を持つuserオブジェクトを生成し、有効でないことを確認してみましょう。
(解答) 有効でない。

irb(main):001:0> u = User.new
  TRANSACTION (0.1ms)  begin transaction
=> #<User:0x00007fdbca71a608 id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
irb(main):002:0> u.name = 'a' * 55
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
irb(main):003:0> u.email = 'a' * 260
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
irb(main):004:0> u.valid?
=> false

②長さに関するバリデーションが失敗した時、どんなエラーメッセージが生成されるでしょうか? 確認してみてください。
(解答) 文字数制限のエラーメッセージが生成。

irb(main):005:0> u.errors.messages
=> {:name=>["is too long (maximum is 50 characters)"], :email=>["is too long (maximum is 255 characters)"]}

6.2.4

①リスト 6.18にある有効なメールアドレスのリストと、リスト 6.19にある無効なメールアドレスのリストをRubularのYour test string:に転記してみてください。その後、リスト 6.21の正規表現をYour regular expression:に転記して、有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
(解答) 確認作業なので飛ばします。

②先ほど触れたように、リスト 6.21のメールアドレスチェックする正規表現は、foo@bar..comのようにドットが連続した無効なメールアドレスを許容してしまいます。まずは、このメールアドレスをリスト 6.19の無効なメールアドレスリストに追加し、これによってテストが失敗することを確認してください。次に、リスト 6.23で示した、少し複雑な正規表現を使ってこのテストがパスすることを確認してください。
(解答) 追加すると確かに失敗する。=有効なメールアドレスとして認識されている

 FAIL UserTest#test_email_validation_should_reject_invalid_addresses (0.06s)
        "foo@bar..com" should be invalid
        test/models/user_test.rb:55:in `block (2 levels) in <class:UserTest>'
        test/models/user_test.rb:53:in `each'
        test/models/user_test.rb:53:in `block in <class:UserTest>'

正規表現VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/iを追加するとパスした。

③foo@bar..comをRubularのメールアドレスのリストに追加し、リスト 6.23の正規表現をRubularで使ってみてください。有効なメールアドレスのみがすべてマッチし、無効なメールアドレスはすべてマッチしないことを確認してみましょう。
(解答) 確認作業なので飛ばします。

6.2.5

①リスト 6.34のように、メールアドレスを小文字で保存するテストをリスト 6.26に追加してみましょう。ちなみに追加するテストコードでは、データベースの値に合わせて更新するreloadメソッドと、値が一致しているかどうか確認するassert_equalメソッドを使っています。リスト 6.34のテストがうまく動いているか確認するために、before_saveの行をコメントアウトすると red になり、コメントアウトを解除すると green になることも確認してみましょう。
(解答) before_saveを外すと以下のように小文字化されずにテスト失敗となった。

 FAIL UserTest#test_email_addresses_should_be_saved_as_lowercase (0.07s)
        Expected: "foo@example.com"
          Actual: "Foo@ExAMPle.CoM"
        test/models/user_test.rb:71:in `block in <class:UserTest>'

②テストスイートの実行結果を確認しながら、before_saveコールバックをemail.downcase!に書き換えてみましょう(リスト 6.35)。(ヒント: メソッドの末尾に!を追加すると、email属性が直接変更されます。)
(解答) この記述でも変わらずうまくいく。

6.3.2

①この時点では、userオブジェクトに有効な名前とメールアドレスを与えても、valid?で失敗してしまうことを確認してみてください。
②なぜ失敗してしまうのでしょうか? エラーメッセージを確認してみてください。
(解答) ①、②合わせて。パスワードが未設定のため失敗する。

irb(main):001:0> user = User.new(name: 'Jiro Tanaka', email: 'j.tanaka@example.com')
  TRANSACTION (0.5ms)  begin transaction
=>                                                                     
#<User:0x00007fb861334a70                                              
...                                                                    
irb(main):002:0> user.valid?
  User Exists? (2.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "j.tanaka@example.com"], ["LIMIT", 1]]                                                
=> false                                                               
irb(main):003:0> user.errors.messages
=> {:password=>["can't be blank"]}

6.3.3

①名前とメールアドレスは有効でも、パスワードが短すぎるとuserオブジェクトが有効にならないことを確認してみましょう。
②上で失敗した時、どんなエラーメッセージになるでしょうか? 確認してみましょう。
(解答) パスワード短すぎ。

irb(main):001:0> user = User.new( name: 'Jiro Tanaka', email: 'j.ranaka@example.com', password: 'foo')
  TRANSACTION (0.1ms)  begin transaction
=>                                                                                                                
#<User:0x00007f5f50f93678                                                                                         
...                                                                                                               
irb(main):002:0> user.valid?
  User Exists? (1.2ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ?  [["email", "j.ranaka@example.com"], ["LIMIT", 1]]                                                                                           
=> false                                                                                                          
irb(main):003:0> user.errors.messages
=> {:password=>["is too short (minimum is 6 characters)"]}

6.3.4

①userオブジェクトを消去するためにコンソールを一度再起動し、本節で作ったuserオブジェクトを検索してみてください。
(解答)

irb(main):001:0> User.all
  User Load (0.2ms)  SELECT "users".* FROM "users"
=>                                                              
[#<User:0x00007fe5bbbcaae0                                      
  id: 1,                                                        
  name: "Michael Hartl",                                        
  email: "michael@example.com",                                 
  created_at: Fri, 14 Apr 2023 16:18:32.698774000 UTC +00:00,   
  updated_at: Fri, 14 Apr 2023 16:18:32.698774000 UTC +00:00,   
  password_digest: "[FILTERED]">]  

②オブジェクトを検索できたら、名前を別の文字列に置き換え、saveメソッドで更新してみてください。うまくいきませんね...、なぜうまくいかなかったのでしょうか?
(解答) パスワードが必要。

irb(main):003:0> user.name = 'Taro Yamada'
=> "Taro Yamada"
irb(main):004:0> user.save
  TRANSACTION (0.2ms)  begin transaction
  User Exists? (0.3ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ?  [["email", "michael@example.com"], ["id", 1], ["LIMIT", 1]]                
  TRANSACTION (0.1ms)  rollback transaction                            
=> false                                                               
irb(main):005:0> user.errors.messages
=> {:password=>["can't be blank", "is too short (minimum is 6 characters)"]}

③今度は6.1.5で紹介したテクニックを使って、userの名前を更新してみてください。
(解答) これならtrueで変更出来た。

irb(main):007:0> user.update_attribute(:name, 'Taro Yamada')
  TRANSACTION (0.2ms)  begin transaction
  User Update (1.8ms)  UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ?  [["name", "Taro Yamada"], ["updated_at", "2023-04-14 16:30:58.847096"], ["id", 1]] 
  TRANSACTION (13.6ms)  commit transaction                      
=> true     

第6章ーまとめー

  • ActiveRecordモデルのnew操作はあくまで新しいオブジェクトをメモリ上に作成するだけ。データベースには保存されない。
  • ActiveRecordの検索は、findfind_bywhereの3種類。
  • メールアドレスは大文字と小文字は区別せず、全て小文字として扱うのが通例。
  • ActiveRecordは通常データベースのレベルでの一意性保証はしていない。そのため、indexを付けてindexに一意性を持たせる。indexは索引機能としても役立つ。
  • has_secure_passwordで安全なパスワード(不可逆的にハッシュ化されたパスワード)が追加される。

2周目時点では曖昧理解で飛ばしていたデータベースの一意性制約などがやっとクリアになった。。。

第6章終わり🐻

Ruby on Railsチュートリアル第5章

Railsチュートリアルの第5章を進めていきます。

第5章(レイアウトを作成する)

Bootstrapやcss関連のところは、飛ばしながらコピペでいきます。

ナビゲーション

  • link_toメソッドはHTMLのaタグを生成する。
  • 第1引数としてリンクテキスト、第2引数としてURL(名前付きルーティング)を取る。
  • <li><%= link_to "Home", '#' %></li>は、<li><a href="#">Home</a></li>を返す。
image_tag
  • image_tagヘルパーは、シンボルを使用してalt属性やwidth属性を設定可能。
  • ソースには、完全なパス、もしくはapp/assets/imagesディレクトリ配下のファイルを指定可能。

例えば、以下のように記述されたERBは

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

以下のHTMLが返される。

<img alt="Rails logo" width="200px" src="/assets/rails-<long string>.svg">

パーシャル

  • ビューの重複する記述に関してはパーシャルにまとめる。
  • パーシャルはディレクトリ内で一目見ただけでわかるように、ファイル名の先頭にアンダースコア「_」を付す。
  • パーシャルの呼出しには<%= render 'layouts/header' %>のような形でrenderヘルパーで呼び出す。この際に探すファイル名はapp/views/layouts/_header.html.erbとなる。

アセットパイプライン

アセットパイプラインはJavaScriptCSSの連結圧縮するフレームワークで、重要な部分なのでまとめておきます。

レイアウトのリンク

名前付きルーティング

ERBテンプレートに記述するリンクは

<a href="/static_pages/about">About</a>

のように、素のHTMLを記述可能であるがRails流では無い。

URLは/static_pages/aboutよりも/aboutの方がよく、ERBの名前付きルーティングを使用するのが慣習。

<%= link_to "About", about_path %>
ルーティングの記述
  • root_pathはルートURL以下の文字列「'/'」を返す。
  • root_urlは完全なURLの文字列「'https://www.example.com/'」を返す。
  • 基本的にはpath書式を使用し、リダイレクト時のみurl書式を使用するのが慣習。
  • これまでのget "static_pages/help"のような記述は、get "/help", to: "static_pages#help"とする。これによって、/helpページをGETリクエストされた際に、helpアクションを呼び出す。更に名前付きルーティングも使用できるようになる。

統合テスト

統合テストに入ります。ここでは、上記のルーティングのテストをおこないます。
まずは、テストテンプレートを生成。

$ rails generate integration_test site_layout

テストの手順の通りに書いてみる。 1. ルートURL(Homeページ)にGETリクエストを送る。
2. 正しいページテンプレートが描画されているかどうか確かめる。
3. Home、Help、About、Contactの各ページへのリンクが正しく動くか確かめる。

[test/integration/site_layout_test.rb]
require "test_helper"

class SiteLayoutTest < ActionDispatch::IntegrationTest

  test "layout links" do
    get root_path                         #ルートURLへアクセス。
    assert_template 'static_pages/home'   #指定のレイアウトが表示されるか
    assert_select "a[href=?]", root_path, count: 2  #各種リンクが表示されるか
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
  end
end

ユーザー登録の実装

rails generateでコントローラを作成する。ユーザー登録はRESTに従いnewアクションを指定。

$ rails generate controller Users new

後は、サインアップ用にルーティングの設定、サインアップ用ボタンへのリンク追加、サインアップページを作成したら最初のステップは完了。

開発環境が動かなくなったとき

  1. 以下のコマンドを実行する。
$ bundle exec rails app:update:bin
  1. git statusでファイルに変更が生じているかどうかを確認。
  2. 変更が生じていたら、git commit -am "Update bin files"を実行。
  3. 再度git push

これで復活出来ることがあるらしいです。

第5章ー演習ー

5.1.1

飛ばします。

5.1.2

飛ばします。

5.1.3

Railsがデフォルトで生成するheadタグの部分を、リスト 5.17のようにrenderに置き換えてみてください。(ヒント: 単純に削除してしまうと後でパーシャルを1から書き直す必要が出てくるので、削除する前にどこかに退避しておきましょう。)
(解答)

[app/views/layouts/application.html.erb]
<!DOCTYPE html>
<html>
  <head>
    <title><%= full_title(yield(:title)) %></title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <%= render 'layouts/rails_default' %>
  </head>
  <body>
    <%= render 'layouts/header' %>
    <div class="container">
      <%= yield %>
      <%= render 'layouts/footer' %>
    </div>
  </body>
</html>

②リスト 5.17のようなパーシャルはまだ作っていないので、現時点ではテストは red になっているはずです。実際にテストを実行して確認してみましょう。
(解答) テストREDになる。

③layoutsディレクトリにheadタグ用のパーシャルを作成し、先ほど退避しておいたコードを書き込み、最後にテストが green に戻ることを確認しましょう。
(解答) パーシャルを追加したらGREENになった。

[app/views/layouts/_rails_default.html.erb]
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>

5.2.2

飛ばします。

5.3.2

確認だけなので飛ばします。

5.3.3

確認だけなので飛ばします。

5.3.4

①footerパーシャルのabout_pathをcontact_pathに変更してみて、テストが正しくエラーを捕まえてくれるかどうか確認してみてください。
(解答) ちゃんとaboutリンクが無いとエラーが出た。

 FAIL SiteLayoutTest#test_ルートURLのリンクテスト (1.78s)
        Expected at least 1 element matching "a[href="/about"]", found 0..
        Expected 0 to be >= 1.
        test/integration/site_layout_test.rb:10:in `block in <class:SiteLayoutTest>'

②リスト 5.34で示すように、Applicationヘルパーで使っているfull_titleヘルパーを、test環境でも使えるようにすると便利です。こうしておくと、リスト 5.35のようなコードを使って、正しいタイトルをテストすることができます。ただし、これは完璧なテストではありません。例えばベースタイトルに「Ruby on Rails Tutoial」といった誤字があったとしても、このテストでは発見することができないでしょう。この問題を解決するためには、full_titleヘルパーに対するテストを書く必要があります。そこで、Applicationヘルパーをテストするファイルを作成し、リスト 5.36の(コードを書き込む)の部分を適切なコードに置き換えてみてください。(ヒント: リスト 5.36ではassert_equal <期待される値>, <実際の値>といった形で使っていましたが、内部では==演算子で期待される値と実際の値を比較し、正しいかどうかのテストをしています。)
(解答) タイトルヘルパーが正しいことの単体テスト

[test/helpers/application_helper_test.rb]
require "test_helper"

class ApplicationHelperTest < ActionView::TestCase
  test "full title helper" do
    assert_equal 'Ruby on Rails Tutorial Sample App', full_title
    assert_equal 'Help | Ruby on Rails Tutorial Sample App', full_title("Help")
  end
end

5.4.1

①表 5.1を参考にしながらリスト 5.40を変更し、users_new_urlではなくsignup_pathを使えるようにしてみてください。
(解答)

[test/controllers/users_controller_test.rb]
class UsersControllerTest < ActionDispatch::IntegrationTest
  test "should get new" do
    get signup_path
    assert_response :success
  end
end

②先ほどの変更を加えたことにより、テストが red になったことを確認してください。なお、この演習はテスト駆動開発(コラム 3.3)で説明した red / green のリズムを作ることを目的としています。このテストは次の5.4.2で green になるよう修正します。
(解答) nameエラーとなった。

ERROR UsersControllerTest#test_should_get_new (0.15s)
Minitest::UnexpectedError:         NameError: undefined local variable or method `signup_path' for #<UsersControllerTest:0x00007f9df09e5af8>
            test/controllers/users_controller_test.rb:5:in `block in <class:UsersControllerTest>'

5.4.2

①もしまだ5.4.1.1の演習に取り掛かっていなければ、まずはリスト 5.40のように変更し、名前付きルーティングsignup_pathを使えるようにしてください。また、リスト 5.42で名前付きルーティングが使えるようになったので、現時点でテストが green になっていることを確認してください。
(解答) GREENになっている。

②先ほどのテストが正しく動いていることを確認するため、signupルートの部分をコメントアウトし、テスト red になることを確認してください。確認できたら、コメントアウトを解除して green の状態に戻してください。
(解答) ルーティングをコメントアウトしたら以下のエラー。解除したらGREEN。

ERROR UsersControllerTest#test_should_get_new (0.13s)
Minitest::UnexpectedError:         NameError: undefined local variable or method `signup_path' for #<UsersControllerTest:0x00007f0062b8b758>
            test/controllers/users_controller_test.rb:5:in `block in <class:UsersControllerTest>'

③リスト 5.31の統合テストにsignupページにアクセスするコードを追加してください(getメソッドを使います)。コードを追加したら実際にテストを実行し、結果が正しいことを確認してください。(ヒント: リスト 5.35で紹介したfull_titleヘルパーを使ってみてください。)
(解答)

[test/integration/site_layout_test.rb]
  test "signup path layout links" do
    get signup_path                                 #サインアップへアクセス。
    assert_template 'users/new'                     #指定のレイアウトが表示されるか
    assert_select "a[href=?]", root_path, count: 2  #各種リンクが表示されるか
    assert_select "a[href=?]", help_path
    assert_select "a[href=?]", about_path
    assert_select "a[href=?]", contact_path
    assert_select "title", full_title("Sign up")    #タイトルが正しいか
  end

第5章ーまとめー

  • ビューの重複するファイルはパーシャルにまとめ、パーシャルはパーシャルであると認識出来るようにファイル名にアンダースコアを付ける。
  • アセットパイプラインはJacaScriptやCSSを結合圧縮し、ページの表示速度を向上させる重要な機能。
  • レイアウトのリンクは名前付きルーティングを使えるような形にする。
  • 統合テストでは、複数の機能が連動するような場面で活用する。

第5章終わり🐻

Ruby on Railsチュートリアル第4章

Railsチュートリアルの第4章を進めていきます。

第4章(Rails風味のRuby

文字列、オブジェクトとメソッド

この章では文字列、オブジェクト、メソッドについての基本的な内容が記載されているのでまとめておきます。

文字列
  • ダブルコートまたはシングルコートで囲ったものは「文字列リテラルリテラル文字)」と呼ばれる。
  • #{ }で{ }内の式展開が可能となる。この場合、ダブルコート「" "」を使用する。シングルコートだと式展開されない。
  • rubyには改行やタブは特殊文字であり、\n、\t で表される。これもシングルコートだと特殊文字化されない。
  • 特殊文字を改行やタブでは無く、そのまま文字列として表現したい場合はエスケープ(\を追加)する。
オブジェクトとメソッド
  • Rubyではあらゆるものをオブジェクトとして扱う。
  • オブジェクトに渡されるメッセージを「メソッド」と呼ぶ。
  • 条件分岐のメソッドには、if-elseやelsifが使用される。
  • Rubyのメソッドには暗黙の戻り値(メソッド内で最後に評価された式の値)がある。

カスタムヘルパー

まず、ヘルパーとはRailsにあらかじめ処理をメソッド化して扱えるようにしたもの。form_withlink_toimage_tagなど。カスタムヘルパーはその名の通りユーザーカスタムのヘルパー。

ヘルパーは、サイトの全てで使用するようなものはapp/helpers/application_helper.rbへ記述する。。

特定のコントローラだけが使うヘルパーであれば、それに対応するヘルパーファイルを置く。例えばStaticPagesコントローラ用ヘルパーは、app/helpers/static_pages_helper.rbとする。

ここでは、全てのページで使用するページのタイトルを返すfull_titleヘルパーを定義。

module ApplicationHelper
  def full_title(page_title = '')
    base_title = 'Ruby on Rails Tutorial Sample App'
    if page_title.empty?
      base_title
    else
      "#{page_title} | #{base_title}"
    end
  end
end

データ構造

配列やブロック、ハッシュといったデータ構造について簡易的にまとめておきます。

配列
  • 配列の要素のアクセスする方法はいくつかあり、[ ]で要素の番号をしているする方法や、.first.lastのような指定も可能。.lastは[-1]とも表記可能。
  • 配列に要素を追加する場合は、pushメソッドもしくは<<演算子を使用する。
  • Rubyの配列は他の多くの言語と異なり、同一の配列の中に異種の方が共存できる。(例えば整数と文字列)
  • 0..9で表されるような「範囲」は、(0..9).to_aのような括弧付けで「配列化」が可能。
ブロック
  • ブロックは「メソッドに渡されるコードの塊部分」のこと。
  • ブロックは{}で囲う、もしくはdo..endで囲む。そのため以下の2つは同義。
>> (1..5).each { |i| puts 2 * i }  #{}で囲う記法

>> (1..5).each do |i|  #do..end記法
?>   puts 2 * i
>> end
  • Rubyの慣習的には、1行で完結するものは{}で記述して、複数行に渡るものはdo..endで記述する。
  • ブロック内の記述には"symbol-to-proc"で省略するのが一般的で、{ |char| char.downcase }は、(&:downcase)とシンボルを使用した記述になる。
  • testメソッドにはdo..endの記法が使用される。
  • eachメソッドはブロックを配列の各要素に対して実行する。しかし、加工するだけで新しい配列を返す訳ではない。
  • mapメソッドはブロックを配列の各要素に対して実行し、更にその戻り値から新しい配列を作成する。
ハッシュとシンボル
  • ハッシュは"キー(key)"と"値(value)"のペアを波括弧{ }で囲んで表現する手法。※ブロックとは全くの別物。
  • ハッシュはデータの集合を表現するため配列のようであるが、配列は「並び順が重要でありインデックスによって要素にアクセス出来る」ことに対し、ハッシュは「並び順は保証されずキーによって値にアクセス出来る」点で異なる。
  • ハッシュのキーには文字列では無くシンボルを使用することが一般的。
  • ハッシュのシンボル記法では、:name =>'Michael Hartl'とするよりも、後置コロンでname: 'Michael Hartl'とする方がシンプルで好まれる。但し、前置コロンの:nameはシンボルとして独立しているが、後置コロンのname:は引数を伴わないと意味が成り立たない。
  • Rubyではネストされたハッシュが多用される。
  • ハッシュに対するeachメソッドの処理では、ブロックの第1引数がkeyで、第2引数がvalueとして扱われる。

Rubyにおけるクラス

クラスの基本事項
  • メソッドがクラス自身に対して呼び出された場合のメソッドは「クラスメソッド」。
  • クラスから生成されたオブジェクトは「インスタンス」。
  • インスタンスに対して呼び出されたメソッドは「インスタンスメソッド」。
  • クラスは継承によって親クラスの性質を受け継ぐ。
  • Rubyにデフォルトで組み込まれているクラス(String、Hashなど)にも、メソッドを自由に追加出来る。但し、真に正当な理由が無い限りはよくない。例えば、blank?メソッドはObjectクラスに設定されている。
attr_accessorについて

Railsチュートリアルattr_accessorは、いきなり出てきてかなり混乱するのでまとめておきます。
インスタンス変数(@userのような)はクラス内であればどこからでもアクセス出来る変数であるが、クラスの外部からはアクセス出来ない。
そのため、参照用のメソッドを作っておくことで外部からもアクセス可能となるが、その記述が面倒なため、attr_accessorという1行だけで済むようになる。
attr_accessorで宣言した属性(attribute)とそれに対応するアクセサー(accessor)が作成され、更に取り出すメソッド(getter)と代入するメソッド(setter)も定義される。

第4章ー演習ー

4.2.1

①city変数に適当な市区町村名を、prefecture変数に適当な都道府県名を代入してください。
(解答)

irb(main):001:0> city = '渋谷'
=> "渋谷"
irb(main):002:0> prefucture = '東京'
=> "東京"

②先ほど作った変数と式展開を使って、「東京都 新宿区」のような住所の文字列を作ってみましょう。出力にはputsを使ってください。
(解答)

irb(main):003:0> puts "#{prefucture}#{city}"
東京都 渋谷区
=> nil             

③上記の文字列の間にある半角スペースをタブに置き換えてみてください。(ヒント: 改行文字と同じで、タブも特殊文字です。)
(解答)

irb(main):004:0> puts "#{prefucture}\t#{city}"
東京都  渋谷区
=> nil    

④タブに置き換えた文字列を、ダブルクォートからシングルクォートに置き換えてみるとどうなるでしょうか?
(解答) シングルコートだと式展開やエスケープされないため、そのまま出力される。

irb(main):005:0> puts '#{prefucture}都\t#{city}区'
#{prefucture}都\t#{city}区
=> nil        

4.2.2

①"racecar" の文字列の長さはいくつですか? lengthメソッドを使って調べてみてください。
(解答) 7文字

irb(main):006:0> 'racecar'.length
=> 7

②reverseメソッドを使って、"racecar"の文字列を逆から読むとどうなるか調べてみてください。
(解答) racecar

irb(main):007:0> 'racecar'.reverse
=> "racecar"

③変数sに "racecar" を代入してください。その後、比較演算子(==)を使って変数sとs.reverseの値が同じであるかどうか、調べてみてください。
(解答) 同じ

irb(main):008:0> s = 'racecar'
=> "racecar"
irb(main):009:0> s == s.reverse
=> true

④リスト 4.9を実行すると、どんな結果になるでしょうか? 変数sに "onomatopoeia" という文字列を代入するとどうなるでしょうか?(ヒント: 上矢印、またはCtrl-Pコマンドを使って以前に使ったコマンドを再利用すれば、コマンドを全部入力せずに済むので便利です。)
(解答) s == s.reverseが成立する場合は、"It's a palindrome"が出力される。

irb(main):010:0> puts "It's a palindrome!" if s == s.reverse
It's a palindrome!
=> nil                                                       
irb(main):011:0> s = 'onomatopoeia'
=> "onomatopoeia"
irb(main):012:0> puts "It's a palindrome!" if s == s.reverse
=> nil

4.2.3

①リスト 4.10の(コードを書き込む)の部分を適切なコードに置き換え、回文かどうかをチェックするメソッドを定義してみてください。(ヒント: リスト 4.9の比較方法を参考にしてください。)
(解答)s == s.reverseを書く。

irb(main):013:1* def parindrome_tester(s)
irb(main):014:2*   if s == s.reverse
irb(main):015:2*     puts "It's a palindrome!"
irb(main):016:2*   else
irb(main):017:2*     puts "It's not a palindrome!"
irb(main):018:1*   end
irb(main):019:0> end
=> :parindrome_tester

②上で定義したメソッドを使って “racecar” と “onomatopoeia” が回文かどうかを確かめてみてください。1つ目は回文である、2つ目は回文でない、という結果になれば成功です。
(解答)

irb(main):022:0> parindrome_tester('racecar')
It's a palindrome!
=> nil                                           
irb(main):023:0> parindrome_tester('onomatopoeia')
It's not a palindrome!
=> nil    

③palindrome_tester("racecar")に対してnil?メソッドを呼び出し、戻り値がnilであるかどうかを確認してみてください(つまりnil?を呼び出した結果がtrueであることを確認してください)。このメソッドチェーンは、nil?メソッドがリスト 4.10の戻り値を受け取り、その結果を返しているという意味になります。
(解答) putsの戻り値はnilのため、.nil?はtrueとなる。

irb(main):024:0> parindrome_tester('racecar').nil?
It's a palindrome!
=> true        

4.3.1

①文字列「A man, a plan, a canal, Panama」を ", " で分割して配列にし、変数aに代入してみてください。
(解答) そのまま配列化してみた。

irb(main):009:0> a = ["A man", "a plan", "a canal", "Panama"]
=> ["A man", "a plan", "a canal", "Panama"]

たぶん正解はこうではなく、splitを使用するのだろうけどスペースの入れ方が不明だったので調べてみたところ、splitに引数を指定すると特定の文字列で分割出来る。

irb(main):016:0> a = "A man,a plan,a canal,Panama".split(",")
=> ["A man", "a plan", "a canal", "Panama"]

②今度は、変数aの要素を連結した結果(文字列)を、変数sに代入してみてください。
(解答)

irb(main):017:0> s = a.join
=> "A mana plana canalPanama"

③変数sを半角スペースで分割した後、もう一度連結して文字列にしてください。(ヒント: メソッドチェーンを使うと1行でもできます。)リスト 4.10で使った回文をチェックするメソッドを使って、(現状ではまだ)変数sが回文ではないことを確認してください。downcaseメソッドを使って、s.downcaseは回文であることを確認してください。
(解答) downcaseにして、全て小文字としたら回文となった。

irb(main):020:1* def palindrome_tester(s)
irb(main):021:2*   if s == s.reverse
irb(main):022:2*     puts "It's a palindrome!"
irb(main):023:2*   else
irb(main):024:2*     puts "It's not a palindrome."
irb(main):025:1*   end
irb(main):026:0> end
=> :palindrome_tester
irb(main):027:0> palindrome_tester(s.split(' ').join)
It's not a palindrome.
=> nil                                           
irb(main):028:0> palindrome_tester(s.split(' ').join.downcase)
It's a palindrome!
=> nil      

④aからzまでの範囲オブジェクトを作成し、7番目の要素を取り出してみてください。同様にして、後ろから7番目の要素を取り出してみてください。(ヒント: 範囲オブジェクトを配列に変換するのを忘れないでください。)
(解答)前から7番目の要素は配列でいう[6]なので"g"、後ろから7番目は[-7]なので"t"。

irb(main):030:0> alphabet = ('a'..'z').to_a
=> 
["a",                                                            
...                                                              
=> 
 "b",                                                            
 "c",                                                            
 "d",                                                            
 "e",                                                            
 "f",                                                            
 "g",                                                            
 "h",                                                            
 "i",                                                            
 "j",                                                            
 "k",
 "l",
 "m",
 "n",
 "o",
 "p",
 "q",
 "r",
 "s",
 "t",
 "u",
 "v",
 "w",
 "x",
 "y",
 "z"]
irb(main):032:0> alphabet[6]
=> "g"
irb(main):033:0> alphabet[-7]
=> "t"

4.3.2

①範囲オブジェクト0..16を使って、各要素の2乗を出力してください。
(解答)

irb(main):040:0> (0..16).each{|i| puts i ** 2}
0
1                                                                 
4                                                                 
9                                                                 
16                                                                
25                                                                
36                                                                
49                                                                
64                                                                
81                                                                
100                                                               
121                                                               
144                                                               
169                                                               
196                                                               
225
256
=> 0..16

②yeller(大声で叫ぶ)というメソッドを定義してください。このメソッドは、文字列の要素で構成された配列を受け取り、各要素を連結した後、大文字にして結果を返します。例えばyeller(['o', 'l', 'd'])と実行したとき、"OLD"という結果が返ってくれば成功です。(ヒント: mapとupcaseとjoinメソッドを使ってみましょう。)
(解答)

irb(main):041:1* def yeller(s)
irb(main):042:1*   s.map(&:upcase).join
irb(main):043:0> end
=> :yeller
irb(main):044:0> yeller(['o', 'l', 'd'])
=> "OLD"

③shuffled_subdomainというメソッドを定義してください。このメソッドは、完全にシャッフルされたアルファベット8文字を文字列として返します。(ヒント: サブドメインを作るときに使ったRubyコードをメソッド化したものです。)
(解答)

irb(main):051:1* def shuffled_subdomain
irb(main):052:1*   ('a'..'z').to_a.shuffle[0..7].join
irb(main):053:0> end
=> :shuffled_subdomain
irb(main):054:0> shuffled_subdomain
=> "pmecotsx"

④リスト 4.12の「?」の部分を、それぞれ適切なメソッドに置き換えてみてください。(ヒント:split、shuffle、joinメソッドを組み合わせると、メソッドに渡された文字列(引数)をシャッフルさせることができます。)
(解答)

irb(main):066:1* def string_shuffle(s)
irb(main):067:1*   s.split('').shuffle.join
irb(main):068:0> end
=> :string_shuffle
irb(main):069:0> string_shuffle('foobar')
=> "orbofa"

4.3.3

①キーが'one'、'two'、'three'となっていて、それぞれの値が'uno'、'dos'、'tres'となっているハッシュを作ってみてください。その後、ハッシュの各要素をみて、それぞれのキーと値を"'#{key}'はスペイン語で'#{value}'"といった形で出力してみてください。
(解答)

irb(main):071:0> h = {one: 'uno', two:'dos', three:'tres'}
=> {:one=>"uno", :two=>"dos", :three=>"tres"}
irb(main):072:1* h.each do|key,value|
irb(main):073:1*   puts "#{key}'はスペイン語で'#{value}"
irb(main):074:0> end
one'はスペイン語で'uno
two'はスペイン語で'dos                                                                
three'はスペイン語で'tres                                                             
=> {:one=>"uno", :two=>"dos", :three=>"tres"}     

②person1、person2、person3という3つのハッシュを作成し、それぞれのハッシュに:firstと:lastキーを追加し、適当な値(名前など)を入力してください。その後、次のようなparamsというハッシュのハッシュを作ってみてください。1)キーparams[:father]の値にperson1を代入、2)キーparams[:mother]の値にperson2を代入、3)キーparams[:child]の値にperson3を代入。最後に、ハッシュのハッシュを調べていき、正しい値になっているか確かめてみてください。(例えばparams[:father][:first]がperson1[:first]と一致しているか確かめてみてください)
(解答) 正しい値になった。

irb(main):084:0> person1
=> {:first=>"Taro", :last=>"Yamada"}
irb(main):085:0> person2
=> {:first=>"Ichiro", :last=>"Sato"}
irb(main):086:0> person3
=> {:first=>"Jiro", :last=>"Tanaka"}

irb(main):081:0> params = {father: person1, mother: person2, child: person3}
=> 
{:father=>{:first=>"Taro", :last=>"Yamada"},                                          
...                                                                                   
irb(main):082:0> params[:father][:first]
=> "Taro"
irb(main):083:0> params[:father][:first] == person1[:first]
=> true

③userというハッシュを定義してみてください。このハッシュは3つのキー:name、:email、:password_digestを持っていて、それぞれの値にあなたの名前、あなたのメールアドレス、そして16文字からなるランダムな文字列が代入されています。
(解答) ランダム文字列は演習4.3.2と同じ。

irb(main):087:0> user = {name: 'Taro Yamada', email: 't.yamada@example.com', password_
digest: nil}
=> {:name=>"Taro Yamada", :email=>"t.yamada@example.com", :password_digest=>nil}
irb(main):088:0> user[:password_digest] = ('a'..'z').to_a.shuffle[0..15].join
=> "zoyaniwhdelprusv"
irb(main):089:0> user
=> 
{:name=>"Taro Yamada",                                                                
 :email=>"t.yamada@example.com",                                                      
 :password_digest=>"zoyaniwhdelprusv"}  

④「Ruby API」や「るりまサーチ」を使って、Hashクラスのmergeメソッドについて調べてみてください。次のコードを実行せずに、どのような結果が返ってくるか推測できますか? 推測できたら、実際にコードを実行して推測があっていたか確認してみましょう。
(解答) bの値がmergeのハッシュの値に結合される。

irb(main):090:0> { "a" => 100, "b" => 200 }.merge({ "b" => 300 })
=> {"a"=>100, "b"=>300}

発展演習①RubyのHashオブジェクトのキーには、「1」や「3」といった数字や、「'email'」や「'name'」といった文字列、そして「:email」や「:name」といったシンボルなどが使えますが、特にシンボルが多く使われています。その理由を考えてみましょう21 。
(解答) 参考サイトとchatGPTに聞いてみた内容。 * シンボルは一度生成したら不変であり、同じキーを持つハッシュが存在する場合にキーとしてのシンボルを共有出来るためメモリ効率が良くなる。 * 文字列と違ってハッシュは整数値として扱われるため高速動作する。 * コロンを付けるだけでシンボル化出来、一目でわかりやすくコードの可読性も上がる。

発展演習②先ほど考えたハッシュのキーにおける「数字 vs. 文字列 vs. シンボル」の比較を、WebブラウザからRubyのコードを実行できるirb.wasmを使って、下記のコードを実行して測定してみましょう。
(解答) ベンチマークがシンボル: 0.000279、文字列: 0.000051、整数: 0.000040で、シンボルが5-7倍高速。

irb(main):002:0>     require 'benchmark'
    symbol = { :foo => "value" } 
    string = { "foo" => "value" }
    integer = { 1 => "value" } 
    Benchmark.benchmark do |x|
      x.report("Symbol") { symbol[:foo] }
      x.report("String") { string["foo"] }
      x.report("Integer") { integer[1] }
    end
Symbol  0.000279   0.000000   0.000279 (  0.000140)
String  0.000051   0.000000   0.000051 (  0.000030)
Integer  0.000040   0.000000   0.000040 (  0.000020)
=> 
[#<Benchmark::Tms:0x2de3b5d4
  @cstime=0.0,
  @cutime=0.0,
  @label="Symbol",
  @real=0.0001399517059326172,
  @stime=0.0,
  @total=0.00027899999999903,
  @utime=0.00027899999999903>,
 #<Benchmark::Tms:0x2de3af6c
  @cstime=0.0,
  @cutime=0.0,
  @label="String",
  @real=3.0040740966796875e-05,
  @stime=0.0,
  @total=5.099999999913507e-05,
  @utime=5.099999999913507e-05>,
 #<Benchmark::Tms:0x2de3a990
  @cstime=0.0,
  @cutime=0.0,
  @label="Integer",
  @real=2.0265579223632812e-05,
  @stime=0.0,
  @total=3.999999999848569e-05,
  @utime=3.999999999848569e-05>]

4.4.1

①1から10の範囲オブジェクトを生成するリテラルコンストラクタは何でしたか?(復習です)
(解答) Range

irb(main):091:0> (1..10).class
=> Range

②今度はRangeクラスとnewメソッドを使って、1から10の範囲オブジェクトを作ってみてください。(ヒント: newメソッドに2つの引数を渡す必要があります。)
(解答)

irb(main):092:0> r = Range.new(1, 10)
=> 1..10

③比較演算子==を使って、上記2つの課題で作ったそれぞれのオブジェクトが同じであることを確認してみてください。
(解答)

irb(main):093:0> (1..10) == r
=> true

4.4.2

①Rangeクラスの継承階層を調べてみてください。同様にして、HashとSymbolクラスの継承階層も調べてみてください。
(解答)

irb(main):094:0> Range.class
=> Class
irb(main):095:0> Hash.class
=> Class
irb(main):096:0> Symbol.class
=> Class
irb(main):097:0> Range.class.superclass
=> Module
irb(main):098:0> Hash.class.superclass
=> Module
irb(main):099:0> Range.class.superclass.superclass
=> Object

②リスト 4.15にあるself.reverseのselfを省略し、reverseと書いてもうまく動くことを確認してみてください。
(解答) 確認のみなので省略

4.4.3

①palindrome?メソッドを使って、“racecar”が回文であり、“onomatopoeia”が回文でないことを確認してみてください。南インドの言葉「Malayalam」は回文でしょうか? (ヒント: downcaseメソッドで小文字にすることをお忘れなく。)
(解答)

irb(main):100:0> palindrome_tester('racecar')
It's a palindrome!
=> nil                                                     
irb(main):101:0> palindrome_tester('onomatopoeia')
It's not a palindrome.
=> nil                                                     
irb(main):102:0> palindrome_tester('Malayalam'.downcase)
It's a palindrome!
=> nil    

②リスト 4.16を参考に、Stringクラスにshuffleメソッドを追加してみてください。(ヒント: リスト 4.12も参考になります。)
(解答)

irb(main):103:1* class String
irb(main):104:2*   def shuffle
irb(main):105:2*     self.split('').shuffle.join
irb(main):106:1*   end
irb(main):107:0> end
=> :shuffle
irb(main):108:0> 'foobar'.shuffle
=> "booarf"

③リスト 4.16のコードにおいて、self.を削除してもうまく動くことを確認してください。
(解答) うまくいく。

irb(main):109:1* class String
irb(main):110:2*   def shuffle
irb(main):111:2*     split('').shuffle.join
irb(main):112:1*   end
irb(main):113:0> end
=> :shuffle
irb(main):114:0> 'foobar'.shuffle
=> "fooarb"

4.4.4

①第2章で作ったToyアプリケーションのディレクトリでRailsコンソールを開き、User.newと実行することでuserオブジェクトが生成できることを確認してみましょう。
②生成したuserオブジェクトのクラスの継承階層を調べてみてください (解答)飛ばします。

4.4.5

①Userクラスで定義されているname属性を修正して、first_name属性とlast_name属性に分割してみましょう。また、それらの属性を使って "Michael Hartl" といった文字列を返すfull_nameメソッドを定義してみてください。最後に、formatted_emailメソッドの@nameの部分を、full_nameに置き換えてみましょう(元々の結果と同じになっていれば成功です)
(解答) 変更したUserクラスメソッド。

class User
  attr_accessor :first_name, :last_name, :email

  def initialize(attributes = {})
    @first_name  = attributes[:first_name]
    @last_name  = attributes[:last_name]
    @email = attributes[:email]
  end

  def formatted_email
    "#{full_name} <#{@email}>"
  end

  def full_name
    "#{@last_name} #{first_name}"
  end
end

コンソールの結果

irb(main):001:0> require './example_user'
=> true
irb(main):002:0> example = User.new
=> #<User:0x00007fb370591bb0 @email=nil, @first_name=nil, @last_name=nil>
irb(main):003:0> example.first_name = 'Taro'
=> "Taro"
irb(main):004:0> example.last_name = 'Yamada'
=> "Yamada"
irb(main):005:0> example.email = 't.yamada@example.com'
=> "t.yamada@example.com"
irb(main):006:0> example.formatted_email
=> "Yamada Taro <t.yamada@example.com>"

②"Hartl, Michael" といったフォーマット(苗字と名前がカンマ+半角スペースで区切られている文字列)で返すalphabetical_nameメソッドを定義してみましょう。
(解答) 定義したメソッド

  def alphabetical_name
    "#{@last_name}, #{@first_name}"
  end

コンソールの結果

irb(main):005:0> example.alphabetical_name
=> "Yamada, Taro"

③full_name.splitとalphabetical_name.split(', ').reverseの結果を比較し、同じ結果になるかどうか確認してみましょう。
(解答)どっちかのメソッドのlastとfirstが逆だったためfalseになった。修正は飛ばします。

irb(main):006:0> example.full_name.split == example.alphabetical_name.split(', ').reve
rse
=> false
irb(main):007:0> example.full_name.split
=> ["Yamada", "Taro"]
irb(main):008:0> example.alphabetical_name.split(', ').reverse
=> ["Taro", "Yamada"]

第4章ーまとめー

  • Rubyでは全てをオブジェクトとして扱う。
  • ブロックのeachメソッドとmapメソッドは、戻り値から新しい配列を返すかどうかで動作が異なる。新しい配列を作るのはmapメソッド。
  • 配列は並び順がありインデックスによって値にアクセス出来る。
  • ハッシュは並び順は無く、キーによって値にアクセス出来る。
  • ハッシュのキーとしてはシンボルが好まれる。シンボルは文字列や整数に比べ利点がある。
  • Rubyはクラスの継承によって親クラスの機能を受け継ぐ。
  • attr_accessorで簡単に外部からアクセス出来るインスタンス変数を定義できる。

第4章終わり🐻

Ruby on Railsチュートリアル第3章

Railsチュートリアルの第3章を進めていきます。

第3章(ほぼ静的なページの作成)

本章からは14章まで通しで取り組むsample appを作成していきます。

静的ページ

コントローラの生成

静的ページ用のコントローラをgenerateする。※コントローラの名前はモデルと違って複数形。またキャメルケース表記で記載することで、自動的にスネークケースのコントローラが生成される。

$ rails generate controller StaticPages home help
rails generatedb:migrateの取り消し操作
  • rails generateコマンドで生成したコントローラはrails destoyで作成した一連の操作の取り消しが可能。
  • モデルについても同様に取り消しが可能。
  • rails db:migrateを一つ前に戻す操作はrails dc:rollback

最初のテスト

ここからテストが入ってきます。テスト駆動開発についてはこの後まとめておきます。
テスト駆動開発は真面目にやっておきたいので、テスト内にコメントを残してテストの内容を視覚化しておくようにします。

[test/controllers/static_pages_controller_test.rb]
require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url   #home_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
  end

  test "should get help" do
    get static_pages_help_url   #help_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
  end
end
テストのGREENとREDの色付け、Guardによる自動化

テストは GREENなのかREDなのか視覚的に分かった方がよいので色付け(minitest-reporters gem)しておきます。

[test/test_helper.rb]
ENV["RAILS_ENV"] ||= "test"
require_relative "../config/environment"
require "rails/test_help"
require "minitest/reporters"
Minitest::Reporters.use!

class ActiveSupport::TestCase
  # 指定のワーカー数でテストを並列実行する
  parallelize(workers: :number_of_processors)

  # test/fixtures/*.ymlにあるすべてのfixtureをセットアップする
  fixtures :all

  # (すべてのテストで使うその他のヘルパーメソッドは省略)
end

また、Guardによる自動化もやっておくとかなり便利でしたので今のうちに設定しておきます。

  1. gemのguard、guard-minitestを入れておく。(既にインストール済み)
  2. $ bundle exec guard initを実行。
  3. Guadfileをカスタマイズ。
  4. $ bundle exec guardを実行。
Aboutページテスト駆動開発

テストの内容は他のページと同じ。

[test/controllers/static_pages_controller_test.rb]
require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url   #home_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
  end

  test "should get help" do
    get static_pages_help_url   #help_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
  end

  test "should get about" do
    get static_pages_about_url   #about_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
  end
end

aboutページへのルーティング、アクション、ビューの全てが出来ていないので作ります。(単純なので中身は省略)

テスト駆動開発に関して

テスト駆動開発には賛否あるようで、メリット・デメリットを挙げてみました。
以下のサイトとchatGPTへの質問を参考にしました。両者の意見は概ね一致してました。
techracho.bpsinc.jp

メリット

バグの減少と早期発見
テストを起点とした開発となるため、バグの早期発見に繋がる。参考サイトでもマイクロソフトでTDDを導入したものとしてないものでは、TDDを導入したことで欠陥密度が40~90%減少したと書いています。驚くべき数値ですね。
品質向上、シンプル化
TDDのサイクルにはリファクタリングも含まれており、自身のコードの品質改善にも寄与し、メンテナンス性も高いようです。

デメリット

開発時間の増加
これも一概には言えないようですが、”導入初期段階では遅くなる可能性がある”ようです。ただ、ここで時間が増加しても最終的にはバグの早期発見や品質の向上によってデメリットとならないとも考えられます。

Railsチュートリアルでも、絶対的にどっちが良いということは名言しておらず、場合分け的に後からテストするか先にテストする(TDD)かを選択すると良いとしておりました。
基本的にはTDDを優先的に考えて、動作の仕様が固まっていない場合やHTMLの細かい部分の修正などをおこなう場合はテストを後回しにすればよいのかなと考えています。

少しだけ動的なページ

ここではページのタイトルが表示ページに応じて動的に変わるように作成。

テストの記述

ここでは、assert_selectメソッドを使用。このメソッドの詳細はRailsガイドに記載。 Rails テスティングガイド - Railsガイド

[test/controllers/static_pages_controller_test.rb] 
require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url   #home_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "Home | Ruby on Rails Tutorial Sample App" #タイトルタグを検証。
  end

  test "should get help" do
    get static_pages_help_url   #help_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App" #タイトルタグを検証。
  end

  test "should get about" do
    get static_pages_about_url   #about_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "About | Ruby on Rails Tutorial Sample App" #タイトルタグを検証。
  end
end

このテストに合うように、各viewを編集。Homeのみ記載。

[app/views/static_pages/home.html.erb]
<!DOCTYPE html>
<html>
  <head>
    <title>Home | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Sample App</h1>
    <p>
      This is the home page for the
      <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
      sample application.
    </p>
  </body>
</html>

リファクタリング

自力でリファクタリングをやっていく。
ここでは、home、help、aboutページが同じ形になるようにERBを使用して記述を変え、最終的にlayputs/application.html.erbにまとめる。 まず、各ビューはprovideメソッド+yieldメソッドの関係を用いる。

[app/views/static_pages/home.html.erb]
<% provide(:title,'Home') %>
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
  </head>
  <body>
    <h1>Help</h1>
    <p>
      Get help on the Ruby on Rails Tutorial at the
      <a href="https://railstutorial.jp/help">Rails Tutorial help
      page</a>.
      To get help on this sample app, see the
      <a href="https://railstutorial.jp/#ebook">
      <em>Ruby on Rails Tutorial</em> book</a>.
    </p>
  </body>
</html>

layputs/application.html.erbにまとめる。

[app/views/layouts/application.html.erb]
<!DOCTYPE html>
<html>
  <head>
    <title><%= yield(:title) %> | Ruby on Rails Tutorial Sample App</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
    <%= javascript_importmap_tags %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

各ページも必要最低限な要素のみにリファクタリング
※タグも必要無かったようです。タグの内側をレイアウトの部分と認識してyieldで表示されるのかと思いましたが、そうでは無く、レイアウトのyield部分にビューの内容全てが挿入されるようです。

[app/views/static_pages/home.html.erb]
<% provide(:title,'Home') %>
<body>
  <h1>Sample App</h1>
  <p>
    This is the home page for the
    <a href="https://railstutorial.jp/">Ruby on Rails Tutorial</a>
    sample application.
  </p>
</body>

一応これで、テストはGREENで、各ページにアクセスした際のタイトルも変わってたのでok。
正解の方のレイアウトには<meta charset="utf-8">も入っていた。

また、csp_meta_tagではクロスサイトスクリプティング攻撃対策用に、コンテンツセキュリティポリシーを実装。
csrf_meta_tagsクロスサイトリクエストフォージェリー攻撃対策用。

第3章ー演習ー

3.2.1

①Fooというコントローラを生成し、その中にbarとbazアクションを追加してみてください。
(解答)
rails g controllerでFooコントローラを作成。

$ rails g controller Foo bar baz
      create  app/controllers/foo_controller.rb
       route  get 'foo/bar'
              get 'foo/baz'
      invoke  erb
      create    app/views/foo
      create    app/views/foo/bar.html.erb
      create    app/views/foo/baz.html.erb
      invoke  test_unit
      create    test/controllers/foo_controller_test.rb
      invoke  helper
      create    app/helpers/foo_helper.rb
      invoke    test_unit

②コラム 3.1で紹介したテクニックを駆使して、Fooコントローラとそれに関連するアクションを削除してみてください。
(解答)
rails destroy controllerでFooコントローラを削除。

$ rails destroy controller Foo bar baz
      remove  app/controllers/foo_controller.rb
       route  get 'foo/bar'
              get 'foo/baz'
      invoke  erb
      remove    app/views/foo
      remove    app/views/foo/bar.html.erb
      remove    app/views/foo/baz.html.erb
      invoke  test_unit
      remove    test/controllers/foo_controller_test.rb
      invoke  helper
      remove    app/helpers/foo_helper.rb
      invoke    test_unit

3.4.2

①StaticPagesコントローラのテスト(リスト 3.26)には、いくつか繰り返しがあったことにお気づきでしょうか? 特に「Ruby on Rails Tutorial Sample App」という基本タイトルは、各テストで毎回同じ内容を書いてしまっています。そこで、setupという特別なメソッド(各テストが実行される直前で実行されるメソッド)を使って、この問題を解決したいと思います。まずは、リスト 3.32のテストが green になることを確認してみてください(リスト 3.32では、2.2.2で少し触れたインスタンス変数や文字列の式展開というテクニックを使っています。それぞれ4.4.5と4.2.1で詳しく解説するので、今はわからなくても問題ありません)
(解答)
assert_selectで評価するタイトルタグの記述が重複している。Ruby on Rails Tutorial部分はsetupメソッドでまとめ、式展開で記述。

[test/controllers/static_pages_controller_test.rb]
require "test_helper"

class StaticPagesControllerTest < ActionDispatch::IntegrationTest

  def setup
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  test "should get home" do
    get static_pages_home_url   #home_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "Home | #{@base_title}" #タイトルタグを検証。
  end

  test "should get help" do
    get static_pages_help_url   #help_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "Help | #{@base_title}" #タイトルタグを検証。
  end

  test "should get about" do
    get static_pages_about_url   #about_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認。
    assert_select "title", "About | #{@base_title}" #タイトルタグを検証。
  end
end

3.4.3

①サンプルアプリケーションにContact(問い合わせ先)ページを作成してください17 。(ヒント: まずはリスト 3.17を参考にして、/static_pages/contactというURLのページに「Contact | Ruby on Rails Tutorial Sample App」というタイトルが存在するかどうかを確認するテストを最初に作成しましょう。次に、3.3.3でAboutページを作ったときのと同じように、Contactページにもリスト 3.42のコンテンツを表示してみましょう。)
(解答) 同じ内容なので飛ばします。

3.4.4

①リスト 3.43にrootルーティングを追加したことで、root_urlというRailsヘルパーが使えるようになりました(以前、static_pages_home_urlが使えるようになったときと同じです)。リスト 3.44の(コードを書き込む)と記された部分を置き換えて、rootルーティングのテストを書いてみてください。
(解答)

  test "should get root" do
    get root_url                #root_urlへアクセス。
    assert_response :success    #HTTPステータスコード200を確認
  end

②実はリスト 3.43のコードを書いていたので、先ほどの課題のテストは既に green になっているはずです。このような場合、テストを変更する前から成功していたのか、変更した後に成功するようになったのかを判断するのが難しくなります。リスト 3.43のコードがテスト結果に影響を与えていることを確認するため、リスト 3.45のようにrootルーティングを試しにコメントアウトし、 red になるかどうか確かめてみましょう(なおRubyのコメント機能については4.2で説明します)。最後に、コメントアウトした箇所を元に戻し(すなわちリスト 3.43に戻し)、テストが green になることを確認してみましょう。
(解答) rootルーティングをコメントアウトしたらテスト失敗になった。

第3章ーまとめー

  • コントローラをgenerateするときは、複数形+キャメルケースで記述。一般にファイル名はスネークケースで記載される。
  • rails generateで作成したコントローラやモデルは、付随して作成されたファイルも含めてdestroyコマンドでまとめて削除可能。DBのマイグレーションロールバック可能。
  • テスト駆動開発は、バグの発見やコードリファクタリング、メンテナンス性といった観点で非常に優れる手法。後からテスト手法も組み合わせて効率的に進める。
  • コードのリファクタリングはシンプル化、整頓の観点から非常に大事。
  • テストはGuardで自動化が可能。色付けしておくと、成功したか失敗したかが一目でわかる。

第3章終わり🐻

Ruby on Railsチュートリアル第2章

Railsチュートリアルの第2章を進めていきます。

第2章(Toyアプリケーション)

第2章はscaffoldジェネレータで簡単なアプリを作成し、railsのおおまかな機能を学習するといった感じです。

序盤のrails newgit pushの内容は第1章と重複するので特に書きません。

UserモデルとMicropostモデル

ここでは、まずtoy_app用のモデル設計を考える。
個々のユーザーを表すUserモデルと、投稿するMicropostモデルの2つを扱う。

ユーザーモデルの設計

ここではユーザー(users)のデータベースが持つ情報はid、name、emailの3つのみ。

カラム名 データ型
id integer
name string
email string
マイクロポストモデルの設計

マイクロポスト(micrioists)のデータベースが持つ情報はid、content、user_idの3つ。contentはstring型だと255文字が限度なため、textが好ましい。
このuser_idはusersと関連付け(belongs_to)のために必要。

カラム名 データ型
id integer
content text
user_id integer

scaffoldジェネレータ

Railsにおけるscaffoldコマンドの機能は、アプリケーションの基本的な機能を一括生成してくれるコマンド。

rails g scaffoldを実行します。※モデルの命名規則は"単数形"のため、Userとする。
DBのマイグレーションも実行しておく。

$ rails g scaffold User name:string email:string
 $ rails db:migrate

最初scaffoldコマンドを使用した際は、これがどれだけ便利なのかはよく分かりませんでしたが、14章まで進めて一度実装を終えると、scaffoldの手軽さが実感出来ます。

Usersリソース

ここではscaffoldで既にほとんど記述されているので、MVCの動きを押さえておきます。

ルーティング

ルーティングのrootURLをusersコントローラのindexアクションへ紐づけられる。

[config/routes.rb]
Rails.application.routes.draw do
  resources :users
  root 'users#index'
end
コントローラ

usersコントローラのindexアクションは、User.allを@usersへ代入するメソッド。

[app/controllers/users_controller.rb]
class UsersController < ApplicationController
  .
  .
  .
  def index
    @users = User.all
  end
  .
  .
  .
end
モデル

Userクラスについても以下のようにモデルで定義し、ApplicationRecordを継承しているためindexアクションのUser.allに対してDB上のユーザーを取り出すことが可能。

[app/models/user.rb]
class User < ApplicationRecord
end
ビュー

コントローラのindexアクションに対応するビューのindex.html.erbをrailsが探し、HTMLとしてブラウザに表示する。
ここでは、@usersに代入されたユーザー一覧を一人ずつ出力(_userパーシャルでnameとemailを表示する内容が記述)する。

[app/views/users/index.html.erb]
<p style="color: green"><%= notice %></p>

<h1>Users</h1>

<div id="users">
  <% @users.each do |user| %>
    <%= render user %>
    <p>
      <%= link_to "Show this user", user %>
    </p>
  <% end %>
</div>

<%= link_to "New user", new_user_path %>

Micropstリソース

マイクロポストの生成

Micropostモデルについてもrails g scaffoldを実行。※モデルの命名規則は"単数形"のため、Micropostとする。
マイグレーションも実行。

$ rails generate scaffold Micropost content:text user_id:integer
$ rails db:migrate
マイクロポストのバリデーション

マイクロポストのコンテントのバリデーションに140文字制限を追加する。

[app/models/micropost.rb]
class Micropost < ApplicationRecord
  validates :content, length: { maximum: 140}
end

ここの記述はハッシュやシンボルが普通に出てきているので理解を定着させるために考えておく。
validates :content, length: { maximum: 140}のうち、:contentはシンボル。length: { maximum: 140}はネストされたハッシュであり、:length => { :maximum => 140 }のようにも記述できる。{ }内の:maximumはキーで140がバリュー。更に:lengthキーに対して{ :maximum => 140 }がバリューとしてネストされている。

ユーザーとマイクロポストの関連付け

後半でも出てくるが、ユーザーとマイクロポストの関連付けをおこなう。
1人のユーザーは複数のマイクロポストを持つ(has_many)ことができ、マイクロポストは必ずユーザーに属している(belongs_to)。これをユーザーモデルとマイクロポストモデルに記述しておく。

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

[app/models/micropost.rb]
class Micropost < ApplicationRecord
  belongs_to :user
  validates :content, length: { maximum: 140 }
end

コンソール操作は飛ばします。

継承

継承について軽くまとめておきます。
ユーザーモデルやマイクロポストモデルは、"ApplicationRecord"クラスを継承している。"ApplicationRecord"クラスは更に"ActiveRecord: :Base"を継承しており、階層下のモデルはRubyの属性のように扱えデータベース操作が可能になる。 コントローラも"ApplicationController"を継承しており、更に"ActionController::Base"を継承している。階層化のコントローラは、モデルオブジェクトの操作や、送られてくるHTTP requestのフィルタリング、ビューをHTMLとして出力するなどの多彩な機能を実行できる。

REST

REST(REpresentational State Transfer)についてチュートリアル内では原則的なものについて詳細は記載されていなかったのでまとめておきます。
ただ、媒体によっては4つであったり6つであったり、若干表現が違っていたりしました。
私は以下の書籍を参考にしました。

イラスト図解式 この一冊で全部わかるWeb技術の基本 | 小林 恭平, 坂本 陽, 佐々木 拓郎 | コンピュータ・IT | Kindleストア | Amazon

また、補足的に図もついてまとめられていた以下のサイトも参考としています。

REST APIとは?ざっくりと理解してみる【初心者向け】 - Wiz テックブログ

1. 統一インターフェース

あらかじめ定義、共有化された方法で情報がやりとりされる。HTTPプロトコルのメソッドであれば、GET、POST、PUT、DELETEという4つ。

2. アドレス可読性

すべての情報が一意なURIで構成されている。

3. 接続性

やり取りの情報にはリンクを含めることが出来る。

4. ステートレス性

やりとりは独立しており、前回のリクエスト/レスポンスの影響を受けない。各リクエスト内で必要な情報(ユーザーID、アクセストークンなど)を保持する。

RESTとCRUDの関係

また、Raisチュートリアル内では、RESTにおけるHTTPリクエストとリレーショナルデータベース(RDB)とが対応していると記載がありました。
最初はよく分からなかったのですが、RESTとCRUDについてまとめられていたサイト(https://www.logicmonitor.jp/blog/rest-vs-crud)を参考にして自分なりに解釈しました。
RESTにおけるHTTPメソッドはHTTPリクエストを介したデータ操作であり、CRUDはリレーショナルデータベース操作であり、対応はしているが本質的には異なる。例えば、CRUDのUpdateはRDBのリソースの一部を更新するが、PATCHはより広義的な操作でリソース全体の更新も可能。おそらくCRUDの方がより限定的な機能と言える。

REST CRUD
POST Create
GET Read
PATCH Update
DELETE Delete

第2章ー演習ー

2.2.1

①emailを入力せず、名前だけを入力しようとした場合、どうなるでしょうか?
(解答) emailが空でもユーザーは作成出来てしまう。

②「@example.com」のような間違ったメールアドレスを入力して更新しようとした場合、どうなるでしょうか?
(解答) これもユーザーは作成出来てしまう。

③上記の演習で作成したユーザーを削除してみてください。ユーザーを削除したとき、Railsはどんなメッセージを表示するでしょうか?
(解答)User was successfully destroyed. というフラッシュメッセージが表示される。

2.2.2

①図 2.12を参考にしながら、/users/1/editというURLにアクセスしたときの振る舞いについて図を書いてみてください。
(解答) 図は省略。流れとしては、(1)/users/1/editにアクセス(GET)する → (2)resources :usersルーティングによってeditアクションへ振り分けられる → (3) editアクションはbefore_actionでset_userメソッドが定義されており、@user変数にはUser.findのuserが代入される。→ (4)editに対応するビュー(index.html.erb)において@userの編集ページが表示される。

②図示した振る舞いを見ながら、Scaffoldで生成されたコードの中でデータベースからユーザー情報を取得しているコードを探してみてください。(ヒント: set_userという特殊な場所の中にあります。)
(解答)①の(3)部分に該当。

③ユーザーの情報を編集するページのファイル名は何でしょうか?
(解答) [app/views/users/edit.html.erb]

2.3.1

①マイクロポストの作成画面で、ContentもUserも空のまま作成してみるとと、どうなるでしょうか?
(解答)両方空でも作成出来てしまう。

②141文字以上の文字列をContentに入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか?(ヒント: WikipediaRubyの記事にある設計思想の引用文が140文字を超えているので、これをコピペしてみましょう。)
(解答)これも作成出来てしまう。

③上記の演習で作成したマイクロポストを削除してみましょう。
(解答)Micropost was successfully destroyed.のフラッシュメッセージと共に削除出来た。

2.3.2

①先ほど2.3.1.1の演習でやったように、もう一度Contentに141文字以上を入力してみましょう。どのように振る舞いが変わったでしょうか?
(解答)「Content is too long (maximum is 140 characters)」のバリデーションエラー。

2.3.3

①ユーザーのshowページを編集し、ユーザーの最初のマイクロポストを表示してみましょう。同ファイル内の他のコードから文法を推測してみてください(コラム 1.2で紹介した技術の出番です)。うまく表示できたかどうか、/users/1にアクセスして確認してみましょう。
(解答) ユーザーのshowページに<%= render @user.microposts %>を追加すると、/users/1に表示されるようになる。

②リスト 2.18は、マイクロポストのContentが存在しているかどうかを検証するバリデーションです。マイクロポストが空でないことを検証できているかどうか、実際に試してみましょう(図 2.18のようになっていると成功です)。
(解答)「Content can't be blank」のバリデーションエラーが発生。

③リスト 2.19の(コードを書き込む)となっている箇所を書き換えて、Userモデルのnameとemailが存在していることを検証してみてください(図 2.19)。
(解答)以下のバリデーションを加えて、空のユーザーを作成すると、「Name can't be blankとEmail can't be blank」が発生。

[app/models/user.rb]
class User < ApplicationRecord
  has_many :microposts
  validates :name, presence: true
  validates :email, presence: true
end

2.3.4

①Applicationコントローラのファイルを開き、ApplicationControllerがActionController::Baseを継承している部分のコードを探してみてください。
(解答) 以下の部分

[app/controllers/application_contoroller.rb]
class ApplicationController < ActionController::Base
end

②ApplicationRecordがActiveRecord::Baseを継承しているコードはどこにあるでしょうか? 先ほどの演習を参考に、探してみてください。(ヒント: コントローラと本質的には同じ仕組みなので、app/modelsディレクトリ内にあるファイルを調べてみましょう。)
(解答) 以下の部分

[app/models/application_record.rb]
class ApplicationRecord < ActiveRecord::Base
  primary_abstract_class
end

第2章ーまとめー

  • scaffoldジェネレータでindex、new、showといった多くのアクションとそれに対応するモデルとビューが自動生成される。
  • scaffoldそのままだと、バリデーションやモデルの関連付けは全くおこなわれていない。
  • RailsMVCモデルによってブラウザからのリクエストからレスポンスまでの流れが決まっている。
  • データモデルの関連付けに、has_manybelongs_toが活用される。
  • RESTという設計は4原則で成り立っており、RESTに従って構成されたものをRESTfulと呼ぶ。
  • RESTのHTTPリクエストメソッドとリレーショナルデータベースのCRUD操作には相関があるが、全く同一のものでは無い。

実際にアプリ作る際はscaffoldなどなるべく工数かからないものをがんがん取り入れていこうと思います。そのための下地としてRailsチュートリアルをちゃんと理解しておきます。
第2章終わり🐻