Rails標準の機能を活用して素早く機能実装する
雑感
この章はRailsの組み込みライブラリについての内容でした。初見の機能も多く、個人的にはなかなかヘビィな内容でしたが、非同期処理やメール送信機能など、かなり実務に近い内容になってきている印象です。以下、不具合の解消法なども交えた忘備録です。
Active Jobによる非同期実行
ジョブの例外処理
- Active Jobはジョブ実行時に発生した例外をキャッチして対応を変える仕組みが用意されている
-
retry_on
はジョブのリトライを実行。ただし、Sidekiqなどバックエンド側でリトライの仕組みを持っていることがあるので二重実行に注意が必要
class AsyncLogJob < ApplicationJob
略
# ジョブのリトライ(wait: 待ち時間, attempts: リトライ回数)
retry_on StandardError, wait: 5.seconds, attempts: 3
# 複数の例外を指定することも可能
retry_on ZeroDivisionError, TypeError, wait: 10.seconds, attempts: 3
end
-
discard_on
はジョブの破棄を実行
class AsyncLogJob < ApplicationJob
略
# ジョブの破棄
discard_on StandardError
# ブロックを使うことも可能
# 引数にジョブオブジェクトと例外オブジェクトを受け取れる
discard on StandardError do |job, error|
SomeNotifier.push_error(error) # 通知を送る
end
end
5-2 Active Storageによるファイルアップロード
Active Strageのセットアップ方法
- Active StrageはRails組み込みのファイルアップロード機能
- 機能を使うためには以下のコマンドの実行が必要
$ bin/rails active_storage:install
- モデルの生成時にActive Storage用の属性を
attachment
オプションで用意する。これにより生成されたモデルファイルには属性が追加される。なお、この属性はテーブルのカラムとしては追加されない
rails generate model User name:string portrait:attachment
class User < ApplicationRecord
validates :name, presence: true
has_one_attached :portrait # Active Strage用の属性
end
- デフォルトだとファイルの保存先はローカルのファイルシステムとなる。S3などのクラウドストレージを利用する際は以下のファイルを編集する
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
# S3の設定の雛形はデフォルトでコメントアウトされた状態で用意されている
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
# amazon:
# service: S3
# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
# region: us-east-1
# bucket: your_own_bucket
略
# ミラーリング用の設定
# 一度に複数のストレージにファイルアップロードしたい場合はこちらを利用する
# mirror:
# service: Mirror
# primary: local
# mirrors: [ amazon, google, microsoft ]
Rails.application.configure do
# Store uploaded files on the local file system (see config/storage.yml for options).
# デフォルトはローカルストレージを利用
config.active_storage.service = :local
# Active Storageのサービスを上記ファイルのS3に設定変更する場合
config.active_storage.service = :amazon
end
Active Strageの追加機能
サムネイル生成
- アップロードした画像を自動でリサイズし、サムネイルを作成して表示することができる。機能を利用するためには以下のコマンドを実行し、リンク表示箇所を修正する
# 以下がコメントアウトされているので解除しておく
# Use Active Storage variant
gem 'image_processing', '~> 1.2'
$ bundle install
$ brew install imagemagick <-- macOSの場合
<p>
<strong>Portrait:</strong>
<%# <%= link_to @user.portrait.filename, @user.portrait if @user.portrait.attached? %>
<%= image_tag @user.portrait.variant(resize_to_limit: [100, 100]) %>
</p>
- 上記までは
ImageMagick
を利用しているが、画像形式が限定される代わりによりメモリ消費量が少なく実行速度が速いlibvips
というライブラリもサポートしている以下の設定で利用できる
$ brew install vips
module PortraitsUploader
class Application < Rails::Application
略
# vipsを使うための設定
config.active_storage.variant_processor = :vips
end
end
ダイレクトアップロード
- ダイレクトアップロードにも対応しており、サーバーを経由しないことでアップロード時間の短縮やサーバーの負荷軽減につながる。以下の設定で利用可能
// Rails6.0以上はデフォルトで有効化
require("@rails/activestorage").start()
略
<div class="field">
<%= form.label :portrait %>
<%# <%= form.file_field :portrait %>
<%# 複数ファイルをアップロードしたい場合は multiple: true に設定 %>
<%= form.file_field :portrait, direct_upload: true %>
</div>
略
Active Strageの問題点
validationヘルパーの不足
例えば、画像のアップローダーに動画がアップされた場合などは、バリデーションエラーとしたいがそのためのヘルパーメソッドが存在しない。自分で実装するかactive_storage_validations
gemを利用する必要がある
cacheの不足
画像をアップロードした後にフォームの入力でバリデーションエラーとなった場合、画像をキャッシュできず、フォームの再入力とともに画像も再アップする必要がある
ActionMailerによるメール送信
Action Mailerのセットアップ
- Action MailerはRails組み込みのメール送信機能
- Action Mailerで使用するファイルは
mailer
コマンドで生成できる- 生成されるファイル
- メイラークラスの定義:
app/mailers/user_mailer.rb
- プレビューの作成:
test/mailers/previews/user_mailer_preview.rb
- テスト:
test/mailers/user_mailer_test.rb
- メイラークラスの定義:
- 生成されるファイル
$ bin/rails g mailer UserMailer
- 上記コマンドで生成した
app/mailers/user_mailer.rb
ファイルの内容を編集し対応するビューファイルをapp/views/user_mailer/
ディレクトリ配下に実装することでメール送信機能を作ることができる
class UserMailer < ApplicationMailer
+ # ユーザー登録完了時のメール送信機能
+ def welcome
+ @name = params[:name]
+ # mailメソッドでHTML/textのテンプレートを探してメールを作成
+ mail(to: params[:to], subject: '登録完了')
+ end
end
以下の2種類のフォーマットを作成し、受信側で選んで表示させることができる
<p> <%= @name %> 様</p>
<p> ユーザー登録が完了しました。</p>
<%= @name %> 様
ユーザー登録が完了しました。
- デフォルトの送信元メールアドレスはすべてのメイラークラスの継承元の
ApplicationMailer
で設定
class ApplicationMailer < ActionMailer::Base
default from: 'ここに任意の送信元アドレスを記述する'
layout 'mailer'
end
- 作成したメイラーとビューの動作による文面作成はプレビュー機能で確認できる。
test/mailers/user_mailer_test.rb
を編集し、localhost:3000/rails/mailers/user_mailer/welcome
へとアクセスすることで確認できる
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer::Preview
+ def welcome
+ UserMailer.with(to: "igarashi@example.com", name: "igaiga").welcome
+ end
end
- 実際にメールを送信する場合は以下のように
deliver_xxx
メソッドを利用する-
deliver_now
: 同期的に送信 -
delilver_later(wait: x.minutes)
: x分後に送信(wait until
にすると送信日時を指定)
-
class UsersController < ApplicationController
def create
@user = User.new(user_params)
respond_to do |format|
if @user.save
# ユーザー登録完了時にメールを送信
UserMailer.with(to: @user.email, name: @user.name).welcome.deliver_now
format.html { redirect_to @user, notice: "User was successfully created." }
format.json { render :show, status: :created, location: @user }
else
略
end
- メール送信の動作を開発環境で確認するには以下の環境設定ファイルを編集する。以下の場合は、メール送信を含むアクションをトリガーすると
tmp/mail
配下にファイルが作成されてその末尾に内容が追記される
Rails.application.configure do
略
config.action_mailer.delivery_method = :file # メールをファイルとして保存
# ファイル保存先を指定
# config.action_mailer.file_settings = { location: Rails.root.join('log/mail') }
# 指定しない場合は、tmp/mail 以下に保存される
end
- 実際にメールを送信するためにはSMTPサーバを用意する必要がある。本書では外部サーバとしてSendGridを利用している
本書の内容通りに進めていくとうまく設定ができなかったので、SendGridの認証設定を最新の推奨方式(APIキー認証)に変更して進めました。設定方法は以下です。
手順:
1. SendGridのダッシュボードにログイン
2. Settings → API Keys → Create API Key
3. API Keyを生成(Full Access または Restricted Access で Mail Send の権限を付与)
4. 生成されたAPIキーをコピーして、上記の【SendGridで生成したAPIキー】の部分に貼り付け
なお、認証情報の登録に関してもエディタを開くために追加のオプションが必要でした。以下はvscodeでエディタを開く方法です。編集してxで閉じると最下部のメッセージが出力され編集が完了します。
$ EDITOR="code --wait" bin/rails credentials:edit
略
File encrypted and saved.
認証情報の編集内容は以下です。
# aws:
# access_key_id: 123
# secret_access_key: 345
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
sendgrid:
api_key: 【SendGridで生成したAPIキー】
- SMTPへ接続するためには以下ファイルを編集する
Rails.application.configure do
略
config.action_mailer.delivery_method = :smtp # SMTPサーバを利用
config.action_mailer.smtp_settings = {
user_name: 'apikey',
password: Rails.application.credentials.dig(:sendgrid, :api_key),
domain: '送信元に設定したアドレスの@以下の文字列',
address: 'smtp.sendgrid.net',
port: 587,
authentication: :plain,
enable_starttls_auto: true
}
end
- 単体テストは最初に生成した
test/mailers/
配下のファイルで行う
Action Mailboxによるメール受信
Action Mailboxのセットアップ
- Action Mailboxはメールを受信した時に処理を行う機能を提供する組み込みライブラリ。各メールサービス、サーバと協調して動作する。対応サービスはRailsガイドを参照
- 機能を使うためには以下のコマンドを使用。受信メールのメタ情報を保存するmigrationファイルも生成されるのでマイグレーションも同時に実行する
$ bin/rails action_mailbox:install
$ bin/rails db:migrate
- コマンド実行後、環境設定は以下のようにする
Rails.application.configure do
略
# Sendgridを使う設定
config.action_mailbox.ingress = :sendgrid
end
認証情報は以下のように追加する
$ EDITOR="code --wait" bin/rails credentials:edit
action_mailbox:
ingress_password: メールサービスのパスワード
本書の内容だとここからさらにSendGridの設定でDomain Authentication設定を行うように記述されていて、メール受信用のドメインが必要とされています。ですが、本書の内容を進める範囲に限ってはこの設定は行わなくても問題ありませんでした。私はこの辺りの設定で少し詰まってしまったので補足のために追記いたします。
Action Mailboxの実装
- 受信メールに対しての処理はActive Jobによる非同期処理キューに登録することで行われる。キューのジョブは以下の設定で各処理に振り分けられる
class ApplicationMailbox < ActionMailbox::Base
# 判断条件 => 振り分け先
# 複数マッチする場合は最初にマッチした処理を実行
routing (/[0-9]+-comment@/i) => :comments # CommentsMailboxクラスへ処理を振り分け
# routing -> (inbound_email) { inbound_email.mail.to.size > 2 } => :multiple_recipients
# routing CustomAddress.new => :custom
routing :all => :backstop
end
- 振り分けられたメールを処理するMailboxクラスは以下のように実装する
$bin/rails g mailbox comments
class CommentsMailbox < ApplicationMailbox
+ before_processing :validate_request
+
+ # メールの内容をコメントとして保存する処理
+ def process
+ board.comments.create!(body: mail.decoded, creator: commenter)
+ end
+
+ # リクエストのバリデーション
+ def validate_request
+ return if commenter && board
+
+ # 引数で受け取ったActionMailerオブジェクトに対してdeliver_laterメソッドを実行し、かつInboundMailの状態をbouncedに変更
+ # bounced状態の時はprocessメソッドが実行されない
+ bounce_with CommentMailer.invalid_request(
+ inbound_email, commenter: commenter, board: board
+ )
+ end
+ # コメント投稿者となるオブジェクトを取得
+ def commenter
+ return @commenter if defined?(@commenter)
+ @commenter = User.find_by(email: mail.from)
+ end
+ # 書き込み先Boardオブジェクトを取得
+ def board
+ return @board if defined?(@board)
+ @board = Board.find_by(id: mail.to.split('-')[0])
+ end
end
メール受信時の処理を確認する開発支援用Web UI
- Action Mailboxには実際にメールを送信しなくてもメール受信時の処理を確認するための開発用のWeb UIが用意されている。下記のURLにアクセスするとメールの送受信をシミュレートできる
http://localhost:3000/rails/conductor/action_mailbox
Action Textによるリッチテキスト機能
Action Textのセットアップ
- Action Textはドラッグ&ドロップによるファイルアップロードなどにも対応したリッチテキスト機能
- 機能を利用するためには以下のコマンドを実行する。また、Active Storageの機能を利用するため、同様に
image_processing
やImageMagick
などのライブラリもインストールする必要がある(上記参照)
$ bin/rails action_text:install
$ bin/rails db:migrate
Action Textの実装
- 実装したいモデルファイルにリッチテキストを属性として定義する
class Message < ApplicationRecord
+ has_rich_text :content # contentという名前で属性を定義
end
ビューテンプレートでrich_text_helper
を利用するとcontent
編集用のフィールドが表示される
<%= form_with(model: message, local: true) do |form| %>
略
+ <div class="field">
+ <%= form.label :content %>
+ <%= form.rich_text_area :content %>
+ </div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
コントローラでは送信されるリッチテキストを許可するように設定
class MessagesController < ApplicationController
略
def message_params
+ params.require(:message).permit(:content)
end
end
- 保存したリッチテキストはモデルの属性として利用できる
<p id="notice"><%= notice %></p>
+ <%= @message.content %>
<%= link_to 'Edit', edit_message_path(@message) %> |
<%= link_to 'Back', messages_path %>
Action TextにおけるN+1問題の対処
-
meesage.content
のようにリッチテキスト属性にアクセスする場合、一見モデルのカラムを参照しているように見えるが実際にはActionText::RichText
というモデルのテーブルへアクセスしている。つまり、その都度クエリが発行されるのでN+1問題が発生することになる。専用のeager loadメソッドが用意されているのでそれで対処できる
class MessagesController < ApplicationController
# GET /messages or /messages.json
def index
- @messages = Message.all # N+1問題が発生する
+ @messages = Message.with_rich_text_content # eager_loadを使ってN+1問題を解消
end
end
Action Cableによるリアルタイム通信
Action CableはWebSocketを使用したチャット機能などのリアルタイム処理を提供する組み込みライブラリ
Action Cableのセットアップ
Action CableのセットアップにはJSファイルの修正も含まれるのでrails s
に加えてbin/webpack-dev-server
も起動しておくとスムーズに開発が進められる
- 以下の
channel
サブコマンドで必要ファイルを生成できる- 生成されるファイル
app/channels/room_channel.rb
: サーバーサイド
app/javascript/channels/room_channel.js
: クライアントサイド
- 生成されるファイル
$ bin/rails g channel room speak
- ファイルを生成した状態で
rails s
してページにアクセスして以下のメッセージがサーバログに出力すればWebSocketの接続が確認できたことになる
$ bin/rails s
略
RoomChannel is streaming from room_channel
ビューの実装
<h1>Chat room</h1>
<div id="messages">
<!-- 部分テンプレートを呼び出してインスタンス変数の内容をレンダリング -->
<%= render @messages %>
<!-- 以下と同等 -->
<%# <%= render partial: "message", collection: @messages %>
</div>
<form>
<label>Say something:</label><br>
<input type="text" data-behavior="room_speaker">
</form>
<div class="message">
<p><%= message.content %></p>
</div>
サーバサイドの実装
class RoomChannel < ApplicationCable::Channel
# 購読後に呼ばれる
def subscribed
+ stream_from "room_channel" # ブロードキャスト用のチャンネル名を指定
end
# 購読解除後に呼ばれる
def unsubscribed
end
# クライアントがチャットメッセージを送信したときに実行される
def speak(data)
+ message = Message.create!(content: data["message"])
+ # 生成したメッセージを部分テンプレートでHTMLに変換して、room_channelにブロードキャスト
+ ActionCable.server.broadcast(
+ "room_channel", { message: render_message(message) }
+ )
end
+ private
+
+ # 部分テンプレートでメッセージのHTMLを生成
+ def render_message(message)
+ # ApplicationController.renderでコントローラーを介さずに部分テンプレ+ートをレンダリング
+ ApplicationController.render(
+ partial: "messages/message",
+ locals: { message: message }
+ )
+ end
end
クライアントサイドの実装
import consumer from "./consumer"
consumer.subscriptions.create("RoomChannel", {
connected() { // 接続時
+ // ユーザーが入力フィールドでEnterキーを押すと、その内容がサーバーに送信される
+ document.
+ querySelector('input[data-behavior=room_speaker]').
+ addEventListener('keypress', (event) => {
+ if (event.key === 'Enter') {
+ this.speak(event.target.value);
+ event.target.value = '';
+ return event.preventDefault();
+ }
+ });
},
disconnected() { // 切断時
},
received(data) { // サーバーからのメッセージ受信時
+ const element = document.querySelector('#messages');
+ // DOM操作でサーバーからのメッセージをHTMLに追加
+ element.insertAdjacentHTML('beforeend', data['message']);
},
speak: function(message) {
+ // サーバーに speak メソッドを実行するように伝える
+ return this.perform('speak', {message: message});
}
});
本番で運用する場合のAction Cableの設定
アダプタを設定
以下のファイルでWebsocketの接続管理を行うアダプターとアダプターへの接続先を指定する
development:
adapter: async # インメモリで接続情報を管理
test:
adapter: test
production:
adapter: redis # 本番では複数の接続情報をを管理するためにredisを使用する
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
channel_prefix: actioncable_sample_production
Action Cableサーバをスタンドアローンで立ち上げる
- デフォルトだとAction Cable用サーバは
/cable
へマウントされ、一つのプロセスにWebサーバとWebSocketサーバが混在している - RackアプリケーションとしてAction Cable用のサーバプロセスを立ち上げることでスタンドアローンで動作させることができる。以下のファイルを作成して
puma
コマンドでサーバを起動させる
require_relative "../config/environment"
Rails.application.eager_load!
run ActionCable.server
$ bundle exec puma cable/config.ru
さらに以下の設定をアンコメントしてWebサーバにAction Cableがマウントされているのを解除する
config.action_cable.mount_path = nil # アンコメントする
- Action Cable用のサーバを立ち上げてもデフォルトではAction Cableへの接続は
localhost
が指定されている。サブドメインを設定するには以下の設定をアンコメントする
# アンコメントする
config.action_cable.url = 'wss://example.com/cable'
# 以下の設定で接続可能なオリジンを制限することも可能
config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]
さらに設定をクライアントサイド全体に反映させるためにビューを修正する。javascript_pack_tag
が書かれている行より前にaction_cable_meta_tag
を配置する
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<%# 以下で生成されたタグを見て接続先を判別する %>
<%= action_cable_meta_tag %>
<%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
Action Cableに認証処理を実装する
コネクションを利用するとWebSocketの認証認可処理を行うことができる。User
モデルでログイン処理を実装した後に以下のファイルに処理を追加する
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user # current_userメソッドが使えるようになる
# クライアントが接続した時に呼ばれる
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.signed[:user_id])
verified_user
else
reject_unauthorized_connection # ユーザーが見つからない場合は接続を拒否
end
end
end
end
Action Cableのワーカ数を変更する
WebSocket経由で受け取ったメッセージはAction Cable用のワーカスレッドで処理される。以下の設定でワーカ数を変更できる
# デフォルトは4つ
# WebとWebSocketを同一プロセスとしている場合は二つのスレッドを掛け合わせた値が必要
# サーバを分離している場合は、Webサーバ用のスレッドを設定
config.action_cable.worker_pool_size = 10
サーバを分離している場合は以下ファイルでWebSocketのワーカ数を設定
development:
adapter: redis
url: redis://localhost:6379/1
worker_pool_size: 10 # WebSocketのワーカ数
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
worker_pool_size: 10
Action Cableのテスト
チャネルとコネクション、二つのテストを用意する
require "test_helper"
class RoomChannelTest < ActionCable::Channel::TestCase
test "subscribes" do
subscribe # 購読作成をシミュレート
# subscriptionメソッドでRoomChannelオブジェクトを取得し、購読ができたか確認
assert subscription.confirmed?
assert_has_stream "room_channel" # ストリームがroom_channelであることを確認
end
test 'broadcast' do
subscribe
text = "hello"
# メッセージをHTMLに変換
broadcast_text = ApplicationController.render(
partial: "messages/message",
locals: { message: Message.new(content: text) }
)
# performメソッドでクライアントからspeakアクションを呼び出して、ブロードキャストされるか確認
assert_broadcast_on("room_channel", message: broadcast_text) do
perform :speak, message: text
end
end
end
require "test_helper"
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
test "connects with accepts" do
user = User.first
cookies.signed[:user_id] = user.id
connect # Connection#connectメソッドを呼び出して、接続を確立
assert_equal connection.current_user.id, user.id
end
test "connection rejects" do
cookies.signed[:user_id] = nil # ユーザーIDがnilの場合
assert_reject_connection { connect } # 接続が拒否されることを確認
end
end