Rails: How to limit number of items in has_many association (from Parent)

Matt Scilipoti picture Matt Scilipoti · Nov 16, 2011 · Viewed 12.6k times · Source

I would like to limit the number of items in an association. I want to ensure the User doesn't have more than X Things. This question was asked before and the solution had the logic in the child:

The offered solution (for similar issue):

class User < ActiveRecord::Base
  has_many :things, :dependent => :destroy
end

class Thing <ActiveRecord::Base
  belongs_to :user
  validate :thing_count_within_limit, :on => :create

  def thing_count_within_limit
    if self.user.things(:reload).count >= 5
      errors.add(:base, "Exceeded thing limit")
    end
  end
end

The hard coded "5" is an issue. My limit changes based on the parent. The collection of Things knows its limit relative to a User. In our case, a Manager may adjust the limit (of Things) for each User, so the User must limit its collection of Things. We could have thing_count_within_limit request the limit from its user:

if self.user.things(:reload).count >= self.user.thing_limit

But, that's a lot of user introspection from Thing. Multiple calls to user and, especially, that (:reload) are red flags to me.

Thoughts toward a more appropriate solution:

I thought has_many :things, :before_add => :limit_things would work, but we must raise an exception to stop the chain. That forces me to update the things_controller to handle exceptions instead of the rails convention of if valid? or if save.

class User
  has_many :things, :before_add => limit_things

  private
  def limit_things
    if things.size >= thing_limit
      fail "Limited to #{thing_limit} things")
    end
  end
end

This is Rails. If I have to work this hard, I'm probably doing something wrong.

To do this, I have to update the parent model, the child's controller, AND I can't follow convention? Am I missing something? Am I misusing has_many, :before_add? I looked for an example using :before_add, but couldn't find any.

I thought about moving the validation to User, but that only occurs on User save/update. I don't see a way to use it to stop the addition of a Thing.

I prefer a solution for Rails 3 (if that matters for this problem).

Answer

rbinsztock picture rbinsztock · Mar 15, 2013

So if you want a different limit for each user you can add things_limit:integer into User and do

class User
  has_many :things
  validates_each :things do |user, attr, value|
   user.errors.add attr, "too much things for user" if user.things.size > user.things_limit
  end
end

class Thing
  belongs_to :user
  validates_associated :user, :message => "You have already too much things."
end

with this code you can't update the user.things_limit to a number lower than all the things he already got, and of course it restrict the user to create things by his user.things_limit.

Application example Rails 4 :

https://github.com/senayar/user_things_limit