概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 4 cool less known Rails features - part 1: ActiveJob, ActiveModel
- 原文公開日: 2018/03/27
- 著者: Paweł Dąbrowsk
Rails tips: あまり知られてない機能1: ActiveJobとActiveModel(翻訳)
私はRailsガイドを通しで読んだことがなかったのですが、同ガイドを自国語に翻訳していて、それまで気づかなかったクールな機能をいくつも発見しました。皆さまのお役に立てばと思います。ActiveJobやActiveModelモジュールの知られざる機能を他にもご存知でしたらぜひお知らせください。
⚓1. ActiveJobのカスタムシリアライズ
大規模なRailsアプリを構築していれば、Sidekiqやdelayed_jobといったバックグラウンド処理用エンジンをおそらくお使いでしょう。私はSidekiqが本当に好きなのですが、中にはSidekiqではできないこともあります。
class Worker
include Sidekiq::Worker
def perform(user)
# 何かすごいことをやる
end
end
user = User.first
Worker.perform_async(user)
これを扱おうと思ったら、次のようにUser#id
を渡してユーザーを手動で代入しなければなりません。
class Worker
include Sidekiq::Worker
def perform(user_id)
user = User.find(user_id)
# 何かすごいことをやる
end
end
user = User.first
Worker.perform_async(user.id)
しかし、SidekiqをアダプタにしたActiveJobを使っていればそんなことをする必要はありません。ActiveJobはデフォルトでActiveRecordオブジェクトのシリアライズとデシリアライズをサポートしていますが、カスタムデータ向けに独自のシリアライズ/デシリアライズを定義することもできます。耳寄りなお話だと思いませんか?
この辺で何かコード例が欲しくなりますね。
位置情報機能を使っていて、Location
オブジェクトがあるとします。次のように緯度(lat
)と経度(lon
)を渡せば新しいlocationオブジェクトを初期化できます。
location = Location.new(lat, lon)
location.city # => どこぞの町
location.country # => どこぞの国
Location
のインスタンスをワーカーに渡して、ワーカーのperform
メソッドでインスタンスにアクセスさせたいとします。どうすればできるのでしょうか?独自のシリアライザ/デシリアライザを定義して、LocationSerializer
と名前を付ければよいのです。
完全に機能するよう実装するには、以下の3つのメソッドを実装しなければなりません。
serialize?
: 指定の引数を現在のシリアライザでシリアライズできるかどうかをチェックしますserialize
: このメソッドは、基本型の含むキーだけを含むハッシュを返さなければなりませんdeserialize
: ハッシュをオブジェクトに変換します
まずはserialize?
メソッドからいきましょう。これはシンプルです。
def serialize?(argument)
argument.kind_of?(Location)
end
指定の引数がLocation
クラスのインスタンスかどうかをチェックしているだけです。
新しいlocationオブジェクトを初期化するには緯度と経度を渡す必要があるので、serialize
メソッドとdeserialize
メソッドも同じくシンプルです。
def serialize(location)
super(
"longitude" => location.longitude,
"latitude" => location.latitude
)
end
def deserialize(hash)
Location.new(hash["latitude"], hash["longitude"])
end
クラス全体は次のようになります。
class LocationSerializer < ActiveJob::Serializers::ObjectSerializer
def serialize?(argument)
argument.kind_of?(Location)
end
def serialize(location)
super(
"longitude" => location.longitude,
"latitude" => location.latitude
)
end
def deserialize(hash)
Location.new(hash["latitude"], hash["longitude"])
end
end
このクラスを使いたいということをRailsに認識させなければなりません。
Rails.application.config.active_job.custom_serializers << LocationSerializer
以上でおしまいです。Location
オブジェクトを渡してperform
メソッドを実行すれば、ワーカー内でオブジェクトを使えます。
⚓2. ActiveJob: 属性をJSONから読み込む
JSONフォーマットで受け取ったレスポンスを、モデルの属性にさくっとマッピングしたいと思いませんか?実は簡単な方法があるのです。JSONを受け取って、文字列から取り出した値を用いて属性を設定してくれる#from_json
メソッドがどのモデルにも実装されています。
User
モデルにfirst_name
属性とlast_name
属性があれば、次のようにできます。
response = {first_name: "John", last_name: "Doe"}.to_json
user = User.new
user.from_json(response)
user.first_name # => John
user.last_name # => Doe
マスアサインメントしても、変更したくない属性には代入されませんのでご心配なく。
response = {fake: "fake"}.to_json
user = User.new
user.from_json(response) # => raises ActiveModel::MassAssignmentSecurity::Error
⚓3. ActiveModelの属性メソッド
賭けてもよいですが、読者の皆様もきっと以下のようなコードを何度も書いたことがあると思います。
user = User.first
user.first_name = "John"
user.first_name_changed?
自分で#first_name_changed?
を定義した覚えもないのにこんなことができるのは、ちょっとしたマジックです。
こんなマジックが欲しいですよね?
Score
クラスを作成し、スキーのジャンプ競技の詳しいスコアをここに保存することにします。スコアをspeed_score
(スピード)、style_score
(芸術)、landing_score
(着地)にそれぞれ分割するとします。
class Score
attr_accessor :speed_score, :style_score, :landing_score
end
今度は、スコアの種類ごとに点数が最高点に達したかどうかをチェックできるようにしたいと思います(10点を最高とする)。これを実装するためにActiveModel::AttributeMethods
のattribute_method_suffix
メソッドが使えます。
class Score
include ActiveModel::AttributeMethods
attr_accessor :speed_score, :style_score, :landing_score
attribute_method_suffix '_max?'
define_attribute_methods 'speed_score', 'style_score', 'landing_score'
private
def attribute_max?(attribute)
send(attribute) == 10
end
end
それではこのクラスで少し遊んでみましょう。
score = Score.new
score.speed_score = 5
score.speed_score_max? #=> false
score.style_score = 10
score.style_score_max? #=> true
プレフィックスメソッドも定義できます。
class Score
include ActiveModel::AttributeMethods
attr_accessor :speed_score, :style_score, :landing_score
attribute_method_prefix 'reset_'
define_attribute_methods 'speed_score', 'style_score', 'landing_score'
private
def reset_attribute(attribute)
send("#{attribute}=", 0)
end
end
score = Score.new
score.speed_score = 10
score.speed_score # => 10
score.reset_speed_score
score.speed_score # => 0
属性メソッドは、属性を別の形式に変換する場合にも便利です。たとえば、_uri
サフィックスを追加して、URI.encode(attribute)
で任意の属性をエンコードするなどです。やってみたくなりませんか?
⚓4. Railsコンソールをsandboxモードにして使う
productionデータベースダンプがあり、データに影響するコードをローカルでちょろちょろっとチェックしたいけど、コードの変更を反映したくない場合があります。こんなときはsandboxモードを使いましょう。rails c
コマンド実行時に--sandbox
オプションをつければsandboxモードがオンになります。
sandboxモードでコンソールを実行すると、データベースにどんな変更を加えてもコンソール終了後にロールバックされます。どうぞお楽しみください!
追伸: sandboxモードにするとレコードがロックされるので、production環境での利用には十分ご注意ください。
次回をお楽しみに!
新着記事を見逃したくない方はTwitterをフォローしてください。もちろん「hello」だけでも構いません!
お知らせ: RSpec & TDDの電子書籍を無料でダウンロード
もっと稼ぎたい方や会社をさらに発展させたい方へ: テスティングのスキルの重要性にお気づきでしょうか?テストを正しく書き始めることが、唯一のファーストステップです。無料でダウンロードいただける私の書籍『RSpec & Test Driven Developmentの無料ebook』をどうぞお役立てください。