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`を使ってバリデーションをモデルから分離する(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ この記事を書いた人と働こう! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の半分ほど、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れてそれぞれ一部を翻訳。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好き。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

週刊Railsウォッチ

インフラ

BigBinary記事より

ActiveSupport探訪シリーズ