0

I have a question and I appreciate if anyone could help me please. I am working on a project, that project has a category and sub-category tables and the association between these two table are below

class Category < ApplicationRecord
  has_many :sub_categories, dependent: :destroy
end

class SubCategory < ApplicationRecord
  belongs_to :category, optional: true
end

And there is a user table, user can select multiple categories and it's sub-categories and store at another database table. I have created UserCategory model and associated the user model with that model to store category ids

class User < ApplicationRecord
  has_many :user_categories, dependent: :destroy
  has_many :categories, through: :user_categories
  accepts_nested_attributes_for :user_categories, :allow_destroy => true
end

class UserCategory < ApplicationRecord
  belongs_to :user, optional: true
  belongs_to :category, optional: true
end

Now my question is that how can I store sleeted subcategory ids also with it's parent selected category ids? because I want to show individual user selected categories and sub categories at view page..

Suman Das
  • 301
  • 1
  • 4
  • 18
  • Can a user select a category without selecting any sub-categories in that category? – SteveTurczyn Dec 24 '20 at 17:40
  • No user can't select sub categories first, user have to select category first after that user can select categories – Suman Das Dec 24 '20 at 17:59
  • In general the whole idea that you have to create join table rows to both the parent and sub-category is misguided. If you have a tree diagram you can always traverse from the leaf nodes to the root. – max Dec 24 '20 at 18:42
  • I didn't ask if users can select sub-categories first, I asked if user can select a category and *not* select sub-categories in that category. – SteveTurczyn Dec 25 '20 at 00:28
  • Yes user can select category without selecting sub categories.. but I can store the data when user also select it's sub categories.. because I need sub categories info – Suman Das Dec 25 '20 at 06:23

1 Answers1

1

I would start by using a single table with a self-referential association instead of two tables for categories and sub-categories:

class AddParentToCategories < ActiveRecord::Migration[6.0]
  def change
    change_table :categories do |t|
      t.references :parent, 
        foreign_key: { to_table: :categories },
        null: true
      t.string :type, index: true
    end
  end
end

type is Rails default inheritance_column and it lets you implement single table inheritance by having different model classes in the same table.

If you have existing data in the sub_categories table you need to move it to the Categories table:

SubCategory.find_each do |sc|
  Category.create!(
    name: sc.name,
    parent_id: sc.category_id,
    type: 'SubCategory'
  )
  sc.destroy
end

Then drop the table. You might also want to fill the nulls in the type column:

Category.where(type: nil).update_all(type: 'Category')

You can then modify your classes to a single table design:

class Category < ApplicationRecord
  # this still refers back to the categories table
  has_many :sub_categories,
    class_name: 'SubCategory',
    foreign_key: :parent_id
  scope :top_level, -> { where(parent_id: nil) }
end
# By inheriting from Category this model uses the table name 
# categories instead of sub_categories
class SubCategory < Category
  belongs_to :parent,
    class_name: 'Category'
end

While this may seem irrelevant, its a far better construct as it lets you build hierarchies of any depth and you can reference the same table in the next step.

When letting the user select categories/categories you can now just use the normal category_ids= setter created by the has_many :categories, through: :user_categories association:

User.create(category_ids: [1, 2, 3, 5])

It doesn't actually matter any more if the ids are top level or sub-categories. You do not need nested attributes here. Thats only actually need if you want to add additional columns to the join table besides the two foreign key columns.

Normally you would do this through the collection helpers:

<%= f.grouped_collection_select :category_ids, @categories, :sub_categories, :name, :id, :name %>
max
  • 96,212
  • 14
  • 104
  • 165