Search code examples
ruby-on-railsruby-on-rails-4activerecordpolymorphic-associations

Issue with polymorphic ActiveRecord query


I have three models with the following associations:

User has_many :owns, has_many :owned_books, :through => :owns, source: :book
Book has_many :owns
Own  belongs_to :user, :counter_cache => true, belongs_to :book

I also have a page that tracks the top users by owns with the following query:

User.all.order('owns_count desc').limit(25)

I would now like to add a new page which can track top users by owns as above, but with a condition:

Book.where(:publisher => "Publisher #1")

What would be the most efficient way to do this?


Solution

  • I'm interesting if there is something special for this case, but my shot would be the following.

    First, I don't see how polymorphic association can be applied here. You have just one object (user) that book can belong to. As I understand, polymorphic is for connecting book to several dif. objects (e.g. to User, Library, Shelf, etc.) (edit - initial text of question mentioned polymorphic associations, now it doesn't)

    Second, I don't believe there is a way to cache counters here, as long as "Publisher #1" is a varying input parameter, and not a set of few pre-defined and known publishers (few constants).

    Third, I would assume that amount of books by single Publisher is relatively limited. So even if you have millions of books in your table, amount of books per publisher should be hundreds maximum.

    Then you can first query for all Publisher's books ids, e.g.

    book_ids = Book.where(:publisher => "Publisher #1").pluck(:id)
    

    And then query in owns table for top users ids:

    Owns.select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order(total_owns: :desc).limit(25)
    

    Disclaimer - I didn't try the statement in rails console, as I don't have your objects defined. I'm basing on group call in ActiveRecord docs

    Edit. In order to make things more efficient, you can try the following:

    0) Just in case, ensure you have indexes on Owns table for both foreign keys.

    1) Use pluck for the second query as well not to create Own objects, although should not be a big difference because of limit(25). Something like this:

    users_ids = Owns.where(book_id: book_ids).group(:user_id).order("count(*) DESC").limit(25).pluck("user_id")
    

    See this question for reference.

    2) Load all result users in one subsequent query and not N queries for each user

    top_users = User.where(:id => users_ids)
    

    3) Try joining User table in the first order:

    owns_res = Owns.includes(:user).select("user_id, book_id, count(book_id) as total_owns").where(book_id: book_ids).group(:user_id).order("total_owns DESC").limit(25)
    

    And then use owns_res.first.user