ActsAsNestedSet's all_children associations

Hello, I am wondering if there is a better way to implement this.

I have to models, a Document model and a Category model. The Category
model is self-referencing through acts_as_nested_set.

category:
  id
  title
  parent_id
  lft
  rgt

documents:
  id
  details...

categories_documents:
  category_id
  document_id

I have my app set up in a way that the Documents controller has a list
view that includes a Category filter utility. This allows the user to
see all documents associated with a selected category and all
descendant categories, (using all_children, not direct_children). The
problem I found was that there wasn't a clean way to return all of the
associated documents for all_children without running a new SELECT for
each child. Something like this:

@documents = category.documents
subcategories = category.all_children
subcategories.each do |subcategories|
  @documents += subcategory.documents
end

This runs a separate query for each of the subcategories to find each
subcategory's documents. There are about ten thousand categories so
this isn't an option in my situation.

I have changed it to something like this which is much faster / efficient:

@categories = category.documents
# turn all subcategory ids into a string
subcategories = category.all_children.map {|subcategory|
subcategory.id}.join(", ")

and then use subcategories as part of my Document look up:

@documents += Document.find(:all,
                                              :conditions =>
"d.category_id IN (#{subcategories})",
                                              :joins => "AS d JOIN
categories_documents AS cd
                                                            ON d.id =
cd.planner_id")

This works great and allows me to get all of the documents in one
query. So my question, is there a better approach to this problem?
Something that allows me to avoid defining the join? If not, no big
deal, I am just trying to see if I missed something here.

Thanks for taking time to read this.

Sam

Sam Morrison wrote:

I have my app set up in a way that the Documents controller has a list
view that includes a Category filter utility. This allows the user to
see all documents associated with a selected category and all
descendant categories, (using all_children, not direct_children). The
problem I found was that there wasn't a clean way to return all of the
associated documents for all_children without running a new SELECT for
each child. Something like this:

@documents = category.documents
subcategories = category.all_children
subcategories.each do |subcategories|
  @documents += subcategory.documents
end

@documents = Documents.find :all, :joins => <<-END
   join categories on categories.id = documents.category_id
   and categories.lft between #{category.lft} and #{category.rgt}
END

Thanks Mark, but that is actually similar to what I am already doing.
I mentioned in my original post that I wasn't using subcategories.each
approach and had already re-coded this as a join.

Thanks for taking the time to respond. I do appreciate it. I will
stick with what I am using until I can find a better solution.

Sam Morrison wrote:

Mark, thanks again. I guess I should have re-worded my post a bit. I
apologize for not being more clear about what I am looking for. I am
not looking for a "more efficient" query. In my situation I am using
an join table to link documents to categories. What I am looking for
is something more "railsish", something along the lines of:

  subcategories = category.all_children
  documents = subcategories.documents

  or even

  documents = category.all_children.documents

I know this particular syntax doesn't work, but is anything similar to
this possible?

Mark, I decided to refactor my app a bit. I was able to move some of
the relationship specific attributes to either of the document /
category models and even get rid of a few things I no longer needed.
This allowed me to simplify the relationship and remove the join table
all together.

Your example is a much more efficient approach than what I was
originally doing. I would still like to be able to do something like
this though:

  documents = category.all_children.documents

though, I am assuming this isn't possible. Either way. thanks again
for your post, I was unaware of the :includes parameter. The SQL
developer side of me makes me want to just dig in and code these
queries myself. I am just trying to find the most efficient, railsish,
way of doing things so I am stay true to the framework.

Again, I appreciate your responses and I learned something new...

Sam Morrison wrote:

originally doing. I would still like to be able to do something like
this though:

  documents = category.all_children.documents

Why don't you just hide the Document find in a Category instance
method:

class Category
   def all_documents
     Document.find :all, :joins => <<-END
       join categories on categories.id = documents.category_id
       and categories.lft between #{lft} and #{rgt}
     END
   end
end

allowing you to write

    documents = category.all_documents

though, I am assuming this isn't possible. Either way. thanks again
for your post, I was unaware of the :includes parameter. The SQL
developer side of me makes me want to just dig in and code these
queries myself. I am just trying to find the most efficient, railsish,
way of doing things so I am stay true to the framework.

Use of :include => :category sacrifices efficiency for simplicity
because it fetches, instantiates, and attaches a Category model
to each document, which you probably don't need. However I've
recently released a plugin which allows you to get the join without
any data by writing :include => :category[]

class Category
   def all_documents
     Document.find :all, :include => :category[],
                         :conditions => {:lft => lft..rgt}
   end
end

Why don't you just hide the Document find in a Category instance
method:

class Category
   def all_documents
     Document.find :all, :joins => <<-END
       join categories on categories.id = documents.category_id
       and categories.lft between #{lft} and #{rgt}
     END
   end
end

Mark, this works well because I access a category's documents from
several locations, so this allows me to keep things DRY and concise.
Thanks for reminding me about the instance method approach. I guess I
was just thinking that this situation would be more common...

However I've recently released a plugin which allows you to get the join without
any data by writing :include => :category[]

Where is this? I would like to take a look.

Thanks again,
sam