Class Methods And Scopes
So today at work we were wondering if ActiveRecord scopes behave like class methods. You may have heard that scopes are just syntactical sugar for class methods, in other words that this:
class Cab
scope :modern, -> { where('aged > ?', 2.years.ago) }
endCould be written also as like this:
class Cab
def self.modern
where('aged < ?', 2.years.ago) }
end
endAnd yes the behaviour will be the same….mostly! Why you might not know is that between scopes and class methods they are subtle differences.
Check the following class methods, one just returns modern cabs and another returns cabs driven by an specific person
class Cab
def self.modern
where('aged < ?', 2.years.ago) }
end
def self.by_driver driver
where(driver: driver)
end
endIf we used them together Cab.by_driver("Mayra").modern, the following query will be generated:
SELECT "cabs".* FROM "cabs" WHERE "cabs"."driver" = "Mayra" AND (aged > "...")Everything works as aspected right? Lets keep working and see what would happen if we continue to use class methods. Imagine that we use the former class methods in a controller, using paramsto set the driver name, and somehow a nil value is passed:
# params[:driver_name] => nil
Cab.by_driver(params[:author_name]).modernWell this is where things get start to get messy, the following query will result like this:
SELECT "cabs".* FROM "cabs" WHERE "cabs"."driver" IS NULL AND (aged > '...')This will return an array of cabs with no driver instead of cabs from all drivers. So whats the big deal you’ll say? Just add a guard validation inside by_driver method
def self.by_driver driver
where(driver: driver) if driver.present? # Returns nil if no driver is present
endAgain:
# params[:author_name] #=> nil
Cab.by_driver(params[:author_name]).modernAnd we get an error :( :
undefined method `modern` for NilClassSo what happened? Here is what: by_driver will return nil because driver is not present, so when we call modern on nil we get the error. So ok we can still fix this with a couple of extra code:
def self.by_driver driver
if driver.present?
where(driver: driver)
else
all
end
endIf driver is not present, we’ll return the all chainable relation, and finally when we do this
# params[:driver_name] => nil
Cab.by_driver(params[:author_name]).modernWe are going to get what we actually intended in the beginning:
SELECT "cabs".* FROM "cabs" WHERE (aged > '...')Even if is working now, all this could have actually been easily resolved with scopes:
class Cab < ActiveRecord
scope :by_driver, ->(driver) { where(driver: driver) }
scope :modern, -> { where('aged > ?', 2.years.ago) }
end Because scopes always return chainables objects ;)