Sonntag, 27. Juli 2014

Merge your ActiveRecord scopes!

ActiveRecord scopes help to abstract out several SQL fragments in a elegant object oriented manner. Since they are combineable and reuseable, they help to maintain database related code.
The original ActiveRecord models:
class Food < ActiveRecord::Base
  has_and_belongs_to_many :ingredients
  scope :preservative, joins(:ingredients).where("ingredients.preservative" => true)
end

class Ingredient < ActiveRecord::Base
end
are tied together via a 1:n relationship. Furthermore there is scope, which shrinks the amount of foods having preservative ingredients. It has a name :preservative and receives a join and a condition (for more detailed explanation of ActiveRecord scopes go read: Scope the model!).
But it suffers knowing about the internal table structure of a different model, which increases the coupling between both classes.
That issue can be solved by merging scopes of different model classes:
class Food < ActiveRecord::Base
  has_and_belongs_to_many :ingredients
  scope :preservative, joins(:ingredients).merge(Ingredient.preservative)
end

class Ingredient < ActiveRecord::Base
  scope :preservative, where("#{table_name}.preservative" => true)
end
Now the Ingredient model takes the responsibility for its scope, where it belongs (please note that using ActiveRecord::ModelSchema::ClassMethods#table_name removes the knowledge about the table name in the scope itself).
The interesting part is the scope Food#preservative. Besides still joining the other model via the association, it uses ActiveRecord::SpawnMethods#merge for merging the scope Ingredient#preservative into the scope Food#preservative.
The usage:
preservative_foods = Food.preservative
Further articles of interest:

Supported by Ruby 2.1.1 and Ruby on Rails 3.2.17