AlexBrand
AlexBrand

Reputation: 12429

Rails - Find model under specific category

I have a Business model that :belongs_to Category

A sample hierarchy for categories:

I am using acts_as_tree for Category hierarchy.

How can I find all Businesses under the Restaurants category?

Upvotes: 1

Views: 209

Answers (3)

rewritten
rewritten

Reputation: 16435

If you want to get all descendants of a node in a tree-like hierarchy you have two options:

  1. Use the classic acts_as_tree, preload the selected category, start a cascade of queries to retrieve all the children, the grandchildren etc, until you get only leaves (nodes without further chidren). This approach is as good as it sounds.

  2. Use a more advanced tree representation, like nested set or closure tree. Under these representation you can get all descendants of a specific node with just one single query.

Then you get the collected categories and query in Businesses:

Business.where(:category => categories)

(technical explanation: nested set)

Under a nested set representation, each node has two indices, assigned in the following way: imagine that each node is a house with two windows, East and West, and the tree is like a bifurcating road where all children are more or less to the north of their parent. So you start to the east of the root house, and put a sequential number on the windows you meet. You never cross any road, you are only allowed to go around houses which has no further road to the north. At the end you'll get again to the root house, and put a number on the west window.

The assigned numbers will have the following properties:

  • All east windows have even numbers, all west windows have odd numbers (provided you started with 0)
  • The delta in each house (difference between west and east number) is always 1 + the number of descendant houses
  • All descendant houses of any house H will have their east and west numbers strictly included between H's own numbers
  • The converse is true too, all houses whose numbers are strictly comprised between H's numbers are actually descendants of H
  • If two houses are not ancestors one of the other (so one's nombers are comprised between the other's) then they are completely separated, that is both numbers of one house are strictly less than both numbers of the other.

So, while inserting a new element in the tree is costly (it needs to update a number of indices), retrieving the whole descendance (all children, grandchildren, ...) is quite easy, just take the nodes whose "east" and "west" numbers are between your chosen category's east and west. You can actully do slightly better, but it doesn't matter here.

A library like https://github.com/collectiveidea/awesome_nested_set will manage all this for you, and you only call

categories = @category.self_and_descendants.to_a

(technical explanation: closure tree)

This approach need an accessory table, where you store the transitive closure of the child->parent relationship (see http://en.wikipedia.org/wiki/Reflexive_transitive_closure#P_closures_of_binary_relations )

The table will contain all pairs ancestor-descendant, so you can join in smart ways with that table to get almost any slicing of the hierarchy.

Again, a library such as https://github.com/mceachen/closure_tree will do the hard work for you, and you will be able to do

categories = @category.self_and_descendants.to_a

Upvotes: 2

Salil
Salil

Reputation: 47542

category = Category.find_by_name("Restaurants")

Then for Business

category.business

IF you want children like (Sushi, Pizza, Chinese.......) Then

category.childrens

To find all the categories add following method in category.rb

  def all_children
    all = []
    self.children.each do |category|
      all << category
      root_children = category.all_children.flatten
      all << root_children unless root_children.empty?
    end
    return all.flatten
  end

and then use

@category.all_children

EDITED To find businesses for category restaurant and all the subcategories of restaurant.

Business.where("category_id = ? OR category_id in (?)", category.id, category.all_children.map(&:id))

Upvotes: 0

Pete Hamilton
Pete Hamilton

Reputation: 7920

Can you not just do something like:

Category.find_by_name("Restaurants").businesses

EDIT:

Didn't realise you wanted businesses in the subcategories of Restaurants too, duh.

For multiple layers of hierarchy you first need to get all the categories and then go over each, finding businesses, then join them together

class Company < ActiveRecord::Base

  ...

  def all_children
    all = []
    self.children.each do |c|
      all << c
      root_cs = c.all_children.flatten
      all << root_cs unless root_cs.empty?
    end
    return all.flatten
  end
end

Then you could call:

root_category = Category.find_by_name("Restaurants")
categories = root_category.all_children
businesses = categories.map{ |c| c.businesses }.flatten

That should return you a list of businesses. It doesn't seem very nice though, I feel like there should be a more optimal way.

Hopefully it should give you some food for thought anyway.

Upvotes: 0

Related Questions