misterinevitable

Creating a Rails Model Scope Based on the Scope in a Related Model

Suppose you have a Rails application with these models:

class Recipe < ApplicationRecord
  has_many :recipe_ingredients, dependent: :destroy
  has_many :ingredients, through: :recipe_ingredients
end

class RecipeIngredients < ApplicationRecord
  belongs_to :recipe
  belongs_to :ingredient
end

class Ingredient < ApplicationRecord
  has_many :recipe_ingredients, dependent: :destroy
  has_many :recipes, through: :recipe_ingredients
  
  scope :refrigerated, -> { where(requires_refrigeration: true) }
end

You want to be able to pull a list of recipes which have at least one ingredient which needs to be refrigerated. One way to do this is create a scope like this:

class Recipe < ApplicationRecord
  has_many :recipe_ingredients, dependent: :destroy
  has_many :ingredients, through: :recipe_ingredients

  scope :with_refrigerated_ingredient, -> { joins(:ingredients).where(ingredients: { requires_refrigeration: true } ).distinct }
end

This works, but it requires Recipe to know how Ingredient represents refrigerated vs unrefrigerated ingredients. Instead, we can reuse the scope on Ingredient to define our new scope on Recipe using ActiveRecord::Relation#merge:

class Recipe < ApplicationRecord
  has_many :recipe_ingredients, dependent: :destroy
  has_many :ingredients, through: :recipe_ingredients
  
  scope :with_refrigerated_ingredient, -> { joins(:ingredients).merge(Ingredient.refrigerated).distinct }
end

When called with another ActiveRecord::Relation, #merge combines conditions from both relations. Here, we’re taking the condition from the Ingredient.refrigerated relation and adding it to the relation we’re building in our new scope. Note that we need to add joins(:ingredients) so the ingredients table is available in the query so the Ingredient.refrigerated scope can filter on it.