has_many, but with limits in Rails
For the purpose of this article, we shall consider a Rails application that includes a model for organizations and a model for users. Each organization can have many users. Furthermore, we shall limit the number of users and organization can have.
There are a handful of strategies to accomplish this, but today let’s explore how association callbacks can solve the situation described above. These callbacks hook into the life cycle of Active Record objects, allowing to work with those objects at various points. More specifically, the before_add
callback can be used to ensure the number of users in an organization is bellow the limit, preventing the object from being saved to the database if not.
class User < ApplicationRecord
belongs_to :organization
end
class Organization < ApplicationRecord
MAX_USERS_IN_ORGANIZATION = 10
has_many :users, before_add: :check_users_limit
private
def check_users_limit(_user)
raise UserLimitExceeded if users.size >= MAX_USERS_IN_ORGANIZATION
end
end
By causing the before_add
callback to throw an exception, the user object does not get added to the collection.
class OrganizationTest < ActiveSupport::TestCase
test 'user limits for organization' do
org = create(:organisation)
org.users = create_list(:user, 10)
assert_equal 10, org.users.size
assert_raises UserLimitExceeded do
org.users << create(:user)
end
end
end
However, this approach comes with a caveat. As association callbacks are triggered by events in the life cycle of a collection, these are called only when the associated objects are added or removed through the association collection.
The following triggers the before_add
callback:
irb(main):001:0> org = Organization.create(name: "Example")
=> #<Organization id: 1, name: "Example", created_at: "2019-11-17 10:33:45", updated_at: "2019-11-17 10:33:45">
irb(main):002:0> 11.times { |i| org.users << User.create(name: "User #{i}") }
Traceback (most recent call last):
4: from (irb):3
3: from (irb):3:in `times'
2: from (irb):3:in `block in irb_binding'
1: from app/models/organization.rb:13:in `check_users_limit'
UserLimitExceeded (UserLimitExceeded)
irb(main):003:0> org.users.size
=> 10
On the othet hand, the following does not trigger the before_add
callback:
irb(main):004:0> User.create(name: "John Doe", organization: org)
=> #<User id: 11, organization_id: 1, name: "John Doe", email: nil, created_at: "2019-11-17 10:37:28", updated_at: "2019-11-17 10:37:28">
irb(main):005:0> org.reload
irb(main):006:0> org.users.size
=> 11