見出し画像

Ruby on Rails6習得編 Ruby on Rails 6 実践ガイド Chapter7 ユーザー認証(1)

こちらの書籍について学んだことです。

Chapter7 ユーザー認証(1)

ユーザー認証の仕組みを作るための準備作業として、職員のメールアドレス、パスワードなどを記録するデータベーステーブルと、そのテーブルを操作するためのモデルクラスを作成する。また、セッションという概念についても学習する。

7.1 マイグレーション

この節では、Naukis2の主要な利用者である職員(staff members)の情報を記録するデータベーステーブルstaff_membersを作成する。
ここからstaffのみの作成になるので、演習をやるときは参考にする。

7.1.1 各種スケルトンの生成

最初に、次のコマンドでmodelを作成する。

bin/rails g model StaffMember

----------------------------------------
bash-4.4$ bin/rails g model StaffMember
Running via Spring preloader in process 80
     invoke  active_record
     create    db/migrate/20200224154257_create_staff_members.rb
     create    app/models/staff_member.rb
     invoke    rspec
     create      spec/models/staff_member_spec.rb
bash-4.4$ 
----------------------------------------

ログ見て分かるとおり、以下のファイルが作成される。
db/migrate/20200224154257_create_staff_members.rb
app/models/staff_member.rb
spec/models/staff_member_spec.rb

1つ目はマイグレーションスクリプト
データベースの構造(スキーマ)を変更するrubyスクリプト
テーブルを追加したり、テーブルの定義を変更したりするのが主な役割
ファイル名の先頭14桁はタイムスタンプ 日本とは9時間の誤差あり

2つ目はStaffMemberクラスを定義するファイル

3つ目はStaffMemberクラスのためのspecファイル

もし間違えたら下記のコマンド
bin/rails destroy model StaffMember

----------------------------------------
bash-4.4$ bin/rails destroy model StaffMember
Running via Spring preloader in process 86
     invoke  active_record
     remove    db/migrate/20200224154257_create_staff_members.rb
     remove    app/models/staff_member.rb
     invoke    rspec
     remove      spec/models/staff_member_spec.rb
bash-4.4$ 
----------------------------------------

3つのファイルが削除されたことが確認できたらもっかい作る

bin/rails g model StaffMember

----------------------------------------
bash-4.4$ bin/rails g model StaffMember
Running via Spring preloader in process 94
     invoke  active_record
     create    db/migrate/20200224154906_create_staff_members.rb
     create    app/models/staff_member.rb
     invoke    rspec
     create      spec/models/staff_member_spec.rb
bash-4.4$
----------------------------------------

7.1.2 マイグレーションスクリプト

初期状態のマイグレーションファイルの中身は次の通り

db/migrate/xxx_create_staff_members.rb
----------------------------------------
class CreateStaffMembers < ActiveRecord::Migration[6.0]
 def change
   create_table :staff_members do |t|
     t.timestamps
   end
 end
end
----------------------------------------

ActiveRecord::Migration[6.0]を継承したCreateStaffMembersクラスが定義されてる

インスタンスメソッドchangeの中でデータベースにテーブルを追加する手順を記述している。

create_tableメソッドは引数に指定した名前のテーブルを作成する。

そのテーブルが持つべきカラムの型や名前はブロックの中で記述する。

ブロック変数tにはTableDefinitionオブジェクトがセットされる。
このオブジェクトの各種メソッドを呼び出すことでテーブルの定義を行う。
初期状態ではtimestampsメソッドだけが定義されてる。
これはcreated_atとupdated_atという名前を持つ日時型のカラムをテーブルに追加するメソッド。
この2つのカラムはrailsがレコードの作成時刻と最終変更時刻を記録するために使用する。

マイグレーションスクリプトを書き換えてみる。

db/migrate/xxx_create_staff_members.rb
----------------------------------------
class CreateStaffMembers < ActiveRecord::Migration[6.0]
 def change
   create_table :staff_members do |t|
     t.string :email, null: false # メールアドレス
     t.string :family_name, null: false # 姓
     t.string :given_name, null: false # 名
     t.string :family_name_kana, null: false # 姓(カナ)
     t.string :given_name_kana, null: false # 名(カナ)
     t.string :hashed_password # パスワード
     t.date :start_date, null: false #開始日
     t.date :end_date #終了日
     t.boolean :suspended, null: false, default: false #無効フラグ
     t.timestamps
   end
   
   add_index :staff_members, "LOWER(email)", unique: true
   add_index :staff_members, [ :family_name_kana, :given_name_kana]
 end
end
----------------------------------------

以下の行を見てみる。
t.string :email, null: false # メールアドレス

TableDefinitionオブジェクトのstringメソッドを呼んで、文字列型のカラムemailを定義している。
オプションnullにfalseを指定すると、このカラムにNOT NULL制約を設定する。
この制約が課せられたカラムにNULL値をセットしようとするとデータベース管理システム(PostgreSQL)側でエラーになる。

nullはfalseですよ、つまりnullはダメですよ、ということになるからエラーになるとおぼえとこう。

hashed_passwordはNOT NULLになっていない。
このカラムがNULLの場合は、パスワードが未設定であることを示したいからである。

開始日と終了日はログインできる期間を限定するために使用する。

suspendedは一時的にアカウントを無効にするために使う。
trueだとログインできない。

コラム TableDefinition#columnメソッド
string,date,booleanの3種を使用した。
他にも整数型や時刻型など、カラム型ごとに専用のメソッドが用意されている。
以降の方で順次紹介されている。
利用できるメソッドやオプションの一覧はどうやって調べればいいか。

Ruby on RailsのAPI検索サイト https://api.rubyonrails.orgでstringを検索しても、TableDefinitionオブジェクトのstringメソッドの説明には辿り着けない。
検索すべきはcolumnメソッド。
実はstringメソッドはcolumnメソッドの短縮メソッド(shorthand)として定義されており、APIドキュメントでもcolumnメソッドの項で説明されている。

次の2つのメソッド呼び出しはまったく同じ意味。
t.column :email, :string, :null: false
t.string :email, :null false

なるほど!
TableDefinitionオブジェクトの正確なクラス名は
ActiveRecord::ConnectionAdapters::TableDefinition

ブラウザで
https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html
を開き、columnメソッドの項を参照してみる。

7.1.3 インデックス(索引)の設定

下記について見てみる。
add_index :staff_members, "LOWER(email)", unique: true

インデックスを作成する理由は、そのカラムを用いた検索やソートの高速化のため。

ここでは更にunique: trueオプションを用いて、カラムemailにunique制約(一意性制約)を課している。
これにより、同じメールアドレスを持つ職員が登録されることがなくなる。

第一引数はテーブル名、第2引数は通常:emailのようなインデックスを作成する対象のカラム名をシンボルで指定する。
しかし、ここでは"LOWER(email)"という文字列が指定されている。

この場合、文字列全体を式として評価した値がインデックス作成で使われることになる。
LOWER関数は文字列に含まれるすべての大文字のアルファベットを小文字のアルファベットに変換して返す。

なぜそんなことをする必要があるのか?
通常インターネットを経由してメールを交換する際、メールアドレスの大文字と小文字の違いは無視される。

ただ、PostgreSQLは大文字と小文字を区別しちゃう。そうなると同一メアドなのに複数のカラムが登録されることに繋がっちゃう。
だから小文字化する。

下記について見てみる。
add_index :staff_members, [ :family_name_kana, :given_name_kana]

フリガナでソートするときに便利

コラム インデックスの効果
インデックスは、検索やソートを高速化するためのデータ構造
探す効率は上がるけど、メモリとか余分に使うし、テーブルへの書き込みにプラスアルファの書き込み時間とか必要になってくるからむやみにインデックスを作成すればいいというものでもない。

7.1.4 マイグレーションの実行

マイグレーションスクリプトを実行してデータベースの構造を変更することをrails用語でマイグレーションという。
じゃあ、webコンテナ側のターミナルで次のコマンドを実行してみよう。

bin/rails db:migrate

----------------------------------------
bash-4.4$ bin/rails db:migrate
== 20200224154906 CreateStaffMembers: migrating ===============================
-- create_table(:staff_members)
  -> 0.0359s
-- add_index(:staff_members, "LOWER(email)", {:unique=>true})
  -> 0.0294s
-- add_index(:staff_members, [:family_name_kana, :given_name_kana])
  -> 0.0190s
== 20200224154906 CreateStaffMembers: migrated (0.0847s) ======================
bash-4.4$ 
----------------------------------------

railsは実行済のマイグレーションスクリプトのタイムスタンプをschema_migrationsという名前のテーブルに記録している。
そのため、繰り返しbin/rails db:migrateを実行してもマイグレーションスクリプトは1度しか実行されない。

bin/rails db:migrate

----------------------------------------
bash-4.4$ bin/rails db:migrate
----------------------------------------


もしマイグレーション実行中にエラーが発生した場合は、マイグレーションスクリプトを見直した上で下記コマンドを実行する。
今回は実験的にやってみる

bin/rails db:migrate:reset
----------------------------------------
bash-4.4$ bin/rails db:migrate:reset
Dropped database 'baukis2_development'
Dropped database 'baukis2_test'
Created database 'baukis2_development'
Created database 'baukis2_test'
== 20200224154906 CreateStaffMembers: migrating ===============================
-- create_table(:staff_members)
  -> 0.0432s
-- add_index(:staff_members, "LOWER(email)", {:unique=>true})
  -> 0.0246s
-- add_index(:staff_members, [:family_name_kana, :given_name_kana])
  -> 0.0214s
== 20200224154906 CreateStaffMembers: migrated (0.0895s) ======================
----------------------------------------

あ、いったんデータベースを削除した上で新たにデータベースを作成してマイグレーションを実行するのか!なるほど

ほんで、マイグレーションするとdbの下にschema.rbができる。

7.1.5 主キー

staff_membersテーブルがどのようなカラムを持っているのかを調べる場合は、次のコマンドが便利。

bin/rails r "StaffMember.columns.each { |c| p [c.name,c.type] }"

このコマンドは、カラムの名前と型のリストをターミナルに出力する。

----------------------------------------
bash-4.4$ bin/rails r "StaffMember.columns.each { |c| p [c.name,c.type] }"
Running via Spring preloader in process 247
["id", :integer]
["email", :string]
["family_name", :string]
["given_name", :string]
["family_name_kana", :string]
["given_name_kana", :string]
["hashed_password", :string]
["start_date", :date]
["end_date", :date]
["suspended", :boolean]
["created_at", :datetime]
["updated_at", :datetime]
bash-4.4$ 
----------------------------------------

これは便利!

idというのが追加してないのに勝手に追加されている。
これはマイグレーションによって勝手に追加される。
主キーという特別なカラム。
レコードの識別に使用される。

何らかの理由で主キーにid以外の名前を採用したい場合は、create_tableメソッドのprimary_keyオプションで指定する。
たとえば主キーの名前をmember_idにする場合は、マイグレーションスクリプトの3行目を次のように変更する。

create_table :staff_members, primary_key: "member_id" do |t|

create_tableの行を変えるのか、なるほどね!

コラム db/schema.rbファイルの役割

開発したrailsアプリケーションのソースコード一式を他人にわたす場合、必ずdb/schema.rbファイルをその中に含める。このファイルはデータベースの初期化に必要となる。一般に、データベース初期化は以下の手順で行う。

1.適宜config/database.ymlを編集する。
2.bin/rails db:setupコマンドを実行する。

bin/rails db:setupコマンドは、以下のコマンドを順に実行するのと同じ効果を持つ。

bin/rails db:create
bin/rails db:schema:load
bin/rails db:seed

2番目のコマンドがdb/schema.rbを用いてデータベースの構造を復元する。
3番目のコマンドは、後述するシードデータをデータベースに投入する。


7.2 モデル

モデルという言葉について基本事項をおさらいしたあと、パスワードをハッシュ関数で処理するメソッドをStaffMemberモデルに実装する。
また、シードデータ(初期データ)をデータベースに投入する方法を学ぶ。

7.2.1 モデルの基礎知識

ActiveRecord
ActiveRecordはRuby on Railsの主要なコンポーネントの1つ。
その役割は、Rubyの世界とリレーショナル・データベースの世界を結びつけること。
Rubyの基本要素は「オブジェクト」。リレーショナルデータベースの基本要素は「リレーション」。
ActiveRecordのようなソフトウェアのことを一般にオブジェクトリレーショナルマッパー
object - relational mapper, ORM
と呼ぶ。

リレーションとは

データベース用語のリレーション(relation)は誤解されやすい。
テーブルとテーブル間の関係性(relationship)をリレーションと呼ぶ人がいるが、これは間違い。
リレーションとは、列と行からなるデータ構造一般を意味する。

このような構造を持つものの代表がテーブル。
このため、リレーションとテーブルはほぼ同義語として使われることもある。
しかし、あくまでリレーションはデータ構造の名前である点に注意。
SELECT文でテーブルから値の一部を抽出した結果も、リレーションというデータ構造を持つ。
なお、テーブルとテーブルの間の関係性は、関連付け(association)と呼ぶ。

モデルとは

ActiveRecordが提供する基底クラスがActiveRecord::Base
このクラスを継承したクラスをモデルクラスあるいはモデルと呼び、そのインスタンスをモデルオブジェクトと呼ぶ。

app/modelsディレクトリにあるapplication_record.rbおよびstaff_member.rbを見てみる。

app/models/application_record.rb
----------------------------------------
class ApplicationRecord < ActiveRecord::Base
 self.abstract_class = true
end
----------------------------------------
app/models/staff_member.rb
----------------------------------------
class StaffMember < ApplicationRecord
end
----------------------------------------

StaffMember < ApplicationRecord < ActiveRecord::Base
の関係であることが確認できる。

どっちもモデルクラス。
ただ、self.abstract_class=trueってすると抽象クラスになる。
抽象クラスがインスタンス化されることはない。
ApplicationRecordクラスには全モデルクラスに共通するメソッドを定義する。

ActiveRecord::Baseを継承しない非ActiveRecordモデルもある。

通常、1つのモデルクラスは1つのデータベーステーブルと対応関係を持つ。
例外もある。Chapter16でやる。
単一テーブル継承(single table inheritance, STI)の仕組みを用いたときで、その場合は複数のモデルクラスが1つのテーブルと関連付けられる。

原則として、モデルクラスの名前からテーブルの名前を導くことができる。
クラス名をばらばらにし、すべて小文字に変え、最後の単語を複数形にし、アンダースコアで連結すればテーブル名になる。
変異の仕方は下記
StaffMember
Staff Member
staff member
staff members
staff_members

つまり、StaffMemberモデルはstaff_membersテーブルに対応する。

テーブルの各カラムはモデルの属性と対応関係を持つ。

staff_membersテーブルにemailというカラムがあり、変数mをStaffMemberクラスのインスタンスであるとすれば、
m.emailメソッドを通じて特定のレコードのemailカラムの値を参照することができる。

コラム テーブル名と属性名のカスタマイズ

しばしば誤解されるのが、railsの命名規約の意味。
railsはデータベースに対してstaff_membersのようなテーブル名を押し付けるので不自由だと考える人がいるが、
モデル名とテーブル名の関連付けは自由に変更できる。
たとえば、StaffMemberモデルとM_SHOKUINテーブルを結びつけたければ、次のようにテーブル名を指定できる。

app/models/staff_member.rb
----------------------------------------
class StaffMember < ApplicationRecord
 self.table_name = "M_SHOKUIN"
end
----------------------------------------

railsを用いずに開発されたシステムをrailsで移植する場合、このテクニックを用いるといい。

また、カラム名とは異なる名前の属性を使いたい場合には、クラスメソッドalias_attributeで別名(alias)を設定できる。

app/models/staff_member.rb
----------------------------------------
class StaffMember < ApplicationRecord
 alias_attribute :section, :BUMON
end
----------------------------------------

第1引数が別名
第2引数がカラム名に由来する本来の属性
BUMONというカラム名に対応する属性BUMONにsectionという別名を設定している。
BUMONカラムの値はm.BUMONまたはm.sectionで参照することができる。

7.2.2 ハッシュ関数

StaffMemberモデルに、平文->ハッシュ関数->hashed_passwordにセット、とする機能を追加する。

gemにあるbcryptの提供するハッシュ関数を使う。変換後の値はハッシュ値と呼ばれる。60文字。
データベースに平文を記録しない。ハッシュ値を記録する。

以下のようにする。

app/models/staff_member.rb
----------------------------------------
class StaffMember < ApplicationRecord
 # 平文のパスワードをハッシュ関数で処理してhashed_passwordに格納する
 def password = (raw_password)
   # 文字列が渡されたらハッシュ化
   if raw_password.kind_of?(String)
     self.hashed_password = BCrypt::Password.create(raw_password)
   # nilはそのままセット
   elsif raw_password.nil?
     self.hashed_password = nil
   end
   # それ以外は何もしない
 end
end
----------------------------------------

specファイルを書いてテストする。

spec/models/staff_member_spec.rb修正前
----------------------------------------
require 'rails_helper'
RSpec.describe StaffMember, type: :model do
 pending "add some examples to (or delete) #{__FILE__}"
end
----------------------------------------

それを次のように書き換える。
(シングルクォートはダブルにする)

spec/models/staff_member_spec.rb修正後
----------------------------------------
require 'rails_helper'
RSpec.describe StaffMember, type: :model do
 describe "#password=" do
   example "文字列を与えると、hashed_passwordは長さ60の文字列になる" do
     # StaffMemberインスタンスを作成する
     member = StaffMember.new
     # passwordに文字列を入れるだけでハッシュ値化されることを確認したい
     member.password = "baukis"
     
     # hashed_passwordは文字列である
     expect(member.hashed_password).to be_kind_of(String)
     
     # hashed_passwordの長さは60固定である
     expect(member.hashed_password.size).to eq(60)
   end
 end
end
----------------------------------------

もうひとつエグザンプルを追加する

spec/models/staff_member_spec.rb修正後
----------------------------------------
require 'rails_helper'
RSpec.describe StaffMember, type: :model do
 describe "#password=" do
   example "文字列を与えると、hashed_passwordは長さ60の文字列になる" do
     # StaffMemberインスタンスを作成する
     member = StaffMember.new
     # passwordに文字列を入れるだけでハッシュ値化されることを確認したい
     member.password = "baukis"
     
     # hashed_passwordは文字列である
     expect(member.hashed_password).to be_kind_of(String)
     
     # hashed_passwordの長さは60固定である
     expect(member.hashed_password.size).to eq(60)
   end
   
   example "nilを与えると、hashed_passwordはnilになる" do
     # StaffMemberインスタンスを作成し、初期値を与える
     member = StaffMember.new(hashed_password: "x")
     
     # パスワードにnilをセットしたときの挙動を見たい
     member.password = nil
     
     # nilであることを期待する
     expect(member.hashed_password).to be_nil
   end
 end
end
----------------------------------------

テストを実行してみる。

rspec
----------------------------------------
bash-4.4$ rspec
An error occurred while loading ./spec/models/staff_member_spec.rb.
Failure/Error:
 RSpec.describe StaffMember, type: :model do
   describe "#password=" do
     example "文字列を与えると、hashed_passwordは長さ60の文字列になる" do
       # StaffMemberインスタンスを作成する
       member = StaffMember.new
 
       # passwordに文字列を入れるだけでハッシュ値化されることを確認したい
       member.password = "baukis"
       
       # hashed_passwordは文字列である
SyntaxError:
 /apps/baukis2/app/models/staff_member.rb:3: syntax error, unexpected '=', expecting ';' or '\n'
   def password = (raw_password)
                ^
 /apps/baukis2/app/models/staff_member.rb:13: syntax error, unexpected end, expecting end-of-input
# /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `require'
# /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:22:in `block in require_with_bootsnap_lfi'
# /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/loaded_features_index.rb:92:in `register'
# /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require_with_bootsnap_lfi'
# /usr/local/bundle/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:30:in `require'
# /usr/local/bundle/gems/zeitwerk-2.2.2/lib/zeitwerk/kernel.rb:16:in `require'
# ./spec/models/staff_member_spec.rb:3:in `<top (required)>'

Finished in 0.00034 seconds (files took 17.48 seconds to load)
0 examples, 0 failures, 1 error occurred outside of examples
bash-4.4$ 
----------------------------------------

エラーになった。
def password = (raw_password)
ではなく
def password=(raw_password)
にしないといけなかった?

修正して再度rspec
----------------------------------------
bash-4.4$ rspec
....
Finished in 0.35114 seconds (files took 7.48 seconds to load)
4 examples, 0 failures
bash-4.4$ 
----------------------------------------

そうみたい。空白入れたらダメなんだね!


7.2.3 シードデータの投入

シードデータは、railsアプリケーションを正常に機能させるためにあらかじめデータベースに投入しておくデータのこと。

本書ではdevelopmentモードのデータベースに投入するデータもシードデータに加えるっぽい。

db/seed.rbが、シードデータを作るためのファイル。

db/seed.rb修正前
----------------------------------------
# This file should contain all the record creation needed to seed the database with its default values.
# The data can then be loaded with the rails db:seed command (or created alongside the database with db:setup).
#
# Examples:
#
#   movies = Movie.create([{ name: 'Star Wars' }, { name: 'Lord of the Rings' }])
#   Character.create(name: 'Luke', movie: movies.first)
----------------------------------------

これを修正する。

db/seed.rb修正後
----------------------------------------
table_names = %w(staff_members)
table_names.each do |table_name|
 path = Rails.root.join("db","seeds",Rails.env,"#{table_name}.rb")
 if File.exist?(path)
   puts "Creating #{table_name}...."
   require(path)
 end
end
----------------------------------------

意味がさっぱり分からない・・・。

次に、dbディレクトリの下にseeds/developmentというサブディレクトリを作成する。
mkdir -p db/seeds/development

seedsディレクトリもなかったけど一緒に作ってくれるんだね!なるほど!

下記コマンドで新規ファイルを作成する。
touch db/seeds/development/staff_members.rb

ほんで下記のようにする。

db/seeds/development/staff_members.rb
----------------------------------------
StaffMember.create!(
 email: "taro@example.com",
 family_name: "山田",
 given_name: "太郎",
 family_name_kana: "ヤマダ",
 given_name_kana: "タロウ",
 password: "password",
 start_date: Date.today
)
----------------------------------------

これpasswordはどうなるんだろうか・・・?

ここまで書いた内容を反映する。
bin/rails db:seed
これを、シードデータを投入するという。
やり直す場合は
bin/rails db/reset
とする。

それぞれのログを取ったあと、シードデータを投入する。

bin/rails db:seed
----------------------------------------
bash-4.4$ bin/rails db:seed
Creating staff_members....
bash-4.4$ 
----------------------------------------

みじかっ!!!

bin/rails db:reset
----------------------------------------
bash-4.4$ bin/rails db:reset
Dropped database 'baukis2_development'
Dropped database 'baukis2_test'
Created database 'baukis2_development'
Created database 'baukis2_test'
Creating staff_members....
bash-4.4$ 
----------------------------------------

これはあれだね、マイグレーションの時と同じで、リセットしてから作られてるね!

正しくデータが投入されたかどうかは、StaffMemberのインスタンス数を出力してみることで確認する。
また、hashed_password属性の値も出力してみる。
個人的に気になっているpasswordがどうなっているかも出力してみる。

bin/rails r "puts StaffMember.count"
bin/rails r "puts StaffMember.first.hashed_password"
bin/rails r "puts StaffMember.first.password"

----------------------------------------
bash-4.4$ bin/rails r "puts StaffMember.count"
Running via Spring preloader in process 417
1
bash-4.4$ bin/rails r "puts StaffMember.first.hashed_password"
Running via Spring preloader in process 422
$2a$12$hZNxrPZlkmAGA6Lvl9fFq.EQIZwHb/esBgciHK/bPVg/rC5yuq4W2
bash-4.4$ bin/rails r "puts StaffMember.first.password"
Running via Spring preloader in process 427
Please specify a valid ruby command or the path of a script to run.
Run 'rails runner -h' for help.
undefined method `password' for #<StaffMember:0x00005633a25a92c8>
Did you mean?  password=
bash-4.4$ 
----------------------------------------


7.3 セッション

ログイン・ログアウト機能を実現するための鍵となる概念、セッションについて学ぶ。


7.3.1 セッションとは

railsでは、セッションは3つの意味で使われている。

1.クライアントがシステムにログインしてからログアウトするまでの期間もしくは状態

2.クライアントが接続を開始してから接続を切断するまでの期間もしくは状態

3.クライアントが接続を開始してから接続を切断するまで、railsアプリケーションがクライアントごとに維持するデータ

本書では1の意味でセッションという言葉を使用する。
3つ目は、セッションオブジェクトという言葉を使用する。

7.3.2 current_staff_memberメソッドの定義

名前空間staffに属するすべてのコントローラにcurrent_staff_memberというprivateメソッドを与えるため、Staff::Baseというクラスを定義する。
app/controllers/staffディレクトリに新規ファイルbase.rbを次のような内容で作成する。

touch app/controllers/staff/base.rb

app/controllers/staff/base.rb修正後
----------------------------------------
class Staff::Base < ApplicationController
 private def current_staff_member
   if session[:staff_member_id]
     @current_staff_member ||= StaffMember.find_by(id: session[:staff_member_id])
   end
 end
 
 helper_method :current_staff_member
end
----------------------------------------
app/controllers/staff/top_controller.rb修正後
----------------------------------------
class Staff::TopController < Staff::Base
 def index
   render action: "index"
 end
end
----------------------------------------

これで、継承関係が以下のように変わる

修正前
Staff::TopController < ApplicationController
修正後
Staff::TopController < Staff::Base < ApplicationController

current_staff_memberは、現在ログインしているStaffMemberオブジェクトを返すメソッド
遅延初期化というテクニックを用いているらしい(は?)

current_staff_member呼び出し初回はfind_byが実行される
current_staff_member呼び出し2回目以降は実行されない
これでfind_byが多くても1回しか呼ばれない

これを遅延初期化と呼ぶらしい

sessionはセッションオブジェクトを返すメソッド
sessionに:staff_member_idというキーがあれば@current_staff_memberに値をセットしてくれる

セッションオブジェクトはクッキーの中に保存される。

helper_methodは、引数に指定したシンボルと同名のメソッドをヘルパーメソッドとして登録する。これでVIEWでも同名メソッドが使えるようになる
app/helpers/application_helper.rbに定義したのと同じ効果を得られる。

7.3.3 ルーティングの決定

職員のログイン・ログアウト機能を実現するためには、以下の3つのアクションを作成しなければならない。
1.ログインフォームを表示する。
2.ログインする(ユーザー認証)
3.ログアウトする

その前に考えておくべきことがある。それはルーティング。
具体的には、それぞれのアクションに対して以下の4つの項目を決める。
1.HTTPメソッド
2.URLパス
3.コントローラ名
4.アクション名

HTTPメソッドの選択肢
GET,POST,PATCH,DELETE

rails4.0から更新に対応するメソッドはPUTからPATCHに変わっている。

以下のようにする。

画像1

ちなみに、railsは基本的に以下のようになっている。


画像2

アクション名は自由に決められるけど、特別な理由が無い限り、基本のアクション名から選ぶ。

すべて決まったら、config/routes.rbにルーティングを追加する。


config/routes.rb修正前
----------------------------------------
Rails.application.routes.draw do
	# 職員
	namespace :staff do
		root "top#index"
	end
	# 管理者
	namespace :admin do
		root "top#index"
	end
	# 顧客
	namespace :customer do
		root "top#index"
	end
end
----------------------------------------
config/routes.rb修正後
----------------------------------------
Rails.application.routes.draw do
	# 職員
	namespace :staff do
		root "top#index"
		get "login" => "sessions#new", as: :login
		post "session" => "sessions#create", as: :session
		delete "session" => "sessions#destroy"
	end
	# 管理者
	namespace :admin do
		root "top#index"
	end
	# 顧客
	namespace :customer do
		root "top#index"
	end
end
----------------------------------------


get "login" => "sessions#new", as: :login
について、GETメソッドによる/staff/loginへのリクエストが届いたら
staff/sessionsコントローラのnewアクションが処理する、というふうに読む。
staffという名前空間で記述されているため、URLパスとコントローラ名には"staff/"という文字列を補う必要がある。


post "session" => "sessions#create", as: :session
delete "session" => "sessions#destroy"
についても試しに読み替えてみる。

POSTメソッドによる/staff/sessionへのリクエストが届いたら
staff/sessionsコントローラのcreateアクションが処理する

DELETEメソッドによる/staff/sessionへのリクエストが届いたら
staff/sessionsコントローラのdestroyアクションが処理する

asはなに?
→ルーティング名に名前をつけるためのもの。
名前つけておくとERB内で:staff_loginや:staff_sessionというシンボルを用いてURLパスを参照できるようになる。

Baukis2では、設定ファイルによってURLパスが変化するため、ルーティングへの名前付けは必須になる。

7.3.4 リンクの設置

ページ上部のヘッダ部分の右端に、状態に応じて「ログイン」または「ログアウト」リンクを設置する。
ヘッダの部分テンプレートを名前空間ごとに独立させる。
webコンテナのターミナルで以下のコマンドを順に実行する。

----------------------------------------
mkdir app/views/staff/shared
mkdir app/views/admin/shared
mkdir app/views/customer/shared
cp app/views/shared/_header.html.erb app/views/staff/shared/
cp app/views/shared/_header.html.erb app/views/admin/shared/
cp app/views/shared/_header.html.erb app/views/customer/shared/
rm app/views/shared/_header.html.erb
----------------------------------------

実行結果は下記の通り

----------------------------------------
bash-4.4$ mkdir app/views/staff/shared
bash-4.4$ mkdir app/views/admin/shared
bash-4.4$ mkdir app/views/customer/shared
bash-4.4$ cp app/views/shared/_header.html.erb app/views/staff/shared/
bash-4.4$ cp app/views/shared/_header.html.erb app/views/admin/shared/
bash-4.4$ cp app/views/shared/_header.html.erb app/views/customer/shared/
bash-4.4$ rm app/views/shared/_header.html.erb
bash-4.4$ 
----------------------------------------

下記3つ、shared/headerになってるものを全て○○/shared/headerに修正する。
app/views/layouts/staff.html.erb
app/views/layouts/admin.html.erb
app/views/layouts/customer.html.erb

職員用のヘッダを下記に書き換える。

app/views/staff/shared/_header.html.erb
----------------------------------------
<header>
 <span class="logo-mark">BAUKIS2</span>
 <%=
   if current_staff_member
     link_to "ログアウト", :staff_session, method: :delete
   else
     link_to "ログイン", :staff_login
   end
 %>
</header>
----------------------------------------

link_toでmethodオプションが指定されていたりされていなかったりする。
指定されていない場合はGETメソッドになる。
それ以外の場合はmethodオプションを指定しなければならない。
HTMLのハイパーリンクは、GET以外実行されないが、それをrails上で許可する仕組みがこのメソッドのオプションで実現できる。

最後にスタイルシートを書き換える。

app/assets/stylesheets/staff/layout.scss
----------------------------------------
@import "colors";
@import "dimensions";
html,body{
margin: 0;
padding: 0;
height: 100%;
}
div#wrapper{
 position: relative;
 box-sizing: border-box;
 min-height: 100%;
 margin: 0 auto;
 padding-bottom: ( $wide + $moderate) * 2 + $standard_line_height;
 background-color: $gray;
}
header{
 padding: $moderate;
 background-color: $dark_cyan;
 color: $very_light_gray;
 span.logo-mark{
     font-weight: bold;
 }
 a{
 	float: right;
 	color: $very_light_gray;
 }
}
footer{
 bottom: 0;
 position: absolute;
 width: 100%;
 background-color: $dark_gray;
 color: $very_light_gray;
 p{
     text-align: center;
     padding: $moderate;
     margin: 0;
 }
}
----------------------------------------

headerのaを追加している。

これで、staffが表示されるはずである。

rspecでテスト
----------------------------------------
....
Finished in 0.31105 seconds (files took 8.37 seconds to load)
4 examples, 0 failures
----------------------------------------

開発用サーバを立てて確認

画像3

本番用サーバを立てて確認

画像4

ちょっと違う
多分アセットプリコンパイルしてないからだ

bin/rails assets:precompile RAILS_ENV=production
を実行してから再度サーバーを立ち上げ直してブラウザで見てみる。
これでできなかったら一旦保留にする。

本番用サーバを立てて確認(再挑戦)

画像5

できた!

7.4 演習問題

問題1
管理者アカウントを記録するデータベーステーブルadministratorsのためのマイグレーションスクリプトを作成してください。
このテーブルにはデフォルトで作成されるカラムid,created_at,updated_atの他に、以下のカラムを定義してください。
・email(文字列型)
・hashed_password(文字列型)
・suspended(ブーリアン型)

bin/rails g model Administrator

----------------------------------------
bash-4.4$ bin/rails g model Administrator
Running via Spring preloader in process 907
     invoke  active_record
     create    db/migrate/20200303145451_create_administrators.rb
     create    app/models/administrator.rb
     invoke    rspec
     create      spec/models/administrator_spec.rb
bash-4.4$ 
----------------------------------------
db/migrate/xxx_create_administrators.rb
----------------------------------------
class CreateAdministrators < ActiveRecord::Migration[6.0]
 def change
   create_table :administrators do |t|
     t.string :email, null: false # メールアドレス
     t.string :hashed_password # パスワード
     t.boolean :suspended, null: false, default: false #無効フラグ
     t.timestamps
   end
 end
end
----------------------------------------

問題2
データベーステーブルadministratorsのためのマイグレーションスクリプトで、式LOWER(email)に対してUNIQUE制約付きのインデックスを設定してください。

db/migrate/xxx_create_administrators.rb
----------------------------------------
class CreateAdministrators < ActiveRecord::Migration[6.0]
 def change
   create_table :administrators do |t|
     t.string :email, null: false # メールアドレス
     t.string :hashed_password # パスワード
     t.boolean :suspended, null: false, default: false #無効フラグ
     t.timestamps
   end
   
   add_index :administrators, "LOWER(email)", unique: true
 end
end
----------------------------------------

問題3
マイグレーションを実行してください。

bin/rails db:migrate

----------------------------------------
bash-4.4$ bin/rails db:migrate
warning Integrity check: Flags don't match                                     
error Integrity check failed                                                   
error Found 1 errors.                                                          

========================================
 Your Yarn packages are out of date!
 Please run `yarn install --check-files` to update.
========================================

To disable this check, please change `check_yarn_integrity`
to `false` in your webpacker config file (config/webpacker.yml).

yarn check v1.12.3
info Visit https://yarnpkg.com/en/docs/cli/check for documentation about this command.

bash-4.4$ yarn install --check-files
yarn install v1.12.3
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.11: The platform "linux" is incompatible with this module.
info "fsevents@1.2.11" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > webpack-dev-server@3.10.3" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0".
[4/4] Building fresh packages...
Done in 144.89s.
bash-4.4$ bin/rails db:migrate
== 20200303145451 CreateAdministrators: migrating =============================
-- create_table(:administrators)
  -> 0.0702s
-- add_index(:administrators, "LOWER(email)", {:unique=>true})
  -> 0.0256s
== 20200303145451 CreateAdministrators: migrated (0.0962s) ====================
bash-4.4$ 
----------------------------------------

問題4
Administratorモデルにhashed_password属性に値をセットするpassword=メソッドを追加してください。

app/models/administrator.rb
----------------------------------------
class Administrator < ApplicationRecord
 # 平文のパスワードをハッシュ関数で処理してhashed_passwordに格納する
 def password=(raw_password)
   # 文字列が渡されたらハッシュ化
   if raw_password.kind_of?(String)
     self.hashed_password = BCrypt::Password.create(raw_password)
   # nilはそのままセット
   elsif raw_password.nil?
     self.hashed_password = nil
   end
   # それ以外は何もしない
 end
end
----------------------------------------


問題5
administratorsテーブルのためのシードデータを投入するスクリプトを作成し、シードデータを投入し直してください。管理者のメールアドレスはhanako@example.com,パスワードはfoobarとしてください。


下記コマンドで新規ファイルを作成する。
touch db/seeds/development/administrators.rb

ほんで下記のようにする。

db/seeds/development/administrators.rb
----------------------------------------
Administrator.create!(
 email: "hanako@example.com",
 password: "foobar"
)
----------------------------------------
db/seed.rb修正後
----------------------------------------
table_names = %w(staff_members administrators)
table_names.each do |table_name|
 path = Rails.root.join("db","seeds",Rails.env,"#{table_name}.rb")
 if File.exist?(path)
   puts "Creating #{table_name}...."
   require(path)
 end
end
----------------------------------------

そしてシードデータを投入する。
bin/rails db:reset

----------------------------------------
bash-4.4$ bin/rails db:reset
Dropped database 'baukis2_development'
Dropped database 'baukis2_test'
Created database 'baukis2_development'
Created database 'baukis2_test'
Creating staff_members....
Creating administrators....
bash-4.4$ 
----------------------------------------

問題6
Administratorモデルのspecファイルにpassword=メソッドのためのエグザンプルを追加し、テストを成功させてください。

spec/models/administrator_spec.rb修正後
----------------------------------------
require 'rails_helper'
RSpec.describe Administrator, type: :model do
 describe "#password=" do
   example "文字列を与えると、hashed_passwordは長さ60の文字列になる" do
     # Administratorインスタンスを作成する
     member = Administrator.new
     # passwordに文字列を入れるだけでハッシュ値化されることを確認したい
     member.password = "baukis"
     
     # hashed_passwordは文字列である
     expect(member.hashed_password).to be_kind_of(String)
     
     # hashed_passwordの長さは60固定である
     expect(member.hashed_password.size).to eq(60)
   end
   
   example "nilを与えると、hashed_passwordはnilになる" do
     # Administratorインスタンスを作成し、初期値を与える
     member = Administrator.new(hashed_password: "x")
     
     # パスワードにnilをセットしたときの挙動を見たい
     member.password = nil
     
     # nilであることを期待する
     expect(member.hashed_password).to be_nil
   end
 end
end
----------------------------------------

rspec

----------------------------------------
bash-4.4$ rspec
......
Finished in 0.57799 seconds (files took 8.34 seconds to load)
6 examples, 0 failures
bash-4.4$ 
----------------------------------------


問題7
Staff::Baseクラスと同様に、Admin::Baseクラスを定義し、privateなインスタンスメソッドcurrent_administratorを実装してください。
セッションオブジェクトのキーは:administrator_idとしてください。

touch app/controllers/admin/base.rb

app/controllers/admin/base.rb修正後
----------------------------------------
class Admin::Base < ApplicationController
 private def current_administrator
   if session[:administrator_id]
     @current_administrator ||= Administrator.find_by(id: session[:administrator_id])
   end
 end
 
 helper_method :current_administrator
end
----------------------------------------

問題8
Admin::TopControllerクラスの親クラスをAdmin::Baseに変更してください。

app/controllers/administrator/top_controller.rb修正後
----------------------------------------
class Admin::TopController < Admin::Base
 def index
   render action: "index"
 end
end
----------------------------------------

問題9
config/routes.rbに、管理者のログイン・ログアウト機能のためのルーティングを追加してください。

config/routes.rb修正後
----------------------------------------
Rails.application.routes.draw do
	# 職員
	namespace :staff do
		root "top#index"
		get "login" => "sessions#new", as: :login
		post "session" => "sessions#create", as: :session
		delete "session" => "sessions#destroy"
	end
	# 管理者
	namespace :admin do
		root "top#index"
		get "login" => "sessions#new", as: :login
		post "session" => "sessions#create", as: :session
		delete "session" => "sessions#destroy"
	end
	# 顧客
	namespace :customer do
		root "top#index"
	end
end
----------------------------------------


問題10
管理者用ページのヘッダ部分右端に、状態に応じて「ログイン」あるいは「ログアウト」リンクが表示されるように管理者用の部分テンプレートとスタイルシートを書き換えてください。

app/views/admin/shared/_header.html.erb
----------------------------------------
<header>
 <span class="logo-mark">BAUKIS2</span>
 <%=
   if current_administrator
     link_to "ログアウト", :admin_session, method: :delete
   else
     link_to "ログイン", :admin_login
   end
 %>
</header>
----------------------------------------
app/assets/stylesheets/admin/layout.scss
----------------------------------------
@import "colors";
@import "dimensions";
html,body{
margin: 0;
padding: 0;
height: 100%;
}
div#wrapper{
 position: relative;
 box-sizing: border-box;
 min-height: 100%;
 margin: 0 auto;
 padding-bottom: ( $wide + $moderate) * 2 + $standard_line_height;
 background-color: $gray;
}
header{
 padding: $moderate;
 background-color: $dark_cyan;
 color: $very_light_gray;
 span.logo-mark{
     font-weight: bold;
 }
 a{
 	float: right;
 	color: $very_light_gray;
 }
}
footer{
 bottom: 0;
 position: absolute;
 width: 100%;
 background-color: $dark_gray;
 color: $very_light_gray;
 p{
     text-align: center;
     padding: $moderate;
     margin: 0;
 }
}
----------------------------------------

最後にアセットプリコンパイルを忘れずに行う
bin/rails assets:precompile RAILS_ENV=production

確認が終わったら確認する。

rspec
ok


開発用サーバを立てて確認
bin/rails s -b 0.0.0.0

画像6


本番用サーバを立てて確認
bin/rails s -e production -b 0.0.0.0

画像7

エラーページが専用ページで開かれなくなったけど、前のChapterでそういう風にしたから別に正常。
このまま次へ行く。


この記事が気に入ったらサポートをしてみませんか?