Open16

パーフェクト Ruby on Rails

にったまにったま

初期構築の際の注意点

  • rails newの際など、不要なコンポーネントが生成しないようにオプションを追加しておくべき

    • 不要なファイルは開発のノイズになるため
    • サーバー起動時に不要なコンポーネントを読み込んだ分、メモリが余計に消費されてしまうから
    • 不要ファイルは可能な限り削除しよう
  • 複数のテンプレートエンジンが存在するとファイルの読み込み順によっては使用しないほうが使われてしまう恐れがあるので、gemは削除しておこう

gemのバージョンアップ戦略

gemは基本的にこまめにバージョンアップしていくべき

  • テストコード書いていないから確認コスト高いといった理由などでバージョンアップしないプロダクトも多い
    • 当然脆弱性は高くなっていくが、パッチも当てずになってしまうケースが多い
  • gemのバージョンアップしていくには、まずテストを書き、その上でバージョンアップを続けられる仕組みを作ることが重要
    • Github Dependabotとか

OAuth

OAuthとは、第三者となるアプリに対して安全にアクセス権限を付与するためのプロトコルのこと

  • OAuthがないと、例えばユーザーのGithubパスワードとかを全部アプリケーション側に預けることになり、アプリケーション側はGithubと同様の権限を持つことになってしまう
    • OAuthはこういったアプリ側に権限を渡さず、必要な権限だけを与える仕組み
  • OmniAuth は様々なユーザー認証機能を提供するgem

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  if Rails.env.development? || Rails.env.test?
    provider :github, "CLIENT_ID", "SECRET"
  else
    provider :github,
      Rails.application.credentials.github[:client_id],
      Rails.application.credentials.github[:client_secret]
  end
end

Rack Rackミドルウェアとは

Railsのproduction環境で使用する平文パスワードに対する対応

  • config/credentials.yml.encファイルに暗号化したyamlファイルを保存している
    • .encはフリーソフト「ED」で暗号化・変換を行ったファイル
    • 復号化用のキーが無い限り中身を読み取ったりはできない
      • 復号化のキーはrails new時に生成されたconfig/master.keyファイルにかかれている
      • これは公開すると大変だが、デフォルトで.gitignoreに設定されている
    • また、master.key以外にもRAILS_MASTER_KEY環境変数に復号化キーを設定してもよい
      • 本番環境ではこっちのほうを利用することが多い
    • rails credentials:edit コマンドでconfig/credentials.yml.encファイルを編集し、暗号化できる
にったまにったま

sessionsコントローラー

class SessionsController < ApplicationController

  def create
    user = User.find_or_create_from_auth_hash!(request.env["omniauth.auth"])
    session[:user_id] = user.id
    redirect_to root_path, notice: "ログインしました"
  end
end
  • request.env["omniauth.auth"]

    • OmniAuth::AuthHashというクラスのオブジェクト
    • ここにGithubから渡されたユーザー情報やアクセストークンが格納されている
      request.env自体はRackの中の仕組み また今度調べる
      https://zenn.dev/zakky/scraps/4d9944b4f5f8f1
  • session[:user_id]にUserオブジェクトのidを入れることで「ログインした」という扱いにする

にったまにったま

find_or_create_from_auth_hash!の実装

app/models/user.rb

def self.find_or_create_from_auth_hash!(auth_hash)
    provider = auth_hash[:provider]
    uid = auth_hash[:uid]
    nickname = auth_hash[:info][:nickname]
    image_url = auth_hash[:info][:image]

    User.find_or_create_by!(provider: provider, uid: uid) do |user|
      user.name = nickname
      user.image_url = image_url
    end
end
  • auth_hashは呼び出し先のrequest.env["omniauth.auth"]に入っているユーザー情報など
  • find_or_create_by!はActiveRecordが用意しているメソッド
    • https://railsdoc.com/page/find_or_create_by
    • 引数で渡した値(providerとuid)を持つレコードが存在すれば、そのオブジェクトを返す
    • 存在しなければ引数及びブロック内の値を設定してレコードを作成し、そのオブジェクトを返す
  • クラメーションマークがついている場合は保存失敗した場合に例外発生を起こしてくれる
にったまにったま

privateに関して

アクションとして呼び出せるのは、publicメソッドだけです。補助メソッドやフィルタのような、アクションとして呼び出したくないメソッドには、privateやprotectedを指定して公開しないようにするのが定石です。
https://railsguides.jp/action_controller_overview.html#メソッドとアクション
Railsに関してはアクションとして呼び出すものにはpublic それ以外はprivateらしい

  • アクションとは、「ユーザーリクエストに対して行う処理」のこと

public private protected

  • publicはクラス外部からでも自由に呼び出せるメソッド
  • privateは外部に公開されないメソッド
    • すなわち「クラスの外からは呼び出せず、クラスの内部のみで使えるメソッド(レシーバーがselfに限定されるメソッド)」
    • ちなRuby2.7からはselfを付けてprivateメソッドを呼び出すことが許容されたらしい(それ以前はエラー)
  • Rubyの特徴として、「privateメソッドはサブクラスでも呼び出せる」という特徴がある
class User

  def goobye
    "goodbye #{self.name}"
  end

  private

    def hello
      'hello'
    end

    def name
      'alice'
    end
end

rb(main):026:0> user = User.new
=> #<User:0x000055fc90f95ac8 id: nil, provider: nil, uid: nil, name: nil, image_url: nil, created_at: nil, updated_at: nil>
irb(main):027:0> user.hello
/usr/local/bundle/gems/activemodel-6.1.7/lib/active_model/attribute_methods.rb:466:in `method_missing': private method `hello' called for #<User id: nil, provider: nil, uid: nil, name: nil, image_url: nil, created_at: nil, updated_at: nil> (NoMethodError)
# これはクラス外部から呼び出しているのでエラー

irb(main):029:0> user.goobye
=> "goodbye alice"
irb(main):030:0>

application_controller.rb

class ApplicationController < ActionController::Base
  helper_method :logged_in?, :current_user

  private

    def logged_in?
      !!session[:user_id]
    end
end
  • logged_in?メソッドはアクションではない(ログイン状態を返すだけでユーザーからのリクエストの処理ではない)ためprivateになっている
  • またコントローラーとビューの両方から利用するメソッドのためapplication_controllerに定義
    • ビューで使用するためにhelper_methodに宣言をさせておく
  • !!にすることで必ずbooleanが返ってくる
にったまにったま

タイムゾーンに関して

  • デフォルトだと当然UTC
  • config.time_zone = "Tokyo"にすればJSTになる
    • ただし、この設定はDBに保存される時間はUTCに変換される
    • DBカラムを直接参照しない限りはJSTをそのまま扱うことになる
    • この仕様は複数のタイムゾーンにまたがるApplicationを開発するときに真価を発揮する
にったまにったま

バリデーションに関して

  • モデルのバリデーションを決める際に文字数制限は忘れられがち
  • 多くのRDBではstringやtext型の入力には制限を設けていないため、例えば1万字とかでも入力できてしまう
    • これらの型のときは必ず文字数の制限を入れておこう
にったまにったま
class User < ApplicationRecord
  before_destroy :check_all_events_finished

  has_many :created_events, class_name: "Event", foreign_key: "owner_id"
  • Userクラスでは「関連元のユーザーが作成したイベント」の関連であることがわかりやすくなるようにcreated_eventsという名前で設定してる
  • また、外部キーもわかりやすく別名をつけている
    • こうやって関連してるクラスとの関係性がよりわかりやすくなるようにするのがいいね
にったまにったま

Railsで日時を扱うときにはTimeDateTimeといったRuby組み込みクラスではなく、Time.zone.nowTime.currentメソッドで返されるActiveSupport::TimeWithZoneオブジェクトを利用するのが一般的

にったまにったま
  • form_withはデフォルトでdata-remote="true"の属性を持ったformタグを生成する
    • このformが送信されると、rails-ujsがそれを捕捉してAjaxで非同期にフォームの情報を送信してくれる
    • これによって通常の画面遷移を行うよりもAjaxを利用して部分的にDOMを変更するほうが画面遷移を速く終わらせることができる
  • Turbolinksも通常のリンクを自動でAjaxに変更してくれる
    • こっちはリンクが通常のリンクと同じように扱えるが、formからAjaxリクエストを送ったときのレスポンスは自動化されておらず、どうするか開発者に委ねられている
  • AjaxでPOSTした結果、バリデーションエラーになった場合の結果は開発者で実装する必要があるが、DHHとしてはSJRで対応するとよいとしている
  • Server-generated Javascript Response
app/views/events/create.js.erb
document.getElementById("errors").innerHTML = "<%= j render("errors", errors: @event.errors) %>"
  • 最終的にJSファイルで返すので、erbで実装されている
  • jメソッドはescape_javascriptメソッドのエイリアス
    • 改行やクオートなどをエスケープし、文字列をJSとして扱えるようにしてくれる
  • ビューテンプレートの探索は app/アクションの所属するコントローラー名で行われるが、それが見つからなかった場合は親コントローラー名で探索される
    • 基本的にどのコントローラーもapplicationControllerを継承しているので、app/views/application/配下にビューテンプレートを配置しておくと汎用的につかえて便利

SJRとはつまり、サーバーサイドでJSを生成して、クライアントサイドではそのままそれを実行するという形式になる
要するにサーバー側でHTMLを生成してクライアントでそれを受け取るだけなので、フロントエンドをほとんど書く必要がなくなる
ただこれはReactとかリッチUIを作るケースとは真っ向から対立するので、開発用途に応じて選ぶべき

にったまにったま
.alert.alert-danger
  %ul.mb-0
    - errors.full_messages.each do |message|
      %li= message

hamlは要素の隣にrubyコードの=を書く場合、要素や属性と=をくっつけないと認識されないっぽい
にったまにったま
def edit
    @event = current_user.created_events.find(params[:id])
end

↑はcreated_eventsメソッド使わなくてもEvent.find_by(owner_id, current_user.id, id: params[:id])で取得できるが、関連を使える場合はなるべく関連を使うこと
例えばowner_idで絞り込む場合、絞り込みがなくてもエラーにならないので誰でも編集できるようになってしまい大事故になりかねない

にったまにったま
class CreateTickets < ActiveRecord::Migration[6.1]
  def change
    create_table :tickets do |t|
      t.references :user, foreign_key: true
      t.references :event, null: false, foreign_key: true, index: false
      t.string :comment
      t.timestamps
    end

    add_index :tickets, %i[user_id event_id], unique: true
  end
end
  • 退会した際にuser_idカラムがnullになることを許容するためにnullを許容している
  • ユーザーがイベントに重複して参加することがないようにユニークインデックスを貼っている
  • 複合インデックスを定義したことになる
    https://qiita.com/towtow/items/4089dad004b7c25985e3