しおブログ言うてますけど

技術ブログ言うてますけど技術以外のことも発信すると思われ。RubyとかRuby on Railsとか生き方とかジャンルにこだわらず自分の好きなように。初心者なのでお手柔らかに。

配列の取得方法について

配列の取得方法について様々なやり方があるのでまとめてみた。

添え字を2つ使う

添え字を2つ使うと添え字の位置と取得する長さを指定することができる。 取得例

配列[位置, 取得する長さ]

具体例

a = [1, ,2, 3, 4, 5]
a[1, 4] #=> [2, 3, 4, 5]

values_atメソッドを使う

values_atを使うと取得したい要素の添え字を複数指定できる。 具体例

a = [1, 2, 3, 4, 5]
a.values_at(1, 3) #=> [2, 4]

添え字に負の値を使う

Rubyの配列では添え字に負の数が使える。例えば-1は最後の要素、-2は最後から2番目の要素というふうに取得することができる。

具体例

a = [1, 2, 3, 4, 5]
a[-1] #=> 5
a[-3, 2] #=> [3, 4]

lastメソッドについて

配列の取得に添え字を使うやり方もあるが、lastメソッドを使うと配列の最後の要素を取得することができる。
引数に0以上の数値を渡すと最後のn個の要素を取得することができる。

具体例

a = [1, 2, 3, 4, 5]
a.last #=> 5
a.last(3) #=> [3, 4, 5]

※lastの反対firstメソッドについて

lastメソッドの反対で配列の最初の要素を取得することができるメソッドとしてfirstメソッドがある。
引数に0以上の数値を渡すと最初のn個の要素を取得することができる。

具体例

 a = [1, 2, 3, 4, 5]
a.first #=> 1
a.first(3) #=> [1, 2, 3]

まとめ

  • 配列の取得の際、添え字を2つ使うことで配列の添え字の位置と取得する長さを指定することができる。
  • values_atメソッドを使うと取得したい添え字を複数指定できる。
  • lastメソッドは配列の最後の要素、firstメソッドは配列の最初の要素を取得することができ、引数に0以上の数値を渡すことで、最後からn番目、最初からn番目の要素を取得することができる。

名前空間ってなに?って疑問を解消してみた

こんにちは。たかあきです。
現場Railsで勉強していると名前空間モジュールといった難しい(?)言葉が出てくる。
モジュールはクラスと違い、継承ができないといった特徴があるが、名前空間ってなんぞや...となったのでRailsのルーティングにおけるモジュールと名前空間についてまとめる。

モジュールについて

まずモジュールの使い方をまとめてみる。

module Animal
  def self.dog
    '犬です'
  end

  def cat
    '猫です'
  end
  module_function :cat
end

puts Animal.dog #=>犬です
puts Animal.cat #=>猫です

モジュールからメソッドを呼び出すときは上記のように呼び出す。

名前空間について

名前空間名前の衝突を避けるために使う概念である。
どういうことか。
プログラムが複雑になると他の人が作成したクラス名やメソッド名と重複してしまう可能性が出てくる。
しかしモジュールを名前空間として使用することで重複を避けることができる。

class Animal
  def dog
    '犬です'
  end
end

# モジュールZooを名前空間としてAnimalクラスを定義
module Zoo
  class Animal
    def dog
      '動物園の犬です'
    end
  end
end

# Userクラスのインスタンスメソッドを実行
puts Animal.new.dog #=>犬です

# 名前空間を利用する場合、「モジュール名 + :: + クラス名」 とする
puts Zoo::Animal.new.dog #=> 動物園の犬です

このように名前空間を利用することで同じクラス名、メソッド名が使えるようになる。

ルーティングにおけるnamespaceとscopeの違い

namespace

namespaceは、URLもcontroller格納フォルダも、指定のパスになる

namespace :admin do
  resources :users
end

#rails routes

   admin_users GET    /admin/users(.:format)          admin/users#index
               POST   /admin/users(.:format)          admin/users#create
new_admin_user GET    /admin/users/new(.:format)      admin/users#new
dit_admin_user GET    /admin/users/:id/edit(.:format) admin/users#edit
    admin_user GET    /admin/users/:id(.:format)      admin/users#show
               PATCH  /admin/users/:id(.:format)      admin/users#update
               PUT    /admin/users/:id(.:format)      admin/users#update
               DELETE /admin/users/:id(.:format)      admin/users#destroy

namespaceはコントローラを名前空間によってグループ化することができる。 管理者用のコントローラはcontrollers/adminディレクトリ以下に作成して管理するみたいなことができる。

scope

scopeはcontrollerの格納フォルダだけ、指定パスになる。

scope module: :admin do
  resources :users
end

#rails rotues

   Prefix Verb   URI Pattern               Controller#Action
    users GET    /users(.:format)          admin/users#index
          POST   /users(.:format)          admin/users#create
 new_user GET    /users/new(.:format)      admin/users#new
edit_user GET    /users/:id/edit(.:format) admin/users#edit
     user GET    /users/:id(.:format)      admin/users#show
          PATCH  /users/:id(.:format)      admin/users#update
          PUT    /users/:id(.:format)      admin/users#update
          DELETE /users/:id(.:format)      admin/users#destroy

URLは変更したくないがコントローラーはグループ化したいという場合はこのようにする。

scopeのみ

また、scope '/admin'のようにすると上記の逆で、URLだけ、指定のパスになる。

scope '/admin' do
  resources :tweets
end

#rails rotues

   Prefix Verb   URI Pattern                     Controller#Action
    users GET    /admin/users(.:format)          users#index
          POST   /admin/users(.:format)          users#create
 new_user GET    /admin/users/new(.:format)      users#new
edit_user GET    /admin/users/:id/edit(.:format) users#edit
     user GET    /admin/users/:id(.:format)      users#show
          PATCH  /admin/users/:id(.:format)      users#update
          PUT    /admin/users/:id(.:format)      users#update
          DELETE /admin/users/:id(.:format)      users#destroy

まとめ

  • 名前空間は名前の衝突を避けるために使う概念。
  • ルーティングにおけるnamespaceはURLもcontroller格納フォルダも、指定のパスになることで、管理者用のURLとコントローラーを作成することができる。

ルーティングの設定とか調べたらまだまだたくさんあったが、名前空間がどういうものなのかということは理解できた。

以外とわかっていなかったRubyにおける範囲

こんにちは。たかあきです。 Rubyでは「1から5までの数字」や「文字列"a"から文字列"d"まで」といった値の範囲を表すオブジェクト(Range)がある。
チェリー本で勉強するまではなんとなくでしかわかっていなかった範囲オブジェクトだが、伊藤大先生の解説により点と点がつながったような理解ができたのでまとめていく。

範囲オブジェクトの使い方

範囲オブジェクトの使い方自体はすごくシンプルである。

最初の値..最後の値(最後の値を含む)
最初の値...最後の値(最後の値を含まない)

ここで重要になってくるのが、最後の値を含むか最後の値を含まないということである。
これがまず自分ではわかったなかった。
適当に..をつけたら範囲が指定されると思っていたが、最後の値を含む含まないかで範囲の指定の仕方が変わる。

# ..を使うと範囲に含まれる(1以上55以下)
point = (1..55)
point.include(0)
#=> false
point.include(55)
#=> true
# ...を使うと範囲に含まれない(1以上55未満)
point = (1...55)
point.include?(55)
#=> false

繰り返しを使った応用

範囲オブジェクトに対してto_aメソッドを呼び出すと値が連続する配列を作成するこることができる

 例
(1..5).to_a  #=> [1, 2, 3, 4, 5]
(1...5).to_a  #=> [1, 2, 3, 4]

数字だけでなく文字列の配列も作成することができる

例
alf = ('a'..'z').to_a  #=> ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
fruits = ('apple'..'applz').to_a  #=>  ["apple", "applf", "applg", "applh", "appli", "applj", "applk", "appll", "applm", "appln", "applo", "applp", "applq", "applr", "appls", "applt", "applu", "applv", "applw", "applx", "apply", "applz"]

このように文字列もアルファベットに沿った連続した配列が作成された。
ここである疑問が生まれた。

「数字の場合、1〜5のように1づつ増える配列は作成されるけど3〜1のような小さくなる場合はどうなるん?」
「文字列の場合、文字列の語尾がe〜zのようになると配列は作成されるけどzからaにするとどうなる?」

気になったので試して見た。

# 数字55から1まで連続した配列は作成されるのか
age = (55..1).to_a
age #=> []

このように空の配列になってしまった。

# 文字列がz〜a反対になったら配列は作成されるのか
alf = ('z'..'a').to_a
alf #=> []

こちらも空の配列になってしまった。

まとめ

Rubyにおける範囲(Range)オブジェクトは最初の値..最後の値(最後の値を含む)最初の値... 最後の値(最後の値を含まない) という風に使う。 範囲オブジェクトに対して.to_aをメソッドを呼び出すと連続した配列を作成することができる。
55〜1や'a'〜'z'といった逆の配列は作成されない。

配列のメソッドまとめ

こんにちは。たかあきです。
Rubyを勉強するときに、最初に買ったチェリー本。いざ学習しようとしたら難しすぎて速攻閉じたのを覚えています。
少したった今、だいぶ慣れ(?)きて、少しずつ理解できるようになってきたのが嬉しくて更にわかるようになりたいと思ってる所存です。
今回はブロックを使う配列のメソッドを参考にまとめていきたいと思う。

map

mapメソッドは各要素に対してブロックを評価した結果を新しい配列にして返す。

例
num = [1, 2, 3, 4 ,5]
new_num = num.map { |n| n * 10 }
new_num #=> [10, 20, 30, 40, 50]

これは各要素の戻り値を10倍にしてnew_numに代入しているため、[10, 20, 30, 40, 50]となる。

さらに省略した書き方ができる。

オブジェクト.map(&:メソッド名)

これは、すべての要素に対して&の後にシンボルで指定したメソッドが繰り返し実行され、結果が配列として返される。

例
country = ["japan", "usa", "china", "brazil"]
big_country = country.map(&:upcase)
big_country #=> ["JAPAN", "USA", "CHINA", "BRAZIL"]

これは変数countryに代入されたjapan、usa、china、brazilという小文字の国名をcountry.map(&:upcase)でcountryのすべての要素を大文字の配列として返している。

select

selectメソッドは戻り値が真の要素を集めた配列を返すメソッド。

age = [23, 1, 33, 72, 35, 5]
even_age = age.select { |n| n.even? }
even_age #=> [72]

これは変数ageに代入された配列[23, 1, 33, 72, 35, 5]から age.select { |n| n.even? }でageの値から偶数だけを配列として返している。
戻り値が真の要素を返すので n.even??が無いと、

NoMethodError (undefined method `even')

のようにエラーが返ってくる。

reject

rejectメソッドはselectメソッドの反対で、戻り値が真の要素を除外した配列を返す。

例
points = [2, 3, 38, 33, 90, 100, 56]
#50以上の数字が真なので 90, 100, 56を除外
below_average = points.reject { |n| n >= 50  }
below_average #=> [2, 3, 38, 33]

少しこんがらがるけど落ち着いて整理すれば処理がわかりやすい。

find

findメソッドはブロックの戻り値が真になった最初の要素を返す

age = [22, 3, 4 ,5 ,66, 42, 55]
old_man = age.find { |n| n > 50  }
# 22, 3, 4 ,5 ,66, 42, 55の中から50より大きい要素は66なので↓
old_man #=> 66

inject

injectはたたみみ込み演算を行うメソッド(最初これが全く意味がわからなかった...)

例
points = [33, 1, 3, 2, ,5, 44, 1, 0, 30]
result = points.inject(0) { |sum, n| sum += n }
result = 119

このコードの解説。
まずinjectの動きとしてブロックの第一引数は初回のみinjectメソッドの引数が入る。上記のコード内でのpoints.inject(0)
第二引数は配列の各要素[33, 1, 3, 2, ,5, 44, 1, 0, 30]が入る。上記のコード内でのn
さらに噛み砕くと、
1回目 : sum = 0。0 + 33 = 33。これが次のsumに入る。
2回目 : sum = 33。33 + 1 = 34。これが次のsumに入る
・ ・ ・ ・ ・ ・
9回目 : sum = sum = 89。89 + 30 = 119。最後の要素に達したので119がinjectメソッドの戻り値になる。
少し複雑な処理だが、噛み砕くとなるほど、、となった。

まとめ

配列はRubyの中でも頻繁に使われるということなので、もっと柔軟な使い方ができるようになりたいと思った。(rejectメソッドはシンプルなのにinjectメソッドはなんでこんなに複雑なのか...もっと頑張るぞ...)

ログイン機能の実装(モデルのアソシエーションまで)③

ログイン機能の実装②まではログイン情報を取得するまで進めた。このままではログインしたままになってしまうので、ここからはログアウト機能の実装をしていきたいと思う。

ログアウト機能の実装

ユーザーがログイン状態ではsession[:user_id]にユーザーのIDが入っていることだった。ログアウトの状態にするにはsession[:user_id]にnilが入っている状態に変えればいいということになる。

session.delete(:user_id)

セッションからuser_idの情報だけピンポイントで消すには上記のようにできる。
セッション内のすべてのデータを削除したい場合には下記のようにする。

reset_session

アクションの設定

ログアウト機能を実装するためにsessionsコントローラーのdestroyアクションを編集していく。

  def destroy
    reset_session
    redirect_to root_url, notice: 'ログアウトしました'
  end

これでdestroyアクションはできた。
続いてログアウトのリンクを追加する。

ul.navbar-nav.ml-auto
  - if current_user
    li.nav-item= link_to 'タスク一覧', tasks_path, class: 'nav-link'
    li.nav-item= link_to 'ユーザー一覧', admin_users_path, class: 'nav-link'
    li.nav-item= link_to 'ログアウト', logout_path, method: :delete, class: 'nav-link'
  - else
    li.nav-item= link_to 'ログイン', login_path, class: 'nav-link'

ログインしているときはタスク一覧、ユーザー一覧、ログアウトの表示が見え、ログインしていないときはログインできるようにログインの表示がされるようになっています。

ログインしていない場合の制限

制限をかけていない今のままではログインしていなくても他の機能が使えたりしてあまり意味がないのでログインしていなければタスク管理を利用できなくするという制限をかけて行きたいと思う。
そのためには「フィルター」という機能を使う。
フィルターとは一般的な処理の流れに従ってメインの処理をアクションと考えると、フィルターとはすべてのアクションに対して、前処理・後処理を設定する役割を提供するものと言える。
つまり、前処理・後処理を必要とする場合だけ、フィルターを設定する。 before_actionでアクションの前処理として他の機能へのリダイレクトを実装するとリダイレクトが行われ、アクションには到達しない。

class ApplicationController < ActionController::Base
  helper_method :current_user
  before_action :login_required

  ....ruby

  def login_required
    redirect_to login_url unless current_user
  end

before_action :login_requiredでアクションを実行する前にlogin_requiredが呼び出される。login_requiredではログインしていなければログイン画面にリダイレクトするという機能を実装しているので、もしログインしていなければアクションは実行されない。
1つ問題があり、アプリケーションのどのURLをリクエストしてもリダイレクトが行われてしまう。
これを防ぐために特定のコントローラーにおいて親クラスなどですでに定義済みのフィルタを通らないようにするにはskip_before_actionを利用する。 sessions_controller.rbを編集し、skip_before_actionを利用する。

class SessionsController < ApplicationController
  skip_before_action :login_required
   ...

これでログインしていないユーザーに制限をかけることができた。

ログインしているユーザーのデータだけを扱えるようにする

ログインしているユーザーのデータだけを扱えるようにするには特定のユーザーに紐付いたTaskデータだけを扱うようにプログラムを変更しなければいけいない。
そのためには

  • UserとTaskを紐付ける。具体的にはtasksテーブルにuser_idというカラムを追加してタスクを所有しているユーザーのidが格納されるようにする。
  • UserとTaskの紐付けを簡単に扱えるよう、Railsの「関連」を定義する
  • ログインしているユーザーに紐付いたTaskデータを登録できるようにする。
  • 一覧、詳細、変更など既存のレコードを扱う機能ではログインしているユーザーに紐付くデータだけを扱うようにする。

とくことが必要である。

UserとTaskを紐付ける

1つのユーザーに対して複数のTaskが存在する1対多の関係になるので「多」にあたるTaskにuser_idを持たせる。

$ rails g migration AddUserIdToTasks

上記のコマンドでマイグレーションファイルを作成する。

class AddUserIdToTasks < ActiveRecord::Migration[5.2]
  def change

    def up
      execute 'DELETE FROM tasks;'
      add_reference :tasks, :user, null: false, index: true
    end

    def down
      remove_reference :tasks, :user, index: true
    end
    
  end
end

execute 'DELETE FROM tasks;'SQL文で今まで作られたタスクをすべて消すというもの。

既存のタスクがある状態でタスクとユーザーの関係を表すカラム(user_id)を追加すると、既存のタスクに紐付くユーザーを決められず、NOT NULL制約に引っかかってしまいます。そのため既存のタスクをすべて削除してから、カラム追加を行うようにしている。

ということなので、これをマイグレーション実行する。
これでTaskとUserが紐付く。

Railsで「関連」という仕組みを利用するには、モデルクラス同時の紐付けを定義する。
UserとTaskは1対多の「関係」にあたるので、Userクラスにはhas_many :tasks、Taskクラスにはbelongs_to :userを定義する。
ここは一番理解しにくかったが、1対多の関係において、1のモデルクラスにはhas_many、多のモデルクラスにはbelongs_toを定義する。
日本語にしたらわかりやすいが、has_manyはいくつかのモデルを持っている(なのでモデルクラスの複数形)belongs_toは1つのモデルに所属している(なのでモデルクラスの単数形)というふうに定義する。
このような定義をすることでUserクラスのインスタンスはuser.tasksといったメソッドで紐付いたTaskオブジェクトの一覧を得られるようになる。また、Taskクラスのインスタンスはtask.userといったメソッドで紐付いたUserオブジェクトを得られるようになる。

ログインしているユーザーのTaskデータの登録

現在の登録アクション(tasks_controller.rb) のcreateアクションでは@task = Task.new(task_params)となっている。
これを@task = current_user.tasks.new(task_params)とすることでログインしているユーザーのuser_idを入れた状態でTaskデータを登録することができる。

まとめ

  • session[:user_id]からログアウトの状態にするにはnilが入っている状態に変えればいいのでピンポイントでuser_idの情報を消すにはsession.delete(:user_id)とする。またセッション内のすべてのデータを削除するのはreset_sessionとする。
  • フィルターとはすべてのアクションに対して、前処理・後処理を設定する役割を提供するもので、 他の機能へのリダイレクトを実装するとリダイレクトが行われ、アクションには到達しない。
  • 親クラスなどですでに定義済みのフィルタを通らないようにするにはskip_before_actionを利用する。
  • 1対多の関係において、1のモデルクラスにはhas_many、多のモデルクラスにはbelongs_toを定義する。

モデル同士の「関連(アソシエーション)」についてはもっと理解を深めていかないといけないと感じた。

ログイン機能の実装(ログイン情報を取得するまで②)

この記事は自分が理解しにくかったログイン機能の仕組みについてアウトプットする記事です。
一つの記事にすると長くなりすぎるのでいくつかの記事に分けて記事を書いてるため、参考にする方はログイン機能の実装(digestを保存するまで①) を先に読んでください。

ログイン機能の実装(digestを保存するまで①)でやったこと

  • ログイン機能を実装する目的はWEBアプリケーションにて、自分以外のユーザーが他のユーザーの情報を操作できないようにするためである。
  • ユーザー(User)モデルを作成する際、属性には必ず必要なname, email, password_digestとする。
  • password_digestにする理由はRails標準のhas_secure_passwordという機能を使うためである。
  • モデルにhas_secure_passwordと記述すると、passwordpassword_confirmationの2つのデータベースのカラムには対応しない属性が追加される。
  • password属性の値とpassword_confirmation属性の値が一致しているとusersテーブルのpassword_digestカラムにハッシュ化されたパスワードが保存される。

ログイン機能の実装

これからログイン機能を作成する。ログイン機能はユーザーがログインするためにのフォーム画面を表示し、送られてきた情報を元にユーザーを認証する。ログイン機能と別にログアウト機能も提供する。
現場Railsではログイン機能についてこういう風に解説している。

ログイン機能を実装する際は、ログインする=「セションリソースを作る」と捉えて、SessinsControllerという名前でコントローラーを作ることがよく行われています。

なるほど。とりあえずSessionsControllerを作成してそこからログイン機能を作るということだと思う。
SessionsControllerに追加したいアクションは下記を参照する。

アクションの内容 HTTPメソッド URL アクション名
ログインのフォームを表示する GET /login new
フォームから送られてきた情報を元にログインを行う POST /login create
ログアウトを行う DELETE /logout destroy

早速SessionsControllerをrails gコマンドで作成する。

$ rails g controller Sessions new create destroy

コマンドによりルーティングが設定されましたが、ログインフォームでは/loginというURLにするので routes.rbを下記のように編集する。

Rails.application.routes.draw do
  get '/login',  to: 'sessions#new'
  post '/login', to: 'sessions#create'
  get '/logout', to: 'sessions#destroy'
  ...

end

これでログインフォームを表示するアクションのURLが/loginになり、ログアウトのリクエストも/logoutになる。

ログインフォームの作成

app/views/sessions/new.html.silmを編集して、ログインフォームの作成をする。

h1 ログイン

= form_with scope: :session, local: true do |f|
  .form-group
    = f.label :email, 'メールアドレス'
    = f.text_field :email, class: 'form-control', id: 'session_email'
  .form-group
    = f.label :password, 'パスワード'
    = f.password_field :password, class: 'form-control', id: 'session_password'
  = f.submit 'ログインする', class: 'btn btn-primary'

このように編集することでログインフォームが完成する。

実際にログインする

ログインフォームができたので実際にログイン機能を開発していく。
先程 rails g コマンドで作成したときに作成されたsessionsコントローラーのcreateアクションを編集する。

  def create
    user = User.find_by(email: session_params[:email])

    if user&.authenticate(session_params[:password])
      session[:user_id] = user.id
      redirect_to root_url, noitce: 'ログインしました'
    else
      render 'new'
    end
  end
  .....


  private
  def session_params
    params.require(:session).permit(:email, :password)
  end

まずuser = User.find_by(email: session_params[:email])で送られてきたメールアドレスでユーザーを探す。
ユーザーが見つかった場合はauthenticateメソッドを使い認証を行う。
ここでauthenticateメソッドが出てきたが、authenticateメソッドはhas_secure_passwordと記述したときに追加される認証のためのメソッドである。
引数で受け取ったパスワードをハッシュ化してUserオブジェクト内部に保存されているdigestと一致するかを調べ、一致していたら認証が成功し、Userオブジェクト自身を返す。
一致していなければfalseを返す。

※ぼっち演算子について

if user&.authenticate(session_params[:password])&.がなんなのか最初全くわからなかった。
これはRubyのぼっち演算子と呼ばれる書き方で正式にはsafe navigation operatorと言われるものらしい。
Rubyにおいて、レシーバーであるオブジェクトに対してメソッドを実行する時、オブジェクトがnilの場合はエラーになる。 実際にrails c を使って調べてみる。

/変数foobarにnilを代入する/

> foobar = nil
=> nil

/nilオブジェクトに対してメソッドを実行する/

> foobar.downcase
Traceback (most recent call last):
NoMethodError (undefined method `downcase' for nil:NilClass)

このようにnilが代入されたオブジェクトに対してメソッドを実行するとエラーになることがわかる。
こういった場合にぼっち演算子を使用し、レシーバーがnilのときはエラーを出すことなく、そのままnilを返すというものである。

/先程nilを代入したfoobarにぼっち演算子を使用した例/

foobar&.downcase
=> nil

上記のようにエラーならずnilが返ってくることがわかる。
オブジェクト&.メソッドでぼっち演算子を使用することができる。
つまりif user&.authenticate(session_params[:password])ではメールアドレスに対応するユーザーのデータが見つからないときはuserはnilを返すので、ユーザーデータが見つからない場合はif文のelseに飛ぶということである。
session[:user_id] = user.idでは認証に成功した場合、セッションにuser_idを格納している。
誰もログインしていない状態。session[:user_id]がnil
誰かがログインしている状態。session[:user_id]にログイン中のユーザーIDが入っている
ということが言える。


ログイン情報の取得

ユーザーがログインしていればsession[:user_id]にユーザーのIDが格納されるので下記のようにユーザー情報を取得することができる。

User.find_by(id: session[:user_id])

現場Railsではこの処理を頻繁に使うためにこのように解説している。

このようなログインしているユーザーを取得する処理は、頻繁に必要になるので、コントローラーやビューから簡単に呼べるようにするのが定石になっています。

すべてのコントローラーからメソッドを使えるようにするにはapplication_controller.rbにメソッドを定義することになるので、application_controller.rbを編集する。

class ApplicationController < ActionController::Base
  helper_method :current_user

  private
  def current_user
    @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
  end
end

ここではすべてのコントローラーとすべてのビューのから使えるようにするため、helper_methodを指定している。

※||=(nilガード)について

@current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]||=がなんなのか理解が浅いので詳しく調べてみる。

これは変数に値を入れるときに、変数がnilかfalseのときのみ値を入れる、というものらしい。
rails c で調べてみる。

/test1に文字列foobar を代入する/

> test1 = "foobar"
=> "foobar"

/nilでもfalseでもない値に||=を使って文字列hogehogeを代入する/

test1 ||= "hogehoge"
=> "foobar"

変数test1はfoobarが代入されており、その状態で||= を使ってもnilでもfalseでもないのでhogehogeは代入されない。
通称これは「nilガード」と言われるものである。
ではnilが代入されていたらどうなるのか。

/test2にnilを代入する/

> test2 = nil
=> nil

/nilが入った変数に||= を使ってhogehogeを代入する/
test2 ||= "hogehoge"
=> "hogehoge"

> test2
=> "hogehoge"

このように||= ではnilまたはfalseが入った変数に対して代入するというものである。

ここまでのまとめ

  • ログイン機能を実装する際は、ログインする=「セションリソースを作る」と捉えて、SessinsControllerという名前でコントローラーを作る。
  • URLは/login、/logoutになるようにルーティングを変更する。
  • authenticateメソッドはhas_secure_passwordと記述したときに追加される認証のためのメソッド
  • &.(ぼっち演算子)は、あるオブジェクトがnilの場合、そのままnilを返しnilでない場合はオブジェクトを返す。
  • User.find_by(id: session[:user_id])で簡単にログインしているユーザーを取得することができ、コントローラーやビューで呼べるようにするため、application_controller.rbにhelper_methodを指定してメソッドを定義する。
  • ||=(nilガード)は変数がnilまたはfalseのみ値を代入するというものである。
    少し長くなってしまったが、ログイン情報を取得するまでできるようになった。もう少し続くのでまたアウトプットとしてまとめて行きたいと思う。

ログイン機能の実装(digestを保存するまで)①

Rsilsチュートリアルや現場Railsでも解説されているログイン機能の実装は理解が追いつかなく、なにがなんだかわからない状態だったので、現場Railsを参考にログイン機能の実装をアウトプットしていきたいと思う。
まずWEBアプリケーションでログイン機能を実装する理由は、例えばTODOアプリを作ったとして、やることリストを投稿したとする。自分一人なら、なんら問題は無いが、第三者に記事を投稿されたり削除されてはいけない。
そういった問題を解決するためにログイン機能があり、ログインしたユーザーのみが自分自身のタスクしか扱えないようにするのが目的である。(少々雑かったらすまそん...)
ログイン機能の実装はかなり長くて、1つの記事にしようとすると多分自分でも後で読む気にならないと思うので、いくつかの記事にしてアウトプットする。

前提に必要な知識

Cookie

HTTPはステートレスなプロトコルであるため、WEBブラウザとWEBサーバーの一連のやりとりにおいて、状態を保持し管理する仕組みがない。そのためにショッピングサイトなどで状態を保持し管理する必要がある場合には、Cookieと呼ばれるデータが用いられる。いわば複数のリクエストの間で共有したい「状態」をブラウザ側に保存する仕組みである。

セッション

セッションとは一連の関連性のある処理の流れのことである。例えばショッピングサイトで商品を買う場合の「商品を選ぶ」「商品を買い物かごに入れる」「買い物かごの中身を確認する」「商品を購入する」といった流れがセッションになる。WEBブラウザからに処理を関連性のある一連の処理(=セッション)として扱いたい場合はCookieを用いてセッションを管理できる。

Railsではコントローラーからsessionというメソッドを呼び出すことで、セッションにアクセスできる。sessionはハッシュのように扱うことができる。

 session[:user_id] = @user.id

値を取り出すには下記のように参照する。

 @user_id = session[:user_id]

Railsではセッションの仕組みの一部がCookieによって実現されている。なので直接Cookieを操作することはあまりない。

(ユーザーを表すUser)モデルの作成

ここではアプリケーションを利用するユーザーを表すUserモデルを作成する。Userクラスのデータ構造は下記のように設計する。

意味 属性名 テータ型
名前 name string(文字列)
メールアドレス email string(文字列)
パスワード password_digest string(文字列)

パスワードの属性名がpassword_digestとなっているのは意味があり、これはRailsに標準で付いているhas_secure_passwordという機能を使った命名ルールに沿った属性名である。
has_secure_passwordを使うことによってセキュアにハッシュ化したパスワードを、usersテーブル内のpassword_digestという属性に保存できるようになる。 下記のコマンドでUserモデルを作成する。

$ rails g model User name:string email:string password_digest:string

するとマイグレーションファイルが作成される。
rails db:migrateする前にマイグレーションを編集する。

class CreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :name, null: false
      t.string :email, null: false
      t.string :password_digest, null: false

      t.timestamps
      t.index :email, unique: true
    end
  end
end

名前、メールアドレス、パスワードには必ず文字列が入らないといけないのでNot Null制約をつけている。 編集したらrails db:migrateでデータベースにusersテーブルを作成する。

digestを保存する

上記で説明したhas_secure_passwordを使ってパスワードをセキュアにハッシュ化する。
そのためにbcryptというgemを使う。 Gemfileにコメントアウトされているgem 'bcrypt'のコメントアウトを外してbundle install

gem 'bcrypt', '~> 3.1.7'
$ bundle install

has_secure_passwordの使いかたは簡単でモデルの中にhas_secure_passwordと記述するだけでパスワードをハッシュ化してくれる。

class User < ApplicationRecord
  has_secure_password
end

モデルの中にhas_secure_passwordを記述すると下記2つのデータベースのカラムには対応しない属性が追加される。

  • password
  • password_confirmation

password属性はユーザーが入力したパスワードを一時的に格納するための属性。
password_confirmation属性はpassword属性で入力した値の確認用の属性。
つまりpassword属性の値とpassword_confirmation属性の値が一致してなければ検証に失敗する。
password属性の値とpassword_confirmation属性の値が一致していればpassword_digestにハッシュ化したパスワードの値が保存される。

実際にrails cを使って本当にハッシュ化されるのか検証してみたいと思う。

> user = User.new(name: "テスト", email: "test@test.com", password: "test", password_confirmation: "test")
=> #<User id: nil, name: "テスト", email: "test@test.com", password_digest: "$2a$12$RyxUd9HvQgnA4ur8UXJYpeXRykmYHKKV3ll3nieLMIu...", created_at: nil, updated_at: nil>

password_digestの部分が特定できないハッシュ化されたパスワードになっている。 これをsaveすると、

> user.save
   (0.1ms)  BEGIN
  User Create (0.5ms)  INSERT INTO "users" ("name", "email", "password_digest", "created_at", "updated_at") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["name", "テスト"], ["email", "test@test.com"], ["password_digest", "$2a$12$RyxUd9HvQgnA4ur8UXJYpeXRykmYHKKV3ll3nieLMIumQw167pkri"], ["created_at", "2021-01-10 07:41:28.956293"], ["updated_at", "2021-01-10 07:41:28.956293"]]
   (4.9ms)  COMMIT
=> true

trueが返ってきて無事に保存されていることがわかる。
ちなみにpassword属性とpassword_confirmation属性の値が一致せずに保存しようとすると、

> test = User.new(name: "テスト", email: "test@test.com", password: "test", password_confirmation: "foobar")
=> #<User id: nil, name: "テスト", email: "test@test.com", password_digest: "$2a$12$U1fzoPdhbC9seU.Ku0kDUeGWSLzxSm5773n/t1KecCy...", created_at: nil, updated_at: nil>
> test.save
   (0.2ms)  BEGIN
   (0.2ms)  ROLLBACK
=> false

falseが返ってきて保存されないということがわかる。

ここまでのまとめ

  • ログイン機能を実装する目的はWEBアプリケーションにて、自分以外のユーザーが他のユーザーの情報を操作できないようにするためである。
  • ユーザー(User)モデルを作成する際、属性には必ず必要なname, email, password_digestとする。
  • password_digestにする理由はRails標準のhas_secure_passwordという機能を使うためである。
  • モデルにhas_secure_passwordと記述すると、passwordpassword_confirmationの2つのデータベースのカラムには対応しない属性が追加される。
  • password属性の値とpassword_confirmation属性の値が一致しているとusersテーブルのpassword_digestカラムにハッシュ化されたパスワードが保存される。
続きます

少し長いので別の記事に続きます。