5章
Active Jobによる非同期処理
Active Job の基礎 - Railsガイド
のような時間がかかる処理で利用される。
1
$ rails g job async_log
1
2
3
4
5
6
7
8
9
class AsyncLogJob < ApplicationJob
queue_as :default
self . queue_adapter = :resque # オーバーライドできる
def perform ( * args )
# Do something later
p 'うぇい'
end
end
ActiveJobはデフォルトでasync
アダプターが設定されている。
Railsプロセス中のスプレッドプールでジョブを実行することができる。
1
2
3
4
5
6
irb ( main ): 001 : 0 > AsyncLogJob . perform_later
Enqueued AsyncLogJob ( Job ID : 866 b1eca - b425 - 4e7 b - bc79 - 3131 b9b7b95a ) to Async ( default )
Performing AsyncLogJob ( Job ID : 866 b1eca - b425 - 4e7 b - bc79 - 3131 b9b7b95a ) from Async ( default ) enqueued at 2020 - 09 - 04 T09 : 15 : 32 Z
=> #<AsyncLogJob:0x00007fbf81b63170 @arguments=[], @job_id="866b1eca-b425-4e7b-bc79-3131b9b7b95a", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @provider_job_id="ebb5736a-57fa-4890-97b2-8ba20c224947">
irb ( main ): 002 : 0 > "うぇい"
Performed AsyncLogJob ( Job ID : 866 b1eca - b425 - 4e7 b - bc79 - 3131 b9b7b95a ) from Async ( default ) in 0 . 09 ms
引数を渡すときはperform_later(hoge)
という感じに渡す。
時間差があるときはwait
(○秒・分・時間後)やwait_until
(実行時刻)などを用いて以下のように記述する。
1
2
3
4
5
6
irb ( main ): 005 : 0 > AsyncLogJob . set ( wait : 5 . second ) . perform_later
Enqueued AsyncLogJob ( Job ID : 11 fb7339 - b726 - 4534 - 9889 - 00 f66087c037 ) to Async ( default ) at 2020 - 09 - 04 09 : 10 : 23 UTC
=> #<AsyncLogJob:0x00007fbf8163a658 @arguments=[], @job_id="1f94b1f8-9451-4fe6-84cc-46a3073a6069", @queue_name="default", @priority=nil, @executions=0, @exception_executions={}, @provider_job_id="6fc6a5e6-06bf-4a6e-b632-9c42cc14209d", @scheduled_at=1599210881.7301629>
irb ( main ): 006 : 0 > Performing AsyncLogJob ( Job ID : 1 f94b1f8 - 9451 - 4 fe6 - 84 cc - 46 a3073a6069 ) from Async ( default ) enqueued at 2020 - 09 - 04 T09 : 14 : 36 Z
"うぇい"
Performed AsyncLogJob ( Job ID : 1 f94b1f8 - 9451 - 4 fe6 - 84 cc - 46 a3073a6069 ) from Async ( default ) in 0 . 1 ms
asyncアダプターはプロセスを再起動するとジョブが消える。
複数キューを使う場合。
特定のジョブを切り分ける
重要度に応じてキューを使い分ける
Active Jobを使うか直接利用するか?
ActiveJobを使うといい場合
標準的な機能のみ使う
バックエンド選択をまだしていない
バックエンドを差し替える可能性がある
Active Jobに依存した機能を使う
Action Mailer
Action Mailbox
Active Storage
Active Recordオブジェクトをキューへ追加するときに、デフォルトで使用されているGlobal IDを使った変換処理を利用する
非同期バックエンドを直接使うといい場合
Active Jobからでは使えない機能がある
Sidekiqでは外部Gem(Sidekiq Pro, Sidekiq Enterpriseなど)を使うとき
Active Storage
1
2
3
4
$ rails active_storage:install
Copied migration 20200905070422_create_active_storage_tables.active_storage.rb from active_storage
$ rails g scaffold user name portrait:attachment
1
2
3
class User < ApplicationRecord
has_one_attached :portrait
end
1
2
3
class Book < ApplicationRecord
has_many_attached :pictures
end
ActiveRecord::Schema.define(version: 2020_09_05_071839) do
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.integer "record_id", null: false
t.integer "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "books", force: :cascade do |t|
t.string "title"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
end
1
2
irb ( main ): 002 : 0 > user . portrait
=> #<ActiveStorage::Attached::One:0x00007fa3aabc5c50 @name="portrait", @record=#<User id: 1, name: "admin", created_at: "2020-09-05 09:01:31", updated_at: "2020-09-05 09:01:31">>
【rails】Active Storage - Qiita
User -< ActiveStorage::Attachment >- ActiveStorage::Blob
Post -<
ポリモーフィックなのでこんな感じのテーブル構造。
サムネイル
Gemによって作成する(アップロード時に生成される)
CarrierWave(アップロード時)
Shrine(アップロード後に非同期で)
ActiveStorageの機能(画像URLにアクセスしたときに生成される)
ImageProcessing
ImageMagick
1
<%= image_tag @user . portrait . variant ( resize_to_limit : [ 100 , 100 ] ) %>
variant
メソッドに渡したオプションは。ImageProcessingライブラリにメソッドが存在していればそっとのメソッドとして、存在していなければImageMagickのコマンドとして実行される。
@user.portrait.variant(resize_to_limit: [100, 100])
と記述した場合、戻り値はBlobのIDと変換形式を含んだURLが生成される。
1
2
3
4
irb ( main ): 004 : 0 > user . portrait . variant ( resize_to_limit : [ 100 , 100 ] )
ActiveStorage :: Attachment Load ( 0 . 4 ms ) SELECT "active_storage_attachments" . * FROM "active_storage_attachments" WHERE "active_storage_attachments" . "record_id" = ? AND "active_storage_attachments" . "record_type" = ? AND "active_storage_attachments" . "name" = ? LIMIT ? [[ "record_id" , 1 ] , [ "record_type" , "User" ] , [ "name" , "portrait" ] , [ "LIMIT" , 1 ]]
ActiveStorage :: Blob Load ( 0 . 2 ms ) SELECT "active_storage_blobs" . * FROM "active_storage_blobs" WHERE "active_storage_blobs" . "id" = ? LIMIT ? [[ "id" , 1 ] , [ "LIMIT" , 1 ]]
=> #<ActiveStorage::Variant:0x00007fa3aaca0f58 @blob=#<ActiveStorage::Blob id: 1, key: "pf42amrmpitko8t7747cbfj8vsj0", filename: "tangchi-lee-jS_LsILp5hk-unsplash.jpg", content_type: "image/jpeg", metadata: {"identified"=>true, "analyzed"=>true}, byte_size: 304017, checksum: "1NwI5yh330iiI8K9LfhzqA==", created_at: "2020-09-05 09:01:31">, @variation=#<ActiveStorage::Variation:0x00007fa3aaca8ac8 @transformations={:resize_to_limit=>[100, 100]}>>
ファイルへのアクセス制限
画像を表示するためのページには制限をかけつつ、画像そのものに対するURLには制限をかけない。
ActiveStorageを利用した場合、リクエストを受けたアプリケーションサーバーはファイル用の期限付きURLを作成し、そこにリダイレクトする(期限はデフォ5分)。
ダイレクトアップロード
省略。
Active STorageの問題点
validationヘルパーの不足
cacheの不足
Action Mailer
Action Mailer の基礎 - Railsガイド
1
2
3
4
5
6
7
8
9
$ rails g mailer UserMailer
Running via Spring preloader in process 73389
create app/mailers/user_mailer.rb
invoke erb
create app/views/user_mailer
invoke test_unit
create test/mailers/user_mailer_test.rb
create test/mailers/previews/user_mailer_preview.rb
1
2
3
4
5
6
class UserMailer < ApplicationMailer
def welcome
@name = params [ :name ]
mail ( to : params [ :to ] , subject : "登録完了" )
end
end
params
で値を取得するのはコントローラと同じ。
UserMailer.with(to: "foo@example.com", name: "foo").welcome.deliver_now
で実行する。
ActiveJobを用いて非同期で送信する場合はdeliver_later
メソッドを使用する。
メールの本文はapp/views/user_mailer/welcome.html.erb
, app/views/user_mailer/welcome.text.erb
を作成して実装する。
普通のViewと同じ。
プレビュー機能
テストでwelcomeメソッドを定義する。
1
2
3
4
5
6
# Preview all emails at http://localhost:3000/rails/mailers/user_mailer
class UserMailerPreview < ActionMailer :: Preview
def welcome
UserMailer . with ( to : "foo@example.com" , name : "foo" ) . welcome
end
end
Action Mailbox
あるメールアドレスにメールを送信されたときに何かをする、など。
自動送信メールへの返信などで使われる。
ActiveJobとActiveStorageが必要。
1
2
3
4
5
6
$ rails action_mailbox:install
Copying application_mailbox.rb to app/mailboxes
create app/mailboxes/application_mailbox.rb
Copied migration 20200905114414_create_action_mailbox_tables.action_mailbox.rb from action_mailbox
$ rails db:migrate
1
2
3
4
Rails . application . configure do
# Prepare the ingress controller used to receive mail
config . action_mailbox . ingress = :sendgrid
end
省略。
Sendgrid
→https://actionmailbox:YOUR_PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
→ApplicationMailbox
のルーティング
→FooMailbox#process
Action Textによるリッチテキスト機能
WYSIWYGエディタ
リッチテキストコンテンツを保存するモデル
リッチテキストを取り扱うヘルパーメソッド
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ rails action_text:install
Copying actiontext.scss to app/assets/stylesheets
create app/assets/stylesheets/actiontext.scss
Copying fixtures to test/fixtures/action_text/rich_texts.yml
create test/fixtures/action_text/rich_texts.yml
Copying blob rendering partial to app/views/active_storage/blobs/_blob.html.erb
create app/views/active_storage/blobs/_blob.html.erb
Installing JavaScript dependencies
run yarn add trix@^1.2.0 @rails/actiontext@^6.0.3-2 from "."
yarn add v1.22.5
[ 1/4] 🔍 Resolving packages...
[ 2/4] 🚚 Fetching packages...
[ 3/4] 🔗 Linking dependencies...
warning " > rails-erb-loader@5.5.2" has unmet peer dependency "webpack@^2.0.0 || >= 3.0.0-rc.0 || ^3.0.0" .
warning " > webpack-dev-server@3.11.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0" .
warning "webpack-dev-server > webpack-dev-middleware@3.7.2" has unmet peer dependency "webpack@^4.0.0" .
[ 4/4] 🔨 Building fresh packages...
success Saved lockfile.
success Saved 2 new dependencies.
info Direct dependencies
├─ @rails/actiontext@6.0.3-2
└─ trix@1.2.4
info All dependencies
├─ @rails/actiontext@6.0.3-2
└─ trix@1.2.4
✨ Done in 13.18s.
Adding trix to app/javascript/packs/application.js
append app/javascript/packs/application.js
Adding @rails/actiontext to app/javascript/packs/application.js
append app/javascript/packs/application.js
Copied migration 20200905120646_create_action_text_tables.action_text.rb from action_text
1
2
3
4
class User < ApplicationRecord
has_rich_text :content # 追加する
has_one_attached :portrait
end
<%= form_with(model: user, local: true) do |form| %>
<!-- 省略 -->
<div class="field">
<%= form.label :content %>
<%= form.rich_text_area :content %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
ドラッグアンドドロップによるファイルアップロード機能
$ brew install imagemagick
1
2
3
4
...
# Use Active Storage variant
gem 'image_processing' , '~> 1.2'
...
image_processing
をbundle installしていなくてもできた……あれ??
注意点
post
と:content
はahs_manyなので、N+1問題に気をつける
eager_loadして呼び出すメソッド
with_rich_text_#{name}
とwith_rich_text_#{name}_and_embeds
@posts = Post.with_rich_text_content
Action Cable
WebSocketを使ったリアルタイム処理を提供するライブラリ。
1
2
3
4
5
6
7
8
$ rails g channel room speak
Running via Spring preloader in process 79960
invoke test_unit
create test/channels/room_channel_test.rb
create app/channels/room_channel.rb
identical app/javascript/channels/index.js
identical app/javascript/channels/consumer.js
create app/javascript/channels/room_channel.js
app/channels/room_channel.rb
とapp/javascript/channels/room_channel.js
がWebSocket処理のサーバーサイドとクライアントサイドを受け持つ。
1
2
3
4
5
6
7
8
$ rails s
...
Completed 200 OK in 127ms ( Views: 114.7ms | ActiveRecord: 1.2ms | Allocations: 21609)
Started GET "/cable" for 127.0.0.1 at 2020-09-06 01:16:48 +0900
Started GET "/cable/" [ WebSocket] for 127.0.0.1 at 2020-09-06 01:16:49 +0900
Successfully upgraded to WebSocket ( REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
RoomChannel is transmitting the subscription confirmation
このように表示されたらWebSocketでの接続ができたということ。
(意味としては、/cableにRackサーバーがマウントされていて、WebSocket通信開始時にそこへアクセスしている)
1
2
3
4
5
6
7
8
9
10
11
12
13
class RoomChannel < ApplicationCable :: Channel
def subscribed # 購読後に呼ばれる
stream_from "room_channel" # ブロードキャスト(接続している全購読者への送信)用のストリーム名を設定
end
def unsubscribed # 購読解除後に呼ばれる
end
def speak ( data ) # クライアント側から呼び出されたときに実行される。メソッド名は任意
# room_cannelに接続している人全員にメッセージを送信する
ActionCable . server . broadcast ( "room_channel" , { message : data [ "message" ] })
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import consumer from "./consumer" ;
consumer . subscriptions . create ( "RoomChannel" , {
connected () {
// チャネルとの接続時
},
disconnected () {
// チャネルとの切断時
},
received ( data ) {
// サーバーからデータを受信したとき
alert ( data [ "message" ] + "を送信しました" );
},
speak : function ( message ) {
// RoomChannel#speakをWebSocket経由で呼び出す
// thisはSubscription
return this . perform ( "speak" , { message : message });
},
});
window.App = consumer.subscriptions.create("RoomChannel", {...
と定義して、App.speak("Hi!")
を実行すると、ActionCable経由でreceived
メソッドが実行される。
チャット
1
2
3
4
5
6
7
8
< div id = "messages" >
< %= render @messages %>
</ div >
< br >
< form >
< label > Say something:</ label >
< input type = "text" data-behavior = "room_speaker" >
</ form >
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
connected () {
// チャネルとの接続時
document
. querySelector ( 'input[data-behavior="room_speaker"]' )
. addEventListener ( "keypress" , ( event ) => {
if ( event . key === "Enter" ) {
this . speak ( event . target . value );
event . target . value = "" ; // inputフィールドを空にする
return event . preventDefault ; // Enterキーを押すことによって生じるイベントを無効化する
}
});
},
received ( data ) {
// サーバーからデータを受信したとき
const element = document . querySelector ( "#messages" );
// div#messagesの内部の最後にdiv.message(部分テンプレート)を追加する
element . insertAdjacentHTML ( "beforeend" , data [ "message" ]);
},
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class RoomChannel < ApplicationCable :: Channel
...
def speak ( data )
message = Message . create! ( content : data [ 'message' ] ) # 送信されたメッセージからデータベースに保存する
# messageが入った部分テンプレートHTMLをフロントに送信する
ActionCable . server . broadcast ( "room_channel" , { message : render_message ( message ) })
end
private
def render_message ( message )
# 部分テンプレートから生成したHTML
ApplicationController . render ( partial : 'messages/message' , locals : { message : message })
end
end
ApplicationController.render
でコントローラ外からテンプレートのレンダリングを行うことができる。
送信時のログはこんな感じ。
1
2
3
4
5
6
7
8
9
10
11
RoomChannel#speak({ "message" = >"てすと" })
( 0.1ms) begin transaction
↳ app/channels/room_channel.rb:10:in ` speak'
Message Create (0.6ms) INSERT INTO "messages" ("content", "created_at", "updated_at") VALUES (?, ?, ?) [["content", "てすと"], ["created_at", "2020-09-06 05:38:21.300677"], ["updated_at", "2020-09-06 05:38:21.300677"]]
↳ app/channels/room_channel.rb:10:in `speak'
( 1.1ms) commit transaction
↳ app/channels/room_channel.rb:10:in ` speak'
Rendered messages/_message.html.erb ( Duration: 0.1ms | Allocations: 9)
[ ActionCable] Broadcasting to room_channel: { :message= >"<div class=\"message\">\n てすと\n</div>\n" }
RoomChannel transmitting { "message" = >"<div class=\"message\">\n てすと\n</div>\n" } ( via streamed from room_channel)
RoomChannel transmitting { "message" = >"<div class=\"message\">\n てすと\n</div>\n" } ( via streamed from room_channel)
注意点など
アダプターはデフォルトで、開発・テスト環境ではasync、本番環境ではredisを使う設定になっている。
config/cable.yml
参照
その他、スタンドアローン構成にする設定、WebSocketの認証認可処理、テスト。省略。