0

Tussle of the State Machines

There’s no shortage of Ruby state machine libraries, but when we needed to implement a formal state machine we don’t find one which met all of our requirements.

I had a same problem in my project.What i needed was a polymorphic class that could have multiple number of state machines in it. Depending on the the relation the appropriate state machine should be used.

What i Wanted

call.rb
class Call
  include Mongoid::Document
  field :scheduled_at,          type: DateTime
  field :is_existing_customer,  type: Boolean
  field :note

  belongs_to :callable, polymorphic: true
  case callable_type
    when 'Car'
      state_machine :state, :initial => :fresh, namespace: 'car' do
        event :schedule do
          transition [:fresh, :schedule] => :scheduled
        end
        .....
        .....
      end
    when 'Personal'
       state_machine :state, :initial => :fresh, namespace: 'Personal' do
        event :schedule do
          transition :fresh => :scheduled
        end
        .....
        .....
      end
     when 'any other'
      .....
  end
end

The Main Problem

Since AASM State Machine does not supports multiple state machine in a single class. So i tried to achive it through state_machine gem with namespaces BUT AGAIN we can not have same states under namespaced state machine in a single class.

How to do it?

My basic requirement was to have a state machine that should be easily composable with other Ruby objects.So what i need to do was to define a state machine as a separate class and selectively apply itto our Rails models. since Mongodb supports embeded obects. I could use it to store states in it.

The Solution

We wanted a state machine that could be easily integrated with other Ruby objects. So we decided to define a state machine as a separate class and selectively apply it to our Rails models. We were using MongoDB, so we embedded these objects.

car_state_machine.rb
class CarStateMachine
  include Mongoid::Document

  field :state
  embedded_in :call

  # no need for name space and we can use AASM directly
  state_machine :state, :initial => :fresh do
    #states: fresh, scheduled, lead, succeed
    event :schedule do
      transition [:fresh, :schedule] => :scheduled
    end
    #...
    #...
  end
end
personal_state_machine.rb
class PersonalStateMachine
  include Mongoid::Document
  include AASM

  field :state
  embedded_in :call

  #states: hello, meet, bye
  state_machine :state, :initial => :hello do
    event :wow do
      transition :hello => :meet
    end
    #...
    #...
  end
end

My Call class now

So to call access these embedded objects i defined a method call_state that returns the embedded on the basis of the callable_type of Call Class.

call.rb
class Call
  include Mongoid::Document

  field :scheduled_at, type: DateTime
  field :is_existing_customer, type: Boolean
  field :note
  field :callable_type

  embeds_one :car_state_machine
  embeds_one :personal_state_machine

  # Method to access state machine
  def call_state
    case self.callable_type
    when 'Car'
      self.car_state_machine || self.build_car_state_machine
    when 'Personal'
      self.personal_state_machine || self.build_personal_state_machine
    end
  end
end

# Example
call = Call.first.callable_type # => "Car"
call.call_state.state # => 'fresh'
call.call_state.schedule!
call.call_state.state # => 'scheduled'

#####

call = Call.last.callable_type # => "Personal"
call.call_state.state # => 'hello'
call.call_state.wow!
call.call_state.state # => 'meet'
0

Rails Caching and Active Record Extensions

As a rails developer, I have gone through situations when we really want to get the most out of the limited resources. So we opt for digging out the areas where we can improve. And caching is one of the option everybody thinks about, and implement.

Situation

Lately I have been looking at the solution to cache the Active Record Objects directly. Also if it could cache the methods frequently used methods too. So I learn’t that Rails.cachecould actually store the Active records directly, that can be configured with redis easily.

The Problem

The code was too repetitive. Every Where I see was a similar pattern like

# Write
Rails.cache.write(key, value)
# Read
Rails.cache.read(key)
# Clear
Rails.cache.del(key)

This was becoming a real issue. So I thought if we can have class method where we could just pass in the method names, and create the cache, that could be shared with all models. So first solution I thought of was creating a Concern that would be included in all models. So came up with this soution.

The Solution

module CachedMethods
  extend ActiveSupport::Concern

  module ClassMethods
    def cached_methods(*methods)
      methods.each do |association|
        define_method("cached_#{association}") do |key = nil, cached = nil|
          key = "#{association}_for_#{self.class.to_s.underscore}_#{id}"
          cached = Rails.cache.read(key) rescue false
          return cached if cached
          load_assoc = send(association)
          Rails.cache.write(key, load_assoc, expires_in: 30.minutes)
          update_cache_keys(key)
          load_assoc
        end

        define_method("#{association}_is_cached?") do
          key = "#{association}_for_#{self.class.to_s.underscore}_#{id}"
          Rails.cache.read(key).present?
        end
      end
    end
  end

  private

  # keeps tracks of all cache keys, for the included class.
  define_method('update_cache_keys') do |key|
    multi_key = "#{self.class.to_s.underscore}_#{id}_cache_keys"
    keys = Rails.cache.read(multi_key) || []
    keys << key
    Rails.cache.write(multi_key, keys.uniq)
  end

  # can be called to clear all cached data for included class
  def delete_cache
    cached = Rails.cache.read("#{self.class.to_s.underscore}_#{id}_cache_keys")
    return unless cached
    cached.each { |key| Rails.cache.delete(key) }
  end
end

But I did not want this to manually including every where in the application. I don’t want to remember which concern to use. So thought of creating an extension for the Active record.

Improvement

So created an ActiveRecordExtension.

# Inculde custom enxtensions here
module ActiveRecordExtension
  extend ActiveSupport::Concern
  included do
    include CachedMethods
  end
end

# include the extension
ActiveRecord::Base.send(:include, ActiveRecordExtension)

Create an initializer called extensions, that will load it during application initialization.

require "active_record_extension"

How to Use

Now anybody can use this extension in the system easily.

# ========================================
#                  USAGE
# ========================================
# // Ruby.
Class Fruit
#   ...
#   # this code generates some instance methods
#   # cached_apples and cached_mangoes
#   # which when called the first time, will cache the ActiveRecord Object,
#   # and after every call will get the object back from the cache only.
#   # You can also cache the method results too.
#   # Also it creates cache check instance methods
#   # as in this case apples_is_cached? And mangos_is_cached?
cached_methods :apples, :mangos
#   # As the name suggests, this method will clear all the cache related
#   # to the class.
after_save :delete_cache
#   ...
end

Now that looks easier to use.

What is provides

fruit = Fruit.first
apples = fruit.cached_apples # cache apples
mangos = fruit.cached_mangoes # cache mangoes

# Now if you this code is run again
apples = fruit.cached_apples # from cache
mangoes = fruit.cached_mangoes # from cache

# the cache will auto expire after 30 minutes
# or if the Fruit object is updated.
# we have added delete_cache callback
after_save :delete_cache

How cool is that.

0

Self Referential Associations in Rails

self-reference

This blog covers associations within a model useful to modelling relationships such as friends on facebook.

I am working on a similar scenario where I had to design a model where a user is friends with other users(many to many relationship).

The real challenge here is that all the users are going to be stored in the User model. That way we somehow have to decide how to generate the backward relationship i.e. accessing friends of a particular user.

Designing this relationship requires one more model which will be used only to store the relationship of user and its friends.

For accessing friends of a particular user we need to have a foriegn_key(user_id) which will be used to fetch all the users(friends) which are related to a particular user.

Before we tackle what we actually want to do, let’s first dive in model relationships and try to understand how associations work inside of models in rails.

Consider a User model where a user belongs to a company.

class User < ActiveRecord::Base
    belongs_to :company
end

If I have describe relationship in above code in one line this is how I would.

Rails internally adds foriegn_key attribute in user table with name campany_id.

Under the Hood.

  1. Rails derives the class name Company using :company and some little ruby magic.
  2. By default the foreign_key is added with the name of Model, in this case it would be company_id.

Interestingly we can override above behaviour.

 

class User < ActiveRecord::Base
    belongs_to :company, :class_name => 'User', :foreign_key => 'user_id'
end

We can specify the class_name & foreign_key here we are doing nothing but overriding rails process where it decides which class to call and which attribute to use as foreign_key.

This way user model will be used rather than company model for this association and foreign_key attribute would be user_id instead of company_id.

Back to our original friends in a social network

Above I mentioned using a second table which will store the relationship between a user and its friends.

This second table should have two columns user_id and friend_id(i’ll come to friend_id after this.)

So, for a particular user with id = 1 having 2 friend connections we will have 2 records in this new table with attrs.

  • NewModel id: 1, friend_id: 2, user_id: 1
  • NewModel id: 2, friend_id: 3, user_id: 1

We have another model to keep track of friends of a user lets think of a name(a good one)…:) Since this model is maintaining friendship b/w users Friendship sounds like a good choice.

Let’s go back to our User model and see how to relate it to Friendship model.

A user can have many friends. It implies that for a given user friendship model will have many records. So a user has many friendships. We also want to be able to access friends of a user for that we should add a has_many :friends Here we are using has_many: through relationship.

class User < ActiveRecord::Base
    has_many :friendships
    has_many :friends, :through => :friendships
end

Some Key points to note in User model

  1. has_many :friendships is the association which actually tells rails that we are going to have multiple records of Friendship model for one record of Usermodel.
  2. has_many :friends, :through => :friendships this line actually tells rails that we will be using .friends to access all the records that are related to object calling friends.
  3. Key thing to note here is through relationship tells rails that friendship table will be used to find out friends.

Lets now define Friendship model.

class Friendship < ActiveRecord:Base
    belongs_to :user                            <!--# This ads the user_id column as foreign key-->
    belongs_to :friend, :class_name => "User"   <!--# This ads as friend_id as the foriegn key but essentially points to objects of User class-->
end

Friendship model has two attributes user_id & friend_id these both are basically same model id’s.

  1. belongs_to :user implicitly ads foriegn_key as user_id remember rails uses :user and does some magic on it.
  2. belongs_to :friend it points to User class and ads foriegn_key as friend_idsince we did not override its foriengn_key.

After this .friends on any user object can be used to fetch all the friends of that user.

All set we now have a working model of friends on a social network. Voilla!!!

After this post I can say I have moved closer to understanding association in rails.

This blog post originally appeared on ashish singh’s blog