47

I need to ensure that when a product is created it has atleast one category. I could do this with a custom validation class, but I was hoping there was a more standard way of doing it.

class Product < ActiveRecord::Base
  has_many :product_categories
  has_many :categories, :through => :product_categories #must have at least 1
end

class Category < ActiveRecord::Base
  has_many :product_categories
  has_many :products, :through => :product_categories
end

class ProductCategory < ActiveRecord::Base
  belongs_to :product
  belongs_to :category
end
davegson
  • 8,205
  • 4
  • 51
  • 71
recursive_acronym
  • 2,981
  • 6
  • 40
  • 59
  • 1. products + categories is a great opportunity to meet `has_and_belongs_to_many` http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_and_belongs_to_many. You don't need join model unless you don't want to store additional attributes alongside association. 2. You can use top answer from this question http://stackoverflow.com/questions/6429389/how-can-i-make-sure-my-has-many-will-have-a-size-of-at-least-2 guess what you have to change :) – jibiel Mar 02 '12 at 15:36

3 Answers3

73

There is a validation that will check the length of your association. Try this:

class Product < ActiveRecord::Base
  has_many :product_categories
  has_many :categories, :through => :product_categories

  validates :categories, :length => { :minimum => 1 }
end
davegson
  • 8,205
  • 4
  • 51
  • 71
wpgreenway
  • 1,619
  • 13
  • 6
  • 3
    How do I write a spec to test this ? – abhishek77in Mar 11 '15 at 11:02
  • @abhishek77in I found something that said to use `it {should validate_length_of(:categories).is_at_least(1)}` but I get an error saying undefined method each for string. I was thikning that testing for presence might do the trick as it would require at least one record in there. – Int'l Man Of Coding Mystery May 26 '18 at 13:32
60

Ensures it has at least one category:

class Product < ActiveRecord::Base
  has_many :product_categories
  has_many :categories, :through => :product_categories

  validates :categories, :presence => true
end

I find the error message using :presence is clearer than using length minimum 1 validation

davegson
  • 8,205
  • 4
  • 51
  • 71
Adrian
  • 9,102
  • 4
  • 40
  • 35
  • 1
    "can't be blank" definitely makes more sense than "is too short (minimum is 1 character)" – mzrnsh Jul 09 '22 at 10:46
4

Instead of wpgreenway's solution, I would suggest to use a hook method as before_save and use a has_and_belongs_to_many association.

class Product < ActiveRecord::Base
  has_and_belongs_to_many :categories
  before_save :ensure_that_a_product_belongs_to_one_category

  def ensure_that_a_product_belongs_to_one_category
    if self.category_ids < 1 
      errors.add(:base, "A product must belongs to one category at least")
      return false
    else
      return true
    end
  end   

class ProductsController < ApplicationController
  def create
    params[:category] ||= []
    @product.category_ids = params[:category]
    .....
  end
end

And in your view, use can use for example options_from_collection_for_select

davegson
  • 8,205
  • 4
  • 51
  • 71
ftanguy
  • 57
  • 1