Ruby on 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章
第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は明らかに分かりにくいのですが、以下のサイトが非常に丁寧にまとめられており、分かりやすかったです。
- Railsでは通常相対パスを指定するが、リダイレクトの場合は完全なURLを返す絶対パスでないといけない。
- そのためパスは
redirect_to(user_url(@user.id))
となる。 - 引数URLがモデルオブジェクトの場合は、Railsはidを自動で返してくれるため、
.id
は省略可能。 - この段階で
redirect_to(user_url(@user))
となっており、メソッドに渡す場合の()は省略可能なことは4章の通り。 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 Command
をbundle 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章
第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 |
string | |
created_at | datetime |
updated_at | datetime |
Active Recordの操作(作成、保存、更新、削除)
User.new
で新規ユーザーオブジェクトをメモリ上に作成。Active Recordの設計で、引数無しの場合はnil。valid
メソッドで有効性の検証が可能。有効かどうかなだけで、DBへ保存されていることを確認する訳ではない。save
メソッドでデータベースへ保存。この際にモデルに対応するid属性とtimestampも変更される。戻り値は真偽値。create
はnew
とsave
を兼ねたメソッド。戻り値は真偽値ではなく、オブジェクト自身を返す。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
一意性の検証
メールアドレスの一意性検証に関する前提
一意性をテストする際は、メモリ上だけでなく、実際にデータベースへレコードを登録している必要がある。そのためメソッドにはsave
やupdate
を加える。
メールアドレスは大文字と小文字は区別せず、全て小文字として扱うことが通例。
メールアドレスの重複についてテスト。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_save
でdowncase
化しておく。
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であればオブジェクトも返す。
*パスワードのハッシュ化には
bcrypt
gemを使用する必要がある。
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)の内容を比べてみてください。
[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の内容が元に戻ったことを確認してください。
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は、どのクラスのインスタンスでしょうか?
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 。
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の検索は、
find
、find_by
、where
の3種類。 - メールアドレスは大文字と小文字は区別せず、全て小文字として扱うのが通例。
- ActiveRecordは通常データベースのレベルでの一意性保証はしていない。そのため、indexを付けてindexに一意性を持たせる。indexは索引機能としても役立つ。
- has_secure_passwordで安全なパスワード(不可逆的にハッシュ化されたパスワード)が追加される。
2周目時点では曖昧理解で飛ばしていたデータベースの一意性制約などがやっとクリアになった。。。
第6章終わり🐻
Ruby on Railsチュートリアル第5章
第5章(レイアウトを作成する)
Bootstrapやcss関連のところは、飛ばしながらコピペでいきます。
ナビゲーション
link_toメソッド
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">
- Railsはassetsディレクトリ直下の画像を
app/assets/images
ディレクトリと紐づけるため、src属性にimageディレクトリは不要。 はファイル名が一意になるようにRailsが追加したもの。
パーシャル
- ビューの重複する記述に関してはパーシャルにまとめる。
- パーシャルはディレクトリ内で一目見ただけでわかるように、ファイル名の先頭にアンダースコア「_」を付す。
- パーシャルの呼出しには
<%= render 'layouts/header' %>
のような形でrenderヘルパーで呼び出す。この際に探すファイル名はapp/views/layouts/_header.html.erb
となる。
アセットパイプライン
アセットパイプラインはJavaScriptやCSSの連結圧縮するフレームワークで、重要な部分なのでまとめておきます。
- アセットディレクトリ、マニフェストファイル、プリプロセッサエンジンの3つが主要な機能。
- アセットディレクトリには、静的ファイルを目的別に分類する3つのディレクトリがある。(固有、ライブラリ用、サードパーティー用)
- マニフェストファイルは、アセットディレクトリに配置されたファイルをまとめる指示書。Sprockets gemによっておこなわれる。
- プリプロセッサエンジンは拡張子に合わせてプリプロセス(前処理)を実行し結合することで、ブラウザへ配信可能な形とする。
- アセットパイプラインの機能は、SPAとも組み合わせることがUX向上に非常に有効。
レイアウトのリンク
名前付きルーティング
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
後は、サインアップ用にルーティングの設定、サインアップ用ボタンへのリンク追加、サインアップページを作成したら最初のステップは完了。
開発環境が動かなくなったとき
- 以下のコマンドを実行する。
$ bundle exec rails app:update:bin
git status
でファイルに変更が生じているかどうかを確認。- 変更が生じていたら、
git commit -am "Update bin files"
を実行。 - 再度
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章
第4章(Rails風味のRuby)
文字列、オブジェクトとメソッド
この章では文字列、オブジェクト、メソッドについての基本的な内容が記載されているのでまとめておきます。
文字列
- ダブルコートまたはシングルコートで囲ったものは「文字列リテラル(リテラル文字)」と呼ばれる。
#{ }
で{ }内の式展開が可能となる。この場合、ダブルコート「" "」を使用する。シングルコートだと式展開されない。- rubyには改行やタブは特殊文字であり、\n、\t で表される。これもシングルコートだと特殊文字化されない。
- 特殊文字を改行やタブでは無く、そのまま文字列として表現したい場合はエスケープ(\を追加)する。
オブジェクトとメソッド
- Rubyではあらゆるものをオブジェクトとして扱う。
- オブジェクトに渡されるメッセージを「メソッド」と呼ぶ。
- 条件分岐のメソッドには、if-elseやelsifが使用される。
- Rubyのメソッドには暗黙の戻り値(メソッド内で最後に評価された式の値)がある。
カスタムヘルパー
まず、ヘルパーとはRailsにあらかじめ処理をメソッド化して扱えるようにしたもの。form_with
、link_to
、image_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章
第3章(ほぼ静的なページの作成)
本章からは14章まで通しで取り組むsample appを作成していきます。
静的ページ
コントローラの生成
静的ページ用のコントローラをgenerateする。※コントローラの名前はモデルと違って複数形。またキャメルケース表記で記載することで、自動的にスネークケースのコントローラが生成される。
$ rails generate controller StaticPages home help
rails generate
やdb: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による自動化もやっておくとかなり便利でしたので今のうちに設定しておきます。
- gemのguard、guard-minitestを入れておく。(既にインストール済み)
$ bundle exec guard init
を実行。- Guadfileをカスタマイズ。
$ 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>
各ページも必要最低限な要素のみにリファクタリング。
※
[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章
第2章(Toyアプリケーション)
第2章はscaffoldジェネレータで簡単なアプリを作成し、railsのおおまかな機能を学習するといった感じです。
序盤のrails new
やgit push
の内容は第1章と重複するので特に書きません。
UserモデルとMicropostモデル
ここでは、まずtoy_app用のモデル設計を考える。
個々のユーザーを表すUserモデルと、投稿するMicropostモデルの2つを扱う。
ユーザーモデルの設計
ここではユーザー(users)のデータベースが持つ情報はid、name、emailの3つのみ。
カラム名 | データ型 |
---|---|
id | integer |
name | string |
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に入力した状態で、マイクロポストを作成しようとするとどうなるでしょうか?(ヒント: WikipediaのRubyの記事にある設計思想の引用文が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そのままだと、バリデーションやモデルの関連付けは全くおこなわれていない。
- RailsはMVCモデルによってブラウザからのリクエストからレスポンスまでの流れが決まっている。
- データモデルの関連付けに、
has_many
やbelongs_to
が活用される。 - RESTという設計は4原則で成り立っており、RESTに従って構成されたものをRESTfulと呼ぶ。
- RESTのHTTPリクエストメソッドとリレーショナルデータベースのCRUD操作には相関があるが、全く同一のものでは無い。
実際にアプリ作る際はscaffoldなどなるべく工数かからないものをがんがん取り入れていこうと思います。そのための下地としてRailsチュートリアルをちゃんと理解しておきます。
第2章終わり🐻