Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to eager load roots using ancestry gem with rails?

I have an Entry model that belongs to a Category. The Category model is using ancestry so that I can nest Categories.

I want to group a selection of entries by their root category. The problem I have is that in order to do this, rails executes a query on every single entry to get the root category. So if I have 20 records, it'll execute 20 queries to group by the root category.

I've already reduced the number of queries by eager loading the category (as below), but I can't figure out how to eager load the category's root as well.

Here's the code I'm using:

Controller

@entries = current_user.entries.includes(:category)
@entries_by_category = @entries.group_by { |entry| entry.category.root }

entry.rb

class Entry < ActiveRecord::Base
  belongs_to :category
end

category.rb

class Category < ActiveRecord::Base
  has_ancestry

  has_many :entries
end

I have tried putting a default scope on the Category model like so: default_scope { includes(:ancestry) }. But that didn't change the number of executed queries. And I can't figure out how to use includes() on the original query to include both the category and the category's root.

As a bit of extra information, categories can only be nested 2 levels deep (just categories and subcategories). So root technically means parent, it doesn't have to traverse multiple levels.

Any ideas?

like image 924
Christian Avatar asked Oct 02 '22 12:10

Christian


1 Answers

I had a very similar problem except I wanted to eager load the category's path instead of just the root. Unfortunately, eager loading only works for associations. The Ancestry navigators (parent, root, path, ancestors, etc.) are all methods, not associations, so you can't eager load them.

My solution was to eager load the entry's category association and then use model-level caching (see Railscast #115) on Category to cache the path (you could amend to root in your case):

category.rb

def cached_path
  Rails.cache.fetch([self, "path"]) { path.to_a }
end

entries_controller.rb

def index
  @entries = Entry.includes(:category)
end

views/entries/index.html.haml

- @entries.each do |entry|
  = entry.category.cached_path.map{ |c| c.name }.join(" / ")

Hope that helps.

like image 152
russellb Avatar answered Oct 18 '22 02:10

russellb