Ruby on Railsチュートリアル第6版の第9章のまとめです。
個人的に気になったところ、難しかったところ、わからなかったところを中心にまとめていきます。基本的に自分の理解を助けることと、復習しやすくすることを目的とした記事ですが、今Ruby on Railsチュートリアルをやってる人に役立つ情報があるかもしれません。
僕の学習の順番は、1周目はRailsチュートリアル第4版(Rails5.1)を使い、2周目で第6版を使うという感じになってます。これは1周目が終わったところで第6版のWebテキスト版がリリースされたからです。
ちなみに、2周目は解説動画を購入してやってます。オススメです。
第9章 発展的なログイン機構
第9章では、第8章で作ったログイン機構を発展させます。具体的にはブラウザを閉じてもログインが継続する仕組みを追加します。
この章はスキップすることも可能とのことです。
9.1 Remember me 機能
Railsチュートリアルでは「Remember me」と表現されてますが、「ログインしたままにする」的なやつですね。それを作っていきます。
では、ブランチを作って進みます。
9.1.1 記憶トークンと暗号化
1周目のときを思い出すと、とにかくテキストを読んで、言われるがままにコードを書く、という感じになり始めたのがこの辺からだったかもしれません。
1つ1つはわかるけど、全体として何をやってるかわからなくなってるという感じです。
第9章でやることをものすごく簡単に言うと、「メールアドレスやパスワードを使わず、ブラウザとサーバーに共通のものを持たせて、それを使って認証する」みたいな感じだと思います。
何をやってるかわからなくなる問題を解消するために、解説動画を見てから進んだ方がいいと思います。図解してくれてるので、何をやってるのかわかりやすいと思います。

最初の方は指示通りにやっていけば大丈夫だと思います。
よくわからなかったのはattr_accessorです。これは第4章でも出てきて調べてありますが、そこでまとめなかったところに関する機能が第9章で使われてるようです。
attr_accessorを使って仮想的な属性(データベースにはないけど、データベースにあるように扱える属性?)を作るみたいです。これはpasswordとpassword_digestでやったのと同じだそうです。
書き方自体は第4章と同じなので、第4章でも仮想的な属性になってたということですね。
よくわからなかったので、attr_accessorをまた調べてみました。
Railsでは特に意識することなく、@tweet.textのように、カラム名をメソッドのように使用するとそのインスタンスが持つ値にアクセスできます。カラムに無い属性についても、attr_accessorを使うことで同じように、属性名をメソッドのように使用できるようになります。
Railsでは、カラム名やattr_accessorで定義した属性をメソッドのように使えるように、見えない部分でインスタンスの持つ値にアクセスするためのメソッドが定義されています。具体的にはActiveRecord::Baseで動的に定義してくれています。
attr_accessorについて – haayaaa’s diary
引用元の記事はattr_accessorの内部処理も書いてあるので、詳しく知りたい場合は見てみることをオススメします。
その記事によると、Railsではカラム名をメソッドのように使えて、そうするとそのインスタンスが持ってる値にアクセスできるとのことです。
これはデータベースを扱うときにやってたやつですね。
user = User.first
user.name = "hoge"
みたいな感じで、カラム名の「name」をメソッド的に使って値を変更したりしてますよね。何となく理解してた感じだったので、これを見て「確かに」と思いました。
で、カラムにない属性はattr_accessorを使うと同じようにできるということです。パスワードのところでは、has_secure_passwordメソッドがやってくれてて、それを今度は自分で実装するということのようです。
つまり、
attr_accessor :remember_token
を追加することで、
user.remember_token
というような使い方ができるということですね。
>> user = User.first
(省略)
>> user.remember_token
=> nil
attr_accessorのところをコメントアウトすると、NoMethodErrorが出ました。これでattr_accessorを使うとメソッド的に仮想的な属性を使えるようになることがわかりますね。
次に、第6章でも出てきたupdate_attributeです。
update_attributeメソッドはupdateメソッドと違い、バリデーションを回避することができるようです。
>> user = User.first
(省略)
user.name
=> "Rails Tutorial"
>> user.update(name: "rails")
(省略)
=> false
updateメソッドを使った場合、nameだけの更新はできませんでした。
>> user.update(name: "rails", password: "foobar")
(省略)
=> true
>> user.name
=> "rails"
updateメソッドの引数にパスワードを追加すると更新できました。バリデーションが機能してる証拠ですね。
今度はupdate_attributeメソッドを試してみます。
>> user.update_attribute(:name, "ruby on rails")
(省略)
=> true
>> user.name
=> "ruby on rails"
パスワードなしで更新できました。バリデーションを回避できてることがわかりますね。
updateメソッドとupdate_attributeメソッドの使い分けについては、解説動画で説明されてます。

ここまでわかれば、コードを読んで何をやってるかがわかるかなと思います。
演習2に出てくるコードはどちらもテストでGREENになるんですが、2つ目はよくわかりません。
1つ目はselfの使い方がわかってれば問題ないと思います。selfは「自身」を表すので、クラスメソッドであればクラス自身ということですね。
2つ目は「class << self」というよくわからないものを追加して、その中で定義されてるメソッドからselfがなくなってます。これは特異クラス方式と呼ぶみたいです。
特異クラス方式では、class << self と書いた行から end までの間に def class_method のようにクラス名を書かずにインスタンスメソッドと同じようなメソッド定義を書いていきます。この間に書いたものはクラスメソッドとして定義されます。
Ruby 初級者のための class << self の話 (または特異クラスとメタクラス)- Rubyist Magazine
何個もクラスメソッドを定義するときに、selfを省略するための仕組みという感じですね。
9.1.2 ログイン状態の保持
cookiesメソッドを使って永続セッションを作ります。cookiesメソッドはハッシュのようなメソッドのようです。
cookies.signed[:user_id] = user.id
これはuserにあるidをクッキーに渡すためのコードですが、signedメソッドは暗号化と復号化の両方の処理をするそうです。
これの元になるコードは、
cookies[:user_id] = user.id
です。cookiesメソッドはハッシュのように使えるので、キーが:user_id、値がuser.idのcookiesハッシュのようなものができます。これをsignedメソッドで暗号化するということですが、暗号化されるのはuser.idということですよね。
ということで、「cookies.signed[:user_id]」はuser.idが暗号化されたものになってると考えるといいのかもしれません。signedメソッドは暗号化と復号化の両方をやるので、正確には違うと思いますが。
どちらにしてもブラウザ側に「cookies.signed[:user_id]」という形で署名付きIDが保存されるということなんだと思います。
それと同時に、remember_tokenもブラウザに保存されます。この2つがサーバー側にあるデータと照合されて、一致すればセッションが確立するということですね。
remember_tokenと対になるものはサーバー側(データベース)にremember_digestとして保存されてます。remember_digestはハッシュ化されてます。
BCryptを使ってremember_tokenとremember_digestを照合するという感じですね。おそらく。
コードとしては大したことないんですが、いくつかの処理を積み重ねる感じになってるので、その流れを理解するのがなかなか難しいと思います。これを書きながら、ちゃんと理解できてるかあやしい感じになってます。
- ログインしてセッションを確立する(Sessionsコントローラ:createアクション)
- remember(user)メソッドを呼び出す(Sessionsコントローラ:createメソッド)
- userに対してrememberメソッドを呼び出す(Sessionsヘルパー:remember(user)メソッド)
- 生成したトークンをremember_tokenに入れて、ハッシュ化した上でデータベースのremember_digestに保存する(Userモデル:rememberメソッド)
- 暗号化したユーザーIDとremember_tokenをブラウザのクッキーに保存する(Sessionsヘルパー:remember(user)メソッド)
- ログインしたユーザーのプロフィールページにリダイレクト(Sessionsコントローラ:createメソッド)
- (ブラウザを再起動)
- セッションが切れてて(session[:user_id]がnilで)、暗号化したユーザーIDがクッキーに保存されてたら、そのIDで検索して、remember_tokenとremember_digestを照合する(Sessionsヘルパー:current_userメソッド)
- 一致したらログインする(Sessionsヘルパー:current_userメソッド)
こんな感じの流れかなと思います。自信ありませんが。
ブラウザの再起動前のところの処理が入れ子構造になってるのがわかりにくい原因の1つかなと思います。「このメソッドは何だっけ?どこにあるんだっけ?」みたいに迷子になってしまいます。ということで、入れ子構造を図にしてみました。

remember(user)メソッドとrememberメソッドがあるのも迷子の原因ですよね。それと、そこでそのメソッドを定義する理由の理解が曖昧になってるのも原因になりそうです。
Sessionsコントローラのcreateアクションはいいとして、remember(user)メソッドとrememberメソッドが問題です。
remember(user)メソッドが何をやってるかというと、ユーザーのセッションの永続化です。「セッション」に関するものなので、Sessionsコントローラで使います。他では使わないのであれば、Sessionsヘルパーに書くということだと思います。
問題はSessionsヘルパーにあるメソッドがデフォルトでどこで読み込まれるのかということです。調べてみましたが、よくわかりませんでした。
rememberメソッドは、remember_digestをデータベースに保存します。データベースに関するものなのでモデルに書くことになります。しかもusersテーブルに保存するのでUserモデルというわけです。
9.1.3 ユーザーを忘れる
今度はログアウトできるように、クッキーから情報を削除します。
ログアウトしたときに、remember_digestにnilを入れ、クッキーにあるIDとremember_tokenを削除するだけです。
9.1.4 2つの目立たないバグ
ここまで来て完璧と思えますが、2つのバグが残ってるようです。それを修正します。
バグを直すので、テストを先に書きます。やること自体は簡単なんですが、処理がどう流れてるのかのイメージが曖昧だと理解が大変です。
今のところ、わかったような、わからないような、という微妙な理解です。Railsチュートリアルが終わってオリジナルアプリケーションを作るときに、また戻ってくるポイントの1つになりそうです。
9.2 [Remember me]チェックボックス
Remember meのチェックボックスを実装します。他のフォームと同じで、form_withメソッドを使います。
ここで重要なのは三項演算子ですね。第8章でも出てきましたが、ここで説明されてます。
「Ruby 2.7.0 リファレンスマニュアル」には条件演算子として説明されてます。
文法:
演算式 – Ruby 2.7.0 リファレンスマニュアル
式1 ? 式2 : 式3
(中略)
if 式1 then 式2 else 式3 end
とまったく同じです。
thenって何だろうと思ったら、Rubyではif文にthenを付けるみたいですね。今まで省略したものしか見てなかったような気がしますが。
上の説明だとわかりにくいので、簡単なコードを書いてみます。
if a == 1
puts "aは1です"
else
puts "aは1ではありません"
end
このif文を三項演算子に書き直すと、
a == 1 ? puts "aは1です" : puts "aは1ではありません"
となります。そう思いますよね。でも、違ったようです。
>> a = 1
=> 1
>> a == 1 ? puts "aは1です" : puts "aは1ではありません"
Traceback (most recent call last):
SyntaxError ((irb):19: syntax error, unexpected tSTRING_BEG, expecting do or '{' or '(')
a == 1 ? puts "aは1です" : puts "aは1で...
^
(irb):19: syntax error, unexpected ':', expecting end-of-input
a == 1 ? puts "aは1です" : puts "aは1ではありま...
調べてみたところ、「Let’sプログラミング」には別の説明がありました。
条件演算子(三項演算子とも呼ばれます)は条件式の結果によって異なる値を返す演算子です。
条件式 ? 真の時の値 : 偽の時の値
条件演算子では条件式を評価し、真(true)だった場合には真の時の値を、偽(false)だった場合には偽の時の値を式全体の値ととして返します。
条件演算子(?:) – Let’sプログラミング
条件式の結果で違う値を返す演算子ということなので、純粋にif文を書き換えてるわけではなさそうです。
という感じで調べてたら、Railsチュートリアルの注釈を読み返して間違いに気づきました。三項演算子では引数の()を省略できないようです。確かにエラー文に「doか { か ( が必要」的なことが書かれてましたね。
ということで、()を付けてやり直しました。
>> a = 1
=> 1
>> a == 1 ? puts("aは1です") : puts("aは1ではありません")
aは1です
=> nil
>> a = 2
=> 2
>> a == 1 ? puts("aは1です") : puts("aは1ではありません")
aは1ではありません
=> nil
三項演算子(条件演算子)を使えばコードが短くなりますが、カッコのつけ忘れでエラーを出しまくりそうです。
9.3 [Remember me]のテスト
Remember meのテストを実装していきます。
9.3.1 [Remember me]ボックスをテストする
テストでもユーザーがログインする必要があるので、そのためのメソッドを作ります。
2つのlog_in_asメソッドを作ることになりますが、メソッドのコードを参照しなくてもテストでログインするときにlog_in_asメソッドを使えばいいというメリットがあるようです。
9.2までで実装したものを理解できてれば、そんなに難しくないと思います。
9.3.2 [Remember me]をテストする
9.3.1で書いたテストではチェックできてないところのテストを書いていきます。
重要なのはraise関数です。これは意図的に例外(エラー)を発生させるものだそうです。つまり、raiseを入れてテストしたときにGREENになったら、raiseを入れたところがテストされてないということがわかります。
テストされてない可能性に気づけるのもすごいなと思いますが、特に問題なく進めると思います。ただ、「テストされてないところはどこか?」と先に聞かれたら、全くわからなかったと思います。
raiseを入れた状態のテストがREDになり、raiseを削除してGREENになったら完了です。
最後に、いつものようにコミット、マージ、プッシュ、デプロイをしましょう。Herokuへのデプロイではマイグレーションが必要で、エラーにならないようにメンテナンスモードにする方法が紹介されてます。
まとめ
第9章はなかなか難しいと思います。解説動画のおかげで何とか理解できた感じです。
たぶん1つ1つはそこまで難しくないけど、Remember meの仕組みと処理の流れを理解するのが大変という感じな気もしてます。
「アクションで使うメソッドで使うメソッド」みたいな感じで、入れ子構造になってるところを把握する必要があるので、混乱しやすいところですね。
第9章をやらなくてもRailsチュートリアルを進めていくことはできるそうなので、どうしてもわからなかったら諦めるという手もあります。僕もちゃんと理解できたとは思ってないので、また戻ってくるかもしれません。
頭の中を整理してから、先に進みましょう。
