I have an album which has_many photos. A counter_cache setup updates the photos_count column in the album table. How do I limit the number of photos for an album?
Asked
Active
Viewed 1.8k times
29
-
1check the count before inserting? – Paul Creasey Feb 14 '10 at 23:24
-
1probably not the most cute of all, but the most safe is @Marcel Jackwerth sulution, others with use of `validates_associated` allow you to create children without limit using `parent.children.create` – max.underthesun Mar 07 '18 at 07:07
4 Answers
47
In my case, it was sufficient to use validates_length_of
:
class Album
has_many :photos
validates_length_of :photos, maximum: 10
end
class Photo
belongs_to :album
validates_associated :album
end
-
3
-
2Can confirm this works really well. Only problem is if you go the other way and just start creating a bunch of "Photos" that belong to album the same cannot be said. You will need to add :validate => true to the belongs_to, though I haven't tested to make sure this will always work as intended. – rovermicrover Nov 30 '15 at 22:50
-
4actually `belongs_to :album, validate: true` according to http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-belongs_to checks associations on the parent save, not on child save... so you can add as many children as you want, and this could be a problem – max.underthesun Mar 07 '18 at 06:50
-
No need of validates_associated :album in Photo model. It will work without validates_associated as well. – Mayuresh Srivastava Sep 11 '18 at 18:43
-
1
29
Use a validation hook:
class Album
has_many :photos
validate_on_create :photos_count_within_bounds
private
def photos_count_within_bounds
return if photos.blank?
errors.add("Too many photos") if photos.size > 10
end
end
class Photo
belongs_to :album
validates_associated :album
end

hurikhan77
- 5,881
- 3
- 32
- 47
-
-
1photos.size is a better way to go - http://blog.hasmanythrough.com/2008/2/27/count-length-size, from a similar question - http://stackoverflow.com/a/4836927/1396904 – andorov Nov 21 '13 at 06:17
-
-
1this solution still have a problem, you can add as many children, as you want with `album.photos.create`, at least with Rails 5.1.4, so it seems @Marcel Jackwerth sulution probably the best from this point of view – max.underthesun Mar 07 '18 at 06:57
-
@max.underthesun Yes, because it only validates on creation of the parent model. You could validate always by removing `_on_create`. – hurikhan77 Mar 15 '18 at 09:57
-
2@hurikhan77 I've checked it without `_on_create`, but with `parent.children.create` call from console I still can create one more `child` and exceed the limit by 1 (at least for my polymorphic association)... I suppose the problem here is with the `photos.size > 10` - on the moment of validation it is valid and became invalid just after the new `photo` creation... it could be only rails console issue though, I didn't check if it shows the same behavior then calling `create` from an application code – max.underthesun Mar 17 '18 at 14:55
-
@max.underthesun Maybe try `photos.length`, and maybe change `has_many :photos, touch: true` so it updates the parent object and reruns the validations defined there. PS: The solution of @jstejada may be the better one with current Rails versions, it may still need `touch`. – hurikhan77 Mar 18 '18 at 11:48
9
How about adding a custom validation method to the Photo model?
LIMIT = 50
validate_on_create do |record|
record.validate_quota
end
def validate_quota
return unless self.album
if self.album.photos(:reload).count >= LIMIT
errors.add(:base, :exceeded_quota)
end
end

Marcel Jackwerth
- 53,948
- 9
- 74
- 88
-
1thanks @Marcel Jackwerth! spend some time in `rails console` checking all 3 solutions, found out yours is the only which really preventing from creating "unwanted children" )) – max.underthesun Mar 07 '18 at 07:00
-
This solution worked best for my similar scenario. Glad you included `.reload` in your solution. I initially omitted it, and found some (but not all) scenarios would use an outdated count, allowing excessive child records to be created. – David Hempy Apr 28 '21 at 15:43
0
ActiveRecord::Base.transaction do
ActiveRecord::Base.connection.execute('LOCK TABLE pictures IN EXCLUSIVE MODE')
if (@album.pictures.count < 10)
@album.pictures.create()
end
end
I believe this is the most correct solution. It guards against concurrency issues/race conditions.

Edward
- 141
- 5
- 18
-
I would add a `reload` in there to ensure ActiveRecord isn't giving you stale results. Something like `@album.pictures.reload.count < 10` – David Hempy Apr 28 '21 at 15:45