Most full-fledged web frameworks come with ORMs built in. ORMs or Object Relational Mappings help to map the programming language data structures to actual data stores without having to worry about the underlying data source.

This helps to abstract the data store interfaces which helps in migrating to a separate data store more of a configuration knob and doesn’t require any change to the actual codebase. ORMs also help in connection pooling, managing database connections, validations, etc.

In this post, we will be assuming a base knowledge of ORMs and we will be looking at how to integrate a Statistics module into Active Record, an ORM layer used popularly by Ruby on Rails.

Problem

We want to keep track of the CRUD operations happening at a model layer and we want to keep the stats layer separate of the data store.

Solution

The first thing that I do in any Rails project is to define a base class which all models are derived from. The base model is derived from ActiveRecord::Base which is the class which defines the methods available to the models. Let’s say we have a User model.

class ApplicationModel < ActiveRecord::Base
  self.abstract_class = true
end
class User < ApplicationModel
end

We need to now override the methods provided by Active Record for CRUD operations.

Some of the common functions are

  • create
  • save
  • update
  • destroy
  • delete
  • find
  • find_by
  • where
  • all

For people who have studied about OOPS, we will be overriding the methods defined in the ActiveRecord class. We will be overriding the create method and incrementing the counter stored inside a hash with the model name as the base key and the action as the nested key.

class ApplicationModel < ActiveRecord::Base

  self.abstract_class = true
  @@stats = {}
  
  def self.create args
    
    @@stats[self.to_s] = {} if not @@stats[self.to_s]
    @@stats[self.to_s][__method__.to_s] = 0 if not @@stats[self.to_s][__method__.to_s]

    @@stats[self.to_s][__method__.to_s] += 1
    
    super
  end
 
end

In the above gist, we are storing the counters in the stats variable.

So if you call User.create(name: “user1”), the stats class variable will have the following representation.

{“User”=>{“create”=>0}}

We can override most of the other methods in the same manner. For example, if we want to override the all method, we can define a self.all method in the application_model.rb class.

However, the approach did we have taken here is not fool proof. There are usually multiple record chaining statements that we usually have to perform which is not derived on the ActiveRecord::Base class.

For example,

User.includes(:customer).where(name: “user1”)

Here, the User.includes(:customer) returns a ActiveRecord::Relation object and the same is returned by the .where(name: “user1”) chain as well.

From the ActiveRecord::Relation docs, it has the following methods.

CLAUSE_METHODS=[:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL=[:distinct, :group, :having] MULTI_VALUE_METHODS=[:includes, :eager_load, :preload, :select, :group, :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS=[:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache]

In order to override the above methods, we can override the ActiveRecord::Relation class by doing the following.

class ApplicationModel < ActiveRecord::Base
  
  self.abstract_class = true
  
  class ActiveRecord::Relation
     
    def where args
      # Do your stuff here
      super
    end
    
  end
  
end

The above set should be enough to override the ORM such that the stats layer can be reliably built in.

We also need to take care of ruby’s metaprogramming aspects in the stats module. Maybe that’s a blog post for another day.

I hope you liked the article. Please let me know if you have any queries regarding the article. Happy reading!!

References