29

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?

chief
  • 301
  • 1
  • 3
  • 3

4 Answers4

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
ase
  • 13,231
  • 4
  • 34
  • 46
jstejada
  • 852
  • 8
  • 8
  • 3
    This is the cleanest solution – fernandohur May 21 '15 at 16:16
  • 2
    Can 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
  • 4
    actually `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
    I could create 11 photos using this code – buncis Sep 10 '22 at 16:05
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
  • Thanks for the advice guys. I have got Marcel's code working. – chief Feb 15 '10 at 00:17
  • 1
    photos.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
  • Amended as you suggested, @andorov – hurikhan77 Nov 22 '13 at 08:23
  • 1
    this 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
  • 1
    thanks @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