Ruby on Rails

4 cool less known Rails features – part 1: ActiveJob, ActiveModel, command line

4 cool less known Rails features – part 1: ActiveJob, ActiveModel, command line March 27, 2018

I never read the whole Rails guides and now when I’m translating them to my native language I discovered some cool features that I didn’t know about before. I hope this will be also useful for you! Let me know if you use other useful but less known features from Active Job or Active Model modules.

Active job custom serialization

If you are building large Rails applications you are probably using background processing engines such as Sidekiq or Delayed Job. I really like Sidekiq but you can’t do something like this:

class Worker
  include Sidekiq::Worker
  
  def perform(user)
    # do something cool here
  end
end

user = User.first
Worker.perform_async(user)

to handle such case you have to pass User#id and then assign user manually:

class Worker
  include Sidekiq::Worker
  
  def perform(user_id)
    user = User.find(user_id)
    # do something cool here
  end
end

user = User.first
Worker.perform_async(user.id)

However, above case is not true if you are using Active Job with Sidekiq as an adapter. While Active Job supports Active Record objects serialization and deserialization by the default, you can also define your own serializers and deserializers for custom data. Sounds interesting?

We need some examples here!

Let’s say you are using geolocation features and you have Location object. You can initialize new location object by passing latitude and longitude, just like this:

location = Location.new(lat, lon)
location.city # => Some city
location.country # => Some country

You want to pass Location instance to your worker and have access to it in the perform method. How to achieve this? By defining own serializer and deserializer. We will name it LocationSerializer.

In order to implement fully working mechanism we have to implement 3 methods:

  • serialize? – to check if we can serialize given argument with the current serializer
  • serialize – this method have to return a hash with the keys of basic types only
  • deserialize – convert hash into your object

Let’s start with the serialize? method, it will be simple:

def serialize?(argument)
  argument.kind_of?(Location)
end

we can simply check if the given argument is an instance of Location class.

Since we need to pass longitude and latitude to initialize new location object, our implementation of serialize and deserialize methods will be also simple:

def serialize(location)
  super(
    "longitude" => location.longitude,
    "latitude" => location.latitude
  )
end

def deserialize(hash)
  Location.new(hash["latitude"], hash["longitude"])
end

The whole class looks following:

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

we have to let Rails know that we want to use it:

Rails.application.config.active_job.custom_serializers << LocationSerializer

and we are done. You can now pass Location object to perform method and you can use it inside your worker.

Active Job - load attributes from JSON

Do you have a response in JSON format and you want to map it quickly to your model attributes? There is an easy way to do this. Each of your models implement #from_json method which takes JSON and sets attributes using values from passed string.

If you have User model with first_name and last_name attributes, you can do this:

response = {first_name: "John", last_name: "Doe"}.to_json
user = User.new
user.from_json(response)
user.first_name # => John
user.last_name # => Doe

but don’t worry, you will not assign any of attributes you don’t want to touch during the mass assignment:

response = {fake: "fake"}.to_json
user = User.new
user.from_json(response) # => raises ActiveModel::MassAssignmentSecurity::Error

Active Model attribute methods

I bet you used many times code similar to this:

user = User.first
user.first_name = "John"
user.first_name_changed?

it’s a little bit of magic because you didn’t define #first_name_changed? method.

Psst… want some magic?

Let’s create Score class where we would keep detailed score granted for a ski jump. We would divide score to a speed_score, style_score and landing_score:

class Score
  attr_accessor :speed_score, :style_score, :landing_score
end

Now, for each type of score, we want to be able to check if it was a maximum score (we have 10 points scale). To implement this we can use attribute_method_suffix method from ActiveModel::AttributeMethods module:

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

now we can play with our class a little bit:

score = Score.new
score.speed_score = 5
score.speed_score_max? #=> false
score.style_score = 10
score.style_score_max? #=> true

You can also define prefix methods:

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

Attribute methods may also be useful when you can convert your attributes to any other format. For example you can add _uri suffix and encode any attribute using URI.encode(attribute) - does it sound good enough to give it a try?

Rails console in the sandbox mode

Have production database dump and you want to check some code locally that affects data but you don’t want to make permanent changes? Welcome sandbox mode. You can turn it on by passing --sandbox option when running rails c command.

It will run the console in the sandbox mode which means that any change to the database will be rollbacked after you will exit the console. Have fun! Update: sandbox mode locks records so be careful in the production environment

To be continued.

You can follow me on Twitter to never miss any article or just say hello!

Download free RSpec & TDD ebook

Do you want to earn more or jump to the next level in your company? Do you know that testing skills are one of the most desired skills? There is only first step: start testing and do it right. My ebook can help you. Subscribe to the newsletter to get a free copy of the book.