0

I have Category class shown below

class Category < ActiveRecord::Base
  has_many :subcategories, class_name: "Category", foreign_key: "parent_category_id"
  belongs_to :parent_category, class_name: "Category"

  belongs_to :main_category
end

and I wonder if I can define main_category association the rails way that I can reference the #main_category on subcategories but leaving the main_category_id empty (as the reference on subcategory#main_category_id will duplicate the data which is in parent_category#main_category_id or it is just premature optimization? ).

category = Category.new main_category: main_category
subcategory = Category.new parent_category: category

assert_equal category.main_category, subcategory.main_category
eldi
  • 1,239
  • 11
  • 19
  • The phrase is "Premature optimization". And yes this does add duplication to your tables and you would have to use callbacks or triggers to ensure that the `main_category_id` column is actually filled. – max May 25 '21 at 12:50
  • thanks for correction :) I asked the question to get know if there is a rails way of defining that association without that duplication of #main_category_id on subcategory – eldi May 25 '21 at 13:03

2 Answers2

1

you can create a proxy that subcategories will delegate main_category to parent, the drawback that there're 3 queries to get main_category

class Category < ActiveRecord::Base
 has_many :subcategories, class_name: "Category", foreign_key: "parent_category_id"
 belongs_to :parent_category, class_name: "Category"

 belongs_to :main_category, class_name: "Category"

 # with subcategories, there're 3 queries: 
 # super return nil -> find parent_category -> find main_category of the parent 
 def main_category
   super || parent_category&.main_category
 end
end
Lam Phan
  • 3,405
  • 2
  • 9
  • 20
  • 2
    You can use `Category.eager_load(parent_catgory: :parent_category)` to force a single query. – max May 25 '21 at 13:08
  • Yes, I thought about that, but wished there is rails way thx @max for `eager_load` – eldi May 25 '21 at 13:45
  • 1
    Sidenote: `belongs_to` is enforced by default as of rails 5 so for the `super` call to return `nil` the association would need to be `belongs_to :main_category, class_name: "Category", optional: true` (same applies for `parent_category` with the safe nav) – engineersmnky May 25 '21 at 16:27
1

You can use indirect assocations to setup "short-cuts" through the tree.

class Category < ActiveRecord::Base

  # Going up...
  has_many :subcategories, 
     class_name: "Category", 
     foreign_key: "parent_category_id"
  has_many :grand_child_categories, 
     class_name: "Category", 
     through: :subcategories
  has_many :great_grand_child_categories, 
     class_name: "Category", 
     through: :grand_child_categories

  # Going down...
  belongs_to :parent_category, 
    class_name: "Category"
  has_one :grand_parent_category, 
    through: :parent_category,
    class_name: "Category"
  has_one :great_grand_parent_category, 
    through: :grand_parent_category,
    class_name: "Category"
end

However if you have a heirarchy of unlimited depth ActiveRecord::Assocations can't really solve the problem of finding the node at the bottom of the tree. That requires more advanced SQL like a recursive common table expression (CTE).

While ActiveRecord does has the basic tools for creating self joining assocations most of the more andvaced stuff is out of scope and can be handled with gems like Ancestry.

max
  • 96,212
  • 14
  • 104
  • 165