Creating a model that has a tree structure - ruby-on-rails

I have categories that are in a tree structure. I am trying to link them together by defining a parent for each one. (I couldn't figure out how to call the property parent so it's just category for now, but it means the parent).
class Category < ActiveRecord::Base
has_one :category # the parent category
end
But the relationship ends up the wrong way around.
The getter function is on the child category (correctly) but the category_id is stored on the parent:
parent = Category.create(:name => "parent")
child = Category.create(:name => "child", :category => parent)
parent.id # 1
child.id # 2
child.category_id # nil
parent.category_id # 2
child.category.name # "parent" (!!)
The parent needs to be able to have multiple children so this isn't going to work.

What you're looking for is self joins. Check this section of the Rails guide out: http://guides.rubyonrails.org/association_basics.html#self-joins
class Category < ActiveRecord::Base
  has_many :children, class_name: "Category", foreign_key: "parent_id"
  belongs_to :parent, class_name: "Category"
end
Every Category will belong_to a parent, even your parent categories. You can create a single category parent that your highest level categories all belong to, then you can disregard that information in your application.

You can use acts_as_tree gem to achieve this, find below example and link.
https://github.com/amerine/acts_as_tree/tree/master
class Category < ActiveRecord::Base
include ActsAsTree
acts_as_tree order: "name"
end
root = Category.create("name" => "root")
child1 = root.children.create("name" => "child1")
subchild1 = child1.children.create("name" => "subchild1")
root.parent # => nil
child1.parent # => root
root.children # => [child1]
root.children.first.children.first # => subchild1

You should take a look at the ancestry gem: https://github.com/stefankroes/ancestry
It provides all the functionality you need and is able to get all descendants, siblings, parents, etc with a single SQL query by using a variant of materialized paths so it'll have better performance than the self-joins and acts_as_tree answers above.

Category should have many categories, and the foreign key of each category should be the parent_id. So, when you do parent.children it lists all the categories which have parent_id=parent.id.
Have you read on Single Table Inheritance?

Full Article - https://blog.francium.tech/best-practices-for-handling-hierarchical-data-structure-in-ruby-on-rails-b5830c5ea64d
A Simple table
Table Emp
id: Integer
name: String
parent_id: Integer
Associations
app/models/emp.rb
class Emp < ApplicationRecord
has_many :subs, class_name: 'Emp', foreign_key: :parent_id
belongs_to :superior, class_name: 'Emp', foreign_key: :parent_id
end
Scope Definition
class Emp < ApplicationRecord
----
----
scope :roots, -> { where(parent_id: nil) }
end
Fetching data
def tree_data
output = []
Emp.roots.each do |emp|
output << data(emp)
end
output.to_json
end
def data(employee)
subordinates = []
unless employee.subs.blank?
employee.subs.each do |emp|
subordinates << data(emp)
end
end
{name: employee.name, subordinates: subordinates}
end
Eager Loading
def tree_data
output = []
Emp.roots.includes(subs: {subs: {subs: subs}}}.each do |emp|
output << data(emp)
end
output.to_json
end

Related

Rails create object has_many through

My models
class Collection < ActiveRecord::Base
has_many :outfits
has_many :products, through: :outfits
end
class Outfit < ActiveRecord::Base
belongs_to :product
belongs_to :collection
end
class Product < ActiveRecord::Base
has_many :outfits
has_many :collections, through: :outfits
end
I want to save product in collection model
so one collection can have few product in it
How can i do it? i'm a bit struggle with it
it have tried something like this
p = Product.find_by_code('0339').id
p.collections.create(product_id:p1)
but i guess i'm wrong
When you're chaining through a through collection you don't need to reference the parent's id since that is known.
Instead of:
p = Product.find_by_code('0339').id # NOTE that you have an 'id' not an object here
p.collections.create(product_id:p1) # you can't call an association on the id
Build the association between two existing models (I'm assuming you have other fields in your models; I'm using name as an example).
p = Product.find_by(code: '0339')
c = Collection.find_by(name: 'Spring 2016 Clothing')
o = Outfit.new(name: 'Spring 2016 Outfit', product: p, collection: c)
o.save!
Assuming p and c exist and assuming o passes validation then you now have an assocaition between one product and one collection using a new outfit as the join table.
p.reload
p.collections.count # => 1
c.reload
c.products.count # => 1

Ruby on Rails / get count on self nested model (has_many / belongs_to)

I have a self nested category model: which has_many and belongs_to it self
class Category < ActiveRecord::Base
has_many :subcategories, class_name: "Category", foreign_key: "parent_id", dependent: :destroy
belongs_to :parent_category, class_name: "Category", foreign_key: "parent_id"
end
In the view I want to display not only the #category.subcategories.count but the count of all nested subcategories
how would I get that?
~~~ UPDATE: ~~~
In the categories controller I get the current category from the parameters like:
def show
#category = Category.find(params[:id])
end
now I want use in the view (but the following example doesn't give me all nested subcategories back)
<div>
<%= #category.name %> has <%= #category.subcategories.count %> subcategories in total
</div>
create a recursive model method...
def deep_count
count = subcategories.count
subcategories.each { |subcategory| count += subcategory.deep_count }
count
end
If in your design it's possible for a child to be the parent of an ancestor
(e.g. "4x4" -> "Jeep" - > "SUV" -> "4x4" -> ...)
Then you could end up with a stack overflow. To avoid that you can track categories to ensure you don't deep_count them twice...
def deep_count(seen_ids=[])
seen_ids << id
count = subcategories.count
subcategories.where("id NOT IN (?)", seen_ids).each do |subcategory|
count += subcategory.deep_count(seen_ids)
end
count
end
As an addition to #SteveTurczyn's epic answer, you may wish to look at using one of the hierarchy gems (we use acts_as_tree).
Not only will this extract your has_many :subcategories association, but provides a myriad of functionality to allow you to better handle nested objects.
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
end
This will allow you to use the following:
#category = Category.find x
#category.children #-> collection of subcategories
#category.parent #-> Category record for "parent"
#category.children.create name: "Test" #-> creates new subcategory called "test"
Because acts_as_tree uses parent_id, you wouldn't have to change anything in your database.
--
You'd still be able to use the deep_count method:
#app/models/category.rb
class Category < ActiveRecord::Base
acts_as_tree order: "name"
def deep_count
count = children.count
children.each {|child| count += child.deep_count }
count
end
end
I'm sure there must be a way to count the "children" but I've not got any code at hand for it.
The main benefit of it is the recursion with displaying your categories. For example, if a Post has_many :categories:
#app/views/posts/index.html.erb
<%= render #post.categories %>
#app/views/categories/_category.html.erb
<%= category.name %>
Subcategories:
<%= render category.children if category.children.any? %>
--
Just seems a lot cleaner than two ActiveRecord associations.

Validation to prevent parent referencing self as children

I'm looking for a solution to prevent "parents" to add his self as "children".
My Model looks like this:
class Category < ActiveRecord::Base
belongs_to :parent, :class_name => 'Category'
has_many :children, :class_name => 'Category', :foreign_key => 'parent_id'
end
Now I look for a solution to prevent things like this
parent = Category.create(name: "Parent")
Category.new(name: "Children", parent_id: parent.id).valid? # should be => false
You can add a custom validation for that.
Something like
class ParentValidator < ActiveModel::Validator
def validate(record)
if record.parent_id == record.id
record.errors[:parent_id] << 'A record\'s parent cannot be the record itself'
end
end
end
class Category
include ActiveModel::Validations
validates_with ParentValidator
end
or even simpler (if it is a one off thing)
class Category < ActiveRecord::Base
validate :parent_not_self, on: :save
def parent_not_self
if parent_id == id
errors.add(:parent_id, 'A record\'s parent cannot be the record itself')
end
end
end
Both cases will generate a validation error when you try to assign the record itself as the parent's record

Rails Thinking Sphinx Indexing Self Join Associations Tree Structure

I have products which belong to a category. And categories make up a tree
structure by having a parent and children using self joins:
Associations:
class Category < ActiveRecord::Base
has_many :children, class_name: "Category", foreign_key: "parent_id"
belongs_to :parent, class_name: "Category"
end
class Product < ActiveRecord::Base
belongs_to :category
end
For example,
Fruits & Vegetables => "High" Category
Fresh Fruits => "Intermediate" Category
Citrus => "Low" Category
Limes Large => Product
I would like to use Thinking Sphinx to index both the "low" category name and
"high" category name for a product, and possibly even all category names in between in the tree hierarchy.
I had no trouble indexing the low category parent name as follows:
class Product < ActiveRecord::Base
indexes :name
indexes category.parent.name, as: :low_category
end
NOTE: The number of nodes between the "High" and "Low" categories are variable. I need a way to dynamically add the hierarchical names.
But how do I go about indexing category names further up in the tree? I know I can't use methods
in TS indexing, so how I do I setup the database?
Most importantly, how do I index the "high" category name?
Can you do this ?
class Product < ActiveRecord::Base
indexes :name
category = category.parent
indexes category.name, as: :low_category
while category.parent do
if category.parent
indexes category.name, as: :root_category
elsif category.parent
indexes category.name, as: :high_category
else
indexes category.name
end
category = category.parent
end
end

Rails: order using a has_many/belongs_to relationship

I was wondering if it was possible to use the find method to order the results based on a class's has_many relationship with another class. e.g.
# has the columns id, name
class Dog < ActiveRecord::Base
has_many :dog_tags
end
# has the columns id, color, dog_id
class DogTags < ActiveRecord::Base
belongs_to :dog
end
and I would like to do something like this:
#result = DogTag.find(:all, :order => dog.name)
thank you.
In Rails 4 it should be done this way:
#result = DogTag.joins(:dog).order('dogs.name')
or with scope:
class DogTags < ActiveRecord::Base
belongs_to :dog
scope :ordered_by_dog_name, -> { joins(:dog).order('dogs.name') }
end
#result = DogTags.ordered_by_dog_name
The second is easier to mock in tests as controller doesn't have to know about model details.
You need to join the related table to the request.
#result = DogTag.find(:all, :joins => :dog, :order => 'dogs.name')
Note that dogs is plural in the :order statement.

Resources