Railsチュートリアル第11章
はじめに
Ruby on Railsチュートリアル(第6版)のメモ、演習の解答例を記述した記事です。
解答は個人のものなので、誤りがあればご指摘ください。
開発環境 Ruby: 2.7.2 , Rails: 6.1.4
メモ
メールアドレスを利用したアカウントの有効化
ユーザーの新規登録の際にメールアドレスが登録したユーザーのものなのかを確認できるようにする
before_createコールバック
オブジェクトが作成される前に処理を実行する
before_create :create_activation_digest
上記のコードはメソッド参照と呼ばれるものでオブジェクトを生成する前にcreate_acrivation_digestを実行する
before_createのタイミング
チュートリアル内で
”before_createコールバックの方はユーザーが作成される前に呼び出される” "User.newで新しいユーザーが定義されると、activation_token属性やactivation_digest属性が得られるようになります"
と書かれており、User.newでオブジェクトを生成した時点でbefore_createコールバックが実行されると思ったが違ったようなので記述しておきます。
createメソッドとは
モデルオブジェクトを生成して保存するという一連の処理のこと
ユーザーを作成する = ユーザーオブジェクトを生成して保存する
とのことだったようです。
before_saveとbefore_create どっちが先に呼び出される??
before_saveの方が先に呼び出される
オブジェクトの作成の際に呼び出される順序は以下のようになります
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit/after_rollback
参考:Active Record コールバック - Railsガイド
”保存”を起点にコールバックが処理される?
参考:ActiveRecordのコールバックの順序・コールバック内のロールバック処理について - Hack Your Design!
上記の記事を参考にコールバックを可視化してみました。
コールバックを可視化するためのコード
class User < ActiveRecord::Base before_validation -> { puts "before_validation is called" } after_validation -> { puts "after_validation is called" } before_save -> { puts "before_save is called" } before_update -> { puts "before_update is called" } before_create -> { puts "before_create is called" } after_create -> { puts "after_create is called" } after_update -> { puts "after_update is called" } after_save -> { puts "after_save is called" } after_commit -> { puts "after_commit is called" } end
新規レコード作成時
Userモデルをnewしてsave
Userモデルをcreate する
irb(main):003:0>user = User.new(name:"yuy", email: "yuy@yuy.yuy", password: "foobar", pa ssword_confirmation: "foobar" ) => #<User id: nil, name: "yuy", email: "yuy@yuy.yuy", created_at: nil, updated_at: n... irb(main):004:0> user.save before_validation is called TRANSACTION (0.2ms) SAVEPOINT active_record_1 User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuy@yuy.yuy"], ["LIMIT", 1]] after_validation is called before_save is called before_create is called User Create (1.5ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "yuy"], ["email", "yuy@yuy.yuy"], [" after_create is called after_save is called TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 after_commit is called => true irb(main):005:0> User.create(name:"yuyy", email: "yuyy@yuy.yuy", password: "foobar", pass word_confirmation: "foobar" ) before_validation is called TRANSACTION (0.1ms) SAVEPOINT active_record_1 User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuyy@yuy.yuy"], ["LIMIT", 1]] after_validation is called before_save is called before_create is called User Create (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest") VALUES (?, ?, ?, ?, ?) [["name", "yuyy"], ["email", "yuyy@yuy.yuy"], ["created_at", "2021-07-14 01:09:11.939377"], ["updated_at", "2021-07-14 01:09:11.939377"], ["password_digest", "$2a$12$UVy2HGBnjaIMadalnA.OmuMzdCYLQGJTnkgaFwWZB4b7mOk6uEFoi"]] after_create is called after_save is called TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 after_commit is called => #<User id: 105, name: "yuyy", email: "yuyy@yuy.yuy", created_at: "2021-07-14 01:09:11.939377000 +0000", updated_at: "2021-07-14 01:09:11.939377000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: nil, activated: false, activated_at: nil>
モデルをnewした時点ではコールバックが呼び出されていません。
save、createするとコールバックが呼び出されるようです。
before_save、before_createどちらも呼び出されてますね。
また、before_updateとafter_update呼び出されていません。
レコード更新時
ユーザーモデルをupdateする
irb(main):006:0> user.update(name: "yuuuuy") before_validation is called TRANSACTION (0.1ms) SAVEPOINT active_record_1 User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? AND "users"."id" != ? LIMIT ? [["email", "yuy@yuy.yuy"], ["id", 104], ["LIMIT", 1]] after_validation is called before_save is called before_update is called User Update (0.1ms) UPDATE "users" SET "name" = ?, "updated_at" = ? WHERE "users"."id" = ? [["name", "yuuuuy"], ["updated_at", "2021-07-14 01:26:17.018216"], ["id", 104]] after_update is called after_save is called TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 after_commit is called => true
こちらは更新なのでbefore_createとafter_createは呼び出されていません。
Action Mailer
アプリケーションでメールの送受信を行えるようにするライブラリ
メイラーの作成
メイラーを生成する
account_activationメイラーとpassword_resetメイラーを生成
$ rails generate mailer UserMailer account_activation password_reset
メイラーごとにテキスト用のメールテンプレートとHTMLメール用のテンプレートが生成される
Running via Spring preloader in process 84968 create app/mailers/user_mailer.rb invoke erb create app/views/user_mailer create app/views/user_mailer/account_activation.text.erb create app/views/user_mailer/account_activation.html.erb create app/views/user_mailer/password_reset.text.erb create app/views/user_mailer/password_reset.html.erb invoke test_unit create test/mailers/user_mailer_test.rb create test/mailers/previews/user_mailer_preview.rb
メールに記述するURLの生成
メールを利用したアカウントの有効化では
メールアドレスと有効化トークンのダイジェストを利用してユーザーが本人であるかデータベースから参照して有効化を行う
そこで送るメールに記載するURLにメールアドレスと有効化トークンのダイジェストを含ませておく
URLの生成
edit_account_activation_url(@user.activation_token, email: @user.email)
上記の名前付きルートから生成されるURLの例
email部分はRailsが自動的にエスケープした文字列を生成
account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com
送信メールのプレビュー
メールのメッセージのプレビューをその場で見ることができる
config/environments/development.rbを変更する
Rails.application.configure do . . . config.action_mailer.raise_delivery_errors = false host = 'example.com' # ここをコピペすると失敗します。自分の環境のホストに変えてください。 # クラウドIDEの場合は以下をお使いください config.action_mailer.default_url_options = { host: host, protocol: 'https' } # localhostで開発している場合は以下をお使いください # config.action_mailer.default_url_options = { host: host, protocol: 'http' } . . . end
受け取ったパラメータに応じて呼び出すメソッドを切り替える
# トークンがダイジェストと一致したらtrueを返す def authenticated?(remember_token) return false if self.remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end
remember部分を変数として扱いたい
self.FOOBAR_digest
変数として扱うことで引数に応じてメソッドを切り替えることができる
メタプログラミング
プログラムでプログラムを作成すること
sendメソッドで実現する
>> user = User.first >> user.activation_digest => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send(:activation_digest) => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send("activation_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> attribute = :activation >> user.send("#{attribute}_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae"
演習
11.1
演習2
表 11.2の名前付きルートでは、_pathではなく_urlを使うように記してあります。なぜでしょうか? 考えてみましょう。ヒント: 私達はこれからメールで名前付きルートを使います。
ページ外部のメールからリクエストを送りたいから絶対リンクを使う
11.1.2
演習2
コンソールからUserクラスのインスタンスを生成し、そのオブジェクトからcreate_activation_digestメソッドを呼び出そうとすると(Privateメソッドなので)NoMethodErrorが発生することを確認してみましょう。また、そのUserオブジェクトからダイジェストの値も確認してみましょう。
yuy@yu sample_app % rails c --sandbox Running via Spring preloader in process 1705 Loading development environment in sandbox (Rails 6.1.4) Any modifications you make will be rolled back on exit irb(main):001:0> User.create(name: "User",email: "user@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: false, activated : false, activated_at: Time.zone.now) (0.5ms) SELECT sqlite_version(*) TRANSACTION (0.0ms) begin transaction TRANSACTION (0.1ms) SAVEPOINT active_record_1 User Exists? (0.1ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "user@railstutorial.org"], ["LIMIT", 1]] User Create (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest", "activated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["name", "User"], ["email", "user@railstutorial.org"], ["created_at", "2021-07-13 23:49:56.915739"], ["updated_at", "2021-07-13 23:49:56.915739"], ["password_digest", "$2a$12$u9f0aKuqXqZFCQXrWyMTa.U6nXimKEX8JA9HnkIbS4b4GXbVSfaJe"], ["activation_digest", "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"], ["activated_at", "2021-07-13 23:49:56 TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 => #<User id: 103, name: "User", email: "user@railstutorial.org", created_at: "2021-07-13 23:49:56.915739000 +0000", updated_at: "2021-07-13 23:49:56.915739000 +0000", password_digest: [FILTERED], remember_digest: nil, admin: false, activation_digest: "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJm...", activated: false, activated_at: "2021-07-13 23:49:56.643115000 +0000"> irb(main):002:0> User.last.create_activation_digestC LIMIT ? [["LIMIT", 1]] Traceback (most recent call last): 1: from (irb):2 NoMethodError (private method `create_activation_digest' called for #<User:0x0000000143b4a688>) Did you mean? restore_activation_digest! irb(main):003:0> User.last.activation_digest User Load (0.1ms) SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT ? [["LIMIT", 1]] => "$2a$12$Tas6a/c9vXYMIf7lbswHruVxuVskADJhPr1NZRNOQJmGio2MT2vIC"
演習3
リスト 6.35で、メールアドレスの小文字化にはemail.downcase!という(代入せずに済む)メソッドがあることを知りました。このメソッドを使って、リスト 11.3のdowncase_emailメソッドを改良してみてください。また、うまく変更できれば、テストスイートは成功したままになっていることも確認してみてください。
#メールアドレスを全て小文字にする def downcase_email self.email.downcase! end
11.3.1
演習1
コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?
irb(main):001:0> user = User.create(name:"yuy", email: "yuy@yuy.yuy", passwo rd: "foobar", password_confirmation: "foobar" ) (0.7ms) SELECT sqlite_version(*) TRANSACTION (0.0ms) begin transaction TRANSACTION (0.0ms) SAVEPOINT active_record_1 User Exists? (0.2ms) SELECT 1 AS one FROM "users" WHERE "users"."email" = ? LIMIT ? [["email", "yuy@yuy.yuy"], ["LIMIT", 1]] User Create (1.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at", "password_digest", "activation_digest") VALUES (?, ?, ?, ?, ?, ?) [["name", "yuy"], ["email", "yuy@yuy.yuy"], ["created_at", "2021-07-14 02:20:18.142063"], ["updated_at", "2021-07-14 02:20:18.142063"], ["password_digest", "$2a$12$2VL3d7UILjexu2t7rmJXiuiyE0jxKbcxr65XsBcW1qIL2/NwqmyPi"], ["activation_digest", "$2a$12$BpedetJAqun5id9xd68UKeT8pC/IYscPU2D5Wk3w7STe8pDSkOsFS"]] TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 => #<User id: 104, name: "yuy", email: "yuy@yuy.yuy", created_at: "2021...
記憶トークンと記憶ダイジェストを更新
irb(main):002:0> user.remember TRANSACTION (0.2ms) SAVEPOINT active_record_1 User Update (0.1ms) UPDATE "users" SET "updated_at" = ?, "remember_digest" = ? WHERE "users"."id" = ? [["updated_at", "2021-07-14 02:23:13.938806"], ["remember_digest", "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"], ["id", 104]] TRANSACTION (0.0ms) RELEASE SAVEPOINT active_record_1 => true irb(main):003:0> user.remember_token => "6s19ZgORpmwTfsAq1uOU4g" irb(main):004:0> user.remember_digest => "$2a$12$CtvDghirVjL61HmrQGrLKuDJIiDTkF2Xe3AmhbE5c48spCYMbbzDC"
演習2
リスト 11.26で抽象化したauthenticated?メソッドを使って、先ほどの各トークン/ダイジェストの組み合わせで認証が成功することを確認してみましょう。
irb(main):007:0> user.authenticated?(:remember, user.remember_token) => true
11.3.3
演習1
リスト 11.35にあるactivateメソッドはupdate_attributeを2回呼び出していますが、これは各行で1回ずつデータベースへ問い合わせしていることになります。リスト 11.39に記したテンプレートを使って、update_attributeの呼び出しを1回のupdate_columns呼び出しにまとめてみましょう。これでデータベースへの問い合わせが1回で済むようになります(注意!update_columnsは、モデルのコールバックやバリデーションが実行されない点がupdate_attributeと異なります)。また、変更後にテストを実行し、 green になることも確認してください。
update_columnsの使い方には注意
def activate update_columns(activated: true, activated_at: Time.zone.now) end
演習2
現在は、/usersのユーザーindexページを開くとすべてのユーザーが表示され、/users/:idのようにIDを指定すると個別のユーザーを表示できます。しかし考えてみれば、有効でないユーザーは表示する意味がありません。そこで、リスト 11.40のテンプレートを使って、この動作を変更してみましょう9 。なお、ここで使っているActive Recordのwhereメソッドについては、13.3.3でもう少し詳しく説明します。
def index @users = User.where(activated: true).paginate(page: params[:page]) end def show @user = User.find(params[:id]) redirect_to root_url and return unless @user.activated? end
演習3
ここまでの演習課題で変更したコードをテストするために、/users と /users/:id の両方に対する統合テストを作成してみましょう。
有効でないユーザが表示されないか、表示できないかのテスト
test "should not show non activated user" do log_in_as(@admin) get users_path assert_select "a[href=?]", user_path(@non_activated_user), count: 0 get user_path(@non_activated_user) assert_redirected_to root_url end
おわりに
アカウントの有効化を実装しました。
普段サービスを利用する際にユーザー登録をすると届くメールはこんなふうに作られているんだ、と楽しみながら実装ができました。
今回コールバックでは、はてなが浮かんで納得しきれない部分があったのでコールバックの内容が多めです笑