エンジニア志望のブログ

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

コンソール内で新しいユーザーを作成してみてください。新しいユーザーの記憶トークンと有効化トークンはどのような値になっているでしょうか? また、各トークンに対応するダイジェストの値はどうなっているでしょうか?

記憶トークンと記憶ダイジェストはnil

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

おわりに

アカウントの有効化を実装しました。
普段サービスを利用する際にユーザー登録をすると届くメールはこんなふうに作られているんだ、と楽しみながら実装ができました。
今回コールバックでは、はてなが浮かんで納得しきれない部分があったのでコールバックの内容が多めです笑