Tech Racho エンジニアの「?」を「!」に。
  • 開発

Rails tips: あまり知られてない機能1: ActiveJobとActiveModel(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rails tips: あまり知られてない機能1: ActiveJobとActiveModel(翻訳)

私はRailsガイドを通しで読んだことがなかったのですが、同ガイドを自国語に翻訳していて、それまで気づかなかったクールな機能をいくつも発見しました。皆さまのお役に立てばと思います。ActiveJobやActiveModelモジュールの知られざる機能を他にもご存知でしたらぜひお知らせください。

1. ActiveJobのカスタムシリアライズ

大規模なRailsアプリを構築していれば、Sidekiqdelayed_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::AttributeMethodsattribute_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』をどうぞお役立てください。

関連記事

RabbitMQはSidekiqと同等以上だと思う: 前編(翻訳)

Rails: Form Objectと`#to_model`を使ってバリデーションをモデルから分離する(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。