Search code examples
ruby-on-railsscopemodel-associations

Refactoring has_many with scopes


I'm a newbie and I just showed my code to an expert, that told me I shouldn't use has_many to filter my variables, but scopes.

I have three models : User, Product and Ownership.

So here is my code in app/models/user.rb :

class User
  has_many :ownerships, foreign_key: "offerer_id",
                         dependent: :destroy
  has_many :owned_products, through: :ownerships,
                             source: :product
  has_many :future_ownerships, -> { where owning_date: nil, giving_date: nil },
                               class_name: "Ownership",
                               foreign_key: "offerer_id"
  has_many :wanted_products, through: :future_ownerships,
                             source: :product
end

So I deleted the has_many :future_ownerships and has_many :wanted_products, and created a scope in app/models/ownership.rb :

class Ownership
  scope :future, -> { where owning_date: nil, giving_date: nil }
end

Now I can find the future ownerships doing this : user.ownerships.future. But what I don't know, is how to retrieve the wanted products ? How can I make a scope in my app/models/product.rb to be able to type something like that :

user.owned_products.wanted

Solution

  • There's nothing inherently bad with conditions in your associations, specially if you need to eager load a subset of products.

    However to achieve the scope you need, you must add it on the Product model and resort to plain sql since the filter is applied on a different model than the one it's defined on.

    class Product
      # not tested 
      scope :wanted, ->{ where("ownerships.owning_dates IS NULL AND ...") }
    end
    

    IMHO you're better off with the first solution. The reason is, if for some reason you apply that scope inside a block of many users, you'll hit the O(n) wall despite eager loading the products.

    User.includes(:owned_products).each do |user|
      user.onwned_products.wanted # => SQL connection
    end
    

    Update : just found out about merge an amazingly undocumented feature of ActiveRecord.

    Among other uses, it allows you to do a join, and filter by a named scope on the joined model

    In other words you can do :

    user.owned_products.merge(Ownership.future)
    

    Quit powerful !