0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

パーフェクトRuby on Rails 6章 メモ・雑感

Last updated at Posted at 2025-01-19

6 Railslアプリケーション開発

雑感

この章は本格的なアプリケーションの開発を通した実践的な内容でした。OAuthの利用による外部サイトと連携したアプリケーションの認証機能やコントローラの粒度を上げる指針などかなり実務に近い内容だったので早いうちに基礎から学べてよかったと感じています。認証機能などは個人開発にも活かせそうな内容ですね。
以下、個人的に押さえておきたかったポイントのメモ・忘備録です。(基本的なMVCの実装に関する内容は意図して省いています。)

6.1 アプリケーションの作成と下準備

Hamlの導入

RailsデフォルトのテンプレートエンジンERBから、より簡潔な記述が可能なHamlへの移行をするには以下の手順

  • gemをインストールしてbundle install
Gemfile.rb

gem 'hamlit-rails', '~> 0.2.3'
  • ERB->Hamlへ変換するgemを追加してbundle install
Gemfile.rb

gem 'html2haml', '~> 2.2.0' # hamlテンプレートエンジンに依存しているので削除する
  • ERBファイル変換コマンドを実行(メッセージにはyを返答する)
% bin/rails hamlit:erb2haml
  • コマンド終了後に、heml2hamlgemを削除しbundle install

6.2 GitHubでログイン機能を作る

OAuthを利用することでアプリケーションにGitHubのパスとIDを渡さずに認証機能を実装することができる

1. GitHubアプリケーションの登録

  1. GitHubへログイン
  2. プロフィールアイコンをクリックしてSettings
  3. 左側リストからDeveloper settings
  4. リストのOAuth Apps
  5. New OAuth Appを選択し必要項目を入力して登録する
    • Authorization callback URLは認証後に遷移するURL(例: http://localhost:3000/auth/github/callback)であり、それ以外の登録情報は認証ページに表示される内容である

2. RailsアプリケーションにOAuthを利用したログイン機能を実装

gemを追加してbundle install

Gemfile.rb

gem 'omniauth', '~> 1.9.1' # 認証用のライブラリ

gem 'omniauth-github', '~> 1.4.0' # GitHub認証用のライブラリ

gem 'omniauth-rails_csrf_protection', '~> 0.1.2' # CSRF対策用のライブラリ

config/initializers/omniauth.rbを作成する

作成したGitHubアプリケーションのページに表示されているClient IDClient Secretを設定する(シークレットはなければ新規に作成する)

config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  if Rails.env.development? || Rails.env.test?
    provider :github, "自分のクライアントID", "自分のクライアントシークレット"
  else
    provider :github,
      Rails.application.credentials.github[:client_id],
      Rails.application.credentials.github[:client_secret]
  end
end

production環境用の設定は./config/credential.yml.encへ記述して暗号化して保存すると上記設定ファイルのように取得できる

EDITOR="code --wait" bin/rails credentials:edit
./config/credential.yml.enc
secret_key_base: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
github:
  client_id: "本番用のクライアントID"
  client_secret: "本番用のクライアントシークレット"

本番起動時は上記ファイルの復号のためにRAILS_MASTER_KEY環境変数を設定して起動する(内容はconfig/master.keyと同じ)

ログインユーザ用のモデルファイルに必要なカラムを追加

カラム名 意味
provider OmniAuthの認証で使用するプロバイダ名(今回の場合はgithub)
uid providerごとに与えられるユーザ識別用の文字列
name GitHubのユーザ名
image_url GitHubのアイコン画像のURL
OmniAuthを利用して認証機能を実装する場合に必要なカラム

最低限必要な基本的なカラム:

  • provider - 認証プロバイダー名(例:google、github)を保存
  • uid - プロバイダーが提供する一意のユーザーID

任意で追加することが多いカラム:

  • name - ユーザー名
  • email - メールアドレス
  • avatar_url - プロフィール画像URL
  • oauth_token - アクセストークン
  • oauth_expires_at - トークンの有効期限

マイグレーションファイルの例:

db/migrate/20250111033040_create_users.rb
class CreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      # NOT NULL制約を追加しておくことで想定外の不整合を防ぐ
      t.string :provider,  null: false 
      t.string :uid,       null: false
      t.string :name,      null: false
      t.string :image_url, null: false
      t.timestamps
    end

    # 一意のデータにはユニークインデックスを付与
    add_index :users, %i[provider uid], unique: true
  end
end

ログイン処理の実装

OmniAuthはデフォルトで/auth/:providerというリンクを通してサービスプロバイダーの認証ページに遷移する。GitHubの場合はビューファイルに以下のようなリンクを記述する(Hamlの例)ことでGitHub認証ページへ遷移できる

%ul.navbar-nav
  - if logged_in?
    %li.nav-item
      = link_to "ログアウト", logout_path, class: "nav-link", method: :delete
  - else
    %li.nav-item
      = link_to "GitHubでログイン", "/auth/github", class: "nav-link", method: :post

さらに認証後のルーティングを定義

config/routes.rb
Rails.application.routes.draw do
  root "welcome#index"
+  # GitHubアプリケーションに登録した認証後のリダイレクトURLにログインアクションを割り当てる
+  get "/auth/:provider/callback", to: "sessions#create"
  delete "/logout", to: "sessions#destroy"  
end

認証後のログインアクションを実装

app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
+    # request.env['omniauth.auth']にはGitHubから渡された情報が格納されている
+    user = User.find_or_create_from_auth_hash!(request.env['omniauth.auth'])
+    session[:user_id] = user.id
+    redirect_to root_path, notice: 'ログインしました'
  end

  def destroy
    reset_session
    redirect_to root_path, notice: 'ログアウトしました'
  end
end
app/models/user.rb
class User < ApplicationRecord
+  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]
+
+    # find_or_create_by!メソッドは、例外を発生させるため、!をつけている
+    User.find_or_create_by!(provider: provider, uid: uid) do |user|
+      user.name = nickname
+      user.image_url = image_url
+    end
+  end
end

6.3 イベントの登録機能を作る

イベント用モデルのリソースを一括で作成

rails g resouceコマンドでモデル、コントローラ、ルーティングを同時に作成できる

カラムの型を指定しなかった場合は自動的にstring型となる

% bin/rails g resource event owner_id:bigint name place start_at:datetime end_at:datetime

イベント作成用アクションをコントローラへ追加

app/controllers/events_controller.rb
class EventsController < ApplicationController
  skip_before_action :authenticate, only: :show

  def show
    @event = Event.find(params[:id])
  end

+  # この時点でオブジェクトを生成(保存はしない)
+  # ビューファイルで適切なフォームのURL/HTTPメソッドの自動設定やエラーハンドリングが容易になる
+  def new
+    @event = current_user.created_events.build
+  end

+  def create
+    @event = current_user.created_events.build(event_params)
+    if @event.save
+      redirect_to @event, notice: '作成しました' # ブラウザ上でもリダイレクト
+    end
+    # 失敗した時はデフォルトのcreateビューを表示(次項で解説)
+  end

+  private

+  def event_params
+    params.require(:event).permit(:name, :place, :content, :start_at, :end_at)
+  end
end

form_withメソッドのバリデーションエラー時の設定

ビューファイルでform_withメソッドを使用するとデフォルトでdata-remote="true"の属性を持つformタグを生成する。この属性が有効であればフォーム情報送信がAjaxで非同期的に行われ、部分的にDOMを変更し画面遷移が高速になる。
しかし、formからAjaxリクエストを送信した場合のレスポンスの扱いは自動化されておらず、開発者が実装する必要がある。

通常、Ajaxリクエストのレスポンスでリダイレクト用のステータスコードが返却されてもブラウザの画面は変化しないが、turbolinks gemによってAjaxかつGET以外のリクエストが届いた後にredirect_toを実行した場合はリダイレクト相当の挙動をする。

上記コントローラはこの仕様によってイベント作成成功時はイベント詳細ページへリダイレクトする挙動となる。一方、@event.saveがバリデーションエラーとなった場合はredirect_toが実行されずデフォルトのビューをレンダリングする。

SJR(Server-generated JavaScript Responses)の作成方法

Ajaxでのバリデーションエラー時の対応はSJRによって対応するのが推奨されている。エラーメッセージを返却するビューをapp/views/events/create.js.erbとしてJavaScriptで表現する(ERBはJavaScriptのコードをそのまま書けるのでここではhamlは使われていない)

app/views/events/create.js.erb
<%# Ajaxでのフォームエラー時はjsを返却する(SJR) %>
<%# 引数でerrors部分テンプレートを呼び出す %>
<%# jメソッドで改行やクォートをエスケープして文字列をjsで扱えるようにする %>
document.getElwmentById("errors").innerHTML = "<%= j render("errors", errors: @event.errors) %>";

エラー用のテンプレート:

app/views/application/_errors.html.haml
.alert.alert-danger
  %ul.mb-0
    - errors.full_messages.each do |message| 
      %li= message
form_withでAjaxの挙動をオフにしたい場合

form_withlocal: trueのオプションを追加すると、data-remote="true"の属性は追加されなくなる。
全てのform_withで適用したい場合はconfig/application.rbに設定を追加する

config/application.rb
config.action_view.form_with_generates_remote_forms = false

アプリケーション固有の辞書データの作成

デフォルトではf.label :nameで表示されるラベルや、バリデーションエラー時のメッセージは全て英語となっている。
日本語出力に変換するためには以下の手順を踏む

  1. config/application.rbの設定を変更する
config/application.rb

module AwesomeEvents
  class Application < Rails::Application
    # Initialize configuration defaults for originally generated Rails version.
    config.load_defaults 6.0
    config.time_zone = "Tokyo"
+    config.i18n.default_locale = :ja # エラーメッセージを日本語化
  end
end

  1. 辞書データ用のgemを追加し、bundle install
Gemfile.rb
gem 'rails-i18n', '~> 6.0.0' # Railsの辞書ファイル
  1. 辞書データをconfig/locales配下に作成する
config/locales/ja.yml
ja:
  activerecord:
    models:
      event: イベント
    attributes:
      event:
        name: 名前
        place: 場所
        start_at: 開始時間
        end_at: 終了時間
        content: 内容

6.4 イベントの閲覧機能を作る

日時のフォーマット

app/views/events/show.html.haml
.card.mb-2
  %h5.card-header 開催時間
  .card-body
    -# lメソッドはI18nモジュールのメソッドで、引数に日時を指定すると、その日時を指定したフォーマットで表示する
    -# ここでは:longを指定している
    %p.card-text= "#{l @event.start_at, format: :long} - #{l @event.end_at, format: :long}"

6.5 イベントの編集・削除機能を作る

確認用ダイアログの表示

data: { confirm:}オプションでボタンを押した際に確認用ダイアログを表示できる

app/views/events/show.html.haml
.col-4
    - if @event.created_by?(current_user)
      = link_to "イベントを編集する", edit_event_path(@event), class: "btn btn-info btn-lg btn-block"
      -# data: { confirm:}で削除ボタンを押した際に確認ダイアログを表示する
      = link_to "イベントを削除する", event_path(@event), class: "btn btn-danger btn-lg btn-block", method: :delete, data: { confirm: "本当に削除しますか?" }

6.6 登録されたイベントへの参加機能・参加キャンセル機能を作る

複合ユニークインデックスの作成

%i[event_id user_id]で複合キーを指定することで複合インデックスを作成し、ユニーク制約を付与して複合ユニークインデックスとする

UserEventの中間モデルTicketに定義する場合:

class CreateTickets < ActiveRecord::Migration[6.0]
  def change
    create_table :tickets do |t|
      t.references :user
      t.references :event, null: false, foreign_key: true, index: false
      t.string :comment

      t.timestamps
    end

+    # %i[event_id user_id]で複合キーを指定し、組み合わせがユニークであることを保証
+    add_index :tickets, %i[event_id user_id], unique: true
  end
end

モーダルの作成

ビューファイル内でモーダルを作成できる。form_withを追加することで簡易的なイベント参加用ビューを作成できる(newアクション用のビューが不要となる)

app/views/events/show.html.haml
-#"data-toggle: "modal""属性は、クリック時にモーダルウィンドウを表示するための属性
-# "data-target" 属性は、クリック時に表示されるモーダルウィンドウのIDを指定
%button.btn.btn-primary.btn-lg.btn-block{ "data-toggle": "modal", "data-target": "#createTicket" }
参加する
%div.modal.fade#createTicket
.modal-dialog
  .modal-content
    .modal-header
      %h4.modal-title#dialogHeader 参加コメント
      -# "data-dismiss"属性は、ボタンがクリックされたときにモーダルを閉じるためのBootstrap属性
      -# &timesは、HTMLエンティティで「×」を表し、閉じるボタンとして使用される
      %button.close{ type: "button", "data-dismiss": "modal" } &times;
    = form_with(model: @event.tickets.build, url: event_tickets_path(@event))  do |f|
      .modal-body
        #createTicketErrors
        = f.text_field :comment, class: "form-control"
      .modal-footer
        %button.btn.btn-default{ type: "button", "data-dismiss": "modal" } 
          キャンセル
        = f.button "送信", class: "btn btn-primary"
        

上記の場合、ビューの作成が不要なのでnewアクションには直接URLを叩かれた場合のエラーハンドリングのみを追加

app/controllers/tickets_controller.rb
class TicketsController < ApplicationController
  # ログイン状態で直接URLを叩かれた場合にエラーを発生させる
  # 未ログイン状態の挙動は、ApplicationControllerで定義
  def new
    raise ActionController::RoutingError, 'ログイン状態で TicketController#new にアクセス'
  end
end

ビューをレンダリングしないアクション内での変数

下記のdestroyのようにビューへ変数を渡す必要がない場合は、インスタンス変数ではなくローカル変数を使用する

app/controllers/tickets_controller.rb
class TicketsController < ApplicationController

  # このアクションは必ずリダイレクトされるため、ビューに渡すインスタンス変数は不要
  def destroy
    # !でレコードが見つからない場合と削除失敗時に例外を発生させる
    ticket = current_user.tickets.find_by!(event_id: params[:event_id])
    ticket.destroy!
    redirect_to event_path(params[:event_id]), notice: 'このイベントの参加をキャンセルしました'
  end
end

6.7 退会機能を作る

コントローラを増やす指針

例えば、レコード削除用の確認ページをレンダリングするアクション(createnew,updateeditに相当)が必要になった場合、通常の7アクション以外の新しいアクションを新設しなければならない。
そのような基本以外のアクションを作成したくなった場合はモデル名とは別名のコントローラを作ることを検討しても良い。

例: retirementsコントローラ(ユーザー退会処理用のコントローラ)
- createで退会処理
- newで退会確認用ページのレンダリング

トランザクションの中断

例えば、モデルレコード削除時のコールバックにthrow(:abort)を記述するとその削除処理自体を中断させることができる。

app/models/user.rb
class User < ApplicationRecord
  before_destroy :check_all_events_finished
  
  def check_all_events_finished
    now = Time.zone.now
    if created_events.where(":now < end_at", now: now).exists?
      errors[:base] << '公開中の未終了イベントが存在します'
    end

    if participating_events.where(":now < end_at", now: now).exists?
      errors[:base] << '未終了の参加イベントが存在します'
    end

+    # throw :abortでコールバックを中断し、例外を発生させる
+    throw :abort unless errors.empty?
  end
end

削除時に関連レコードの外部キーをnullにする

関連レコードの外部キーにNOT NULL制約を付与していない場合、nullfyを指定してレコード削除時に関連レコードの外部キーをnullにできる

app/models/user.rb
class User < ApplicationRecord
  before_destroy :check_all_events_finished

  # nullifyで削除した際に関連付けられたレコードの外部キーをnullにする
+  has_many :created_events, class_name: 'Event', foreign_key: 'owner_id', dependent: :nullify
+  has_many :tickets, dependent: :nullify
  has_many :participating_events, through: :tickets, source: :event
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?