Search code examples
ruby-on-railsruby-on-rails-4overridinghas-manyeager-loading

Rails4 override has_many getter method


I have a Rails 4.1.7 application, where we have users and themes:

class User < ActiveRecord::Base
  has_many :themes
end

class Theme < ActiveRecord::Base
  belongs_to :user
end

Now, the user can create some own themes (then these themes will have their user_id set to the user's id), or can use any of the predefined themes (having user_id set to null)

What I would like to do is the following: to somehow change the has_many association so that when I call @user.themes, this bring me the predifined themes along with those of the user.

What I have tried:

1) to define an instance method instead of the has_many association:

class User < ActiveRecord::Base
  def themes
    Theme.where user_id: [id, nil]
  end
end

but since I would like to eager-load the themes with the user(s) (includes(:themes)), that won't really do.

2) to use some scope (has_many :themes, -> { where user_id: nil }), but it gives mySql queries like ... WHERE user_id = 123 AND user_id IS NULL, which returns empty. I guess with Rails5 I could do it with something like has_many :themes, -> { or.where user_id: nil }, but changing the Rails version is not an option for now.


Solution

  • Since posting my question, I tried many things to achieve my goal. One was interesting and, I guess, worth to be mentioned:

    I tried to unscope the has_many assiciation using unscope or rewhere, and it looked like this:

    has_many :themes, -> user = self { # EDIT: `= self` can even be omitted
      u_id = user.respond_to?(:id) ? user.id : user.ids
    
      unscope(where: :user_id).where(user_id: [u_id, nil])
      # or the same but with other syntax:
      rewhere(user_id: [u_id, nil])
    }
    

    When I tried @user.themes, it worked like wonder, and gave the following mySql line:

    SELECT `themes`.* FROM `themes`
           WHERE ((`themes`.`user_id` = 123 OR `themes`.`user_id` IS NULL))
    

    But when I tried to eager load it (why I started my research after all), it simply refused to unscope the query, and gave the same old user_id = 123 AND user_id = NULL line.

    After all, @Ilya's comment convinced me along with this answer, that it is one thing to use the has_many for querying, but it has other sides, like assigning for example, and overriding it for one's sake can spoil many other things.

    So I decided to remain with my nice method, only I gave it a more specific name to avoid future confusion:

    def available_themes
      Theme.where user_id: [id, nil]
    end
    

    As for @AndreyDeineko's response - since he continually refuses to answer my question, always answering something that has never been asked -, I still fail to understand why his method (having the same reults as my available_themes, but using 3 additional queries) would be a better solution.