Reputation: 1101
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:
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.
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
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).
Upvotes: 23
Views: 15233
Reputation: 4551
I thought I'd chime in here. It seems like most of the answers here fail under race-conditions. I'm trying to limit the number of users who can sign up at a certain price point in our app. Checking the limit in Rails means that 10 simultaneous registrations could get through, even if it exceeds the limit I'm trying to set.
For example, say I want to restrict registrations to no more than 10. Let's say that I already have 5 users registered. Let's also say that 6 new users attempt to register at the same time. In 6 different threads, Rails reads the number of slots remaining, and it gets the answer 5
. This passes validation. Rails then allows all registrations to go through, and I have 11 registrations. :/
Here's how I solved this problem:
def reserve_slot(price_point)
num_updated = PricePoint.where(id: price_point.id)
.where('num_remaining <= max_enrollments')
.update_all('num_remaining = num_remaining + 1')
if num_updated == 0
raise ActiveRecord::Rollback
end
end
Using this approach, I never allow more registrations than max_enrollments
, even when the app is under load. This is because the validation and increment is done in a single, atomic database operation. Note, too that I always call this method from within a transaction, so it rolls back under failure.
Upvotes: 3
Reputation: 558
try this, just like string:
class User < ActiveRecord::Base
has_many :things, :dependent => :destroy
validates :things, length: {maximum: 4}
end
Upvotes: 4
Reputation: 2481
In Rails 4
, perhaps earlier versions you can simply validate on the value of the counter_cache
.
class User
has_many :things
validates :things_count, numericality: { less_than: 5 }
end
class Thing
belongs_to :user, counter_cache: true
validates_associated :user
end
note that I've used :less_than
because :less_than_or_equal_to
would allow the things_count
to be 6
since it is validated after the counter cache update.
If you want to set a limit on a per user basis, you can create a things_limit
column to dynamically compare with the limit value you've set.
validates :things_count, numericality: { less_than: :things_limit }
Upvotes: 1
Reputation: 3205
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
Upvotes: 28
Reputation: 9857
Validating on the current count causes the count to become greater than the limit once the save has completed. The only way I've found way to prevent the create from occurring is to validate that before the create, the number of things is less than the limit.
This is not to say that it isn't useful having a validation on the count in the User model, but doing so doesn't prevent User.things.create
from being called because the user's count collection is valid until the new Thing object is saved, and then becomes invalid after the save.
class User
has_many :things
end
class Thing
belongs_to :user
validate :on => :create do
if user && user.things.length >= thing_limit
errors.add(:user, :too_many_things)
end
end
end
Upvotes: 6
Reputation: 13
You should try this.
class Thing <ActiveRecord::Base
belongs_to :user
validate :thing_count, :on => :create
def thing_count
user = User.find(id)
errors.add(:base, "Exceeded thing limit") if user.things.count >= 5
end
end
Upvotes: -4
Reputation: 371
You could try validates_length_of
and validates_associated
:
class Client < ActiveRecord::Base
has_many :orders
validates :orders, :length => { :maximum => 3 }
end
class Order < ActiveRecord::Base
belongs_to :client
validates_associated :client
end
A quick test shows that the valid?
method works as expected, but it does not stop you from adding new objects.
Upvotes: 0
Reputation: 3015
How you investigated using accepts_nested_attributes_for?
accepts_nested_attributes_for :things, :limit => 5
http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
That said, I think accepts_nested_attributes_for seems to only be appropriate for certain types of situations. For example, if you were creating a command line API, I think it's a pretty awful solution. However, if you have a nested form it works well enough (most of the time).
Upvotes: 0