I am working on a Rails 3 application, and am having some trouble with my user/edit view. In this application, Users can subscribe to Categories. Users and Categories have a has_and_belongs_to_many relationship through a join table (categories_users).

In my user/edit view, I need to display a list of checkboxes for the various categories so the user can edit his subscriptions. I am using the following code to achieve this:

<div id="category-checklist">
  <% Category.roots.order('name ASC').each do |category| %>
      <li class="parent <%= category.name %>">
        <%= check_box_tag "user[category_ids][]", category.id, @user.categories.include?(category) %> <span><%= category.name %></span>

      <ul id="children-<%= category.id %>" class="children-list">
        <% category.children.order('name ASC').each do |child| %>
          <li class="child <%= child.name %>">
            <%= check_box_tag "user[category_ids][]", child.id, @user.categories.include?(child) %> <span><%= child.name %></span>
        <% end %>
  <% end %>

The Category model uses the ancestry gem for hierarchy, which is there the roots and children methods come from. Roots returns all the parent categories, and children returns all of the sub-categories for a given parent.

The really problematic part of this code seems to be @user.categories.include?(child) which is running 81 separate queries (one for each child category). This line is used to check if the category is currently subscribed to by the user -- if so, the checkbox is marked checked. Is there a better/faster way to accomplish this?

For reference, here is the join table's schema definition - I tried to put an index on the foreign keys thinking it would speed things up, but this is still very slow compared to the rest of my app.

create_table "categories_users", :id => false, :force => true do |t|
  t.integer "category_id"
  t.integer "user_id"

add_index "categories_users", ["category_id"], :name => "index_categories_users_on_category_id"
add_index "categories_users", ["user_id"], :name => "index_categories_users_on_user_id"

When you load the user, you can include the categories using eager loading.

@user = User.includes(:categories).find(params[:id])


@user = User.find(params[:id])
@categories = @user.categories.all
  • \$\begingroup\$ Thanks - you know, I had it setup this way in my users controller and I didn't realize that Devise moves everything to use their registrations_controller. I added "@categories = @user.categories.all" to the Devise controller's edit method and page load dropped exponentially! Thanks for pointing me in the right direction. \$\endgroup\$
    – Jim
    Aug 2 '12 at 15:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.