Rails Diary

プログラミングの学習記録です。

PFメモ日記(5) 結局deviseとsorceryどちらがいいのか?

teratail.com

以下回答より

  • 結局はどんな認証にしたいのかによる
  • deviseを使えば簡単な認証機能を作れるが、カスタマイズするためにはdeviseの中身をよく知らないと厳しい
  • sorceryは認証の部品しか提供してくれないためカスタマイズしやすいが、部品同士を繋げていく作業は自分でしなくてはならない
  • 注意点として、認証系は少しのミスが脆弱性につながるため、その道に詳しい方が作成した認証系のライブラリを使った方が無難
  • 少なくともdeviseやsorceryのソースコードを見て、何をしているのか全て把握できるくらいでないと自分で作るのは避けた方がいい

PFメモ日記(4) sorceryの復習

deviseを使おうと思ったけど、せっかくなのでsorceryの復習をしたい。

※ 内容ごちゃごちゃしてかなり分かりづらいです。

目次

1. sorceryをインストール

gem 'sorcery'
$ bundle exec rails g sorcery:install

create  app/models/user.rb
create  db/migrate/XXXXXXXXX_sorcery_core.rb

生成されたマイグレーションファイルにその他欲しい属性と制約を追加

# db/migrate/20220615092132_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[6.1]
  def change
    create_table :users do |t|
      t.string :email,            null: false, index: { unique: true } # ユニークインデックスは生成時に最初から記載されていた
      t.string :crypted_password
      t.string :salt
      t.string :name, null: false # name属性とNOTNULL制約を追加

      t.timestamps                null: false
    end
  end
end

【メモ】 emailのユニークインデックスをadd_indexの書き方に変更したところ、emailに対してno method errorが出てしまったのでemailの箇所は生成された状態からいじらずにdb:migrateした。なんでエラーになったのかよく分からない。

crypted_passwordとsaltとは?

↓簡単に書くと

  • crypted_password(暗号化されたパスワードという意味)
  • saltとはハッシュ化する前のパスワード前後につける文字列のこと
    どういうこと?🤔

→パスワードをそのままDBに保存してしまうのはセキュリティ的な面で危険。このためハッシュ関数というものに通し、暗号化することでその安全性を高めている。ただし、このままだとハッシュ関数から元のパスワード文字列を割り出せてしまうかもしれないので、関数に通す前にsaltという文字列を結合した上でハッシュ関数に通すようにしている。

↓詳しく

関数とは

  • 何かを入れると(引数として渡すと)、何かを返してくれる(戻り値)プログラムの部品のこと

ハッシュ関数とは

  • 入力されたデータを「特定のルールに沿って」ぐちゃぐちゃにした値を返してくれる
  • 同じものを入れれば同じハッシュ値が帰ってくる
  • 適当な値に見えるが特定のルールに従って出力された値であり、デタラメな訳ではない

ハッシュ関数から返ってくる値(戻り値)がハッシュ値

  • ハッシュ値から元のデータは特定するのはほぼ不可能
  • パスワードなどそのままDBに保存したらセキュリティ的にアウトなものをハッシュ関数に渡して出てきたハッシュ値を保存している
  • 入力されたパスワードが正しいかチェックするときは、入力されたパスワードをハッシュ関数に入れ、出てきたハッシュ値とデータベースに保存されている値を比較する

  • このままだとハッシュ関数から元の値を割り出せてしまうかもしれない

  • 先人がさらに強固にするために、入力された値の前後に適当な文字列をくっつけてからハッシュ関数に入れる方法を考えた

→この文字列こそが「salt(ソルト)」

https://wa3.i-3-i.info/word16974.html

★ Sorceryでは、パスワードの末尾にランダムな文字列を結合し、その文字列をsaltフィールドに記憶することでパスワードハッシュの安全性を高めている

2. バリデーション(長い)

sorcery:installで追加されたUserモデルにバリデーションを追加する。

まずSorceryのログイン機能において、どういったバリデーションが追加されているのか確認する。

Sorceryのチュートリアルに記載されているバリデーションの例

# app/models/user.rb
class User < ActiveRecord::Base
  authenticates_with_sorcery!

  validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
  validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }

  validates :email, uniqueness: true
end

→このif: -> { new_record? || changes[:crypted_password] }は何なのか

条件付きバリデーションとは?

特定の条件を満たす場合にのみバリデーションを実行したい場合があります。:ifオプションや:unlessオプションを使うことでこのような条件を指定できます。引数にはシンボル、ProcまたはArrayを使えます。:ifオプションは、特定の条件でバリデーションを行なうべきである場合に使います。特定の条件でバリデーションを行なうべきでない場合は、:unlessオプションを使います。
Active Record バリデーション - Railsガイド

  • 特定の条件の時のみ検証を行うことらしい
  • sorceryのバリデーションにおけるこれif: -> { new_record? || changes[:crypted_password] }は、バリデーションにつけられた条件
  • この条件の時だけバリデーション(検証)にかけてくださいね的なものだと思う

->(lambda記法)とは

呼び出したいProcオブジェクト:if:unlessで使うこともできます。Procオブジェクトを使うと、個別のメソッドを指定する代わりに、その場で条件を書けるようになりますワンライナーに収まる条件を使いたい場合に最適です。
Active Record バリデーション - Railsガイド

どういうこと?🤔

この辺り、そもそもProcやlambdaというものが分からないと先を理解するのが難しいと思った。

Procとlambda(ラムダ)とは?

【Ruby】ブロック・Proc・lambda を理解する - Qiita
↑こちらをかなり参考にしています。

Proc

  • do~endや{}で表現されるブロックを持ち運びに便利なオブジェクトにしたもの
  • Procはクラスであり、Proc.newでオブジェクトを作ることができる
  • 上記で生成されたProcオブジェクトはcallで呼び出しができる
# ブロックをProc.newでオブジェクト化している
proc = Proc.new { |food| p food }

proc.call("りんご") #=> "りんご"
# callで生成したProcオブジェクトを呼び出している
# 引数を渡すこともできる

ブロックを引数で明示的に受け取る方法

def method(&proc) # ブロックをprocオブジェクトに変換している
  proc.call
end

method { p 'とまと' } #=> "とまと"
  • メソッド呼び出し時にブロックを引数として渡している
  • 受け取り側のメソッドは&から始まるブロック引数を使用することで、ブロックを明示的に受け取ることができる
  • (&proc)では渡されたブロックがProcオブジェクトに変換されている

(Proc難しい)

lambda(ラムダ)

Procオブジェクトを作る方法の一つ。

lamd = lambda { |n| p n }

みたいな形でprocオブジェクトを生成できる

ちなみにギリシャ文字の第11文字であるΛ・λのことを言うらしい。

余談ですが、ラムダ技術部というYouTuberさんの名前の由来がよく分かりました。

procとlambdaは何が違うのか

引数チェック

  • lambdaの場合、渡す引数の数が違うとArgumentErrorになる
  • procの場合は、先頭から必要な数だけとって後は無視し、少ないと足りない部分にnilを割り当てる
proc = proc { |n| p n }
proc.call( 'proc', 'lambda' )

lamd = lambda { |n| p n }
lamd.call('lambda') #=> "lambda"
lamd.call('lambda', 'proc') #=> wrong number of arguments (given 2, expected 1)

returnの挙動

  • lambdaの場合、returnした後にメソッドに戻り、メソッドを最後まで実行する
  • Proc.newの場合はreturn後にメソッド自体を抜けてしまう
def lambda_method
  proc = Proc.new { return p "りんご" }
  proc.call
  p "ごりら"
end

# lambdaの場合、return後はメソッドに戻り最後まで実行するので"ごりら"も出力されている
method #=> "りんご"
              #=> "ごりら"

def proc_method
  proc = Proc.new { return p "わかめ"}
  proc.call
  p "たまねぎ"
end

# procはreturn後にメソッド自体を抜けてしまうので、出力されるのは"わかめ"のみ
proc_method #=> "わかめ"

まとめ

  • いずれもブロックを持ち運びに便利なprocオブジェクトにしたもの
  • Proc.new {ブロック}lambda {ブロック}でブロックをオブジェクト化できる(他にもやり方があるが省略)
  • 渡す引数が多い場合、Proc.newでは先頭から必要な分だけ取り後は無視され、足りない部分にはnilが当て嵌められる。lambdaでは引数の数が異なるとArgumentErrorになる
  • return後の挙動として、Procではreturn後にメソッドを抜け、lambdaではreturn後にメソッドに戻り処理を最後まで実行する

条件付きバリデーション、->(lambda記法)続き

validates :password, length: { minimum: 3 }, if: -> { new_record? || changes[:crypted_password] }
  • 条件付きバリデーションはある条件下の時のみ検証を行う
  • ->(lambda記法)はProcの一種。Procオブジェクトの生成を短く書くことができる
unless: Proc.new { |a| a.password.blank? }
↓
unless: -> { |a| a.password.blank? }

★ lambda記法(->)を用いて { new_record? || changes[:crypted_password] }のブロックをProcオブジェクト化している。それをif:オプションに渡している。

  • 条件付きバリデーションの引数にはシンボル、Array、Procが使えるのでいずれか使用
  • Procオブジェクトを使うメリットとしてはその場で条件が書けること。シンボルの場合は個別のメソッドを指定するという形になる
  • ワンライナーで収まる条件を場合はProcオブジェクトを使うといい
class Order < ApplicationRecord
  # シンボル使用の例
  validates :card_number, presence: true, if: :paid_with_card?

  def paid_with_card?
    payment_type == "card"
  end
end

バリデーションの条件詳細

if: -> { new_recrod? || changes[:crypted_password] }

↑これはどういう意味なのか

new_record? インスタンスメソッド

Active Recordのnew_record?インスタンスメソッドを使うと、オブジェトが既にデータベース上にあるかどうかを確認できます。
Active Record バリデーション - Railsガイド

newメソッドで新しくオブジェクトを作成しただけでは、オブジェクトはDBに属していない。saveメソッドを呼ぶことで、オブジェクトは適切なDBのテーブルに保存される。

以下のコンソール結果で納得

irb> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>

irb> p.new_record?
=> true

irb> p.save
=> true

irb> p.new_record?
=> false

保存する前のデータである場合にnew_record?の結果がtrueになる。ニューレコードである。つまりオブジェクトが新規作成されたものであれば、検証を通す。

changes[:crypted_password]

if: -> { new_record? || changes[:crypted_password] }

ユーザーがパスワード以外のプロフィール項目を更新したい場合に、パスワードの入力を省略できるようになる。

irb(main):010:0> user.first_name = "太郎"
=> "太郎"
irb(main):011:0> user.changes
=> {"first_name"=>["たろう", "太郎"]}
irb(main):012:0> user.changes[:first_name]
=> ["たろう", "太郎"]
irb(main):013:0> user.changes[:crypted_password]
=> nil

色々調べてみたけど、どういう理屈でパスワードの入力が省略されているのか分からなかった。。
→パスワードの入力が省略されているというより、if: -> { new_record? || changes[:crypted_password] }によって、そのuserオブジェクトが新規作成されたものか、変更を加えようとしているのがuserオブジェクトのパスワードなのかを判定し、そうならば前述の検証にかけるという仕組み なのかな?と思った。

★ とりあえず、パスワード意外を編集したい場合にパスワード入力を省略できる記載なんだなと納得しておく。

validates :password, length: { minimum: 8 }, if: -> { new_record? || changes[:crypted_password] }
  • パスワードの入力は8文字以上
validates :password, confirmation: true, if: -> { new_record? || changes[:crypted_password] }
validates :password_confirmation, presence: true, if: -> { new_record? || changes[:crypted_password] }
  • confirmation:バリデーションヘルパーの一つ
  • 二つのフィールドで受け取る内容が完全に一致する必要がある場合に使う
  • このヘルパーによって、確認したい属性(この場合password)に「_confirmation」を追加した仮想の属性を作成される

ビューでは下記のようなフィールドを用意

<%= f.text_field :user, :email %>
<%= f.text_field :user, :email_confirmation %>
  • このバリデーションチェックは、password_confirmationがnilでない場合のみ行われる
  • 確認を必須にするために、password_confirmation属性にpresence: trueを追加する
  • presence: trueは指定された属性が空でないこと(値がnilや空でもホワイトスペースでもないこと)を確認するため、内部でblank?メソッドを使っている
validates :email, uniqueness: true
  • これはemailが一意であり、重複していないことを検証する

3. コントローラ作成

ユーザー新規作成コントローラ

$ bundle exec rails g controller users

ルーティング

Rails.application.routes.draw do
  # root_pathで飛ぶページは適当に作っておく
  root 'home#top'

  # ログイン用のuser_sessionsコントローラ
  get 'login', to: 'user_sessions#new'
  post 'login', to: 'user_sessions#create'
  delete 'logout', to: 'user_sessions#destroy'

  # user新規作成機能のresources
  resources :users, only: %i[new create]
end
class UserController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.new(user_params)
    if @user.save
      redirect_to login_path # 新規作成成功後はログイン画面に
    else
      render :new
    end
  end

  private

  def user_params
    params.require(:user).permit(:email, :password, :password_confirmation, :name)
  end
end

新規作成フォーム

<!-- 新規作成はユーザーをテーブルに登録する関係でモデルと紐づいているのでmodel:に@userを渡す-->
<%= form_with model: @user, local: true do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>

  <%= f.label :email %>
  <%= f.text_field :email %>

  <%= f.label :password %>
  <%= f.password_field :password %>

  <%= f.label :password_confirmation %>
  <%= f.password_field :password_confirmation %>

  <%= f.submit '登録' %>
<% end %>

ログイン用コントローラ

$ bundle exec rails g controller user_sessions
class UserSessionsController < ApplicationController
  def new; end

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_back_or_to root_path, success: "ログインしました"
    else
      flash.new[:danger] = "ログインに失敗しました"
      render :new
  end

  def destroy
    logout
    redirect_to root_path, success: "ログアウトしました"
  end
end

ログインフォーム

<h1>ログイン</h1>
<%= form_with url: login_path, local: true do |f| %>
  <%= f.label :email %>
  <%= f.text_field :email, class: 'form-control' %>

  <%= f.label :password %>
  <%= f.password_field :password, class: 'form-control' %>
       
  <%= f.submit 'ログイン', class: 'btn btn-primary' %>
<% end %>
<%= link_to '登録ページへ', new_user_path %>
<a href="#">パスワードをお忘れの方はこちら</a>
  • ログインは新たに情報を登録するわけではないので、モデル(テーブル)と紐づいていない
  • このためurl:オプションでパスを指定するだけでいい
  • フォーム内容はHTTP POSTメソッドとして送信されるため、post 'login', to: 'user_sessions#create'のルートでcreateメソッドが実行されログインできる

form_withの話

modelオプション

  • 基本的にform_withのオプションはmodel:を使用する
  • 引数にはモデルクラスのインスタンス(保存したいテーブルのクラスのインスタンスを指定する
    usersテーブルに新たにレコードを作成したい場合、下記のように記述
def new
  @user = User.new
end
# 新規作成したレコードなのでcreateアクションが動く

def edit
  @user = User.find(params[:id])
end
# 既存のレコードを取得しているためupdateアクションが動く

↑ここで作成したインスタンスをフォームに渡す

<%= form_with model: @user %>
  • ここでモデル.newで新たに作成され、何も情報を持っていなければ自動的にcreateアクションへ
  • findメソッドなどで作成され、既に情報を持っている場合はupdateアクションへ振り分けられる
url:オプションを使う場合
  • モデルとフォームが紐づかない場合(例: ログインフォーム: user_sessions)
  • createやupdateを行うコントローラとモデルが紐付かない場合
    (model: @userと設定したが、別のコントローラに処理が遷移して欲しい場合)
  • @userのようなインスタンス変数を渡さずにurlオプションのみを設定した場合、入力ミスなどで編集処理が失敗した時はフォームへの入力値が一々消えてしまう。失敗しても元入力した値を残しておきたいのならmodelオプションにインスタンスを渡す

認証機能の補足

以下メソッドについて

  • requre_login
  • not_authenticated
  • redirect_back_or_to
not_authencticated
  • authenticatedは認証済みという意味。ということは認証済みでないユーザーに対して行われる処理

公式リファレンス

# The default action for denying non-authenticated users.
# You can override this method in your controllers,
# or provide a different method in the configuration.
def not_authenticated
  redirect_to root_path
end
  • 認証されていないユーザーを拒否する
  • コントローラーでこのメソッドを上書きできる
  • または構成で別の方法を提供している

→ 認証されていないユーザーをroot_pathに飛ばす?

redirect_back_or_toメソッド
# used when a user tries to access a page while logged out, is asked to login,
# and we want to return him back to the page he originally wanted.
def redirect_back_or_to(url, flash_hash = {})
  redirect_to(session[:return_to_url] || url, flash: flash_hash)
  session[:return_to_url] = nil
end
  • ユーザーがログインしていない状態であるページにアクセスした場合、まずログインするように促し、ログイン後はユーザーがアクセスしていたページに戻してあげるメソッド
require_login
# To be used as before_action.
# Will trigger auto-login attempts via the call to logged_in?
# If all attempts to auto-login fail, the failure callback will be called.
def require_login
  return if logged_in? # ログインユーザーがいるならメソッドを終了する

  # ここは解釈が合っているのか分からない
  # たぶんredirect_back_or_toのリクエストを保存する過程なのかなと・・・
  if Config.save_return_to_url && request.get? && !request.xhr? && !request.format.json?
    session[:return_to_url] = request.url
  end

  # 未ログインの場合以下の処理が実行され、結果not_authenticatedメソッドでリダイレクトが行われる
  send(Config.not_authenticated_action)
end
  • before_actionとして使用される
  • logged_inの呼び出しを介して自動ログイン
def logged_in?
  !!current_user
end
  • current_userが存在するのか存在しないのかtrue/falseを返す

!!←こいつは何なのか

結論:戻り値をtrueかfalseに揃える

!!」の動作: すべてをtruefalseにする
ご存じのとおり、Rubyではnilとfalse以外のオブジェクトはすべてtrueとして扱われます。そして否定の論理演算子「!」はtrueとfalseを反転します。

適当に試してみた

irb(main):008:0> !nil
=> true
irb(main):009:0> !!nil
=> false
irb(main):010:0> !!true
=> true
irb(main):011:0> !true
=> false
irb(main):012:0> !!false
=> false
irb(main):013:0> !false
=> true
irb(main):014:0> !!""
=> true
irb(main):015:0> !!"あいう"
=> true
irb(main):016:0> !""
=> false

感想

サクッと復習するつもりが意外と分かっていないことが分かってモリモリになってしまった。。最初期の頃よりかは公式文書への抵抗感が少なくなって良かった。

参考にしたサイト

Simple Password Authentication · Sorcery/sorcery Wiki · GitHub

sorceryによるログイン機能|tumu|note

【Rails】ログイン機能を実装 - Qiita

PFメモ日記(3) railsコマンドが実行できないエラー

deviseを使おうとgemをインストールし、bundle exec rails g devise:installした際に下記のエラーが発生。

Calling DidYouMean::SPELL_CHECKERS.merge!(error_name => spell_checker)' has been deprecated. Please callDidYouMean.correct_error(error_name, spell_checker)' instead. You don't have net-smtp installed in your application. Please add it to your Gemfile and run bundle install /Users/ユーザー名/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:430:in `visit_Psych_Nodes_Alias': Unknown alias: default (Psych::BadAlias)

  • deviseに起因するエラーではない
  • 序盤に書いてあるDidYouMean::SPELL_CHECKRSruby3.1環境で出てくる警告文らしい
  • Rubyの3.3バージョンではDidYouMean::SPELL_CHECKRSの使用が非推奨になり、削除されることの注意書きであり、Ruby3.3以前を使用している分には特に支障はない

https://stackoverflow.com/questions/70800753/rails-calling-didyoumeanspell-checkers-mergeerror-name-spell-checker-h

thorというタスク実行のためのコマンドを作成するgemに起因する注意書きらしく、updateすればなくなる旨が記載されていた。

後半のエラーについて

You don't have net-smtp installed in your application. Please add it to your Gemfile and run bundle install /Users/ユーザー名/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/visitors/to_ruby.rb:430:in `visit_Psych_Nodes_Alias': Unknown alias: default (Psych::BadAlias)

  • net-smtpがインストールされていないのでインストールする旨書かれている
    →これはメール機能に関するgemらしく、今は使用する予定がないので飛ばした。必要になったらインストールしてみる。

  • 色々調べてみるとrailsコマンドが使えないのは、visit_Psych_Nodes_Alias': Unknown alias: default (Psych::BadAlias)の部分に起因する何からし

  • psychというYAML解釈用のgemがあり、4系と3系で解釈法が変わったことによるエラーらしい
    → 3系でないとdatabase.ymlの解釈に失敗している

gemfile.lockを確認すると4系がインストールされていたのでgemfileにgem 'psych', ''~> 3.1を記載してbundle installし直すと解決した

他に試したこと

rails6.1.4以降で解決されているとのことで、gemfileのrailsバージョンを書き換え、bundle updateしてみたが変化なし

Psychとは

Ruby本体のdefault gemでYAMLライブラリのバックエンド実装になります。YAML自体がPsychに依存する形で実装されています。 Ruby の YAML.load が非互換になる(かもしれない) - Secret Garden(Instrumental)

参考にしたサイト

thor

【Ruby】thorの仕組みを調べてみた DSL編 - サーバーワークスエンジニアブログ

psychのエラーに関して

エラーUnknown alias: defaultが出て、railsサーバを起動できない

psych::badalias: unknown alias: defaultで Railsコマンドが使えないを解決 - Qiita

Psych::BadAlias: Unknown alias: defaultというエラーの対処法 - Qiita

PFメモ日記(2) rails new

githubリポジトリを作り、README作成後、特に何も考えずにrails newしてgit branchが訳分からないことになってしまったのでお作法を元にやり直すことに。。

  • Rubyは最新版を使用
  • bundle initから始める(Gemfileが作成される)
  • Railsも原則最新の安定版使用とのことだが、操作に不安があるため6系を使用
  • Minitest、JSライブラリ使用の有無、デプロイ環境のことを考え、オプションを付与してrails newする

※ hotwireとやらを気にすると進まなくなりそうだったので深入りしないことにした
クライアント側のJavaScriptを最小限にするHotwire - ログミーTech

Hotwireとは何なのか?

Rubyバージョン設定

Ruby最新バージョン確認

https://www.ruby-lang.org/ja/downloads/
↑2022年6月現在の時点では3.1.2が最新の安定版

② インストール可能バージョン一覧を確認

$ rbenv install --list

※ 一覧に最新版が表示されなかった場合、下記コマンドでrbenvとruby-buildを更新する
$ brew upgrade rbenv ruby-build

③ 最新版をインストール

$ rbenv install 3.1.2

④ インストールされているか確認

$ rbenv versions

⑤ 最新版がインストールされていたので適用

$ rbenv local 3.1.2

ruby -vでバージョンが適用されているか確認

rbenvを利用してRubyのバージョンを最新安定版にする - Qiita

「bundle init」してGemfileを作成する

$ bundle init

treeコマンドで生成されているか確認

生成されたGemfileのRailsに関する記載を確認する

# gemfile

# 下記の記載があるので任意のバージョンを指定する
# 特に記載しない場合は自動的に最新の安定版がインストールされる
gem "rails", "6.1.6"

7系の仕様に自信がないので6系にする

Gemfileを編集したらbundle install

rails newにオプションを設定する

【完全網羅】rails newの基礎からオプションまで(db, rspec) | 侍エンジニアブログ
↑オプション一覧の記載あり

  • RSpecを使う前提でminitestが生成されないようにする
  • Herokuでデプロイする前提でPostgreSQLを採用する
    ※ Herokuでデプロイする場合はMySQLよりも作業に楽になるらしい
  • JSライブラリを使用する前提でhotwireまたはturbolinksのインストールをスキップする
$ bundle exec rails new . -d postgresql --skip-test --skip-turbolinks

競合するファイルを上書きするかどうかで処理が止まるので、最初から-sオプションで既にするファイルをスキップすればよかった。

エラーメモ

You don't have net-smtp installed in your application. Please add it to your Gemfile and run bundle install
rails aborted!
LoadError: cannot load such file -- net/smtp

net/smtpとやらのエラーが出た

https://github.com/ruby/net-smtp

メール送信関連で必要なファイルらしい🤔
とりあえず今は必要ないから良いのかな??何かあれば、gemをイントールすることに。。

おまけ:アプリ共通で入れることが多いGem

Lint, コード解析
  • rubocop
  • rubocop-rails
  • bullet
テスティングフレームワーク
デバッグ
  • better_errors
  • binding_of_caller

※ ruby3系から標準でdebug.gemが同梱されるため、あえてpry-railsなどを入れる必要がないとのこと

コード補完
  • solargraph
その他
  • annotate

PFメモ日記(1) DBからランダムに値を取ってくる

複数ある中からある一つをランダムで出力する機能を作りたい。

課題で作った掲示板アプリの機能を使って以下のメソッドを試してみる。

機能:掲示板に投稿されたコメントの中から一つランダムで出力する

  • sampleメソッド
  • whereメソッド
  • offsetメソッド

sampleメソッド

配列の要素を1個(引数を指定した場合は自身の要素数を超えない範囲でn個)ランダムに選んで返します。クエリ:sample | るりまサーチ

board.comments.all.sample
  Comment Load (0.1ms)  SELECT "comments".* FROM "comments" WHERE "comments"."board_id" = ?  [["board_id", 39]]
=> #<Comment id: 48, body: "卵", user_id: 1, board_id: 39, created_at: "2022-06-10 08:28:18", updated_at: "2022-06-10 08:28:18">
デメリット

処理に時間がかかりすぎるらしい

offsetメソッド

テーブルからデータを取得する際、どのデータから取得するか(取得開始位置)を指定したい場合、offsetメソッドを使います。 【Rails】offsetメソッドで取得するデータの範囲を指定する方法を解説 | CODE MARINE

IT用語辞典

オフセット【offset】
ITの分野では、何かの位置を指し示す際に、基準となる位置からの差(距離、ズレ、相対位置)を表す値のことをオフセットということが多い。オフセットとは - 意味をわかりやすく - IT用語辞典 e-Words

モデル名.offset(2)

↑先頭は0から始まるため(インデックス番号)、引数にはそれに応じた番号を渡している

randメソッド

指定した範囲の中からランダムな値を返す。

board.comments.offset(rand(board.comments.count)).first
   (0.1ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."board_id" = ?  [["board_id", 39]]
  Comment Load (0.1ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."board_id" = ? ORDER BY "comments"."id" ASC LIMIT ? OFFSET ?  [["board_id", 39], ["LIMIT", 1], ["OFFSET", 1]]
=> #<Comment id: 49, body: "いちご", user_id: 1, board_id: 39, created_at: "2022-06-10 08:28:23", updated_at: "2022-06-10 08:28:23">

whereメソッド

board.comments.where('id >= ?', rand(board.comments.first.id..board.comments.last.id)).first
  Comment Load (0.1ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."board_id" = ? ORDER BY "comments"."id" ASC LIMIT ?  [["board_id", 39], ["LIMIT", 1]]
  Comment Load (0.1ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."board_id" = ? ORDER BY "comments"."id" DESC LIMIT ?  [["board_id", 39], ["LIMIT", 1]]
  Comment Load (0.1ms)  SELECT  "comments".* FROM "comments" WHERE "comments"."board_id" = ? AND (id >= 50) ORDER BY "comments"."id" ASC LIMIT ?  [["board_id", 39], ["LIMIT", 1]]
=> #<Comment id: 50, body: "納豆", user_id: 1, board_id: 39, created_at: "2022-06-10 08:28:28", updated_at: "2022-06-10 08:28:28">
  • id >= ??プレースホルダーと呼ばれており、第二引数で指定した値に置き換えられる
  • 第二引数のrand(board.comments.first.id..board.comments.last.id)で取得した値が?に入る
デメリット

IDに抜けがあるとランダムの正確性に欠ける

まとめ

  • 自分が試した中ではそもそものコメント総数が多くなかったので、大きな速度差がなかった
  • 実際はwhere>offset>>sampleの順に速さが変わるらしい
  • 正確性の面ではoffset>where

参考にしたサイト

https://izanagi-portfolio-site.com/blog/articles/3yg4y3i-ih_e/

【Rails】offsetメソッドで取得するデータの範囲を指定する方法を解説 | CODE MARINE

【Rails】 whereメソッドを使って欲しいデータの取得をしよう! | Pikawaka

Ruby学習まとめ

自販機をシミュレートしたコードを問題点を完全しながら、オブジェクト指向についての考え方を学ぶ。

名前は省略しない

引数にはiとkind_of_drinkが記載されており、このままだとiが何を指すのか分からない。

def buy(i, kind_of_drink)
  if i != 100 && i != 500
  @change += i
  return nil
end

名前は省略せずに、誰が見ても分かるようにする

def buy(payment, kind_of_drink)
  if payment != 100 && payment != 500
  @change += payment
  return nil
end

ValueObject

intやstringと言ったプリミティブ型(基本的な型)をラップする。

意味のある値に対して型を定義すること。

改善前のコードでは自販機クラスbuyメソッドに、支払額として整数の数値をそのままと、Drinkクラスに直接記載されたドリンクの種類を渡していた(引数)。これを、それぞれCoinクラスとDrinkTypeクラスに分けて、enum型として定義しなおす。

一見面倒にしか思えなけれど、下記のメリットがある。

  • コードの可読性を高める
  • 変更に強い
  • 拡張しやすい

ValueObjectという考え方 - Qiita

https://codezine.jp/article/detail/10184

ファーストクラスコレクション

Rubyで作るファーストクラスコレクション - Qiita

配列をラップしたクラスのことで、対象の配列に対する処理を全て集約したクラスになります。別名コレクションオブジェクトとも呼ばれるそうです。

メリット
  • 配列の複雑さを専用の小さなクラスに集約できる
  • これにより対象の配列に関する処理がプログラムのあちこちに散らばることなく管理ができる
  • メンテナンス性の向上やプログラム自体の分かりやすさにつながる

GetterとSetter、プロパティを使用しない

GetterとSetter

【Ruby】「ゲッター」と「セッター」を理解する - Qiita

インスタンス変数そのものはクラス内からしか取得できない。これを外部でも取得できるように定義したのがゲッター。セッターも同様、更新はクラス内からしかできないため、外部から更新できるようにするためクラス内部に更新用のメソッドを定義する必要がある。

アクセスメソッド

ゲッター、セッターを定義しなくてもインスタンス変数の参照・更新を可能にする方法。

  • attr_reader: 変数名…参照を可能にする。ゲッター。
  • attr_writer: 変数名…更新を可能にする。セッター。
  • attr_accessor: 変数名…参照・更新の両方を可能にする。セッターとゲッターを一気に使える。

  • オブジェクト内部状態を使用した処理を実現したい場合は、オブジェクト内で処理し、その結果だけを返すようにする。

まとめ

  • これらはオブジェクト指向カプセル化を壊す不適切なメソッドになりやすい
  • オブジェクト内部状態を使用した処理を実現したい場合は、オブジェクト内で処理し、その結果だけを返すようにする。(処理結果を返すメソッドをクラス内に定義すると言うこと)
  • これにより、オブジェクトが管理する内容が変わっても、他のクラスやメソッドに影響することなく、クラスを変更することができる!

一つのクラスにつき、インスタンス変数は二つまで

※ あくまでもオブジェクト指向エクササイズの規約内容ではインスタンス変数を二つに絞るように書かれているが、同じ役割を一つのクラスにまとめることを考えること。

反対に、一つのクラスが複数の役割を担っている場合は、枠割ごとにクラスを分割することも検討する。

else句を使用しない

現状、自販機クラスのbuyメソッド内には「ジュース一本100円の時、投入された硬貨が100円ならば、お釣り管理オブジェクトに支払額を入れる。投入額が500円ならばお釣り管理から400円分のお釣りを取り出す処理をして、お釣り管理の在庫内を更新する。」みたいな処理が一緒くたに記載されている。

非効率で分かりづらいコード。

お釣り管理(coinmech)に対して、お支払額(payment)を渡すことで、後の内部処理はお釣り管理でするして、その結果のみ返すようにすれば、もっと見通しよく簡潔なコードが書ける。

「else句を使用しない」と言うのはそう言うこと。

関連が強いもの同士を同じパッケージにまとめる

お金関連ならmoneyディレクトリに、在庫関連なら在庫ディレクトリにまとめてファイルを見通し良く使おう。

アルゴリズムメモ

組み込み変数ARGV(argument vector)

【Rubyの基礎】コマンドラインからの実行で利用するARGVの基本 | ポテパンスタイル

Rubyプログラムを実行する際に、引数として指定したオプションをプログラムの中で利用するための仕組み。argumentは引数のことで、ARGVはプログラムを実行した際に指定された引数を格納する配列。

# sample.rb
arg = ARGV[0].to_i
$ sample.rb 9
  • sample.rbファイルをターミナルで実行。その際、引数として適当な数字を渡す
  • 渡される引数はstring型になっているので、もし数値そして使いたいのであればto_iメソッドで変換する
times, upto, downtoメソッド

timesメソッド、uptoメソッド、downtoメソッド - 繰り返し - Ruby入門

いずれもIntegerクラスで用意されている

upto

# sample2.rb
arg = ARGV[0].to_i

1.upto(arg) do |num|
  p num
end
$ ruby sample2.rb 4

#=>
1
2
3
4
  • uptoメソッドはx.upto(y)のように使用した場合に、xからyまでの数値を順にブロック内に代入していることがわかる

MySQLメモ

M1 Mac MySQLのインストール

課題に際してbundle installしようとするとエラーが出て上手くいかなかったため、調べた自分用メモです。間違った記載が多くあると思われるので、あまり参考にしない方が良いかも知れないです。

※ 記事を参考にコマンド実行する前に記事の鮮度・信頼性、本当に自分と同じ環境なのか考えてから実行するようにすること。

bundle install時に下記エラーが出ました。

34 warnings generated.
linking shared-object mysql2/mysql2.bundle
ld: library not found for -lssl
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [mysql2.bundle] Error 1

make failed, exit code 2

Gem files will remain installed in
/Users/ユーザー名.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/mysql2-0.5.3 for
inspection.
Results logged to
/Users/ユーザー名/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/extensions/-darwin-20/2.6.0/mysql2-0.5.3/gem_make.out

An error occurred while installing mysql2 (0.5.3), and Bundler cannot
continue.
Make sure that `gem install mysql2 -v '0.5.3' --source 'https://rubygems.org/'`
succeeds before bundling.

An error occurred while installing mysql2 (0.5.3), M1 Mac

↑こちらのブログを参考にインストールすることができました。

どうやら、デフォルトで使われているSSLに互換性がないためgem installで探し出すことができないとかなんとか・・

試したこと

  1. 互換性のあるopensslというものに変更
  2. シェルにopensslのパスを通して、反映させる
  3. bundle configのパスを変更
  4. ライブラリのパスを変更

  5. 使用されているsslを確認

$ openssl version
LibreSSL 2.8.3
  • opensslのパス確認
$ brew --prefix openssl

bundle installがmysql2でコケるときに試すやつ

$ brew install openssl
$ echo 'export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zshrc
$ source ~/.zshrc
$ bundle config --local build.mysql2 "--with-ldflags=-L/opt/homebrew/opt/openssl@3/lib"
$ export LIBRARY_PATH=$LIBRARY_PATH:/opt/homebrew/Cellar/zstd/1.5.2/lib:/opt/homebrew/Cellar/openssl@3/1.1.1m/lib/

これでbundle installできるようにはなったけど、正直良く分かってない。。🥲

OpenSSLとは

インターネット上で標準的に利用される暗号通信プロトコルであるSSLおよびTLSの機能を実装したプログラムの一つ。他のソフトウェアに組み込んで使用するライブラリ(プログラム部品)となっており、オープンソースソフトウェアとして公開されている。OpenSSLとは - IT用語辞典 e-Words

まずSSL(Secure Sockets Layer...『安全な受け口』層)とは

「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典より

通信プロトコル(通信する際に双方で足並みを揃えるための約束事)の一つ。
インターネット上でやり取りする情報を暗号化して送受信するための仕組み。

SSLは、公開鍵暗号方式で共通鍵の受け渡しを行い、あとは受け渡した共通鍵を使って共通鍵暗号方式(秘密鍵暗号方式)でやり取りすることで、通信の安全性を高める仕組みです。

公開鍵暗号方式ってどんなもの?

  1. クライアントがサーバに挨拶する。
    🙋🏻‍♂️ < Hi! 🤖

  2. サーバはクライアントにサーバ証明書(自己紹介)を送る。このサーバ証明書の中にはサーバの公開鍵(「これを使って暗号化して下さい」の鍵)が入っている。
    💁🏻‍♂️ サーバ証明書📝🗝送る >🤖

  3. クライアントはサーバ証明書を送ってきたサーバが本物か確かめるために、認証局に問い合わせる。🏢< 本物です。 アレは本物ですか? >🤷🏻‍♂️ 🤖

  4. サーバが本物だった場合、サーバと秘密の会話をするためのルールである共通鍵を作成し、先ほど受け取ったサーバの公開鍵に従って共通鍵を暗号化する。
    📄+🗝🙆🏻‍♂️(暗号化) 🔐✨

  5. 暗号化された共通鍵(暗号化された共通ルール)をサーバに送る。
    💁🏻‍♂️< 共通鍵渡すね 🔐🤖どうも

  6. サーバは、暗号化を解除する鍵であるサーバの秘密鍵を使って、暗号化された共通鍵を復号する。 🤖🔑 🔓✨(復号)

これにより、クライアントもサーバもやり取りするための共通鍵(ルール)を手に入れた。あとはこのルール(共通鍵)を使ってやり取りすることで第三者にやり取りを盗み見られたとしても、暗号化されているので何が書いてあるのか分からない。これが共通暗号方式(秘密鍵暗号方式)でやり取りをするということ。
🙆🏻‍♂️🔐✨⇄🤖🔐✨ ❌👿(くそぉ...)

TLSとは

SSLが1.0→2.0→3.0までバージョンアップし、次のバージョンアップでSSL4.0になると思いきや「TLS1.0」になったらしい。SSLの後継。(厳密には違うらしい)

このあとrails db:createしようとするも上手くいかない

これに関してはusernameとpasswordが記載されていないことが原因でした。

各自の環境に合わせてusernameとpasswordを修正するように指示があったので、初期設定のrootログインをしてみることに。

MySQLのデフォルトパスワードを確認する - Qiita

$ mysql -u root
パスワードを設定する
mysql> set password for root@localhost='新しいパスワード';
Query OK, 0 rows affected (0.00 sec)
MySQLにログインする
$ mysql -u root -p

よく使うMySQLコマンド&構文集 - Qiita