Hooking in a Stats module in Rails Active Record
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