ハイパーニートプログラマーへの道

頑張ったり頑張らなかったり

Ruby on Rails Tutorial Chapter6をやる (いつも通り)断片的なメモ

ほんとはもうとっくに終わっているのですが・・・第6章いきます。

Chapter 6: Modeling users | Ruby on Rails Tutorial (3rd Ed.) | Softcover.io

やったこと

  • モデルの作成とマイグレーション
  • ActiveReocordの使用。データモデルを作ったり操作したりするためのメソッドがたくさんある
  • Active Recordのバリデーションでモデルに制約を付加する
  • 主なバリデーションはpresence,length,format
  • 正規表現は強力
  • has_secure_passwordを使ってパスワードを保護

$ git checkout -b modeling-users

$ rails g model User name:string email:string

Userモデルを作る。:name, :email属性を持つ。

例えばActive Recordでemailを元にユーザーを探す場合は

User.find_by(email: "noriyo.akita@gmail.com")

find_by_xxxなどの動的ファインダーメソッドは非推奨になっている。
find_by(xxx: "xxx")などの形で使用すること。

参考記事:

Rails で十分に活用されていなくてもったいない ActiveRecord::Relation のメソッド TOP 10 - 杉風呂2.0 - A Lifelog -

user.update_attribute(:name, "The Dude") 一つの属性だけアプデするにはupdate_attribute(単数形)

引数は2つ (:name, "The Dude")カンマ区切り。(:name => "The Dude")これダメ。

6.2 User validations

ActiveRecordの方でバリデーションしても、データベースレベルではしてくれない。そうなると2クリック問題?が起こる。

Chapter 6: Modeling users | Ruby on Rails Tutorial (3rd Ed.) | Softcover.io

1.Alice signs up for the sample app, with address alice@wonderland.com.
2.Alice accidentally clicks on “Submit” twice, sending two requests in quick succession.
3.The following sequence occurs: request 1 creates a user in memory that passes validation, request 2 does the same, request 1’s user gets saved, request 2’s user gets saved.
4.Result: two user records with the exact same email address, despite the uniqueness validation

If the above sequence seems implausible, believe me, it isn’t: it can happen on any Rails website with significant traffic (which I once learned the hard way). Luckily, the solution is straightforward to implement: we just need to enforce uniqueness at the database level as well as at the model level. Our method is to create a database index on the email column (Box 6.2), and then require that the index be unique.

第6章 ユーザーのモデルを作成する | Rails チュートリアル

1.アリスはサンプルアプリケーションにユーザー登録します。メールアドレスはalice@wonderland.comです。
2.アリスは誤って “Submit” を素早く2回クリックしてしまいます。そのためリクエストが2つ連続で送信されます。
3.次のようなことが順に発生します。リクエスト1は、検証にパスするユーザーをメモリー上に作成します。リクエスト2でも同じことが起きます。リクエスト1のユーザーが保存され、リクエスト2のユーザーも保存されます。
4.この結果、一意性の検証が行われているにもかかわらず、同じメールアドレスを持つ2つのユーザーレコードが作成されてしまいます。

上のシナリオが信じがたいもののように思えるかもしれませんが、どうか信じてください。RailsのWebサイトでは、トラフィックが多いときにこのような問題が発生する可能性があるのです。幸い、解決策の実装は簡単です。実は、この問題はデータベースレベルでも一意性を強制するだけで解決します。具体的には、emailカラムにデータベースのインデックスを作成し、そのインデックスが一意であることを要求します。

DBレベルでunique実装

$ rails generate migration add_index_to_users_email
class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end

add_indexメソッドでusersテーブルのemailカラムにunique制約

そしてモデルではDBに保存する前に小文字にしてしまおうと。

before_saveを使う。

user.rb

class User < ActiveRecord::Base
  before_save { save.email = email.downcase }
  .
  .
  .
end

selfを付けてこのようにも書けるが、
before_save { save.email = self.email.downcase }

(where self refers to the current user), but inside the User model the self keyword is optional on the right-hand side:

右辺においてselfはオプショナルなので、別につけなくても良い。

6.3 Adding a secure password

$ rails generate migration add_password_digest_to_users password_digest:string

これでto_usersでusers tableにpassword_digestカラムを追加。

Gemfile

source 'https://rubygems.org'

gem 'rails',                '4.2.0'
gem 'bcrypt',               '3.1.7'
.
.
.

User modelにhas_secure_password methodを追加。

そうなると、password,password_confirmationという2つの属性が必要になる。

user_test.rb

def setup
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

setupメソッドで定義しているUser.newに追加。

6.3.3 Minimum password length

パスワードの最低限の長さを決めないと。1文字だけでも困るのでw

Userモデルのテスト

user_test.rb

.
.
.
test "password should have a minimum length" do
    @user.password = @user.password_confirmation = 'a' * 5
    assert_not @user.valid?
  end

passwordとpassword_confimationが一致していて、なおかつ5文字しかないよと。
それでは通らないよね、というテストメソッドを定義。

user.rb

validates :password, length: { minimum: 6 }

パスワードは最低6文字だよ、というバリデーションをUserモデルに定義。

6.3.4 Creating and authenticating a user

$ rails c
>> User.create(name: "Michael Hartl", email: "mhartl@example.com", password: "foobar", password_confirmation: "foobar")

実際にユーザーを作成。

>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"

password_digestが生成されているのが確認できる。(いったんrails consoleから抜けて、入り直したほうがいいかも)

As noted in Section 6.3.1, has_secure_password automatically adds an authenticate method to the corresponding model objects.

>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-25 02:58:28", updated_at: "2014-07-25 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">
>> !!user.authenticate("foobar")
=> true

trueかどうかだけを確認したいなら、!!が使える。


herokuにpushしてあとはマイグレートする。
herokuコンソールに入って確認してみる。

$ heroku run console --sandbox
>> User.create(name: "Michael Hartl", email: "michael@example.com",
?>             password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "michael@example.com",
created_at: "2014-08-29 03:27:50", updated_at: "2014-08-29 03:27:50",
password_digest: "$2a$10$IViF0Q5j3hsEVgHgrrKH3uDou86Ka2lEPz8zkwQopwj...">

Exercises

1.assert_equalメソッドを使用したテスト

test/models/user_test.rb

test "email addresses should be saved as lower-case" do
    mixed_case_email = "Foo@ExAMPle.CoM"
    @user.email = mixed_case_email
    @user.save
    assert_equal mixed_case_email.downcase, @user.reload.email
  end

そして

emailを小文字にしたものとDBから再取得したemail(@user.reload.email)がイコールであるかをasser_equalでチェック。

つまりちゃんと小文字でDBに保存されているかどうか。
その確認のためには一旦saveしてからの>reloadによる再取得、という流れ

Userモデルのbefore_saveを一旦コメントアウトしてみる。そうするとテストこける。
before_saveを復活させるとテスト通る。ちゃんと保存する前に小文字に変換してますよ、ということで。


DBからレコードを再取得する。

reload - リファレンス - Railsドキュメント

Model.reload


2.そのbefore_save methodをbefore_save { email.downcase! }にする。email属性を直接小文字にしてしまう。

3.Disallowing double dots

foo@bar..comのようにドットが2つ続いてるのはいけない。
というわけで正規表現・・・うっ、頭が

user.rb

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i

ドットの後には必ず文字が入ってないといけない。


Guardが動いてくれなくて

guardで

You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install とでてしまいました。
bundle exec rake testは大丈夫。

bcryptはちゃんと入ってるけどなあ・・・。

調べてみると

Rails: "This error occurred while loading the following files: bcrypt" - Stack Overflow

WEBrickを再起動しなよ! みたいなこと言ってるけど、(その時は)立ち上げてないんだよなあ・・・。   Nitrous.ioのBoxを再起動後、guard起動>return押してall testsを走らせる>オールグリーンだった。

*あとでrails sしたときに

 server is already running. Check /home/action/workspace/sample_app/tmp/pids/server.pid.

と出ました(ということはやっぱり起動させていたんだろうか?)

サーバーのpidを確認

$ cat /home/action/workspace/sample_app/tmp/pids/server.pid.
573

とでたので、それをkillしました。

$ kill -9 573

するとrails sでサーバー動きました。