The active record models with the status
columns generally indicate that there are different states that the model can be in.
And usually, when there are status
columns within a model, it’s common to query for specific statuses and to check for the status of the model. For example, let’s say that we have an Invoice
model that have three different statuses: pending
, paid
, overdue
. Most people would write code for querying for these statuses and checking for the status of the model like this.
1 2 3 4 5 6 7 8 9 10 11 12 |
class InvoicesController < ApplicationController def index # Scoping for paid invoices for example @invoices = Invoice.where(status: 'paid') end def show # Finding invoices that are only paid @invoice = Invoice.find(params[:id]) render :nothing and return unless @invoice.status == 'paid' end end |
This works. Some developers, who are aware of creating scopes may create scopes for the different statuses instead. And also, they may add methods to the Invoice
model to determine the status of the Invoices
(because you know, refactoring).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
class Invoices < ApplicationRecord scope :paid, -> { where(status: 'paid') } scope :pending, -> { where(status: 'pending') } scope :overdue, -> { where(status: 'overdue') } def paid? status == 'paid' end def pending? status == 'pending' end def overdue? status == 'overdue' end end class InvoicesController < ApplicationController def index @invoices = Invoice.paid end def show @invoice = Invoice.find(params[:id]) render :nothing and return unless @invoice.paid? end end |
The above is definitely better, and I (and a lot of people) would probably go, “Good Enough”, and get along with our lives. But our Invoice
model can be made more succinct and more flexible if we utilize a little bit of underutilized Ruby features.
Let’s say that the Invoice
model can have a lot more statuses, like 10 different statuses, like paid
, pending
, overdue
, overpaid
, underpaid
, read
, unread
, clicked
, missing
, scammed
(Note, some of these status names don’t make sense, I just wanted to quickly come up with 10). Well, that’s a lot of custom scopes and methods you have to write. Thankfully, there’s a little trick to creating scopes with a little bit of Ruby magic during runtime.
For the scopes, we can store the pre-defined statuses in a constant, loop over them, and create scopes during runtime. For the methods that check for the Invoice’s current status, Ruby has this thing called define_method
that allows you to define methods during run time.
https://ruby-doc.org/core-2.2.0/Module.html#method-i-define_method
At first glance, one may go, “Why would I ever use this define_method thingy instead of actually writing the method myself?” Well, in the context of creating methods that check for predefined statuses on the fly, it can be pretty useful in that it can create all of these methods for us rather than us having to write the scopes by hand. To utilize these two techniques so that we don’t have to manually write 10 different scopes and methods by hand, we need to do the following.
- Create a constant that defines what statuses the Invoice model is allowed to have.
- Loop through the statuses, and for each status, create a scope and then define a custom method using Ruby’s
define_method
.
Below is how you do it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Invoice < ApplicationRecord STATUSES = %w(paid pending overdue overpaid underpaid read unread clicked missing scammed) STATUSES.each do |s| name = s.parameterize(separator: '_') define_method("#{name}?") { status == s } scope(name), -> { where(status: s) } end end class InvoicesController < ApplicationController def index @invoices = Invoice.paid end def show @invoice = Invoice.find(params[:id]) render :nothing and return unless @invoice.paid? end end |
Try the refactoring of the Invoice
model above and you’ll see that the controller still works as it should. And the Invoice
model in this form is much more concise and flexible since if we want to add more scopes and methods that pertain to the status, all we have to add is the new statuses in the Invoice::STATUS
constant.