Search code examples
ruby-on-railspolymorphismruby-on-rails-5has-many-throughsingle-table-inheritance

Multiple Category Models vs Single Category Model with STI? - Rails


I have four models:

  • Post
  • Product
  • Article
  • Location

Each of these can have multiple Categories. The only logic rule is that the Categories are completely separate for each Class, so that a Post cannot have an ArticleCategory, a Location cannot have a ProductCatgory and so on.

Option 1: Multiple Category Models

Models:

  • PostCategory
  • ProductCategory
  • ArticleCategory
  • LocationCategory

Models for has_many through:

  • PostCategorization
  • ProductCategorization
  • ArticleCategorization
  • LocationCategorization

This works, but it doesn't adhere to the DRY (don't repeat yourself) philosophy. So, how about using STI?

Option 2: Single Category Model with STI

Models:

  • Category
  • Categorization

SubModels:

  • PostCategory << Category
  • ProductCategory << Category
  • ArticleCategory << Category
  • LocationCategory << Category

This seems fine, but I don't know if I need to even use STI if the columns are the same, and the logic is the same. The only difference between them is the associations.

Option 3: Single Category Model without STI?

Would it be better to have a "category_class" column, and do something like this:

class Post < ApplicationRecord

  has_many :categories, -> { where category_class: "Post" }

end

To save on the number of classes and subclasses, and simplify the entire solution. I have used this before but not on something with polymorphic associations, would this work?


Solution

  • Perhaps I'm misunderstanding. But, it seems to me...

    You could use an enum to specify what each Category record categorizes. Something like:

    # == Schema Information
    #
    # Table name: categories
    #
    #  id                :integer          not null, primary key
    #  name              :string           not null
    #  categorizes       :integer          not null
    #  created_at        :datetime         not null
    #  updated_at        :datetime         not null
    #
    class Category < ApplicationRecord
      has_many :categorizations
      has_many :categorizeables, through: :categorizations
    
      enum categorizes: {
        post:         0,
        product:      1,
        article:      2,
        location:     3
      }
    
      class << self 
    
        def not_for(categorizeable_type)
          where.not(categorizes: categorizeable_type)
        end
    
      end
    
    end
    

    Then, you can use your polymorphic join model, Categorization Something like:

    # == Schema Information
    #
    # Table name: categorizations
    #
    #  id                   :integer          not null, primary key
    #  category_id          :integer          not null
    #  categorizeable_id    :integer          not null
    #  categorizeable_type  :string           not null
    #  created_at           :datetime         not null
    #  updated_at           :datetime         not null
    #
    class Categorization < ApplicationRecord
      belongs_to :category
      belongs_to :categorizeable, polymorphic: true
    end
    

    And then you can associate your categorizations and categories using a has :many, through:

    # == Schema Information
    #
    # Table name: posts
    #
    #  id                   :integer          not null, primary key
    #  created_at           :datetime         not null
    #  updated_at           :datetime         not null
    #
    class Post < ApplicationRecord
      has_many :categorizations, as: :categorizeable
      has_many :categories, through: :categorizations
    
      validate :correct_categorization
    
      def correct_categorization
        if categories.not_for(:post).any?
          errors.add(:categorization, "is incorrect")
        end
      end
    
    end
    

    I added that validation since you stated "Categories are completely separate for each Class". You may need to fiddle with that a bit, but hopefully it gives you an idea of how it might work.