静的型付け言語原理主義者によるRailsチュートリアル(第6章)

image

第6章 ユーザーのモデルを作成する

  • 第6章から第12章を通して Rails のログインと認証システムを一通り開発する
  • 第6章ではユーザー用のデータモデルの作成と、データを保存する手段の確保について

6.1 User モデル

  • ユーザー登録のために必要なのは、まずは情報を保存するためのデータ構造を作成すること
  • Rails ではデータモデルとして扱うデフォルトのデータ構造のことをモデルと呼ぶ
    • データ永続化のデフォルトの選択肢はデータベース
    • データベースとやりとりするデフォルトのライブラリは Active Record
      • SQLを意識しなくてよい
        • 意識しなくてよいことのメリットよりも、ブラックボックス化するデメリットの方が大きいと思えてならない
    • Rails にはマイグレーション機能もある
    • データ定義を Ruby で記述可能
      • Rails はデータベースの細部をほぼ完全に隠蔽し、切り離してくれる

6.1.1 データベースの移行

  • 永続化可能なユーザーのモデルを作成する
  • モデルを作成するときには generate model コマンドを使用する
    • Rails の慣習として、コントローラ名には複数形を、モデル名には単数形を用いる
    • マイグレーションファイルやテストクラスも作成される便利コマンド
    • マイグレーションファイルによりインクリメンタルな変更が可能になる
    • マイグレーション自体はデータベースに与える変更を定義した change メソッドの集合体
    • t.timestamp により created_atupdated_at のマジックカラムが生成される
      • このカラム名、業務で見たことのあるやつだ…!!
      • 作成・更新時に自動更新される
  • マイグレーションは rails db:migrate で実行可能
    • db/schema.rb が更新される
  • rails db:rollback でもとに戻すことも可能
    • ロールバックすると db/schema.rb の内容も元に戻る
    • 中では drop_table を呼び出している
      • こわやこわや

6.1.2 model ファイル

  • 以後モデル用ファイルを理解することに専念する

6.1.3 ユーザーオブジェクトを作成する

  • Rails コンソールをサンドボックスモードで起動
    • すべての変更は終了時にロールバックされる
  • User.new を引数無しで呼んだ場合はすべての属性が nil
  • user.valid? でオブジェクトの有効性が確認できる
  • DBに保存するためには user.save する必要がある
    • これだけでSQLが実行される
    • 当然クエリは指定できない
  • Active Record では User.create でモデルの生成と保存を同時に行うことが可能
    • 相変わらず強力。。。
    • destroy で delete 可能
      • SQLはもちろん自動的に実行される
      • destroy しても削除されたオブジェクトはまだメモリ上に残っている点に注意
        • 本当に削除されたのかはどうやって分かるのか??

6.1.4 ユーザーオブジェクトを検索する

  • Active Record ではオブジェクトを検索する方法が複数ある
    • find メソッドで結果が得られなかったときは例外が発生する
      • ActiveRecord::RecordNotFound
    • 属性でユーザーを検索することも可能
    • find だけでなく firstall もある

6.1.5 ユーザーオブジェクトを更新する

  • 基本的な更新方法は2つ

    • ひとつは属性を個別に代入する
      • save を忘れずに
    • もうひとつは update_attributes を使う方法

      >> user.update_attributes(name: "The Dude", email: "dude@abides.org")
      => true
      >> user.name
      => "The Dude"
      >> user.email
      => "dude@abides.org"
  • update_attributes メソッドは属性のハッシュを受け取り、成功時には更新と保存を続けて同時に行う

    • どんな SQL が吐かれているのか?単純に update しているだけ?
    • 検証に一つでも失敗すると、この処理は成功しない

6.2 ユーザーを検証する

  • User モデルの属性に制限を設ける
    • Active Record の Validationを使う

6.2.1 有効性を検証する

  • バリデーション機能実装には TDD がぴったり
  • User 用のテストを書いていく
  • setup メソッド
    • 各テストの直前に実行される
      • よくあるテスティングフレームワークと同じ
  • rails test:models コマンドでモデルのテストだけが実行可能

6.2.2 存在性を検証する

  • もっとも基本的なバリデーションは存在性
  • User モデルに存在性に関するテストを追加する
  • validates メソッドに presence:true の引数を与えると、属性の存在を検査できる

    class User < ApplicationRecord
    validates :name, presence: true
    end
  • 変数が有効かどうかを valid? メソッドでチェック可能になる

    • すなわち、name 属性が空の場合は検証に失敗するということ
    • erros.full_messages を使えばさらに詳細なエラーが確認できる
    • errors.messages だとハッシュでエラー詳細が確認可能

6.2.3 長さを検証する

  • User モデルの属性に名前の長さの制限を加える

    class User < ApplicationRecord
    validates :name,  presence: true, length: { maximum: 50 }
    validates :email, presence: true, length: { maximum: 255 }
    end
  • valid? メソッドで長さも検証可能になる

  • 検証に失敗した場合は同様に errors.messages で詳細が確認可能

6.2.4 フォーマットを検証する

  • メールアドレスの有効性を検証する
    • パターンが多いので大変
  • メールアドレスのフォーマット検証用に format というオプションを使用する

    validates :email, format: { with: /<regular expression>/ }
  • 引数に正規表現を取る

    • Rubular というサイトで学習可能
      • 正規表現を今まで避けてきたけどやってみると結構わかりやすい気がしてきた
  • User モデルに検証用定数を追加

    • 大文字で始まる名前は Ruby では定数を意味する

      VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
      validates :email, presence: true, length: { maximum: 255 },
                  format: { with: VALID_EMAIL_REGEX }
  • ただし、上記パターンでは foo@bar..com を検出できないので、以下のように変更する

    class User < ApplicationRecord
    validates :name, presence: true, length: { maximum: 50 }
    VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
    validates :email, presence:   true, length: { maximum: 255 },
                    format:     { with: VALID_EMAIL_REGEX }
    end
  • ()* がポイント

    • () でグループ化して
    • * は直前の文字がないか、直前の文字が1つ以上連続するかをチェック

6.2.5 一意性を検証する

  • メールアドレスの一意性を検証する

    • validates メソッドの :unique オプションを使用

      test "email addresses should be unique" do
      duplicate_user = @user.dup
      @user.save
      assert_not duplicate_user.valid?
      end
  • @user.dup@user と同じメールアドレスのユーザーは作成できないことをテストする

  • 上記テストをパスさせるために、 User モデルの email のバリデーションに uniquness:true を追加する

  • 通常メールアドレスでは大文字小文字が区別されないため、検証でもこの点を考慮する必要がある

  • バリデーションを uniqueness: { case_sensitive: false } に変更すると、 uniquness:true のまま大文字小文字を区別しなくなる

  • しかしこの時点では Active Record がデータベースレベルでは一意性を保証していないという問題が残る

    • データベースレベルでも一意性を強制することで解決する
      • 具体的には email カラムにインデックスを追加し、そのインデックスが一位であるようにする
  • email インデックスの追加にはデータモデリングの変更が必要

    • Rails ではマイグレーションでインデックスを追加する
      • migration ジェネレーターを使用
      • まさかマイグレーションファイルを用意し、 Rails のメソッドでインデックスを追加できるとは。。。
  • この時点では DB 用のサンプルデータが含まれている fixture 内で一意性の制限が保たれていないためテストが通らない

    • fixutre とは
      • 第8章で再登場するので今回はスルー
  • データベースのアダプタによっては常に大文字小文字を区別するインデックスを使っているとは限らない

    • DBに保存される直前にすべての文字列を小文字に変換する
    • Active Record のコールバックメソッドを使用

      • 特定の時点で呼び出されるメソッド
      • 今回はオブジェクトが保存される時点で処理を実行したいので before_save を使用する

        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: { case_sensitive: false }
        end
  • 左辺の self は省略できないが、右辺の self は省略可能

  • self.email = email.downcaseemail.downcase! と書いても結果は変わらない

    • downcase! は属性を直接変更できる

6.3 セキュアなパスワードを追加する

  • セキュアパスワード
    • ユーザーにパスワードとパスワードの確認を入力させる
    • パスワードをハッシュ化したものをDBに保存する
  • ユーザーの認証手順
    • パスワードの送信
    • ハッシュ化
    • DB内のハッシュ化された値との比較
      • 比較結果が一致すれば送信されたパスワードが正しいと認識される

6.3.1 ハッシュ化されたパスワード

  • セキュアなパスワード実装は has_secure_password という Rails メソッドを呼び出すだけでほとんど終わってしまう
    • 相変わらずなんて強力な。。。
  • User モデルに has_secure_password メソッドを追加
    • セキュアにハッシュ化したパスワードをDB内の password_digest という属性に保存できるようになる
    • passwordpassword_confirmation という仮想的なペア属性が使えるようになる
      • 存在性と値が一致するかどうかのバリデーションも追加される
    • authenticate メソッドが使えるようになる
  • 「この魔術的な has_secure_password 機能を使えるようにするには一つだけ条件があります。」
    • 自分で魔術って言っちゃったよ
    • モデル内に password_digest という属性が含まれていることが条件
  • password_digest カラム用のマイグレーションを実施する
    • 末尾に to_users を付けたマイグレーション名にしておくと、 users テーブルにカラムを追加するマーグレーションが Rails によって自動的に追加される
      • どこまでやってくれるんだ Rails は。。。
  • has_secure_password を使ってパスワードをハッシュ化するためには、ハッシュ関数の bcrypt が必要
    • bcrypt gem を Gemfile に追加しておく

6.3.2 ユーザーがセキュアなパスワードを持っている

  • テストが通るようにしていく
    • has_secure_password には仮想的な password 属性と password_confirmation 属性に対してバリデーションする機能が強制的に追加されているため、その対応が必要

6.3.3 パスワードの最小文字数

  • Rails でパスワードの長さを設定する方法はたくさんある
    • たくさんある。。。
    • 簡単でないことと最小文字数(6文字)を設定する
  • maximum と同様に minimum というオプションで最小文字数のバリデーションを実装できる

    validates :password, length: { minimum: 6 }
  • has_secure_password メソッドは存在性のバリデーションもしてくれるが、新しくレコードが追加されたときだけに適用される

    • 更新はバリデーションが効かない

6.3.4 ユーザーの作成と認証

  • User モデルの基本部分が完成
    • Web からのユーザー登録はまだできないので、Rails コンソールから手動で追加する
    • 追加後に password_digest 属性が参照可能
    • さらに authenticate メソッドに正しいパスワードを渡すと、モデルがユーザーオブジェクトを返す

6.4 最後に

  • ゼロから User モデルを作成し、name, email, password の各属性を追加、さらにそれらのバリデーションも追加した
  • パスワードをセキュアに認証できる機能も実装
  • これらが12行で実現できた
    • Rails 恐ろしや

所感

Rails チュートリアルもようやく折返し地点まできた。今まで避けてきた正規表現にトライし、見た目ほど難しくないということが実感できたのが大きな収穫だった。Rails は相変わらず強力な機能を発揮している。わずか12行のコードで多機能なモデルを構築できたことには驚きしかない。当初は強力すぎるがゆえにマイナスの印象が強かった Rails だが、徐々に「適切に使えばこれほど便利なものはない」という認識に変わりつつある。それがどれほど難しいかはプロダクトコードで使用してみないと分からないのだろうが。


3857 Words

2020-01-17 11:37 +0000